- Home /
Custom Yield Instruction for FixedUpdate frames
Hi,
I'm implementing some unit tests for some time sensitive calculations.
I'm doing all in the Fixed Update. Sadly there is some "desync" when using WaitForSeconds(_numberOfFrames*Time.fixedDeltaTime)
, so I basically need to yield multiple WaitForFixedUpdate()
. Since there is a need to wait for multiple FixedUpdate frames, I was trying to write my CustomYieldInstruction
in order to avoid an ugly for
loop with the WaitForFixedUpdate()
inside it.
My CustomYieldInstruction
looks as follows:
public class WaitForCertainFixedUpdate : CustomYieldInstruction
{
private readonly float _endTime;
public WaitForCertainFixedUpdate(int frames)
{
Debug.Log("Start: " + Time.fixedTime);
_endTime = Time.fixedTime + Time.fixedDeltaTime * frames;
Debug.Log("End: " + _endTime);
}
public override bool keepWaiting
{
get { return !(Time.fixedTime >= _endTime); }
}
}
And everything here looks fine: _endTime is 0.02s later to when the constructor is called.
This is called in the following part of the Unit Test:
[UnityTest, Category("Units")]
public IEnumerator AvailableCapacity()
{
CommonInstall();
Assert.IsEqual(2, _purchasingUnit.AvailableCapacity);
Debug.Log("Assert1 [" + Time.fixedTime + "]");
yield return new WaitForCertainFixedUpdate(10);
Assert.IsEqual(1, _purchasingUnit.AvailableCapacity);
Debug.Log("Assert2 [" + Time.fixedTime + "]");
}
And here is where it gets strange. According to the documentation, the CustomYieldInstruction
property keepWaiting
is queried each frame after MonoBehaviour.Update
and before MonoBehaviour.LateUpdate
.
But it doesn't seem the case: logging the keepWaiting
property only shows one get access (omitted here in this example), right at the beginning, and then a second one only 0.32s later, as seen below in the output of the console.
Assert1 [0.36]
Start: 0.36
End: 0.56
Assert2 [0.68]
So, I wonder what exactly is happening here. Does anyone have a clue? Or any suggestion in order to WaitForFixedUpdate multiple times in a clean way?
Thank you for your time!
EDIT:
I've moved everything into the Update in order to avoid any other confusions. And the following method in my object is called every frame:
public virtual void Tick()
{
Debug.Log("try tick: " + Time.time);
if (NextUpdate.CompareTo(TimeController.CurrentTime) > 0) return;
Debug.Log("TICK" + i + " @ " + TimeController.CurrentTime.FormatSecsAndMili() + " [" + Time.time + "]");
i++;
RunCycle();
NextUpdate = NextUpdate.AddSeconds(ProcessingTime);
}
And the following is run in a unit test:
yield return new WaitForSeconds(_makeTime);
Debug.Log("Assert2 " + _timeController.CurrentTime.FormatSecsAndMili() + " [" + Time.time + "]");
yield return new WaitForSeconds(_makeTime);
Debug.Log("Assert3 " + _timeController.CurrentTime.FormatSecsAndMili() + " [" + Time.time + "]");
The following outputs:
try tick: 0.6825249
TICK2 @ 40s 337ms [0.6825249]
Assert2 40s 337ms [0.6825249]
try tick: 0.7003322
try tick: 0.7168999
...
try tick: 0.7832022
TICK3 @ 40s 437ms [0.7832022]
try tick: 0.8000034
...
try tick: 0.8850218
Assert3 40s 539ms [0.8850218]
As you can see, the first tick is in sync: TICK2 and Assert2 happen at the same time. The first yield return new WaitForSeconds(_makeTime);
matches the time the object takes to tick. However, from there on, it is completely out of sync...
Answer by Bunny83 · Nov 22, 2017 at 02:57 AM
Ok i have a feeling this will get long ^^
First of all lets address the actual question title. The short answer is: That's not possible.
The basics
First you have to understand that FixedUpdate is not some magic function that runs seperately from Update. FixedUpdate is handled inside the main loop just like Update. For reference have a look at the flowchart on this page. The Flowchart is a very rough simplification and not 100% correct because it's possible to have a "frame" without any FixedUpdate / Physics loop running but it should help to get a better overview.
The physics loop (which included the invokation of FixedUpdate) is simply a way to allow the fixedTime to "catch up" with the current game time. Unlike the normal game time the fixedTime increases in fix steps (fixedDeltaTime). So each frame when Unity handles the physics catch up it runs a while loop until fixedTime is greater or equal to the game time. Each iteration fixedTime will be increased by the fixedDeltaTime value. That means at a high visual framerate Unity might need to skip the whole physics loop since fixedTime is actually still ahead of the game time because the fixed time step is much larger than deltaTime. On the other hand at very low framerates deltaTime will be larger than our fixedDeltaTime so Unity need to do several physics iteration in one frame to catch up with the game time.
Next important thing is to understand what a coroutine is and how Unity manages them. Coroutines actually are not methods but statemachine objects. C# has the magic keyword "yield" which can turn a method into a "generator". So calling a coroutine (a method that contains a return yield
and returns IEnumerator) actually creates an instance of a compiler generated statemachine class which is returned by the method. You have to pass this object to StartCoroutine in order to actually run that statemachine. An IEnumerator is a very simple interface. It has a MoveNext method which will move the iterator / generator to the next value and it returns if there actually is another value. So when MoveNext returns false the iterator has finished. It also as a "Current" property which simply holds the current value of the iterator. Unity uses the yielded value to determine when to continue the next iteration.
So a started coroutine is simple stored in an internal list of the engine. If you yield a "WaitForFixedUpdate" instance in a coroutine, Unity will simply call MoveNext of your generator object from inside the physics loop. The exact point can be seen in the flowchart where it says "yield WaitForFixedUpdate".
CustomYieldInstruction
When Unity introduced the CustomYieldInstruction class they also added another nice feature which makes the use of CustomYieldInstructions even possible. In the past when you wanted to start another coroutine inside a coroutine and wait for that "nested" coroutine to finish we had to use yield return StartCoroutine(NestedCoroutine());
So we had to actually yield the Coroutine object that StartCoroutine returns. However now it's possible to just yield an IEnumerator object. When Unity's coroutine scheduler detects an IEnumerator as yield value it will automatically start a new nested coroutine. So now we can simply do
yield return NestedCoroutine();
The CustomYieldInstruction base class is actually a statemachine object like the one that is generated when using the yield keyword and the compiler magic. However if you look at the implementation you will notice that "Current" will always return "null". This is almost the same as creating a coroutine like this:
IEnumerator CustomYieldInstruction(System.Func<bool> aCondition)
{
while (aCondition())
yield return null;
}
The condition here is simply evaluated every frame. Again have a look at the flowchart i've linked above and seek the yield null
position to understand when this coroutine will actually check it's condition.
However even if you create a "helper coroutine" like this one:
IEnumerator WaitForFixedUpdate(int aCount)
{
for(int i = 0; i < aCount; i++)
yield return new WaitForFixedUpdate();
}
It most likely doesn't give you the effect you're after. Keep in mind that i said when you yield a custom yield instruction / nested coroutine you actually yield on a second coroutine. Again checking the flowchart you will find that yield StartCoroutine
is also only executed once every frame. So even when the nested coroutine will actually wait for the desired FixedUpdates, the coroutine that is waiting for the nested coroutine will not continue until the point where it says "yield StartCoroutine".
WaitForSeconds
You also seem to be confused how WaitForSeconds work. Again keep in mind Unity runs in frames (or main loop cycles in a single thread). One frame takes a certain amount of time to complete. The time the last frame took is measured and can be read through Time.deltaTime. "Time.time" represents the current ingame time. This time value is only updated between frames. The changed from frame to frame is actually the deltaTime value which can vary depending on how long the frame took.
Now if you yield a WaitForSeconds object, the coroutine is actually checked every frame at the point where is says "yield WaitForSeconds" in the flowchart if the desired wait time has expired. Of course the wait time you pass doesn't need to be the time it actually waits. Imagine you have a more or less constant framerate of 20 fps (just as example) and you want to wait for "0.3333" seconds. DeltaTime is 1/20 == "0.05". If the Time.time was "123.0" when you yield your WaitForSeconds in theory you want to resume your coroutine at the time "123.3333". Though Unity check the time only once per frame and the time increases in steps of 0.05 in our case.
Time | wait time passed | Has expired?
//------------------------------------------
123.0 | 0 | no
123.05 | 0.05 | no
123.1 | 0.1 | no
123.15 | 0.15 | no
123.20 | 0.20 | no
123.25 | 0.25 | no
123.30 | 0.30 | no
123.35 | 0.35 | yes
Note that we wanted to wait "0.3333" seconds but the time actually passed until the coroutine is resumed is 0.35 seconds so an error of "0.0167" seconds. Any kind of timespan you want to measure can has a max possible error of the current deltaTime value as this is the actual time interval we're going forward. You can't get anything to be executed in between. It's either done this frame or the next. There's nothing in between.
Conclusion
Time can be a confusing factor in a discrete time step simulation. Your question doesn't really explain for what purposes you actually need the time. That makes it difficult to give you suggestions what you "should do".
Hopefully this answer (if you managed to read it up to this point) gave you some guidance and clarification where the results you're observing actually come from.
First of all, thank you very much for the patiently-long explanation. All makes sense. $$anonymous$$y main confusion was with things related with the FixedUpdate and Physics loop. Indeed I thought that "FixedUpdate is some magic function that runs seperately from Update". I thought it would give me the discrete, fixed intervals that would be nice to have in this situation.
I understood now that I do not need such a precision. It does not matter if it is in frame X-1 or X+1, or 0.01s late or early.
$$anonymous$$y issue was mostly having a desync between real time and game time. If a product takes 0.2s to make, I really wanted the player to have 40 products after 200s. The problem was that I was testing it at a too small scale. I'd make a check every 0.2s, if the produced quantity was x+1. And here everything was falling apart. But I realized that what matters is the long term simulation.
I've moved everything into the Update loop. I've also made a few tests to make sure that in the long run, the amounts are correct: after yielding for 200s I'd check if the total output is 40. And in fact it works, no desync. I'll have to make another test for a really long-term (maybe 1h?) to see if there is any noticeable offset from the output and the time lapsed.
Thank you once again for your time!
The main Problem with WaitForSeconds is that as i said it will almost always "overshoots" the desired time. The solution is to keep the "overshoot" amount and give the next interval a "headstart". Actually that's what FixedUpdate does to ensure "50" calls per second.
For this purpose i made the simple CustomFixedUpdate helper class. It does the same as Unity's FixedUpdate and ensures a constant "call-rate" per second. Though you can pick any call-rate you want. $$anonymous$$eep in $$anonymous$$d it does not run in sync with the realtime or the game time. The first example actually implements a custom rate of 10 times per second. So after say 20 seconds the method would have been called 200 times (give or take one call)
Answer by elexis · Oct 02, 2021 at 10:44 PM
I ran into the same issue and discovered this ticket with a websearch. So in case it's relevant to anyone else:
I simply solved the issue of overshooting by not implementing a custom yield instruction and instead doing a yield return WaitForSecondsFixed(fixed_time)
:
public static IEnumerator WaitForSecondsFixed(float max_time)
{
float time_finished = Time.fixedTime + max_time;
while (Time.fixedTime < time_finished)
yield return new WaitForFixedUpdate();
}
So if I'm running my CI tests with 100x gamespeed and I just want to wait 0.1 'ingame' seconds, I won't overshoot by orders of magnitude more time.
Answer by abandon_games · Jan 24 at 12:38 PM
@Bunny83 thanks for details!
what are your thoughts on @elexis's helper? if my understanding is correct - if a coroutine is started in FixedUpdate
, then starts WaitForSecondsFixed
, you'd get your desired fixedTime
count without a margin of error