Typewriter, jitter and wave effects at once?

Hello,

For our game, we want to allow dialogue to have various text effects at different points. For example, if a character is scared some of their dialogue may jitter, or if they’re taunting the text might wave. To accomplish this, I wrote a custom parser that would take formatted text and get letter indices from it.

For example:
If I type the text <shake>These words shake.</shake> These words don't do anything. <wave>And these words wave.</wave>, the script would recognize the starting and ending indices of each group of tags. (In this case, there would be a “ShakeText” object starting at 0 and ending around 18 or so.)

Next, I want to push this information to the TextMesh Pro object itself so the relevant vertices can be modified. For the sake of simplicity here’s only the part of the coroutine that runs for the shaking letters (worked out using the TextMesh Pro example code):

    public IEnumerator ShakeText(List<int> shakeIndices, List<int> waveIndices) {
        textMesh.ForceMeshUpdate();

        TMP_TextInfo textInfo = textMesh.textInfo;
        Vector3[][] copyOfVertices = new Vector3[0][];
        hasTextChanged = true;

        TMP_MeshInfo[] cachedMeshInfo = textInfo.CopyMeshInfoVertexData();

        Vector3[] cachedVertexInfo = cachedMeshInfo[textMesh.textInfo.characterInfo[0].materialReferenceIndex].vertices;

        float g = 0f; // used for the sinewave
        while (true) {
            foreach (int index in shakeIndices) {

                if (hasTextChanged)
                {
                    if (copyOfVertices.Length < textInfo.meshInfo.Length)
                        copyOfVertices = new Vector3[textInfo.meshInfo.Length][];
                       

                    for (int i = 0; i < textInfo.meshInfo.Length; i++)
                    {
                        int length = textInfo.meshInfo[i].vertices.Length;
                        copyOfVertices[i] = new Vector3[length];
                    }

                    hasTextChanged = false;
                }



                if (!textMesh.textInfo.characterInfo[index].isVisible) {
                    continue; // dont need to shake it if its not visible!
                }
                Debug.Log("shake: " + textMesh.textInfo.characterInfo[index].character.ToString());
                float modX = Random.Range(-shakeAmount,shakeAmount);
                float modY = Random.Range(-shakeAmount, shakeAmount);

                Vector3 modifier = new Vector3(modX, modY, 0f);
                int materialIndex = textMesh.textInfo.characterInfo[index].materialReferenceIndex;
                Vector3[] sourceVertices = cachedMeshInfo[materialIndex].vertices;
                int vertexIndex = textInfo.characterInfo[index].vertexIndex;

                for (int i = 0; i < textMesh.textInfo.meshInfo.Length; i++) {
                    copyOfVertices[materialIndex] = textMesh.textInfo.meshInfo[i].mesh.vertices;
                }

                copyOfVertices[materialIndex][vertexIndex + 0] = sourceVertices[vertexIndex + 0] + modifier;
                copyOfVertices[materialIndex][vertexIndex + 1] = sourceVertices[vertexIndex + 1] + modifier;
                copyOfVertices[materialIndex][vertexIndex + 2] = sourceVertices[vertexIndex + 2] + modifier;
                copyOfVertices[materialIndex][vertexIndex + 3] = sourceVertices[vertexIndex + 3] + modifier;
               
                
                Debug.Log(textMesh.textInfo.meshInfo[materialIndex].mesh.vertices[0]);
                Debug.Log(textMesh.textInfo.meshInfo[materialIndex].mesh.vertices[1]);
                Debug.Log(textMesh.textInfo.meshInfo[materialIndex].mesh.vertices[2]);
                Debug.Log(textMesh.textInfo.meshInfo[materialIndex].mesh.vertices[3]);
                Debug.Log("-------");

                for (int i = 0; i < textMesh.textInfo.meshInfo.Length; i++) {
                    textMesh.textInfo.meshInfo[i].mesh.vertices = copyOfVertices[i];
                    textMesh.UpdateGeometry(textInfo.meshInfo[i].mesh, i);
                }
            }
            yield return new WaitForSeconds(0.1f);

        }
    }

By itself, this code works okay - there is a notable frame rate drop, but the text animation itself looks good. I have run into a bigger issue, however, when attempting to do the “typewriter” effect at the same time as this. The shake animation will not run fully until the “typing” is complete.

The typing effect itself I accomplish by incrementing the textMesh.maxVisibleCharacters property by one every few fractions of a second, via a function in my script DialogueParse.cs (same one that transforms the vertices) that is called from OverworldDialogueEngine.cs (one that regulates dialogue flow).

Does anyone know why this is happening, and how I can fix it? (Additionally, if there is a more performant way of accomplishing this it would be very helpful to hear.)

In case the information I provided above wasn’t enough, here’s the full code of both scripts that play a part in the dialogue process:

DialogueParse.cs - used for parsing raw dialogue text and transforming the TextMesh Pro object’s vertices: https://drive.google.com/open?id=1dQjH-va8Hked9c_x3vuNckyMvVhGtijL

OverworldDialogueEngine.cs - used for regulating the flow of dialogue, calling for the “typewriter” effect: https://drive.google.com/open?id=1EX1IKSfif63V-tk8zKqTHwf3AEd3F0vh

Please let me know if there’s any more information you need. I’m happy to supply it!

Thanks,
Malcolm

1 Like

Hi there! I’m working on a very similar project, and came across your question hoping to find a solution. No such luck, so I continued to think about it myself, and the solution I came up with is to animate the characters appearing by setting their alpha value instead of using maxVisibleCharacters.

I based my code off of the VertexColorCycler example. So far, at least in my project, this hasn’t seemed to hurt performance much, if at all. Best of luck to you!

1 Like

Any luck?

Not on my end, unfortunately. :frowning:

Update: Just tried your code, it makes my game lag a lot with a shakeAmount of 5. I’ll see what I can do.

Update2: Okay, commented out the Debug.log that was in the code, so now it’s no longer lagging. My bad!

ADD this line at the beginning of the for loop:
if (index >= textMesh.maxVisibleCharacters) continue;

And REMOVE the:
if (!textMesh.textInfo.characterInfo[index].isVisible) { continue; }

The fix sometimes works, sometimes doesn’t. Not sure why…

Edit: now the fix stopped working. What a strange bug. I understand your code but don’t know what’s wrong. Maybe incrementing maxVisibleCharacters resets the character position?
Edit2: As I suspected. If you increase the wait time for the yield return WaitForSeconds in the for loop that increments maxVisibleCharacters, then it is fixed. The reason why is because incrementing maxVisibleCharacters does really seem to reset the position of the characters, and because it does it at a high frequency, then it overwrites your shake offset before rendering the frame. See TMP and vertex changes

Thus, a hacky fix is to “yield return new WaitForEndOfFrame();” after yielding WaitForSeconds in the for loop where you increment the maxVisibleChars. It shakes a bit less until the characters are all visible. Incrementing maxVisibleCharacters re-renders.

To fix properly, you need to implement your own maxVisibleChars. Maybe by moving the characters offscreen or something and then putting it where it belongs. If you do, would be nice if you could share with us here.

Another way, is in the same loop as you are incrementing maxVisChars, get the vertices before incrementing, and then revert them back after incrementing.

the parser code is not the parser code lol. Can you share it :smile:

Any luck?

One of the programmers on our team actually was able to make a system that does this… unfortunately I’m not really sure how it works exactly, haha. I’ll ask him if he’s cool with me posting it online for public use.

EDIT: He said he’s cool with it! Here’s the link to the repo: https://github.com/Meorge/TextMeshProAnimator
There’s an HTML file in the About folder that explains how to use it, but unfortunately I don’t have time at the moment to write up/rewrite the documentation for Markdown. Hope it’s helpful - and let me know if you have any questions!

2 Likes

Hi Meorge, this is really comprehensive stuff! Unfortunately, as it doesn’t support ‘<’ or ‘>’ you end up losing the awesome Rich Text tags that make Textmesh Pro so brilliant. Is there something I’m missing here, or does the TextMeshProAnimator have a way of utilising all the Rich Text tags?

We’ve also spun up our own library that does this. Figured I’d share it, in case you want to adapt anything for use. Feel free to use it if you want, but I’m not trying to plug it. Just trying to provide some more solutions. GitHub - redbluegames/unity-text-typer: Text typing effect for Unity uGUI Text components

2 Likes

I just pushed an update to it that should add support for rich text color and size tags. If there are other tags you want to add support for you can add them in pretty easily (currently line 172).

@edwardrowe

Thank you so much for sharing your work! That curve editor functionality is truly awesome!

@Meorge

Thanks to you for adding additional support for rich text tags! After trialling this out, although it is receiving the rich text, it is suffering from applying the animation correctly across the entire word.

For example:

What’s going on <color=red>HERE then!?

The shake animation is being applied on the final E in HERE and carrying on to the ‘then’ characters too.

If I switch the position of the tags as follows:

What’s going on <color=red>HERE then!?

Now the shake is being applied from the beginning of HERE and encapsulating all of ‘then’ also.


Thanks again to you both, you’ve been a great help!

Ary

1 Like

The examples you posted have incorrectly nested tags; I have a feeling that’s what’s causing the issues.
Try this:
What's going on <color=red><b><shake>HERE</shake></b></color> then!?

Giving this a go, it makes the final E of ‘here’ shake and the ‘the’ of then, which are outside of all the rich text.

Apologies for the delay in getting back to you mate.

Ary

Hello Meorge, is your script still supposed to work ? I tried to use it but everytime it makes my TMP text invisible.

If I change the geometry of the TMP window on runtime the text appear briefly but disapear when the window size is fixed.

Maybe I use the script in wrong way but I simply followed the readme document.

I’m using Unity 2019.1.4f1 and TMP 2.0.1.

EDIT : I managed to make it visible. I just had to uncomment the TMProGUI.ForceMeshUpdate() on line 661. But the shaking (or other effects) is still not working. It could be really usefull as I need to apply effect on single words.

EDIT2 : Finaly the text is shaking (I forget to increase the “Chars Visible” value and the line 661 need to stay in comment) but I have the same problem has Ary. The closing tag doesn’t work properly. The shaking is applied to 5 more characters (same number of character in the word “shake” ?) And the intensity doesn’t work with decimal value.

Hi all,

It’s strange to hear that it’s not working for you - last I was using it it seemed to be working perfectly for me. I haven’t been working on the project using that tool for a while, but hopefully I’ll get a chance to go back to it and look into these issues soon.

@Meorge
Hi. It’s working fine for me.
But i do have a problem when using the charsVisible = i, for a typewriter effect.
It seems when there’s a ContentSizeFitter, changing TMP text (or the color), will mark it to be rebuilt at the end of frame.
So, when i change text, then start the Typewriter coroutine, it’ll display the whole text for 1 frame (due to the rebuild), THEN the typewriter starts from 0 correctly in the next frame.

Even in this order:

  1. Change text
  2. charsVisible = 0
  3. Manual TMPAnimator.Update(), since it contains all the vertex modifying, set Vector3.zero for any char > charsVisible
  4. Start typewriter (incrementing charsVisible per frame)

The full text flicker for 1 frame still happens.
Even when changing TMPAnimator.Update to LateUpdate, it still happens (shouldn’t matter since #3 already manually calls it)

@Stephan_B
I wonder if there’s a way to manually rebuild after a text change.
Something like:

  1. Change text
  2. tmp.ManualRebuild() → i assume it also set isDirty = false; so it doesn’t get called again
  3. I can do the vertex modifying here in the same frame
    Or something similar already exists?

Cheers

1 Like

@Crouching-Tuna
I know this is an old post, but what I do is cover the text so it doesn’t show the flicker and when it starts to appear I disable the cover and no one notices, right now I’m just testing the code , just seem to have problems mixing tags

Did you ever figure this out, @Crouching-Tuna ?