264 lines
7.7 KiB
C#
264 lines
7.7 KiB
C#
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;
|
|
int visibleChars = 0;
|
|
if (typewriterCharsPerSecond > 0f)
|
|
{
|
|
while (m_currentText.Length < totalChars)
|
|
{
|
|
typeTime += Time.deltaTime;
|
|
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);
|
|
}
|
|
}
|