Typewriter with Jitter effect

Announcements, support questions, and discussion for the Dialogue System.
Post Reply
VoodooDetective
Posts: 222
Joined: Wed Jan 22, 2020 10:48 pm

Typewriter with Jitter effect

Post by VoodooDetective »

Before I started using Dialogue System for Unity, I'd written a script that looked through all <link> tags in a text mesh pro component and applied animations to the characters therein.

Example:
<link="jitter">this text will jitter</link>

or something like that.

The code looked something like this:

Code: Select all

    /// <summary>
    /// Handles animation of rich text animation tags.
    /// </summary>
    [RequireComponent(typeof(TextMeshProUGUI))]
    public class RichTextAnimationTagRunner : MonoBehaviour
    {
        // Constants
        private const int ClockPrecisionDecimals = 2; // Centisecond
        private static readonly float ClockIncrement = (1f / Mathf.Pow(10f, ClockPrecisionDecimals)) * 5;
        // References 
        public TextMeshProUGUI textScript;
        // Properties
        private AnimationHandle animationHandle;
        private readonly object animationHandleLock = new object();

        void Reset()
        {
            textScript = GetComponent<TextMeshProUGUI>();
        }

        public void StartAnimations()
        {
            lock (animationHandleLock)
            {
                animationHandle = new AnimationHandle(textScript, ClockIncrement, ClockPrecisionDecimals);
                TMP_TextInfo textInfo = textScript.textInfo;
                int linkCount = textInfo.linkCount;
                Coroutine[] coroutines = new Coroutine[linkCount];
                for (int i = 0; i < linkCount; i++)
                {
                    int start = textInfo.linkInfo[i].linkTextfirstCharacterIndex;
                    int end = start + textInfo.linkInfo[i].linkTextLength;
                    RecitationTextEmotion emotion = (RecitationTextEmotion)Enum.Parse(typeof(RecitationTextEmotion), textScript.textInfo.linkInfo[i].GetLinkID());
                    switch (emotion)
                    {
                        case RecitationTextEmotion.Jitter:
                            JitterAnimationConfig jitterConfig = new JitterAnimationConfig();
                            jitterConfig.curveScale = 1.0f;
                            jitterConfig.angleMultiplier = 1.0f;
                            jitterConfig.speedMultiplier = 0.85f;
                            jitterConfig.startIndex = start;
                            jitterConfig.endIndex = end;
                            jitterConfig.animationHandle = animationHandle;
                            coroutines[i] = StartCoroutine(SpeechAnimationRoutines.AnimateJitter(jitterConfig));
                            break;
                        case RecitationTextEmotion.SingSong:
                            SingSongAnimationConfig singSongConfig = new SingSongAnimationConfig();
                            singSongConfig.angle = 10.0f;
                            singSongConfig.angleIncrement = 2.5f;
                            singSongConfig.speedMultiplier = 1.0f;
                            singSongConfig.startIndex = start;
                            singSongConfig.endIndex = end;
                            singSongConfig.animationHandle = animationHandle;
                            coroutines[i] = StartCoroutine(SpeechAnimationRoutines.AnimateSingSong(singSongConfig));
                            break;
                        default:
                            throw new Exception("Unknown text emotion: " + emotion);
                    }
                }
                animationHandle.shutdownAnimation = () =>
                 {
                     foreach (Coroutine coroutine in coroutines)
                     {
                         StopCoroutine(coroutine);
                     }
                 };
            }
        }

        public void StopAnimations()
        {
            lock (animationHandleLock)
            {
                animationHandle?.TearDown();
            }
        }
    }

    /// <summary>
    /// Any animations that should be applied to the text.  This is used in <link="Emotion here"> tags.
    /// </summary>
    public enum RecitationTextEmotion
    {
        Jitter,
        SingSong,
    }
with some coroutines like this:

Code: Select all

    /// <summary>
    /// A set of animation routines for speech text.
    /// </summary>
    public static class SpeechAnimationRoutines
    {
        /// <summary>
        /// Animate a jitter in the letters.
        /// </summary>
        /// <param name="jitterConfig">Configuration information for the jitter.</param>
        /// <returns>IEnumerator for the coroutine.</returns>
        public static IEnumerator AnimateJitter(JitterAnimationConfig jitterConfig)
        {
            Matrix4x4 matrix;
            TMP_TextInfo textInfo = jitterConfig.animationHandle.textScript.textInfo;
            AnimationHandle animHandle = jitterConfig.animationHandle;
            float waitTime = animHandle.clockIncrement / jitterConfig.speedMultiplier;
            while (true)
            {
                int i;
                for (i = jitterConfig.startIndex; i < jitterConfig.endIndex && animHandle.cachedMeshInfo != null; i++)
                {
                    // Grab Character
                    TMP_CharacterInfo charInfo = textInfo.characterInfo[i];
                    if (charInfo.character == ' ')
                    {
                        continue;
                    }

                    // Make sure character is visible
                    int materialIndex = charInfo.materialReferenceIndex;
                    int vertexIndex = charInfo.vertexIndex;
                    Color32[] newVertexColors = textInfo.meshInfo[materialIndex].colors32;
                    if (newVertexColors[vertexIndex + 0].a == 0 || !charInfo.isVisible)
                    {
                        break;
                    }

                    // Get the cached vertices of the mesh used by this text element (character or sprite).
                    Vector3[] sourceVertices = animHandle.cachedMeshInfo[materialIndex].vertices;

                    // Determine the center point of each character.
                    Vector2 charMidBasline = (sourceVertices[vertexIndex + 0] + sourceVertices[vertexIndex + 2]) / 2;

                    // Need to translate all 4 vertices of each quad to aligned with middle of character / baseline.
                    // This is needed so the matrix TRS is applied at the origin for each character.
                    Vector3 offset = charMidBasline;
                    Vector3[] destinationVertices = textInfo.meshInfo[materialIndex].vertices;
                    destinationVertices[vertexIndex + 0] = sourceVertices[vertexIndex + 0] - offset;
                    destinationVertices[vertexIndex + 1] = sourceVertices[vertexIndex + 1] - offset;
                    destinationVertices[vertexIndex + 2] = sourceVertices[vertexIndex + 2] - offset;
                    destinationVertices[vertexIndex + 3] = sourceVertices[vertexIndex + 3] - offset;

                    // Apply Jitter
                    Vector3 jitterOffset = new Vector3(UnityEngine.Random.Range(-.25f, .25f), UnityEngine.Random.Range(-.25f, .25f), 0);
                    matrix = Matrix4x4.TRS(jitterOffset * jitterConfig.curveScale, Quaternion.Euler(0, 0, UnityEngine.Random.Range(-5f, 5f) * jitterConfig.angleMultiplier), Vector3.one);
                    destinationVertices[vertexIndex + 0] = matrix.MultiplyPoint3x4(destinationVertices[vertexIndex + 0]);
                    destinationVertices[vertexIndex + 1] = matrix.MultiplyPoint3x4(destinationVertices[vertexIndex + 1]);
                    destinationVertices[vertexIndex + 2] = matrix.MultiplyPoint3x4(destinationVertices[vertexIndex + 2]);
                    destinationVertices[vertexIndex + 3] = matrix.MultiplyPoint3x4(destinationVertices[vertexIndex + 3]);

                    // Translate all 4 vertices of each quad back to their original positions
                    destinationVertices[vertexIndex + 0] += offset;
                    destinationVertices[vertexIndex + 1] += offset;
                    destinationVertices[vertexIndex + 2] += offset;
                    destinationVertices[vertexIndex + 3] += offset;

                    // Push changes into meshes
                    textInfo.meshInfo[materialIndex].mesh.vertices = textInfo.meshInfo[materialIndex].vertices;
                    animHandle.textScript.UpdateGeometry(textInfo.meshInfo[materialIndex].mesh, materialIndex);
                }
                yield return new WaitForSeconds(waitTime);
            }
        }

        /// <summary>
        /// Animate sing song letters. 
        /// </summary>
        /// <param name="singSongConfig">Configuration for the sing song animation.</param>
        /// <returns>IEnumerator for the coroutine.</returns>
        public static IEnumerator AnimateSingSong(SingSongAnimationConfig singSongConfig)
        {
            Matrix4x4 matrix;
            TMP_TextInfo textInfo = singSongConfig.animationHandle.textScript.textInfo;
            AnimationHandle animHandle = singSongConfig.animationHandle;
            float waitTime = animHandle.clockIncrement / singSongConfig.speedMultiplier;
            float angleCounter = 0f;
            float doubleTheAngle = singSongConfig.angle * 2;
            while (true)
            {
                angleCounter += singSongConfig.angleIncrement;
                float newAngle = Mathf.PingPong(angleCounter++, doubleTheAngle) - singSongConfig.angle;
                for (int i = singSongConfig.startIndex; i < singSongConfig.endIndex && animHandle.cachedMeshInfo != null; i++)
                {
                    // Grab Character
                    TMP_CharacterInfo charInfo = textInfo.characterInfo[i];
                    if (charInfo.character == ' ')
                    {
                        continue;
                    }

                    // Make sure character is visible
                    int materialIndex = charInfo.materialReferenceIndex;
                    int vertexIndex = charInfo.vertexIndex;
                    Color32[] newVertexColors = textInfo.meshInfo[materialIndex].colors32;
                    if (newVertexColors[vertexIndex + 0].a == 0 || !charInfo.isVisible)
                    {
                        break;
                    }

                    // Get the cached vertices of the mesh used by this text element (character or sprite).
                    Vector3[] sourceVertices = animHandle.cachedMeshInfo[materialIndex].vertices;

                    // Determine the center point of each character.
                    Vector2 charMidBasline = (sourceVertices[vertexIndex + 0] + sourceVertices[vertexIndex + 2]) / 2;

                    // Need to translate all 4 vertices of each quad to aligned with middle of character / baseline.
                    // This is needed so the matrix TRS is applied at the origin for each character.
                    Vector3 offset = charMidBasline;
                    Vector3[] destinationVertices = textInfo.meshInfo[materialIndex].vertices;
                    destinationVertices[vertexIndex + 0] = sourceVertices[vertexIndex + 0] - offset;
                    destinationVertices[vertexIndex + 1] = sourceVertices[vertexIndex + 1] - offset;
                    destinationVertices[vertexIndex + 2] = sourceVertices[vertexIndex + 2] - offset;
                    destinationVertices[vertexIndex + 3] = sourceVertices[vertexIndex + 3] - offset;

                    // Apply SingSong 
                    matrix = Matrix4x4.TRS(Vector3.one, Quaternion.Euler(0, 0, newAngle), Vector3.one);
                    destinationVertices[vertexIndex + 0] = matrix.MultiplyPoint3x4(destinationVertices[vertexIndex + 0]);
                    destinationVertices[vertexIndex + 1] = matrix.MultiplyPoint3x4(destinationVertices[vertexIndex + 1]);
                    destinationVertices[vertexIndex + 2] = matrix.MultiplyPoint3x4(destinationVertices[vertexIndex + 2]);
                    destinationVertices[vertexIndex + 3] = matrix.MultiplyPoint3x4(destinationVertices[vertexIndex + 3]);

                    // Translate all 4 vertices of each quad back to their original positions
                    destinationVertices[vertexIndex + 0] += offset;
                    destinationVertices[vertexIndex + 1] += offset;
                    destinationVertices[vertexIndex + 2] += offset;
                    destinationVertices[vertexIndex + 3] += offset;

                    // Push changes into meshes
                    textInfo.meshInfo[materialIndex].mesh.vertices = textInfo.meshInfo[materialIndex].vertices;
                    animHandle.textScript.UpdateGeometry(textInfo.meshInfo[materialIndex].mesh, materialIndex);
                }
                yield return new WaitForSeconds(waitTime);
            }
        }
    }

    /// <summary>
    /// Handle for a running animation.
    /// </summary>
    public class AnimationHandle
    {
        public TextMeshProUGUI textScript;
        public TMP_MeshInfo[] cachedMeshInfo;
        public float clockIncrement;
        public int clockPrecisionDecimals;
        public UnityAction shutdownAnimation { private get; set; }

        public AnimationHandle(TextMeshProUGUI textScript, float clockIncrement, int clockPrecisionDecimals)
        {
            this.textScript = textScript;
            this.clockIncrement = clockIncrement;
            this.clockPrecisionDecimals = clockPrecisionDecimals;
            this.cachedMeshInfo = textScript.textInfo.CopyMeshInfoVertexData();
            TMPro_EventManager.TEXT_CHANGED_EVENT.Add(OnTextChange);
        }

        public void TearDown()
        {
            TMPro_EventManager.TEXT_CHANGED_EVENT.Remove(OnTextChange);
            shutdownAnimation();
        }

        public void UpdateCachedMesh()
        {
            cachedMeshInfo = textScript.textInfo.CopyMeshInfoVertexData();
        }

        private void OnTextChange(UnityEngine.Object obj)
        {
            if (textScript == obj)
            {
                UpdateCachedMesh();
            }
        }
    }

    /// <summary>
    /// Base class for animation configuration.
    /// </summary>
    public abstract class AnimationConfig
    {
        public AnimationHandle animationHandle;
        public int startIndex;
        public int endIndex;
        public float speedMultiplier;
        public TextMeshProUGUI textScript;
        public TMP_MeshInfo[] cachedMeshInfo;
        public bool[] characterIsVisible;
        public float clockIncrement;
        public int clockPrecisionDecimals;
    }

    /// <summary>
    /// Information for the jitter animation.
    /// </summary>
    public class JitterAnimationConfig : AnimationConfig
    {
        public float angleMultiplier;
        public float curveScale;
    }

    /// <summary>
    /// Information for the SingSong animation.
    /// </summary>
    public class SingSongAnimationConfig : AnimationConfig
    {
        public float angle;
        public float angleIncrement;
    }
Definitely not super pretty, but it got the job done. The problem is I can't use this code when maxCharactersVisible is used. In order to use the typewriter and text animation together, the typewriter effect has to be done by setting individual character alpha.


Dialogue System is a lot of code so my question is, does this feature already exist and/or is it something that's planned at any point? Just trying to figure out how much custom code I've got to write.
User avatar
Tony Li
Posts: 22055
Joined: Thu Jul 18, 2013 1:27 pm

Re: Typewriter with Jitter effect

Post by Tony Li »

Hi,

You can replace (or subclass) TextMeshProTypewriterEffect with your own code. Or you can get Febucci Creations' Text Animator, which can do typed jitter text with TextMesh Pro and has Dialogue System integration.
VoodooDetective
Posts: 222
Joined: Wed Jan 22, 2020 10:48 pm

Re: Typewriter with Jitter effect

Post by VoodooDetective »

Oh that's perfect! Thank you!
User avatar
Tony Li
Posts: 22055
Joined: Thu Jul 18, 2013 1:27 pm

Re: Typewriter with Jitter effect

Post by Tony Li »

Glad to help! The Text Animator integration just came out last week, so your timing is perfect.
Post Reply