Search Unity

  1. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice
  2. Unity is excited to announce that we will be collaborating with TheXPlace for a summer game jam from June 13 - June 19. Learn more.
    Dismiss Notice

Resolved Input Bleeding Issue with Spacebar in Dialogue System

Discussion in 'Scripting' started by ArcadiaGame, May 2, 2024.

  1. ArcadiaGame

    ArcadiaGame

    Joined:
    Nov 11, 2022
    Posts:
    16
    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:

    Code (CSharp):
    1.  
    2. // Choice selection
    3.  void Update()
    4.   {
    5.     if (BackgroundChoices.activeSelf) //
    6.     {
    7.         if (Input.GetKeyDown(KeyCode.UpArrow))
    8.         {
    9.             selectedIndex = Mathf.Clamp(selectedIndex - 1, 0, choiceButtons.Count - 1);
    10.             AudioManager.Instance.PlaySoundUi("Click (3)");
    11.         }
    12.         else if (Input.GetKeyDown(KeyCode.DownArrow))
    13.         {
    14.             selectedIndex = Mathf.Clamp(selectedIndex + 1, 0, choiceButtons.Count - 1);
    15.             AudioManager.Instance.PlaySoundUi("Click (3)");
    16.         }
    17.         else if (Input.GetKeyDown(KeyCode.Space))
    18.         {
    19.    
    20.           DialogueManager.instance.SelectChoice(DialogueManager.instance.GetCurrentDialogue().choices[selectedIndex]);
    21.             AudioManager.Instance.PlaySoundUi("ui_menu_button_click_24");
    22.         }
    23.  
    24.         HighlightChoice();
    25.     }
    26.   }
    27.  
    28.  
    29. // Coroutine display text
    30.  
    31. private IEnumerator TypeText(string line)
    32. {
    33.  
    34.     foreach (char letter in line.ToCharArray())
    35.     {
    36.         if (Input.GetKeyDown(KeyCode.Space)
    37.         {
    38.             // Display the full line immediately
    39.             Lines.text = line;
    40.             yield break;
    41.         }
    42.         // Typewriter effect logic here
    43.     }
    44. }
    45.  

    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!
     
  2. flashframe

    flashframe

    Joined:
    Feb 10, 2015
    Posts:
    806
    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. ArcadiaGame

    ArcadiaGame

    Joined:
    Nov 11, 2022
    Posts:
    16
    Indeed, that was the right solution!

    I hadn't thought of that... Thanks again for this very important reminder :)

    Code (CSharp):
    1. private IEnumerator TypeText(string line)
    2.   {
    3.     yield return null; // Wait a frame
    4.  
    5.     AudioManager.Instance.PlaySoundCoroutineText("Click2");
    6.     Lines.text = "";
    7.     StringBuilder sb = new StringBuilder();
    8.  
    9.  
    10.     foreach (char letter in line.ToCharArray())
    11.     {
    12.       sb.Append(letter);
    13.       Lines.text = sb.ToString();
    14.  
    15.       if (Input.GetKeyDown(KeyCode.Space))
    16.       {
    17.  
    18.         Lines.text = line;
    19.         AudioManager.Instance.StopSoundCoroutineText();
    20.         fleche.SetActive(true);
    21.         yield break;
    22.       }
    23.  
    24.       float delay = textSpeed;
    25.       if (letter == '.'){delay = textSpeed * 3;}
    26.       else if (letter == ' '){delay = textSpeed * 1.5f;}
    27.  
    28.       yield return new WaitForSeconds(delay);
    29.     }
    30.  
    31.     AudioManager.Instance.StopSoundCoroutineText();
    32.   }

    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
     
    Last edited: May 2, 2024
    flashframe likes this.
  4. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    39,002
    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)

    https://docs.unity3d.com/Manual/ExecutionOrder.html
     
    ArcadiaGame likes this.
  5. ArcadiaGame

    ArcadiaGame

    Joined:
    Nov 11, 2022
    Posts:
    16
    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 ;)
     
    Last edited: May 2, 2024
  6. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    39,002
    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.
     

    Attached Files:

    ArcadiaGame likes this.
  7. flashframe

    flashframe

    Joined:
    Feb 10, 2015
    Posts:
    806
    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.
     
    ArcadiaGame likes this.
  8. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    8,148
    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.
     
    ArcadiaGame likes this.
  9. ArcadiaGame

    ArcadiaGame

    Joined:
    Nov 11, 2022
    Posts:
    16
    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?

    Code (CSharp):
    1. public class InputManager : MonoBehaviour {
    2.  
    3.     public static InputManager Instance { get; private set; }
    4.     public HashSet<KeyCode> keysPressedThisFrame = new HashSet<KeyCode>();
    5.     public HashSet<KeyCode> keysHeld = new HashSet<KeyCode>();
    6.  
    7.     void Awake()
    8.     {
    9.         if (Instance == null)
    10.         {
    11.             Instance = this;
    12.             DontDestroyOnLoad(gameObject);
    13.         }
    14.  
    15.         else
    16.         {
    17.             Destroy(gameObject);
    18.         }
    19.     }
    20.  
    21.     void Update()
    22.     {
    23.         keysPressedThisFrame.Clear();
    24.  
    25.         foreach (KeyCode key in System.Enum.GetValues(typeof(KeyCode)))
    26.         {
    27.             if (Input.GetKeyDown(key))
    28.             {
    29.                 keysPressedThisFrame.Add(key);
    30.                 keysHeld.Add(key);
    31.             }
    32.  
    33.             else if (Input.GetKeyUp(key))
    34.             {
    35.                 keysHeld.Remove(key);
    36.             }
    37.         }
    38.     }
    39.  
    40.     public bool GetKeyOnce(KeyCode key)
    41.     {
    42.         return keysPressedThisFrame.Contains(key);
    43.     }
    44.  
    45.     public bool GetKeyHeld(KeyCode key)
    46.     {
    47.         return keysHeld.Contains(key);
    48.     }
    49. }
    And my modified TypeTex Coroutine :

    Code (CSharp):
    1.  
    2.  
    3. public IEnumerator DisplayLine(string line, string sound)
    4.   {
    5.     if (!string.IsNullOrEmpty(sound))
    6.     {
    7.       AudioManager.Instance.PlaySoundUi(sound);
    8.     }
    9.  
    10.     if (currentLineIndex < DialogueManager.instance.GetCurrentDialogue().dialogueLines.Count)
    11.     {
    12.  
    13.         if (typeTextCoroutine != null)
    14.         {
    15.           StopCoroutine(typeTextCoroutine);
    16.           typeTextCoroutine = null;
    17.         }
    18.  
    19.         typeTextCoroutine = StartCoroutine(TypeText(line));
    20.  
    21.         yield return typeTextCoroutine;
    22.  
    23.         fleche.SetActive(true);
    24.         currentLineIndex++;
    25.         Debug.Log("Index de ligne: " + currentLineIndex);
    26.     }
    27.   }
    28.  
    29. private IEnumerator TypeText(string line)
    30.   {
    31.     yield return null; // Wait For a frame
    32.  
    33.     AudioManager.Instance.PlaySoundCoroutineText("Click2");
    34.     Lines.text = "";
    35.     StringBuilder sb = new StringBuilder();
    36.  
    37.  
    38.     foreach (char letter in line.ToCharArray())
    39.     {
    40.       sb.Append(letter);
    41.       Lines.text = sb.ToString();
    42.  
    43.       if(InputManager.Instance.GetKeyOnce(KeyCode.Space))
    44.       {
    45.         Lines.text = line;
    46.         AudioManager.Instance.StopSoundCoroutineText();
    47.         fleche.SetActive(true);
    48.         yield break;
    49.       }
    50.  
    51.       float delay = textSpeed;
    52.       if (letter == '.'){delay = textSpeed * 3;}
    53.       else if (letter == ' '){delay = textSpeed * 1.5f;}
    54.  
    55.       yield return new WaitForSeconds(delay);
    56.     }
    57.  
    58.     AudioManager.Instance.StopSoundCoroutineText();
    59.  
    60.   }
    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 :

    Code (CSharp):
    1. public IEnumerator DisplayLine(string line, string sound)
    2.   {
    3.     PlayDialogueSound(sound);
    4.  
    5.     yield return null; // Wait For a frame
    6.     dialogueText.text = "";
    7.     StringBuilder sb = new StringBuilder();
    8.  
    9.     AudioManager.Instance.PlaySoundCoroutineText("Click2");
    10.  
    11.     foreach (char letter in line.ToCharArray())
    12.     {
    13.         sb.Append(letter);
    14.         dialogueText.text = sb.ToString();
    15.  
    16.         if(InputManager.Instance.GetKeyOnce(KeyCode.Space))
    17.         {
    18.           dialogueText.text = line;
    19.           StopDialogueSound();
    20.           fleche.SetActive(true);
    21.           break;
    22.         }
    23.  
    24.       float delay = GetDelayForCharacter(letter);
    25.       yield return new WaitForSeconds(delay);
    26.     }
    27.  
    28.     StopDialogueSound();
    29.     fleche.SetActive(true);
    30.     currentLineIndex++;
    31.     Debug.Log("Index de ligne: " + currentLineIndex);
    32.   }
     
    Last edited: May 3, 2024
  10. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    39,002
    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):

    https://gist.github.com/kurtdekker/775bb97614047072f7004d6fb9ccce30

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

    https://gist.github.com/kurtdekker/2f07be6f6a844cf82110fc42a774a625

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