From bf3edee29cb0a187711f2a9448f3214e18ca66ba Mon Sep 17 00:00:00 2001 From: Pere Prm Date: Sat, 18 May 2024 20:56:02 +0200 Subject: [PATCH] Improved synchronization of the audio with the display of letters Improved sound playback for each letter, now the sound plays once a letter has been displayed. Before, the sound was played at the beginning of the interpretation of each letter, regardless of how long it took to be displayed, being out of sync with the audio and the display of the letter. --- Assets/Scripts/DialogueVertexAnimator.cs | 397 ++++++++++++----------- 1 file changed, 209 insertions(+), 188 deletions(-) diff --git a/Assets/Scripts/DialogueVertexAnimator.cs b/Assets/Scripts/DialogueVertexAnimator.cs index a52ec2d..31fe29c 100644 --- a/Assets/Scripts/DialogueVertexAnimator.cs +++ b/Assets/Scripts/DialogueVertexAnimator.cs @@ -4,216 +4,237 @@ using TMPro; using UnityEngine; -public class DialogueVertexAnimator { - public bool textAnimating = false; - private bool stopAnimating = false; - - private readonly TMP_Text textBox; - private readonly float textAnimationScale; - private readonly AudioSourceGroup audioSourceGroup; - public DialogueVertexAnimator(TMP_Text _textBox, AudioSourceGroup _audioSourceGroup) { - textBox = _textBox; - audioSourceGroup = _audioSourceGroup; - textAnimationScale = textBox.fontSize; - } +public class DialogueVertexAnimator +{ + public bool textAnimating = false; + private bool stopAnimating = false; - private static readonly Color32 clear = new Color32(0, 0, 0, 0); - private const float CHAR_ANIM_TIME = 0.07f; - private static readonly Vector3 vecZero = Vector3.zero; - public IEnumerator AnimateTextIn(List commands, string processedMessage, AudioClip voice_sound, Action onFinish) { - textAnimating = true; - float secondsPerCharacter = 1f / 150f; - float timeOfLastCharacter = 0; - - TextAnimInfo[] textAnimInfo = SeparateOutTextAnimInfo(commands); - TMP_TextInfo textInfo = textBox.textInfo; - for (int i = 0; i < textInfo.meshInfo.Length; i++) //Clear the mesh - { - TMP_MeshInfo meshInfer = textInfo.meshInfo[i]; - if (meshInfer.vertices != null) { - for (int j = 0; j < meshInfer.vertices.Length; j++) { - meshInfer.vertices[j] = vecZero; - } - } - } + private readonly TMP_Text textBox; + private readonly float textAnimationScale; + private readonly AudioSourceGroup audioSourceGroup; + public DialogueVertexAnimator(TMP_Text _textBox, AudioSourceGroup _audioSourceGroup) + { + textBox = _textBox; + audioSourceGroup = _audioSourceGroup; + textAnimationScale = textBox.fontSize; + } - textBox.text = processedMessage; - textBox.ForceMeshUpdate(); + private static readonly Color32 clear = new Color32(0, 0, 0, 0); + private const float CHAR_ANIM_TIME = 0.07f; + private static readonly Vector3 vecZero = Vector3.zero; + public IEnumerator AnimateTextIn(List commands, string processedMessage, AudioClip voice_sound, Action onFinish){ + textAnimating = true; + float secondsPerCharacter = 1f / 150f; + float timeOfLastCharacter = 0; - TMP_MeshInfo[] cachedMeshInfo = textInfo.CopyMeshInfoVertexData(); - Color32[][] originalColors = new Color32[textInfo.meshInfo.Length][]; - for (int i = 0; i < originalColors.Length; i++) { - Color32[] theColors = textInfo.meshInfo[i].colors32; - originalColors[i] = new Color32[theColors.Length]; - Array.Copy(theColors, originalColors[i], theColors.Length); + TextAnimInfo[] textAnimInfo = SeparateOutTextAnimInfo(commands); + TMP_TextInfo textInfo = textBox.textInfo; + for (int i = 0; i < textInfo.meshInfo.Length; i++){ //Clear the mesh + TMP_MeshInfo meshInfer = textInfo.meshInfo[i]; + if (meshInfer.vertices != null){ + for (int j = 0; j < meshInfer.vertices.Length; j++){ + meshInfer.vertices[j] = vecZero; } - int charCount = textInfo.characterCount; - float[] charAnimStartTimes = new float[charCount]; - for (int i = 0; i < charCount; i++) { - charAnimStartTimes[i] = -1; //indicate the character as not yet started animating. + } + } + + // To save the letters already displayed. + List charactersShowed = new List(); + + textBox.text = processedMessage; + textBox.ForceMeshUpdate(); + + TMP_MeshInfo[] cachedMeshInfo = textInfo.CopyMeshInfoVertexData(); + Color32[][] originalColors = new Color32[textInfo.meshInfo.Length][]; + for (int i = 0; i < originalColors.Length; i++){ + Color32[] theColors = textInfo.meshInfo[i].colors32; + originalColors[i] = new Color32[theColors.Length]; + Array.Copy(theColors, originalColors[i], theColors.Length); + } + int charCount = textInfo.characterCount; + float[] charAnimStartTimes = new float[charCount]; + for (int i = 0; i < charCount; i++){ + charAnimStartTimes[i] = -1; //indicate the character as not yet started animating. + } + int visableCharacterIndex = 0; + while (true){ + if (stopAnimating){ + for (int i = visableCharacterIndex; i < charCount; i++){ + charAnimStartTimes[i] = Time.unscaledTime; } - int visableCharacterIndex = 0; - while (true) { - if (stopAnimating) { - for (int i = visableCharacterIndex; i < charCount; i++) { - charAnimStartTimes[i] = Time.unscaledTime; - } - visableCharacterIndex = charCount; - FinishAnimating(onFinish); - } - if (ShouldShowNextCharacter(secondsPerCharacter, timeOfLastCharacter)) { - if (visableCharacterIndex <= charCount) { - ExecuteCommandsForCurrentIndex(commands, visableCharacterIndex, ref secondsPerCharacter, ref timeOfLastCharacter); - if (visableCharacterIndex < charCount && ShouldShowNextCharacter(secondsPerCharacter, timeOfLastCharacter)) { - charAnimStartTimes[visableCharacterIndex] = Time.unscaledTime; - PlayDialogueSound(voice_sound); - visableCharacterIndex++; - timeOfLastCharacter = Time.unscaledTime; - if (visableCharacterIndex == charCount) { - FinishAnimating(onFinish); - } - } - } - } - for (int j = 0; j < charCount; j++) { - TMP_CharacterInfo charInfo = textInfo.characterInfo[j]; - if (charInfo.isVisible) //Invisible characters have a vertexIndex of 0 because they have no vertices and so they should be ignored to avoid messing up the first character in the string whic also has a vertexIndex of 0 - { - int vertexIndex = charInfo.vertexIndex; - int materialIndex = charInfo.materialReferenceIndex; - Color32[] destinationColors = textInfo.meshInfo[materialIndex].colors32; - Color32 theColor = j < visableCharacterIndex ? originalColors[materialIndex][vertexIndex] : clear; - destinationColors[vertexIndex + 0] = theColor; - destinationColors[vertexIndex + 1] = theColor; - destinationColors[vertexIndex + 2] = theColor; - destinationColors[vertexIndex + 3] = theColor; - - Vector3[] sourceVertices = cachedMeshInfo[materialIndex].vertices; - Vector3[] destinationVertices = textInfo.meshInfo[materialIndex].vertices; - float charSize = 0; - float charAnimStartTime = charAnimStartTimes[j]; - if (charAnimStartTime >= 0) { - float timeSinceAnimStart = Time.unscaledTime - charAnimStartTime; - charSize = Mathf.Min(1, timeSinceAnimStart / CHAR_ANIM_TIME); - } - - Vector3 animPosAdjustment = GetAnimPosAdjustment(textAnimInfo, j, textBox.fontSize, Time.unscaledTime); - Vector3 offset = (sourceVertices[vertexIndex + 0] + sourceVertices[vertexIndex + 2]) / 2; - destinationVertices[vertexIndex + 0] = ((sourceVertices[vertexIndex + 0] - offset) * charSize) + offset + animPosAdjustment; - destinationVertices[vertexIndex + 1] = ((sourceVertices[vertexIndex + 1] - offset) * charSize) + offset + animPosAdjustment; - destinationVertices[vertexIndex + 2] = ((sourceVertices[vertexIndex + 2] - offset) * charSize) + offset + animPosAdjustment; - destinationVertices[vertexIndex + 3] = ((sourceVertices[vertexIndex + 3] - offset) * charSize) + offset + animPosAdjustment; - } - } - textBox.UpdateVertexData(TMP_VertexDataUpdateFlags.Colors32); - for (int i = 0; i < textInfo.meshInfo.Length; i++) { - TMP_MeshInfo theInfo = textInfo.meshInfo[i]; - theInfo.mesh.vertices = theInfo.vertices; - textBox.UpdateGeometry(theInfo.mesh, i); + visableCharacterIndex = charCount; + FinishAnimating(onFinish); + } + if (ShouldShowNextCharacter(secondsPerCharacter, timeOfLastCharacter)){ + if (visableCharacterIndex <= charCount){ + ExecuteCommandsForCurrentIndex(commands, visableCharacterIndex, ref secondsPerCharacter, ref timeOfLastCharacter); + if (visableCharacterIndex < charCount && ShouldShowNextCharacter(secondsPerCharacter, timeOfLastCharacter)){ + charAnimStartTimes[visableCharacterIndex] = Time.unscaledTime; + visableCharacterIndex++; + timeOfLastCharacter = Time.unscaledTime; + if (visableCharacterIndex == charCount){ + FinishAnimating(onFinish); } - yield return null; + } } - } + } + + for (int j = 0; j < charCount; j++){ + TMP_CharacterInfo charInfo = textInfo.characterInfo[j]; + if (charInfo.isVisible){ //Invisible characters have a vertexIndex of 0 because they have no vertices and so they should be ignored to avoid messing up the first character in the string whic also has a vertexIndex of 0 + int vertexIndex = charInfo.vertexIndex; + int materialIndex = charInfo.materialReferenceIndex; + Color32[] destinationColors = textInfo.meshInfo[materialIndex].colors32; + Color32 theColor = j < visableCharacterIndex ? originalColors[materialIndex][vertexIndex] : clear; + destinationColors[vertexIndex + 0] = theColor; + destinationColors[vertexIndex + 1] = theColor; + destinationColors[vertexIndex + 2] = theColor; + destinationColors[vertexIndex + 3] = theColor; - private void ExecuteCommandsForCurrentIndex(List commands, int visableCharacterIndex, ref float secondsPerCharacter, ref float timeOfLastCharacter) { - for (int i = 0; i < commands.Count; i++) { - DialogueCommand command = commands[i]; - if (command.position == visableCharacterIndex) { - switch (command.type) { - case DialogueCommandType.Pause: - timeOfLastCharacter = Time.unscaledTime + command.floatValue; - break; - case DialogueCommandType.TextSpeedChange: - secondsPerCharacter = 1f / command.floatValue; - break; - } - commands.RemoveAt(i); - i--; + Vector3[] sourceVertices = cachedMeshInfo[materialIndex].vertices; + Vector3[] destinationVertices = textInfo.meshInfo[materialIndex].vertices; + float charSize = 0; + float charAnimStartTime = charAnimStartTimes[j]; + if (charAnimStartTime >= 0){ + float timeSinceAnimStart = Time.unscaledTime - charAnimStartTime; + charSize = Mathf.Min(1, timeSinceAnimStart / CHAR_ANIM_TIME); + + // When the letter reaches its maximum size, it is saved as already displayed, and then the sound is played. + if (charSize == 1 && !charactersShowed.Contains(j)){ + PlayDialogueSound(voice_sound); + charactersShowed.Add(j); } + } + + Vector3 animPosAdjustment = GetAnimPosAdjustment(textAnimInfo, j, textBox.fontSize, Time.unscaledTime); + Vector3 offset = (sourceVertices[vertexIndex + 0] + sourceVertices[vertexIndex + 2]) / 2; + destinationVertices[vertexIndex + 0] = ((sourceVertices[vertexIndex + 0] - offset) * charSize) + offset + animPosAdjustment; + destinationVertices[vertexIndex + 1] = ((sourceVertices[vertexIndex + 1] - offset) * charSize) + offset + animPosAdjustment; + destinationVertices[vertexIndex + 2] = ((sourceVertices[vertexIndex + 2] - offset) * charSize) + offset + animPosAdjustment; + destinationVertices[vertexIndex + 3] = ((sourceVertices[vertexIndex + 3] - offset) * charSize) + offset + animPosAdjustment; } - } + } + + // Animations - private void FinishAnimating(Action onFinish) { - textAnimating = false; - stopAnimating = false; - onFinish?.Invoke(); + textBox.UpdateVertexData(TMP_VertexDataUpdateFlags.Colors32); + for (int i = 0; i < textInfo.meshInfo.Length; i++){ + TMP_MeshInfo theInfo = textInfo.meshInfo[i]; + theInfo.mesh.vertices = theInfo.vertices; + textBox.UpdateGeometry(theInfo.mesh, i); + } + yield return null; } + } - private const float NOISE_MAGNITUDE_ADJUSTMENT = 0.06f; - private const float NOISE_FREQUENCY_ADJUSTMENT = 15f; - private const float WAVE_MAGNITUDE_ADJUSTMENT = 0.06f; - private Vector3 GetAnimPosAdjustment(TextAnimInfo[] textAnimInfo, int charIndex, float fontSize, float time) { - float x = 0; - float y = 0; - for (int i = 0; i < textAnimInfo.Length; i++) { - TextAnimInfo info = textAnimInfo[i]; - if (charIndex >= info.startIndex && charIndex < info.endIndex) { - if (info.type == TextAnimationType.shake) { - float scaleAdjust = fontSize * NOISE_MAGNITUDE_ADJUSTMENT; - x += (Mathf.PerlinNoise((charIndex + time) * NOISE_FREQUENCY_ADJUSTMENT, 0) - 0.5f) * scaleAdjust; - y += (Mathf.PerlinNoise((charIndex + time) * NOISE_FREQUENCY_ADJUSTMENT, 1000) - 0.5f) * scaleAdjust; - } else if (info.type == TextAnimationType.wave) { - y += Mathf.Sin((charIndex * 1.5f) + (time * 6)) * fontSize * WAVE_MAGNITUDE_ADJUSTMENT; - } - } + private void ExecuteCommandsForCurrentIndex(List commands, int visableCharacterIndex, ref float secondsPerCharacter, ref float timeOfLastCharacter){ + for (int i = 0; i < commands.Count; i++){ + DialogueCommand command = commands[i]; + if (command.position == visableCharacterIndex){ + switch (command.type){ + case DialogueCommandType.Pause: + timeOfLastCharacter = Time.unscaledTime + command.floatValue; + break; + case DialogueCommandType.TextSpeedChange: + secondsPerCharacter = 1f / command.floatValue; + break; } - return new Vector3(x, y, 0); + commands.RemoveAt(i); + i--; + } } + } - private static bool ShouldShowNextCharacter(float secondsPerCharacter, float timeOfLastCharacter) { - return (Time.unscaledTime - timeOfLastCharacter) > secondsPerCharacter; - } - public void SkipToEndOfCurrentMessage() { - if (textAnimating) { - stopAnimating = true; + private void FinishAnimating(Action onFinish){ + textAnimating = false; + stopAnimating = false; + onFinish?.Invoke(); + } + + private const float NOISE_MAGNITUDE_ADJUSTMENT = 0.06f; + private const float NOISE_FREQUENCY_ADJUSTMENT = 15f; + private const float WAVE_MAGNITUDE_ADJUSTMENT = 0.06f; + private Vector3 GetAnimPosAdjustment(TextAnimInfo[] textAnimInfo, int charIndex, float fontSize, float time){ + float x = 0; + float y = 0; + for (int i = 0; i < textAnimInfo.Length; i++){ + TextAnimInfo info = textAnimInfo[i]; + if (charIndex >= info.startIndex && charIndex < info.endIndex){ + if (info.type == TextAnimationType.shake){ + float scaleAdjust = fontSize * NOISE_MAGNITUDE_ADJUSTMENT; + x += (Mathf.PerlinNoise((charIndex + time) * NOISE_FREQUENCY_ADJUSTMENT, 0) - 0.5f) * scaleAdjust; + y += (Mathf.PerlinNoise((charIndex + time) * NOISE_FREQUENCY_ADJUSTMENT, 1000) - 0.5f) * scaleAdjust; } + else if (info.type == TextAnimationType.wave) + { + y += Mathf.Sin((charIndex * 1.5f) + (time * 6)) * fontSize * WAVE_MAGNITUDE_ADJUSTMENT; + } + } } + return new Vector3(x, y, 0); + } - private float timeUntilNextDialogueSound = 0; - private float lastDialogueSound = 0; - private void PlayDialogueSound(AudioClip voice_sound) { - if (Time.unscaledTime - lastDialogueSound > timeUntilNextDialogueSound) { - timeUntilNextDialogueSound = UnityEngine.Random.Range(0.02f, 0.08f); - lastDialogueSound = Time.unscaledTime; - audioSourceGroup.PlayFromNextSource(voice_sound); //Use Multiple Audio Sources to allow playing multiple sounds at once - } + private static bool ShouldShowNextCharacter(float secondsPerCharacter, float timeOfLastCharacter){ + return (Time.unscaledTime - timeOfLastCharacter) > secondsPerCharacter; + } + public void SkipToEndOfCurrentMessage(){ + if (textAnimating){ + stopAnimating = true; } + } - private TextAnimInfo[] SeparateOutTextAnimInfo(List commands) { - List tempResult = new List(); - List animStartCommands = new List(); - List animEndCommands = new List(); - for (int i = 0; i < commands.Count; i++) { - DialogueCommand command = commands[i]; - if (command.type == DialogueCommandType.AnimStart) { - animStartCommands.Add(command); - commands.RemoveAt(i); - i--; - } else if (command.type == DialogueCommandType.AnimEnd) { - animEndCommands.Add(command); - commands.RemoveAt(i); - i--; - } - } - if (animStartCommands.Count != animEndCommands.Count) { - Debug.LogError("Unequal number of start and end animation commands. Start Commands: " + animStartCommands.Count + " End Commands: " + animEndCommands.Count); - } else { - for (int i = 0; i < animStartCommands.Count; i++) { - DialogueCommand startCommand = animStartCommands[i]; - DialogueCommand endCommand = animEndCommands[i]; - tempResult.Add(new TextAnimInfo { - startIndex = startCommand.position, - endIndex = endCommand.position, - type = startCommand.textAnimValue - }); - } - } - return tempResult.ToArray(); + private float timeUntilNextDialogueSound = 0; + private float lastDialogueSound = 0; + private void PlayDialogueSound(AudioClip voice_sound){ + if (audioSourceGroup == null || voice_sound == null) { return; } // Sanity Check + + if (Time.unscaledTime - lastDialogueSound > timeUntilNextDialogueSound){ + timeUntilNextDialogueSound = UnityEngine.Random.Range(0.02f, 0.08f); + lastDialogueSound = Time.unscaledTime; + audioSourceGroup.PlayFromNextSource(voice_sound); //Use Multiple Audio Sources to allow playing multiple sounds at once + } + audioSourceGroup.PlayFromNextSource(voice_sound); + } + + private TextAnimInfo[] SeparateOutTextAnimInfo(List commands){ + List tempResult = new List(); + List animStartCommands = new List(); + List animEndCommands = new List(); + for (int i = 0; i < commands.Count; i++){ + DialogueCommand command = commands[i]; + if (command.type == DialogueCommandType.AnimStart){ + animStartCommands.Add(command); + commands.RemoveAt(i); + i--; + } + else if (command.type == DialogueCommandType.AnimEnd) + { + animEndCommands.Add(command); + commands.RemoveAt(i); + i--; + } + } + if (animStartCommands.Count != animEndCommands.Count){ + Debug.LogError("Unequal number of start and end animation commands. Start Commands: " + animStartCommands.Count + " End Commands: " + animEndCommands.Count); + } + else + { + for (int i = 0; i < animStartCommands.Count; i++){ + DialogueCommand startCommand = animStartCommands[i]; + DialogueCommand endCommand = animEndCommands[i]; + tempResult.Add(new TextAnimInfo{ + startIndex = startCommand.position, + endIndex = endCommand.position, + type = startCommand.textAnimValue + }); + } } + return tempResult.ToArray(); + } } -public struct TextAnimInfo { - public int startIndex; - public int endIndex; - public TextAnimationType type; +public struct TextAnimInfo{ + public int startIndex; + public int endIndex; + public TextAnimationType type; }