- Home /
Terminal-like GUI, wait for input
Some time ago I asked a question about coroutines, while still not really understanding what they do. http://answers.unity3d.com/questions/1219701/coroutine-is-being-skipped.html#comment-1222535 (Link to topic)
I finally think I got it now.
So what I tried to do last time was
IEnumerator waitForInput()
{
while(!inputChanged)
{
yield return null;
}
inputChanged = false;
}
void mainMenu()
{
StartCoroutine(waitForInput());
if(input=="blabla")
{
dothis();
}
}
As dumb as I were, I couldn't understand that it wouldn't actually halt my mainMenu function.
So I got that now, and changed my mainMenu to an Ienumerator, and put my while loop inside the mainMenu too, so it looked like this:
void start()
{
StartCoroutine(mainMenu());
}
Ienumerator mainMenu()
{
while(!inputChanged)
{
yield return null;
}
inputChanged = false;
if(input=="blabla")
{
dothis();
}
}
That works perfectly! Althought this limits me, and also goes against one of the best coding practices. "Don't repeat yourself".
If I do it like this, I'll have to start a new coroutine for every program I make, and I'd have to repeat myself by checking for inputChanged, instead of having a single function that I could just call in all of my programs.
Let's say I want to create "programs" from my main menu. For example
IEnumerator calculator()
{
while(!inputChanged)
{
yield return null;
}
inputChanged = false;
if(input == "blabla")
{
}
}
Then in my mainMenu I'll do if input = blabla, StartCoroutine(calculator());
Wouldn't it be bad to create a coroutine inside a coroutine?
Also, I'd probably want an exit command in my calculator program, which would return to my mainMenu.
So I'd just start a new coroutine running mainMenu. (But what about the old mainMenu coroutine I had running? Am I not creating a duplicate coroutine when calling it on input == "exit"?)
Fx
>StartCoroutine(mainMenu());
>StartCoroutine(calculator());
>StartCoroutine(mainMenu());
If I keep doing stuff like this, wouldn't it create an enormous amount of coroutines?
What could I do to create a terminal-like system, without getting myself into a unending coroutine mess?
Answer by troien · Aug 15, 2016 at 11:24 AM
Well, If you would call StartCoroutine(mainMenu()); and inside that you would call StartCoroutine(calculator()); and inside that you would call StartCoroutine(mainMenu()); again, then you would indeed cause an infinite loop, as no coroutine would ever finish (sinse they wait for a new coroutine to finish before they finish themselves, and that new croutine does the exact same thing thus never finishing at all. Only as long a you would yield those coroutines aswell though, otherwise the original coroutine continues and doesn't wait for the new coroutine to finish and thus there is technically no infinite loop, just some odd design perhaps :p). This infinite loop is hard to detect though, as everything will probably run as expected up untill the point where something runs out of memory, which could take a long time before that happens :p
However, if you call yield return StartCoroutine(calculator()); inside your mainmenu function and only handle calculator thing inside calculator this would be totally fine as this is simply a nested coroutine. As at some point the calculator method finishes and the mainMenu coroutine will continue executing after the yied return statement. So as long as you don't create a infinite loop that would cause a StackOverFlowException in normal situations you are good to go...
So as to your part for not having to write the same code twice, you could still do this...
IEnumerator waitForInput()
{
while(!inputChanged)
{
yield return null;
}
inputChanged = false;
}
And then call that like this:
Ienumerator mainMenu()
{
yield return StartCoroutine(waitForInput());
if(input=="blabla")
{
dothis();
}
}
And then call that like you do now:
StartCoroutine(mainMenu());
Just some extra things to make it more clear:
You can yield StartCoroutine(waitForInput()); calling just yield waitForInput() does not work as you'd expect ;)
you could put a while(true) loop (or while(!cancel) or something like that) inside your mainMenu coroutine to make it execute forever as long as: your application is running, the MonoBehaviour that called StartCoroutine still exists and StopAllCoroutines isn't called. This will make it loop forever without freezing Unity. That way, once all nested coroutines you call inside your mainmenu method have been executed you would just start over again, accepting new input again. (As long as you yield something inside this infinite while loop, as otherwise Unity would obviously freeze on your infinite loop).
In this example, your calculator method could also call yield return StartCoroutine(waitForInput()); without problems and you could call yield return StartCoroutine(calculator()); from inside mainMenu just fine, as long as you then don't call yield return StarCoroutine(mainMenu()); from any of those methods aswell.
Thank you! This worked exactly as it should! Still a bit messy, as you would imagine, though.
How would you go about creating a menu like this? Is this way the way to go?
I feel like this method is a temporary fix for a ter$$anonymous$$al gui system, although I just can't seem to find any other workaround on how to do this...
Anyways, thanks a lot! I really appreciate it!
I think in Unity this might be the best way to go. But whether it actually is also depends on your use case probably. And how complicated you want to make it.
For 'returning to the menu' I would probably make the main$$anonymous$$enu() coroutine have a while(true) loop. And in order to return to the menu from any coroutine that gets called by main$$anonymous$$enu all you would have to do is call return from inside the nested coroutine, as that would finish that nested coroutine immediatly and continue in the main$$anonymous$$enu, which will loop and start over from the start. Only problem with that is that when main$$anonymous$$enu calls Calculator() and calculator() calls doSomething() and doSomething wants to go back to the menu, then you would have to return from both doSomething() and calculator() which might be annoying (But you probably have the same problem if you would simply call StartCoroutine(main$$anonymous$$enu()) from there as you would have 2 coroutines running synchronosly untill the calling one finishes, which if not dealth with correctly can cause strange bugs :p
Another thing you could look into is creating your own thread (this is the first tutorial I found, there are probably better ones :p). As threads can be more easily suspended while waiting for input I suppose (Thread.Sleep). Few downsides are that receiving keyboard input is handled on the unity thread and getting the input correctly in your other thread might be a bit difficult... They also are a bit of a more advanced topic, not really that difficult perhaps, but a lot harder to debug. Another note, you can't call (most/any) Unity related stuff from inside threads (You would have to make something that runs on the Unity thread that periodically checks for changes made by the thread, and when those changes happen, handle the Unity stuff). Another downside is that Exceptions that occur on other threads don't end up on the Console and you don't get notified at all. (Can be overcome by creating a try catch in your thread main method, where you call Debug.LogError in the catch). Another note is that you should never forget to stop a thread, as they can continue to execute outside of playmode :D Once you are able to handle that though this might be your preferred way to go as you can then for instance make a method that returns a string after the user entered a new line and halt your thread (ins$$anonymous$$d of Unity's main thread) untill that happens (The same way Console.ReadLine() works in console applications).