I’ve used coroutines for things like camera moves and alpha fades, and they always work as expected. But I’ve run into a case where I can’t get a coroutine to work.
I have a long calculation to perform (several seconds) and want to show an animated busy graphic. I show the graphic (set active to true), set a flag to tell the coroutine when to stop, call the coroutine, then call the calculations function. The coroutine updates the graphic after calling yield WaitForSeconds, and checks the flag in a loop that repeats until the flag is true.
function DoBusy () {
var count: int;
count = 3;
while (!filterDone) {
waitCursor.texture = cursors[count];
count -= 1;
if (count < 0)
count = 3;
yield WaitForSeconds (0.3);
}
waitMessage.active = false;
originalImageGUI.color.a = 1.0;
waitCursor.enabled = false;
}
The problem is, the graphic never shows up until the calculations are finished. It flashes briefly right before it’s hidden again. The loop that updates the graphic is only executed once, apparently just before it quits.
DoBusy(); // call coroutine
// do calculations...
filterDone = true;
This calls the coroutine and then immediately sets filterDone to true. So,
while (!filterDone) {
will only ever execute once.
The problem is, you can’t actually do that with a coroutine, unless your long calculation is itself a coroutine that yields every so often. But that’s pretty dodgy at best. That’s the sort of thing you want to use actual threads for, and coroutines aren’t threads. They don’t run while your long calculations are calculating; all scripting in Unity is normally done in one thread.
I think you misunderstood. The comment “// do calculations” is for brevity in this snippet. That’s where the code goes that does the number crunching. So the done flag is not set until that code is finished, which can take several seconds.
No, I understood that. It doesn’t matter if the number-crunching code takes a nanosecond or 100 years to finish. What happens with your code when you do “DoBusy()”, is that DoBusy runs, once, up to the “yield WaitForSeconds” statement. Then your “// do calculations” run. Then “filterDone” is set to true. Then your coroutine runs again the next frame (since .3 seconds probably has long since passed), and since filterDone is true, the loop never runs again. As I said, coroutines are not threads.
The only way to make coroutines behave “sort of” like threads is to make your number-crunching code into a coroutine that yields occasionally, in order to give other coroutines a chance to run, but that’s generally a poor way of handling things. If you need actual threads, which you would in this case, then you’d use actual threads, not coroutines.
That’s the part I didn’t understand. I thought when you did a yield WaitForSeconds, it would continue with other code, then come back and pick up after the yield after the time elapsed. I didn’t know I could use actual threads. I can’t find anything about threads in any of the Unity docs.
One other thing I still don’t understand is why my GUITexture (waitMessage in the sample code) never shows. It gets set to active before the yield, yet it only flashes briefly when the DoBusy function finishes. Could it just be a matter of the screen not updating while in the calculations loop? I’d be (relatively) happy if I could just show a busy message while the calculations code is running.
You are exactly right, the screen is not updating while doing the calculations.
As Eric said, coroutines are not threads. They are more like pausable functions - the yield statement does just what it sais, it yields control so other code can execute.
But when you yield, you yield atliest until the next frame. In essence, this is what unity does with your code:
update screen
execute DoBusy until yield statement
execute heavy calculation
update screen
execute DoBusy after yield statement
If you want to show a busy animation, you’ll need to put your heavy calculation in its own coroutine and stuff it full of yield’s.
Or simply just
function HeavyCalculation() {
ShowBusySign();
yield;
// Do heavy calculations
HideBusySign();
}
Yep Google is a great source - buit was hoping that perhaps there were some Unity specific examples/references (being at work I havent had a chance to go digging properly)
I just tried this, taken from some .Net sample code:
var job: System.Threading.ThreadStart;
var theThread: System.Threading.Thread;
job = new System.Threading.ThreadStart(DoBusy);
theThread = new System.Threading.Thread(job);
theThread.Start();
But it doesn’t work. The DoBusy function never gets called. Maybe I’m not translating the C# example correctly. I’ll second afalk’s request for some sample code.
Also, is there any way to force a screen update in Unity? Will yield do that? Like I said, I’d be satisfied with just showing a graphic and forget the animation.
That way you give the graphics a chance to show for one frame before the game is essentially paused while doing the intensive stuff.
As far as threads, the only thing Unity-specific about them is that Unity functions aren’t thread-safe and are guaranteed to crash sooner or later. So you can only use thread-safe .net functions. Here’s an example though (might want to increase the value of “loops” if your computer is a lot faster than mine):
import System.Threading;
var loops = 100000;
private var progress = 0.0;
function Start () {
var tex = new Texture2D(2, 2);
var guiTex = new GameObject("GUITex", GUITexture);
guiTex.guiTexture.texture = tex;
guiTex.guiTexture.color = Color.white;
guiTex.transform.position.y = .5;
guiTex.transform.localScale = Vector3(0.0, 0.1, 1.0);
Thread(DoStuff).Start();
while (progress < 1.0) {
guiTex.guiTexture.pixelInset.width = Screen.width*progress;
yield;
}
Destroy(guiTex);
Destroy(tex);
}
// NOTE: UNDER NO CIRCUMSTANCES SHOULD ANY UNITY-SPECIFIC FUNCTIONS BE CALLED IF THIS IS BEING USED AS A THREAD.
// THEY ARE NOT THREAD-SAFE AND WILL CRASH RANDOMLY. ONLY USE THREAD-SAFE .NET FUNCTIONS.
// DO NOT REMOVE THIS LABEL UNDER PENALTY OF LAW.
function DoStuff () {
var rand = new System.Random();
for (count = 1; count <= 1000; count++) {
for (i = 0; i < loops; i++) {
var foo = System.Math.Sqrt(rand.Next());
}
progress = count*.001;
}
}