feat : add SubtitleSequencePlayer and SubtitleTriggerZone for managing subtitle playback

This commit is contained in:
Thibault Pouch
2026-03-10 14:41:42 +01:00
parent 6dd1c5efb8
commit b418333d67
8 changed files with 151 additions and 46 deletions

View File

@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: a315c9c77ad8a49238033181ece48806

View File

@@ -1,13 +1,13 @@
using System;
using System.Collections; using System.Collections;
using UnityEngine; using UnityEngine;
using UnityEngine.SceneManagement;
/// <summary> /// <summary>
/// Auto-spawns in Level01 and displays an intro subtitle sequence. /// Reusable subtitle player that renders and plays subtitle lines loaded from JSON.
/// </summary> /// </summary>
public class Level01IntroSubtitles : MonoBehaviour public class SubtitleSequencePlayer : MonoBehaviour
{ {
[System.Serializable] [Serializable]
private struct SubtitleLine private struct SubtitleLine
{ {
public string speaker; public string speaker;
@@ -15,17 +15,15 @@ public class Level01IntroSubtitles : MonoBehaviour
public float duration; public float duration;
} }
[Header("Trigger")] [Serializable]
[SerializeField] private string targetSceneName = "Level01"; private struct SubtitleFile
[SerializeField] private float initialDelay = 2.5f;
[Header("Subtitle Sequence")]
[SerializeField] private SubtitleLine[] lines =
{ {
new SubtitleLine { speaker = "SYSTEME", text = "...Ici, quelque chose cloche.", duration = 2.5f }, public SubtitleLine[] lines;
new SubtitleLine { speaker = "SYSTEME", text = "Reste calme. Observe la piece.", duration = 2.5f }, }
new SubtitleLine { speaker = "SYSTEME", text = "Trouve une sortie.", duration = 2.2f },
}; [Header("Optional Default Data")]
[Tooltip("Used only if trigger zone calls PlayDefault().")]
[SerializeField] private TextAsset defaultSubtitleJson;
[SerializeField] private float typewriterCharsPerSecond = 40f; [SerializeField] private float typewriterCharsPerSecond = 40f;
[SerializeField] private float fadeDuration = 0.2f; [SerializeField] private float fadeDuration = 0.2f;
@@ -40,58 +38,55 @@ public class Level01IntroSubtitles : MonoBehaviour
[SerializeField] private Color speakerColor = new Color(1f, 0.85f, 0.35f, 1f); [SerializeField] private Color speakerColor = new Color(1f, 0.85f, 0.35f, 1f);
[SerializeField] private Color backgroundColor = new Color(0f, 0f, 0f, 0.62f); [SerializeField] private Color backgroundColor = new Color(0f, 0f, 0f, 0.62f);
private static bool s_bootstrapped;
private string m_currentSpeaker; private string m_currentSpeaker;
private string m_currentText; private string m_currentText;
private GUIStyle m_textStyle; private GUIStyle m_textStyle;
private GUIStyle m_speakerStyle; private GUIStyle m_speakerStyle;
private Texture2D m_background; private Texture2D m_background;
private bool m_isShowing; private bool m_isShowing;
private bool m_isPlaying;
private float m_alpha; private float m_alpha;
private SubtitleLine[] m_runtimeLines = Array.Empty<SubtitleLine>();
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] public bool IsPlaying => m_isPlaying;
private static void ResetBootstrapFlag()
public bool TryPlay(TextAsset subtitleJson, float initialDelay = 0f)
{ {
s_bootstrapped = false; if (m_isPlaying)
return false;
if (!TryReadLinesFromJson(subtitleJson, out SubtitleLine[] parsedLines))
return false;
m_runtimeLines = parsedLines;
StartCoroutine(PlaySequence(initialDelay));
return true;
} }
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] public bool PlayDefault(float initialDelay = 0f)
private static void Bootstrap()
{ {
if (s_bootstrapped) return TryPlay(defaultSubtitleJson, initialDelay);
return;
s_bootstrapped = true;
GameObject go = new GameObject(nameof(Level01IntroSubtitles));
go.hideFlags = HideFlags.DontSave;
go.AddComponent<Level01IntroSubtitles>();
} }
private void Start() private IEnumerator PlaySequence(float initialDelay)
{ {
Scene activeScene = SceneManager.GetActiveScene(); m_isPlaying = true;
if (activeScene.name != targetSceneName)
if (m_runtimeLines == null || m_runtimeLines.Length == 0)
{ {
Destroy(gameObject); m_isPlaying = false;
return; yield break;
} }
StartCoroutine(PlaySequence());
}
private IEnumerator PlaySequence()
{
if (initialDelay > 0f) if (initialDelay > 0f)
yield return new WaitForSeconds(initialDelay); yield return new WaitForSeconds(initialDelay);
for (int i = 0; i < lines.Length; i++) for (int i = 0; i < m_runtimeLines.Length; i++)
{ {
if (string.IsNullOrWhiteSpace(lines[i].text) || lines[i].duration <= 0f) if (string.IsNullOrWhiteSpace(m_runtimeLines[i].text) || m_runtimeLines[i].duration <= 0f)
continue; continue;
yield return StartCoroutine(ShowLine(lines[i])); yield return StartCoroutine(ShowLine(m_runtimeLines[i]));
if (gapBetweenLines > 0f) if (gapBetweenLines > 0f)
yield return new WaitForSeconds(gapBetweenLines); yield return new WaitForSeconds(gapBetweenLines);
@@ -99,7 +94,31 @@ public class Level01IntroSubtitles : MonoBehaviour
m_currentSpeaker = string.Empty; m_currentSpeaker = string.Empty;
m_currentText = string.Empty; m_currentText = string.Empty;
Destroy(gameObject); 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) private IEnumerator ShowLine(SubtitleLine line)
@@ -233,7 +252,6 @@ public class Level01IntroSubtitles : MonoBehaviour
clipping = TextClipping.Clip, clipping = TextClipping.Clip,
}; };
m_speakerStyle.normal.textColor = speakerColor; m_speakerStyle.normal.textColor = speakerColor;
} }
private void OnDestroy() private void OnDestroy()

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4947743d7bc9b4589b9932d429517d3a

View File

@@ -0,0 +1,51 @@
using UnityEngine;
/// <summary>
/// Trigger zone that starts a subtitle JSON sequence on a linked SubtitleSequencePlayer.
/// Put this on the zone collider object, and link the player on your empty object.
/// </summary>
[RequireComponent(typeof(Collider))]
public class SubtitleTriggerZone : MonoBehaviour
{
[Header("References")]
[SerializeField] private SubtitleSequencePlayer subtitlePlayer;
[SerializeField] private TextAsset subtitleJson;
[Header("Playback")]
[SerializeField] private float initialDelay = 0f;
[SerializeField] private bool oneShot = true;
private bool m_hasPlayed;
private void Reset()
{
Collider col = GetComponent<Collider>();
col.isTrigger = true;
}
private void OnTriggerEnter(Collider other)
{
if (!IsPlayer(other))
return;
if (oneShot && m_hasPlayed)
return;
if (subtitlePlayer == null)
return;
if (subtitlePlayer.TryPlay(subtitleJson, initialDelay))
m_hasPlayed = true;
}
private bool IsPlayer(Collider other)
{
if (other.CompareTag("Player"))
return true;
if (other.GetComponentInParent<PlayerMovement>() != null)
return true;
return false;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 33a33c65f75e2443383c2e29bd6bf5f1

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3952fe191e7e945b3ba35d76408a51a6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,19 @@
{
"lines": [
{
"speaker": "SYSTEME",
"text": "...Ici, quelque chose cloche.",
"duration": 2.5
},
{
"speaker": "SYSTEME",
"text": "Reste calme. Observe la piece.",
"duration": 2.5
},
{
"speaker": "SYSTEME",
"text": "Trouve une sortie.",
"duration": 2.2
}
]
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: c4b9d13b29337441dbdb06a8a45e32c3
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: