- Home /
Loop with WaitForSeconds in IEnumerator appears to be incorrect by 10-15%
So I've had a quick look around and I've seen some posts about not using loops in IEnumerators, however after reading the information available on the scripting reference there's nothing that I can see about them being particularly resource heavy or incorrect, so I figured I'd ask here.
This is the script I'm using for testing:
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
public class iEnumeratorTest : MonoBehaviour
{
public bool isRecording;
public float recordingTime;
public float timeTaken;
public float tickTime;
void Update()
{
if (isRecording)
{
recordingTime += Time.deltaTime;
}
else
{
if (recordingTime != 0f)
{
Debug.Log("That took: " + recordingTime);
recordingTime = 0f;
}
}
}
public void ButtonClick()
{
StartCoroutine(Tick());
}
IEnumerator Tick()
{
isRecording = true;
for (float f = timeTaken; f >= 0; f -= tickTime)
{
yield return new WaitForSeconds(tickTime);
}
isRecording = false;
}
}
Update() returns values roughly 10-15% higher than the time it should take to wait. For example, when tickTime was set to 0.1 and timeTaken set to 5, results returned varied between 5.15234 and 5.657412. The results have been similar when using both while and for loops and the actual time is closer to the desired time when increasing tickTime and decreasing timeTaken. This wouldn't be such a big deal however as the wait times are intended to increase dramatically towards the end 10-15% is a great deal of time to be wasted.
2 facts you should know.
1) Coroutines will halt a main thread
2) Frame times vary and it is impossible to get an exact wait time as a result :- Especially when your frames are lengthened by waiting on a coroutine.
Answer by Baste · Jun 29, 2015 at 04:25 PM
This is due to how Coroutines and WaitForSeconds works. A coroutine is run together with Update, until it's done. If a coroutine yields another IEnumerator, that one is called until it's done, before the main coroutine starts getting called.
WaitForSeconds is implemented somewhat like this:
IEnumerator WaitForSeconds(float duration) {
float startTime = Time.Time;
while(Time.time - startTime < duration)
yield return null;
}
This means that it will return on the first Update frame that more time than the duration has passed.
Since this is inevitably more than exactly the duration given as the argument, each WaitForSeconds will add a slight positive error. So, the lower your ticktime, the larger the error.
This isn't actually a big problem - if you want to wait 5 seconds, use WaitForSeconds(5). The effect won't kick in at exactly 5 seconds, but it will kick in at the first frame that's drawn after 5 seconds has passed, so it will look exactly the same as if you somehow magically managed to make the coroutine return at exact 5 seconds.
The only reason I'm using a tick system is so I can have a bar fill up gradually while reducing unnecessary resource usage in update, any idea if I can still do this without using IEnumerators or update?
Yeah, you set the bar's fill to the current fill percentage:
bar.fillAmount = (Time.time - startTime) / duration;
Where I assume fillAmount is between 0 and 1.
In general, if you have a UI bar (or element) that's reflecting some variable in a script, you shouldn't have the script both update the value, and keep the bar filled - that'll lead to your script getting huge, and possible introduce bugs where the two gets seperated.
Ins$$anonymous$$d, you should put a new script on the bar that looks at the value in the first script, and fills the bar according to that, blind to what the value actually is representing.
Filling the bar was never an issue, deciding when to do it was the issue since the previous solution was inaccurate. Having a separate script for essentially 2 lines of code and reading across scripts each frame is going to end up even more inefficient, guess the only other solution is on Update(). Thanks again for the help.
Answer by jenci1990 · Jun 29, 2015 at 04:40 PM
The 'Update' method not depend of time. It's called when the camera render was completed. Your coroutine wait fix seconds, so there are some difference. Try frame base coroutine:
IEnumerator Tick() {
isRecording = true;
float f = 0;
while (f <= timeTaken) {
yield return new WaitForEndOfFrame();
f += Time.deltaTime;
}
isRecording = false;
}
Sorry for my english!