Input Bleeding Issue with Spacebar in Dialogue System

Hello everyone,

I’m experiencing an issue with input bleeding in my dialogue system where pressing the spacebar to select a dialogue choice inadvertently triggers the immediate display of the next dialogue, skipping the typewriter effect. This seems to happen because the spacebar input from the choice selection is carried over into the next dialogue.

Here’s a brief overview of how my dialogue system works:

  • The system uses coroutines to display dialogue text character by character.
  • Users can press the spacebar to immediately finish displaying the current line.
  • If the dialogue line has choices and the user selects one (using the spacebar), it triggers a new dialogue which should also start with the typewriter effect.

However, the spacebar press for selecting the choice seems to “bleed” into triggering the immediate completion of the first line of the subsequent dialogue. I’ve tried to isolate input handling between different parts of the dialogue system, but the issue persists.

Here’s a snippet of how I handle the typewriter effect and the choice selection:

// Choice selection
 void Update()
  {
    if (BackgroundChoices.activeSelf) //
    {
        if (Input.GetKeyDown(KeyCode.UpArrow))
        {
            selectedIndex = Mathf.Clamp(selectedIndex - 1, 0, choiceButtons.Count - 1);
            AudioManager.Instance.PlaySoundUi("Click (3)");
        }
        else if (Input.GetKeyDown(KeyCode.DownArrow))
        {
            selectedIndex = Mathf.Clamp(selectedIndex + 1, 0, choiceButtons.Count - 1);
            AudioManager.Instance.PlaySoundUi("Click (3)");
        }
        else if (Input.GetKeyDown(KeyCode.Space))
        {
   
          DialogueManager.instance.SelectChoice(DialogueManager.instance.GetCurrentDialogue().choices[selectedIndex]);
            AudioManager.Instance.PlaySoundUi("ui_menu_button_click_24");
        }

        HighlightChoice();
    }
  }


// Coroutine display text

private IEnumerator TypeText(string line)
{

    foreach (char letter in line.ToCharArray())
    {
        if (Input.GetKeyDown(KeyCode.Space)
        {
            // Display the full line immediately
            Lines.text = line;
            yield break;
        }
        // Typewriter effect logic here
    }
}

I suspect the issue might be related to how Unity handles input states across frames or perhaps something specific in my coroutine management. If anyone has encountered a similar issue or has suggestions on how to properly isolate inputs in such a scenario, your advice would be greatly appreciated!

Thank you in advance for your help!

Doesn’t sound like an input problem, more likely that your coroutine runs on the same frame as the choice selection.

GetKeyDown will be true for the entire frame. Wait a frame before checking if it’s been pressed again.

3 Likes

Indeed, that was the right solution!

I hadn’t thought of that… Thanks again for this very important reminder :slight_smile:

private IEnumerator TypeText(string line)
  {
    yield return null; // Wait a frame

    AudioManager.Instance.PlaySoundCoroutineText("Click2");
    Lines.text = "";
    StringBuilder sb = new StringBuilder();


    foreach (char letter in line.ToCharArray())
    {
      sb.Append(letter);
      Lines.text = sb.ToString();

      if (Input.GetKeyDown(KeyCode.Space))
      {
 
        Lines.text = line;
        AudioManager.Instance.StopSoundCoroutineText();
        fleche.SetActive(true);
        yield break;
      }

      float delay = textSpeed;
      if (letter == '.'){delay = textSpeed * 3;}
      else if (letter == ' '){delay = textSpeed * 1.5f;}

      yield return new WaitForSeconds(delay);
    }

    AudioManager.Instance.StopSoundCoroutineText();
  }

I’d like to ask you another question, still related to this coroutine and dialogue System.

I tested my script in another Unity scene and the behavior of my script is not the same.

Indeed, when I press the Space key to display the entire dialogue line, I notice that this action is only taken into account about one time out of three. This is very strange because everything works very well in my other big scene.

This test scene is almost empty, so there is little chance that another script could be interfering with the detection of this Space input

1 Like

You’re using Input.GetKeyDown() outside of Update()

This MAY work but is never guaranteed. See docs.

Same goes for Input.GetMouseButtonDown() and of course the related Up() calls.

The only thing valid all the time is Input.GetKey()… from that Unity synthesizes the Down() and Up() data, but it is only going to be valid for the single frame that the button went up and down.

I highly recommend you untangle ALL the input from being scattered all over your dialog system before it gets any hairier. Instead, make an input API for yourself that gathers, queues and hands out all the input in an orderly fashion, ensuring that any given caller to get input will only get something once, and that even if another person calls the input the same frame, they won’t see the input as a duplicate.

Here is some timing diagram help: (note the section called INPUT EVENTS)

1 Like

Thank you for your answer! Yes, indeed, I had that in mind but I don’t think that’s the main issue.

I did a quick test with an InputManager script and it didn’t change anything. I have a 100% success rate with my coroutine and Input.GetKeyDown in my main scene. If I switch to other scene, the success rate drops to barely 30%. Both scenes use the same script to manage the coroutine.

I can’t understand this behavior. I’ve been researching for several days and haven’t found anything that would explain it. If the problem was largely due to input management, then I wouldn’t have a 100% success rate in my main scene. I would likely have many errors, just like in the other scene.

I can provide more details if someone would like to investigate this with me :wink:

Why not? Perhaps the script orders are different.

Feel free to investigate whatever you want, but I’m telling you that you are violating the written published documentation about where to use that function, AND that function appears to be malfunctioning. If you don’t want to fix it, well, at that stage I think it’s on you.

You’re welcome to my TypewriterWithHTMLStrings package, attached here.

8936637–1225557–TypewriterWithHTMLStrings.unitypackage (5.17 KB)

1 Like

A way to test it is to remove the input check from your coroutine and place it into the update loop.

Set a bool instead and listen for that to change in your coroutine.

1 Like

You don’t even need the coroutine. You’re just DIY-ing an Update loop. You could do the same check in Update have it be more stable.

1 Like

I agree with you on this point. Part of my method is far from being perfect and optimized.
Thank you for your answer!

Here is singleton “InputManager” script. What do you think of this first script?

public class InputManager : MonoBehaviour {

    public static InputManager Instance { get; private set; }
    public HashSet<KeyCode> keysPressedThisFrame = new HashSet<KeyCode>();
    public HashSet<KeyCode> keysHeld = new HashSet<KeyCode>();

    void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject);
        }

        else
        {
            Destroy(gameObject);
        }
    }

    void Update()
    {
        keysPressedThisFrame.Clear();

        foreach (KeyCode key in System.Enum.GetValues(typeof(KeyCode)))
        {
            if (Input.GetKeyDown(key))
            {
                keysPressedThisFrame.Add(key);
                keysHeld.Add(key);
            }
  
            else if (Input.GetKeyUp(key))
            {
                keysHeld.Remove(key);
            }
        }
    }

    public bool GetKeyOnce(KeyCode key)
    {
        return keysPressedThisFrame.Contains(key);
    }

    public bool GetKeyHeld(KeyCode key)
    {
        return keysHeld.Contains(key);
    }
}

And my modified TypeTex Coroutine :

public IEnumerator DisplayLine(string line, string sound)
  {
    if (!string.IsNullOrEmpty(sound))
    {
      AudioManager.Instance.PlaySoundUi(sound);
    }

    if (currentLineIndex < DialogueManager.instance.GetCurrentDialogue().dialogueLines.Count)
    {
  
        if (typeTextCoroutine != null)
        {
          StopCoroutine(typeTextCoroutine);
          typeTextCoroutine = null;
        }

        typeTextCoroutine = StartCoroutine(TypeText(line));

        yield return typeTextCoroutine;

        fleche.SetActive(true);
        currentLineIndex++;
        Debug.Log("Index de ligne: " + currentLineIndex);
    }
  }

private IEnumerator TypeText(string line)
  {
    yield return null; // Wait For a frame

    AudioManager.Instance.PlaySoundCoroutineText("Click2");
    Lines.text = "";
    StringBuilder sb = new StringBuilder();


    foreach (char letter in line.ToCharArray())
    {
      sb.Append(letter);
      Lines.text = sb.ToString();

      if(InputManager.Instance.GetKeyOnce(KeyCode.Space))
      {
        Lines.text = line;
        AudioManager.Instance.StopSoundCoroutineText();
        fleche.SetActive(true);
        yield break;
      }

      float delay = textSpeed;
      if (letter == '.'){delay = textSpeed * 3;}
      else if (letter == ' '){delay = textSpeed * 1.5f;}

      yield return new WaitForSeconds(delay);
    }

    AudioManager.Instance.StopSoundCoroutineText();

  }

Originally, my dialogue system was based on several nested coroutines. In my example, the text was displayed using two coroutines: DisplayLine() and TypeText().

To improve the clarity of the code, I decided to merge these two coroutines into one :

public IEnumerator DisplayLine(string line, string sound)
  {
    PlayDialogueSound(sound);

    yield return null; // Wait For a frame
    dialogueText.text = "";
    StringBuilder sb = new StringBuilder();

    AudioManager.Instance.PlaySoundCoroutineText("Click2");

    foreach (char letter in line.ToCharArray())
    {
        sb.Append(letter);
        dialogueText.text = sb.ToString();

        if(InputManager.Instance.GetKeyOnce(KeyCode.Space))
        {
          dialogueText.text = line;
          StopDialogueSound();
          fleche.SetActive(true);
          break;
        }

      float delay = GetDelayForCharacter(letter);
      yield return new WaitForSeconds(delay);
    }

    StopDialogueSound();
    fleche.SetActive(true);
    currentLineIndex++;
    Debug.Log("Index de ligne: " + currentLineIndex);
  }

Steps to success:

  • get it working
  • get it working well

Often after you do Step #1 you’ll find that Step #2 has already been satisfied.

If it works then I love it. If it doesn’t, then I suggest fixing it.

Either way, what does the computer think of it? That’s what matters.

I would never implement a singleton the way you do in InputManager.Awake() above.

This is the only singleton approach I will use in Unity3D:

Simple Unity3D Singleton (no predefined data):

Unity3D Singleton with a Prefab (or a ScriptableObject) used for predefined data:

These are pure-code solutions, DO NOT put anything into any scene, just access it via .Instance

1 Like