0
\$\begingroup\$

My problem:

I am using a coroutine to add a typewriter effect to my texts, but the delay (using yield return new WaitForSeconds();) isn't consistent on different framerates (Application.targetFrameRate).

This seems to be more visible with smaller delays, as one of my texts has a delay of just 0.0005 seconds and thats where I first noticed it.

I have tried to multiply the yield return new WaitForSeconds(); with Time.deltaTime but that didn't work.

\$\endgroup\$

3 Answers 3

4
\$\begingroup\$

The smallest delay that WaitForSeconds can give you is one frame. That's because the way it's implemented is it checks once each frame (after Update) whether enough time has elapsed. If so, it resumes the coroutine, and if not, it waits a whole frame before checking again.

So if you're rendering at say 60 fps, you can't get any delay shorter than 0.016667 seconds.

You can simulate more granular delays using a scheme like this:

IEnumerator TypeWriter(string text, float delay) {
    int charactersTyped = 0;
    float accumulatedTime = 0f;

    while (charactersTyped < text.Length) {
        // Wait till after Update calls for this frame are done.
        yield return null;
        // Add elapsed time to our accumulator.
        accumulatedTime += Time.deltaTime;
      
        // As long as enough time has elapsed to type another character... 
        while (accumulatedTime >= delay) {
            // type one more, and deduct the delay it came with.
            charactersTyped++;
            accumulatedTime -= delay;
        }

        // Once all the typing is done for this frame, update the display.
        display.text = text.Substring(0, Mathf.Min(text.Length, charactersTyped));
    }
}

This will type out multiple characters in a single frame as needed to "catch up" to the amount of time that's elapsed. So if your delay is 0.0005 (half a millisecond) and your framerate is 60 fps (16.667 ms per frame), then it will race ahead 9 characters in one frame, to keep pace with the number of characters that "should have been" typed by now, had the simulation been running continuously. Since the player only sees the rendered frames anyway, this does not make the typing more jerky than it would be otherwise - the scree is just not displaying enough frames in-between to show every character being typed individually.

By retaining the accumulatedTime between frames, you save any "loose change" milliseconds leftover from one frame into the next, which helps avoid temporal aliasing to get a smoother typewriting animation.

\$\endgroup\$
1
  • \$\begingroup\$ Yep, seems to work, I tested around with some debug logs, and tested it with 60, 120 and no targetFPS and it seems that the variations in the write times are negligible now and go under in the random fps fluctuations from test to test. Thank you very much! :D \$\endgroup\$
    – Felix193
    Jul 2, 2023 at 18:12
0
\$\begingroup\$

using System.Collections;
using TMPro;
using UnityEngine;

public class TypewriterEffect : MonoBehaviour
{
    public float typingSpeed = 0.05f; // Characters per second

    private TMP_Text textComponent;
    private string fullText;
    private bool isTyping = false;

    private void Awake()
    {
        textComponent = GetComponent<TMP_Text>();
        fullText = textComponent.text;
        textComponent.text = "";
    }

    public void StartTyping()
    {
        if (!isTyping)
        {
            StartCoroutine(TypeText());
        }
    }

    private IEnumerator TypeText()
    {
        isTyping = true;
        float startTime = Time.realtimeSinceStartup;
        float elapsedTime = 0f;
        int characterIndex = 0;

        while (characterIndex < fullText.Length)
        {
            elapsedTime = Time.realtimeSinceStartup - startTime;
            int visibleCharacters = Mathf.FloorToInt(elapsedTime / typingSpeed);

            if (visibleCharacters > characterIndex)
            {
                characterIndex = visibleCharacters;
                string currentText = fullText.Substring(0, characterIndex);
                textComponent.text = currentText;
            }

            yield return null;
        }

        isTyping = false;
    }
}

\$\endgroup\$
4
  • \$\begingroup\$ For this we can use Unity's Time.realtimeSinceStartup for time measurements. \$\endgroup\$ Jul 3, 2023 at 10:31
  • \$\begingroup\$ Note that subtracting two large floats will lead to catastrophic cancellation, so after the game's been running for several hours you'll find the precision of your timing logic degrades badly. Consider upgrading to doubles anytime you're working with an absolute timestamp, rather than an accumulated delta. \$\endgroup\$
    – DMGregory
    Jul 3, 2023 at 11:39
  • \$\begingroup\$ got it, i will think of a new way... i hope you find the solution to this problem🤔 \$\endgroup\$ Jul 3, 2023 at 13:42
  • \$\begingroup\$ I already posted a solution that was verified and accepted by the one who originally asked the question. \$\endgroup\$
    – DMGregory
    Jul 3, 2023 at 13:50
-2
\$\begingroup\$

using System.Collections;
using TMPro;
using UnityEngine;
public class TypewriterEffect : MonoBehaviour
{
    public float typingSpeed = 0.05f;
    private TMP_Text textComponent;
    private string fullText;
    private bool isTyping = false;
    private void Awake()
    {
        textComponent = GetComponent<TMP_Text>();
        fullText = textComponent.text;
        textComponent.text = "";
    }
    public void StartTyping()
    {
        if (!isTyping)
        {
            StartCoroutine(TypeText());
        }
    }
    private IEnumerator TypeText()
    {
        isTyping = true;
        float delay = typingSpeed;
        for (int i = 0; i <= fullText.Length; i++)
        {
            string currentText = fullText.Substring(0, i);
            textComponent.text = currentText;
            yield return new WaitForSeconds(delay);
        }
        isTyping = false;
    }
}
Bu şekilde, farklı kare hızlarında daha tutarlı bir daktilo yazma efekti elde etmelisiniz...

\$\endgroup\$
4
  • 1
    \$\begingroup\$ This code has the same problem described in the question (WaitForSeconds cannot give a delay shorter than one frame, and always rounds the delay to a whole number of frames). Answers here should be written in English, not Turkish. \$\endgroup\$
    – DMGregory
    Jul 2, 2023 at 19:07
  • \$\begingroup\$ It's usually better to not just dump some fixed code, but to also explain what the problem in the original code is and how your code fixes it. That makes it much easier for people to learn something from you. \$\endgroup\$
    – Philipp
    Jul 3, 2023 at 7:23
  • \$\begingroup\$ I just realized the problem...time in game is usually based on screen speed. if we base it on world time then it will work more stable 🤔 \$\endgroup\$ Jul 3, 2023 at 10:25
  • \$\begingroup\$ For this we can use Unity's Time.realtimeSinceStartup for time measurements. \$\endgroup\$ Jul 3, 2023 at 10:27

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .