using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using TMPro; using UnityEngine; namespace SimpleTools.DialogueSystem { public class DialogueVertexAnimator { public bool textAnimating = false; bool stopAnimating = false; readonly TMP_Text textBox; string[] audioSourceGroup; public void SetAudioSourceGroup(params string[] _audioSourceGroup) { audioSourceGroup = _audioSourceGroup; } public DialogueVertexAnimator(TMP_Text _textBox) { textBox = _textBox; } static readonly Color32 clear = new Color32(0, 0, 0, 0); const float CHAR_ANIM_TIME = 0.07f; static readonly Vector3 vecZero = Vector3.zero; public IEnumerator AnimateTextIn(List commands, string processedMessage, 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++) { TMP_MeshInfo meshInfer = textInfo.meshInfo[i]; if (meshInfer.vertices != null) { for (int j = 0; j < meshInfer.vertices.Length; j++) { meshInfer.vertices[j] = vecZero; } } } 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; } 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(); 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) { 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; for (int i = 0; i < 4; i++) { Vector3 animVertexAdjustment = GetAnimVertexAdjustment(textAnimInfo, j, textBox.fontSize, Time.unscaledTime + i); destinationVertices[vertexIndex + i] += animVertexAdjustment; Color animColorAdjustment = GetAnimColorAdjustment(textAnimInfo, j, Time.unscaledTime + i, destinationVertices[vertexIndex + i]); if (animColorAdjustment == Color.white) continue; destinationColors[vertexIndex + i] += animColorAdjustment; } } } 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; } } 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; case DialogueCommandType.Sound: AudioManager.AudioManager.instance.PlayOneShot(command.stringValue); break; case DialogueCommandType.PlayMusic: string[] split0 = command.stringValue.Split(','); AudioManager.AudioManager.instance.FadeIn(split0[0], float.Parse(split0[1], CultureInfo.InvariantCulture)); break; case DialogueCommandType.StopMusic: string[] split1 = command.stringValue.Split(','); for (int j = 0; j < split1.Length; j++) { Debug.Log(split1[j]); } AudioManager.AudioManager.instance.FadeOut(split1[0], float.Parse(split1[1], CultureInfo.InvariantCulture)); break; } commands.RemoveAt(i); i--; } } } void FinishAnimating(Action onFinish) { textAnimating = false; stopAnimating = false; onFinish?.Invoke(); } const float NOISE_MAGNITUDE_ADJUSTMENT = 0.06f; const float NOISE_FREQUENCY_ADJUSTMENT = 15f; const float WAVE_MAGNITUDE_ADJUSTMENT = 0.06f; const float WOBBLE_MAGNITUDE_ADJUSTMENT = 0.5f; const float RAINBOW_LENGTH_ADJUSTMENT = .001f; 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); } Vector3 GetAnimVertexAdjustment(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.wobble) { float scaleAdjust = fontSize * NOISE_MAGNITUDE_ADJUSTMENT; x = Mathf.Sin(time * 3.3f) * scaleAdjust * WOBBLE_MAGNITUDE_ADJUSTMENT; y = Mathf.Cos(time * 2.5f) * scaleAdjust * WOBBLE_MAGNITUDE_ADJUSTMENT; } } } return new Vector3(x, y, 0); } Color GetAnimColorAdjustment(TextAnimInfo[] textAnimInfo, int charIndex, float time, Vector3 destinationVertice) { Color color = Color.white; for (int i = 0; i < textAnimInfo.Length; i++) { TextAnimInfo info = textAnimInfo[i]; if (charIndex >= info.startIndex && charIndex < info.endIndex) { if (info.type == TextAnimationType.rainbow) { color = Color.HSVToRGB(Mathf.Repeat((time + destinationVertice.x * RAINBOW_LENGTH_ADJUSTMENT), 1f), .75f, 1); } } } return color; } static bool ShouldShowNextCharacter(float secondsPerCharacter, float timeOfLastCharacter) { return (Time.unscaledTime - timeOfLastCharacter) > secondsPerCharacter; } public void SkipToEndOfCurrentMessage() { if (textAnimating) { stopAnimating = true; } } public bool IsMessageAnimating() { return textAnimating; } float timeUntilNextDialogueSound = 0; float lastDialogueSound = 0; void PlayDialogueSound() { if (Time.unscaledTime - lastDialogueSound > timeUntilNextDialogueSound) { timeUntilNextDialogueSound = UnityEngine.Random.Range(0.02f, 0.08f); lastDialogueSound = Time.unscaledTime; if (audioSourceGroup[0] != string.Empty) AudioManager.AudioManager.instance.PlayRandomSound(audioSourceGroup); } } 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; } }