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!
I hadn’t thought of that… Thanks again for this very important reminder
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
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)
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
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.
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);
}