From b418333d679f511e813723831ce6aba4c48c2014 Mon Sep 17 00:00:00 2001 From: Thibault Pouch Date: Tue, 10 Mar 2026 14:41:42 +0100 Subject: [PATCH] feat : add SubtitleSequencePlayer and SubtitleTriggerZone for managing subtitle playback --- .../Level/Level01IntroSubtitles.cs.meta | 2 - ...Subtitles.cs => SubtitleSequencePlayer.cs} | 106 ++++++++++-------- .../Level/SubtitleSequencePlayer.cs.meta | 2 + .../Code/Scripts/Level/SubtitleTriggerZone.cs | 51 +++++++++ .../Scripts/Level/SubtitleTriggerZone.cs.meta | 2 + Assets/Code/Subtitles.meta | 8 ++ .../Code/Subtitles/Level01IntroSubtitles.json | 19 ++++ .../Subtitles/Level01IntroSubtitles.json.meta | 7 ++ 8 files changed, 151 insertions(+), 46 deletions(-) delete mode 100644 Assets/Code/Scripts/Level/Level01IntroSubtitles.cs.meta rename Assets/Code/Scripts/Level/{Level01IntroSubtitles.cs => SubtitleSequencePlayer.cs} (73%) create mode 100644 Assets/Code/Scripts/Level/SubtitleSequencePlayer.cs.meta create mode 100644 Assets/Code/Scripts/Level/SubtitleTriggerZone.cs create mode 100644 Assets/Code/Scripts/Level/SubtitleTriggerZone.cs.meta create mode 100644 Assets/Code/Subtitles.meta create mode 100644 Assets/Code/Subtitles/Level01IntroSubtitles.json create mode 100644 Assets/Code/Subtitles/Level01IntroSubtitles.json.meta diff --git a/Assets/Code/Scripts/Level/Level01IntroSubtitles.cs.meta b/Assets/Code/Scripts/Level/Level01IntroSubtitles.cs.meta deleted file mode 100644 index 727a995..0000000 --- a/Assets/Code/Scripts/Level/Level01IntroSubtitles.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: a315c9c77ad8a49238033181ece48806 \ No newline at end of file diff --git a/Assets/Code/Scripts/Level/Level01IntroSubtitles.cs b/Assets/Code/Scripts/Level/SubtitleSequencePlayer.cs similarity index 73% rename from Assets/Code/Scripts/Level/Level01IntroSubtitles.cs rename to Assets/Code/Scripts/Level/SubtitleSequencePlayer.cs index c2878f5..7446f11 100644 --- a/Assets/Code/Scripts/Level/Level01IntroSubtitles.cs +++ b/Assets/Code/Scripts/Level/SubtitleSequencePlayer.cs @@ -1,13 +1,13 @@ +using System; using System.Collections; using UnityEngine; -using UnityEngine.SceneManagement; /// -/// Auto-spawns in Level01 and displays an intro subtitle sequence. +/// Reusable subtitle player that renders and plays subtitle lines loaded from JSON. /// -public class Level01IntroSubtitles : MonoBehaviour +public class SubtitleSequencePlayer : MonoBehaviour { - [System.Serializable] + [Serializable] private struct SubtitleLine { public string speaker; @@ -15,17 +15,15 @@ public class Level01IntroSubtitles : MonoBehaviour public float duration; } - [Header("Trigger")] - [SerializeField] private string targetSceneName = "Level01"; - [SerializeField] private float initialDelay = 2.5f; - - [Header("Subtitle Sequence")] - [SerializeField] private SubtitleLine[] lines = + [Serializable] + private struct SubtitleFile { - new SubtitleLine { speaker = "SYSTEME", text = "...Ici, quelque chose cloche.", duration = 2.5f }, - new SubtitleLine { speaker = "SYSTEME", text = "Reste calme. Observe la piece.", duration = 2.5f }, - new SubtitleLine { speaker = "SYSTEME", text = "Trouve une sortie.", duration = 2.2f }, - }; + 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; @@ -40,58 +38,55 @@ public class Level01IntroSubtitles : MonoBehaviour [SerializeField] private Color speakerColor = new Color(1f, 0.85f, 0.35f, 1f); [SerializeField] private Color backgroundColor = new Color(0f, 0f, 0f, 0.62f); - private static bool s_bootstrapped; - 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(); - [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] - private static void ResetBootstrapFlag() + public bool IsPlaying => m_isPlaying; + + 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)] - private static void Bootstrap() + public bool PlayDefault(float initialDelay = 0f) { - if (s_bootstrapped) - return; - - s_bootstrapped = true; - - GameObject go = new GameObject(nameof(Level01IntroSubtitles)); - go.hideFlags = HideFlags.DontSave; - go.AddComponent(); + return TryPlay(defaultSubtitleJson, initialDelay); } - private void Start() + private IEnumerator PlaySequence(float initialDelay) { - Scene activeScene = SceneManager.GetActiveScene(); - if (activeScene.name != targetSceneName) + m_isPlaying = true; + + if (m_runtimeLines == null || m_runtimeLines.Length == 0) { - Destroy(gameObject); - return; + m_isPlaying = false; + yield break; } - StartCoroutine(PlaySequence()); - } - - private IEnumerator PlaySequence() - { if (initialDelay > 0f) 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; - yield return StartCoroutine(ShowLine(lines[i])); + yield return StartCoroutine(ShowLine(m_runtimeLines[i])); if (gapBetweenLines > 0f) yield return new WaitForSeconds(gapBetweenLines); @@ -99,7 +94,31 @@ public class Level01IntroSubtitles : MonoBehaviour m_currentSpeaker = string.Empty; m_currentText = string.Empty; - Destroy(gameObject); + m_isPlaying = false; + } + + private bool TryReadLinesFromJson(TextAsset subtitleJson, out SubtitleLine[] parsedLines) + { + parsedLines = Array.Empty(); + + if (subtitleJson == null || string.IsNullOrWhiteSpace(subtitleJson.text)) + return false; + + SubtitleFile file; + try + { + file = JsonUtility.FromJson(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) @@ -233,7 +252,6 @@ public class Level01IntroSubtitles : MonoBehaviour clipping = TextClipping.Clip, }; m_speakerStyle.normal.textColor = speakerColor; - } private void OnDestroy() diff --git a/Assets/Code/Scripts/Level/SubtitleSequencePlayer.cs.meta b/Assets/Code/Scripts/Level/SubtitleSequencePlayer.cs.meta new file mode 100644 index 0000000..5ebda90 --- /dev/null +++ b/Assets/Code/Scripts/Level/SubtitleSequencePlayer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4947743d7bc9b4589b9932d429517d3a \ No newline at end of file diff --git a/Assets/Code/Scripts/Level/SubtitleTriggerZone.cs b/Assets/Code/Scripts/Level/SubtitleTriggerZone.cs new file mode 100644 index 0000000..8410dd1 --- /dev/null +++ b/Assets/Code/Scripts/Level/SubtitleTriggerZone.cs @@ -0,0 +1,51 @@ +using UnityEngine; + +/// +/// 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. +/// +[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(); + 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() != null) + return true; + + return false; + } +} diff --git a/Assets/Code/Scripts/Level/SubtitleTriggerZone.cs.meta b/Assets/Code/Scripts/Level/SubtitleTriggerZone.cs.meta new file mode 100644 index 0000000..739926a --- /dev/null +++ b/Assets/Code/Scripts/Level/SubtitleTriggerZone.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 33a33c65f75e2443383c2e29bd6bf5f1 \ No newline at end of file diff --git a/Assets/Code/Subtitles.meta b/Assets/Code/Subtitles.meta new file mode 100644 index 0000000..9c9ba8f --- /dev/null +++ b/Assets/Code/Subtitles.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3952fe191e7e945b3ba35d76408a51a6 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Code/Subtitles/Level01IntroSubtitles.json b/Assets/Code/Subtitles/Level01IntroSubtitles.json new file mode 100644 index 0000000..afcce3e --- /dev/null +++ b/Assets/Code/Subtitles/Level01IntroSubtitles.json @@ -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 + } + ] +} diff --git a/Assets/Code/Subtitles/Level01IntroSubtitles.json.meta b/Assets/Code/Subtitles/Level01IntroSubtitles.json.meta new file mode 100644 index 0000000..3d5ec86 --- /dev/null +++ b/Assets/Code/Subtitles/Level01IntroSubtitles.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: c4b9d13b29337441dbdb06a8a45e32c3 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: