feat : add SubtitleSequencePlayer and SubtitleTriggerZone for managing subtitle playback
This commit is contained in:
262
Assets/Code/Scripts/Level/SubtitleSequencePlayer.cs
Normal file
262
Assets/Code/Scripts/Level/SubtitleSequencePlayer.cs
Normal file
@@ -0,0 +1,262 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Reusable subtitle player that renders and plays subtitle lines loaded from JSON.
|
||||
/// </summary>
|
||||
public class SubtitleSequencePlayer : MonoBehaviour
|
||||
{
|
||||
[Serializable]
|
||||
private struct SubtitleLine
|
||||
{
|
||||
public string speaker;
|
||||
public string text;
|
||||
public float duration;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
private struct SubtitleFile
|
||||
{
|
||||
public SubtitleLine[] lines;
|
||||
}
|
||||
|
||||
[Header("Optional Default Data")]
|
||||
[Tooltip("Used only if trigger zone calls PlayDefault().")]
|
||||
[SerializeField] private TextAsset defaultSubtitleJson;
|
||||
|
||||
[SerializeField] private float typewriterCharsPerSecond = 40f;
|
||||
[SerializeField] private float fadeDuration = 0.2f;
|
||||
[SerializeField] private float gapBetweenLines = 0.15f;
|
||||
|
||||
[Header("Visual")]
|
||||
[SerializeField] private int fontSize = 28;
|
||||
[SerializeField] private int speakerFontSize = 18;
|
||||
[SerializeField] private float horizontalPadding = 28f;
|
||||
[SerializeField] private float bottomOffset = 56f;
|
||||
[SerializeField] private Color textColor = new Color(1f, 1f, 1f, 1f);
|
||||
[SerializeField] private Color speakerColor = new Color(1f, 0.85f, 0.35f, 1f);
|
||||
[SerializeField] private Color backgroundColor = new Color(0f, 0f, 0f, 0.62f);
|
||||
|
||||
private string m_currentSpeaker;
|
||||
private string m_currentText;
|
||||
private GUIStyle m_textStyle;
|
||||
private GUIStyle m_speakerStyle;
|
||||
private Texture2D m_background;
|
||||
private bool m_isShowing;
|
||||
private bool m_isPlaying;
|
||||
private float m_alpha;
|
||||
private SubtitleLine[] m_runtimeLines = Array.Empty<SubtitleLine>();
|
||||
|
||||
public bool IsPlaying => m_isPlaying;
|
||||
|
||||
public bool TryPlay(TextAsset subtitleJson, float initialDelay = 0f)
|
||||
{
|
||||
if (m_isPlaying)
|
||||
return false;
|
||||
|
||||
if (!TryReadLinesFromJson(subtitleJson, out SubtitleLine[] parsedLines))
|
||||
return false;
|
||||
|
||||
m_runtimeLines = parsedLines;
|
||||
StartCoroutine(PlaySequence(initialDelay));
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool PlayDefault(float initialDelay = 0f)
|
||||
{
|
||||
return TryPlay(defaultSubtitleJson, initialDelay);
|
||||
}
|
||||
|
||||
private IEnumerator PlaySequence(float initialDelay)
|
||||
{
|
||||
m_isPlaying = true;
|
||||
|
||||
if (m_runtimeLines == null || m_runtimeLines.Length == 0)
|
||||
{
|
||||
m_isPlaying = false;
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (initialDelay > 0f)
|
||||
yield return new WaitForSeconds(initialDelay);
|
||||
|
||||
for (int i = 0; i < m_runtimeLines.Length; i++)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(m_runtimeLines[i].text) || m_runtimeLines[i].duration <= 0f)
|
||||
continue;
|
||||
|
||||
yield return StartCoroutine(ShowLine(m_runtimeLines[i]));
|
||||
|
||||
if (gapBetweenLines > 0f)
|
||||
yield return new WaitForSeconds(gapBetweenLines);
|
||||
}
|
||||
|
||||
m_currentSpeaker = string.Empty;
|
||||
m_currentText = string.Empty;
|
||||
m_isPlaying = false;
|
||||
}
|
||||
|
||||
private bool TryReadLinesFromJson(TextAsset subtitleJson, out SubtitleLine[] parsedLines)
|
||||
{
|
||||
parsedLines = Array.Empty<SubtitleLine>();
|
||||
|
||||
if (subtitleJson == null || string.IsNullOrWhiteSpace(subtitleJson.text))
|
||||
return false;
|
||||
|
||||
SubtitleFile file;
|
||||
try
|
||||
{
|
||||
file = JsonUtility.FromJson<SubtitleFile>(subtitleJson.text);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (file.lines == null || file.lines.Length == 0)
|
||||
return false;
|
||||
|
||||
parsedLines = file.lines;
|
||||
return true;
|
||||
}
|
||||
|
||||
private IEnumerator ShowLine(SubtitleLine line)
|
||||
{
|
||||
m_currentSpeaker = line.speaker;
|
||||
m_currentText = string.Empty;
|
||||
m_isShowing = true;
|
||||
|
||||
if (fadeDuration > 0f)
|
||||
{
|
||||
float fadeIn = 0f;
|
||||
while (fadeIn < fadeDuration)
|
||||
{
|
||||
fadeIn += Time.deltaTime;
|
||||
m_alpha = Mathf.Clamp01(fadeIn / fadeDuration);
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_alpha = 1f;
|
||||
}
|
||||
|
||||
float typeTime = 0f;
|
||||
int totalChars = line.text.Length;
|
||||
if (typewriterCharsPerSecond > 0f)
|
||||
{
|
||||
while (m_currentText.Length < totalChars)
|
||||
{
|
||||
typeTime += Time.deltaTime;
|
||||
int visibleChars = Mathf.Clamp(Mathf.FloorToInt(typeTime * typewriterCharsPerSecond), 0, totalChars);
|
||||
m_currentText = line.text.Substring(0, visibleChars);
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_currentText = line.text;
|
||||
}
|
||||
|
||||
float holdDuration = Mathf.Max(0f, line.duration - (typewriterCharsPerSecond > 0f ? typeTime : 0f));
|
||||
if (holdDuration > 0f)
|
||||
yield return new WaitForSeconds(holdDuration);
|
||||
|
||||
if (fadeDuration > 0f)
|
||||
{
|
||||
float fadeOut = fadeDuration;
|
||||
while (fadeOut > 0f)
|
||||
{
|
||||
fadeOut -= Time.deltaTime;
|
||||
m_alpha = Mathf.Clamp01(fadeOut / fadeDuration);
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
|
||||
m_alpha = 0f;
|
||||
m_isShowing = false;
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
if (!m_isShowing || string.IsNullOrEmpty(m_currentText))
|
||||
return;
|
||||
|
||||
EnsureStyles();
|
||||
|
||||
float maxWidth = Mathf.Min(Screen.width - 24f, 940f);
|
||||
float textWidth = maxWidth - horizontalPadding * 2f;
|
||||
float speakerHeight = string.IsNullOrEmpty(m_currentSpeaker)
|
||||
? 0f
|
||||
: m_speakerStyle.CalcHeight(new GUIContent(m_currentSpeaker), textWidth);
|
||||
float textHeight = m_textStyle.CalcHeight(new GUIContent(string.IsNullOrEmpty(m_currentText) ? " " : m_currentText), textWidth);
|
||||
|
||||
float boxWidth = maxWidth;
|
||||
float boxHeight = speakerHeight + textHeight + 28f;
|
||||
float boxX = (Screen.width - boxWidth) * 0.5f;
|
||||
float boxY = Screen.height - bottomOffset - boxHeight;
|
||||
|
||||
Rect boxRect = new Rect(boxX, boxY, boxWidth, boxHeight);
|
||||
Color previousColor = GUI.color;
|
||||
GUI.color = new Color(1f, 1f, 1f, m_alpha);
|
||||
GUI.DrawTexture(boxRect, m_background);
|
||||
|
||||
float yOffset = boxRect.y + 10f;
|
||||
if (!string.IsNullOrEmpty(m_currentSpeaker))
|
||||
{
|
||||
Rect speakerRect = new Rect(
|
||||
boxRect.x + horizontalPadding,
|
||||
yOffset,
|
||||
textWidth,
|
||||
speakerHeight);
|
||||
GUI.Label(speakerRect, m_currentSpeaker, m_speakerStyle);
|
||||
yOffset += speakerHeight + 2f;
|
||||
}
|
||||
|
||||
Rect textRect = new Rect(
|
||||
boxRect.x + horizontalPadding,
|
||||
yOffset,
|
||||
textWidth,
|
||||
textHeight);
|
||||
|
||||
GUI.Label(textRect, m_currentText, m_textStyle);
|
||||
GUI.color = previousColor;
|
||||
}
|
||||
|
||||
private void EnsureStyles()
|
||||
{
|
||||
if (m_textStyle != null)
|
||||
return;
|
||||
|
||||
m_background = new Texture2D(1, 1);
|
||||
m_background.SetPixel(0, 0, backgroundColor);
|
||||
m_background.Apply();
|
||||
|
||||
m_textStyle = new GUIStyle(GUI.skin.label)
|
||||
{
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
fontSize = fontSize,
|
||||
wordWrap = true,
|
||||
richText = false,
|
||||
clipping = TextClipping.Clip,
|
||||
};
|
||||
m_textStyle.normal.textColor = textColor;
|
||||
|
||||
m_speakerStyle = new GUIStyle(GUI.skin.label)
|
||||
{
|
||||
alignment = TextAnchor.MiddleCenter,
|
||||
fontSize = speakerFontSize,
|
||||
fontStyle = FontStyle.Bold,
|
||||
wordWrap = false,
|
||||
clipping = TextClipping.Clip,
|
||||
};
|
||||
m_speakerStyle.normal.textColor = speakerColor;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (m_background != null)
|
||||
Destroy(m_background);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user