Introduction
The game loop system of Unity contains different Update methods where we can create our own logic. C# provides three different kinds of loops: while, for (along with foreach) and do/while where we can create logic that repeats until something evaluates to true. Adding these loops inside one of the update methods, can have negative effects to our code. From making certain frames slower, to completely freeze Unity.
Although having these loops inside one of the update methods, can decrease the performance of our game, sometimes we cannot avoid it. There are different ways to use these loops, let’s see the most common ones starting with the one that we should never do, as it will freeze our editor and continuing with the other options we have depending on our use case.
In the following examples, I will use the while loop, but those examples apply to both for and do/while loops too.
The One That Should Never Be Done
Let’s suppose we want to move one of our game objects, by using the MovePosition method of Unity. We want to keep moving this object until it reaches a specific point, so we write a method that as long that object in not near the destination point, it keeps moving towards that point. In code we will have in our FixedUpdate
method something that looks like this:
private void FixedUpdate()
{
if(_isMoving)
MoveToPositionWithWhile(_randomPoint);
}
If the MoveToPositionWithWhile
method looks like the following, then our Unity Editor will freeze:
private void MoveToPositionWithWhile(Vector2 position)
{
// Won't work fixed update stops running until while loop is finished =>
// Prevents physics simulation from running
Vector2 currentPos = transform.position;
while ((position - currentPos).sqrMagnitude >= EPSILON)
{
_rb.MovePosition(currentPos +
speed * Time.fixedDeltaTime * (position - currentPos).normalized);
}
transform.position = position;
_isMoving = false;
}
Let’s see what is happening here and Unity freezes.
Inside the Unity’s game loop, there are many systems that run every frame. One of those systems is responsible for the physics simulation. Those systems run sequentially one after the other each frame. You can see their order here. Although in this example, the order doesn’t matter.
This is not an exhaustive list of the systems that run each frame. If you are interested in that, you can see it by executing the code in The player loop system
paragraph in my How to create an early and a super late update in Unity post.
Until each of those systems finishes running, the others won’t execute. What we are doing here in our fixed update, is that we execute code, that essentially says to Unity: “While our object is not near the position we want, use your physics system to move it.”, but the physics system won’t run, until the system that is responsible for the FixedUpdate
method, finishes. This obviously will never happen, because our while loop is dependent on the physics system to move the object and exit the loop, and our physics system is waiting for the FixedUpdate
system to finish executing before it can start running.
The FixedUpdate
is part of the Unity’s game loop. The game loop essentially does something like: “While the game is running keep executing in order all the systems”. In this case, it is like having a while within a while that the inner while’s condition is dependent on code that executes in the outer while. It will never stop. The solution, now is pretty obvious: We can change the inner while loop, with an if statement: “If something is true execute some code”. If it is not, Unity will keep going with the outer while loop’s code. The right way to write the above code would be this:
private void MoveToPositionWithIf(Vector2 position)
{
Vector2 currentPos = transform.position;
if ((position - currentPos).sqrMagnitude >= EPSILON)
{
_rb.MovePosition(currentPos + speed * Time.fixedDeltaTime * (position - currentPos).normalized);
}
else
{
transform.position = position;
_isMoving = false;
}
}
The MoveToPositionWithWhile
method’s effects are obvious to anyone that tries to execute it, because the Unity’s editor will freeze, but what about loops that don’t depend on other Unity systems? Some of those loops can greatly increase the time it takes for a frame to finish, our FPS might not be affected, but FPS are an average.
FPS and Performance
Frames per second, are not a very good way to measure performance. FPS are an average of how many frames per second are executed, but that doesn’t mean that all of those frames will take an equal amount of time. The same game, that runs at the same FPS in a certain machine, can have completely different visual performance to our players. Imagine these two cases: In the first case our game runs at 60 FPS and each frame will take 1/60th of a second (or 16.67 milliseconds). In the second case our game still runs at 60 FPS, but the first 59 frames take 0.01 of a second each (10 milliseconds), while the 60th frame takes 0.41 seconds (410 milliseconds). The second case will have a really negative effect to our players, as the delay of over 4/10ths of a second will be noticeable.
Let’s suppose that we have a Vector2
array that has the positions of all our enemies in a game. Every once in a while, we want to find the distance of the enemy that is closest to our player and that enemy’s position. We can write a for loop, or even a while loop that does something like this whenever we want to find the nearest enemy:
private void Update()
{
if (Keyboard.current.spaceKey.wasPressedThisFrame)
{
while (_indexer < _distances.Length)
{
float distance = Vector2.Distance(_position, _distances[_indexer]);
if (distance < _minDistance)
{
_minDistance = distance;
_nearestPosition = _distances[_indexer];
}
_indexer++;
}
}
}
This can be done any number of ways, we could have used a for or a foreach loop, or if we don’t care about the distance we could have calculated the squared magnitude of the difference of the vectors. Also this should probably be its own method, but these things don’t matter for the example. What actually matters here, is that whenever we press the space key, the distance is calculated in that frame.
This gives us precision, we get the result the moment we need it, but that means, the frame when the distance is calculated, will take significantly more time than the other frames, especially if our array is big. If precision is not important, we could have distributed the time this calculation needs in more than one frame.
As long as our array doesn’t change, or we don’t actually care about precision, we can use different ways to calculate the distance that the enemies had, the moment we pressed the space key.
Let’s see some ways this can be done.
Using an if Statement
We can use an if statement that checks one element of the array each frame:
private void Update()
{
if (Keyboard.current.spaceKey.wasPressedThisFrame && !_calculating)
{
_indexer = 0;
_calculating = true;
}
if (_calculating)
{
if (_indexer < _distances.Length)
{
_distance = Vector2.Distance(_position, _distances[_indexer]);
if ( _distance < _minDistance)
{
_minDistance = _distance;
_nearestPosition = _distances[_indexer];
}
_indexer++;
}
else
{
_doneCalculating = true;
_calculating = false;
}
}
if (_doneCalculating)
{
Debug.Log(_nearestPosition);
_doneCalculating = false;
}
}
Obviously this can be refactored to smaller methods, but wanted to have the whole code in one go here.
A simpler way, is to use a coroutine, if we don’t care about zero garbage.
Using a Coroutine
private void Update()
{
if (Keyboard.current.spaceKey.wasPressedThisFrame && !_calculating)
StartCoroutine(FindNearestWithWhileCoroutine());
if (_doneCalculating)
{
Debug.Log(_nearestPosition);
_doneCalculating = false;
}
}
IEnumerator FindNearestWithWhileCoroutine()
{
int i = 0;
_calculating = true;
while (i < _distances.Length)
{
_distance = Vector2.Distance(_position, _distances[i]);
if ( _distance < _minDistance)
{
_minDistance = _distance;
_nearestPosition = _distances[i];
}
i++;
yield return null;
}
_doneCalculating = true;
}
Here after each iteration we yield the execution to continue at the next frame.
The same thing can be achieved using the new Awaitable that exists in Unity versions 2023.1+
Using Awaitable on the Main Thread
private async void Update()
{
if (Keyboard.current.spaceKey.wasPressedThisFrame && !_calculating)
await FindNearestWithAwaitable();
if (_doneCalculating)
{
Debug.Log(_nearestPosition);
_doneCalculating = false;
}
}
private async Awaitable FindNearestWithAwaitable()
{
int i = 0;
_calculating = true;
while (i < _distances.Length)
{
_distance = Vector2.Distance(_position, _distances[i]);
if ( _distance < _minDistance)
{
_minDistance = _distance;
_nearestPosition = _distances[i];
}
i++;
await Awaitable.EndOfFrameAsync();
}
_doneCalculating = true;
_calculating = false;
}
Here we declare the Update method as async and our Awaitable
method, works like the coroutine method. It compares one element of our array each frame.
Finally, we can also use the Awaitable
to do the calculation on another thread.
Using Awaitable on a Background Thread
private async void Update()
{
if (Keyboard.current.spaceKey.wasPressedThisFrame && !_calculating)
await FindNearestWithAwaitableThread();
if (_doneCalculating)
{
Debug.Log(_nearestPosition);
_doneCalculating = false;
}
}
private async Awaitable FindNearestWithAwaitableThread()
{
int i = 0;
_calculating = true;
await Awaitable.BackgroundThreadAsync();
while (i < _distances.Length)
{
_distance = Vector2.Distance(_position, _distances[i]);
if ( _distance < _minDistance)
{
_minDistance = _distance;
_nearestPosition = _distances[i];
}
i++;
}
await Awaitable.MainThreadAsync();
_doneCalculating = true;
_calculating = false;
}
Here, we use Awaitable
to do the calculation on another thread. The difference from the previous examples, is that this calculation won’t use one frame for each comparison. The calculation will run parallel or pseudo-parallel with our code and will give the result when ready.
The code between the statements await Awaitable.BackgroundThreadAsync()
and await Awaitable.MainThreadAsync()
won’t run on the main thread. This method can be better as it gives the best compromise between precision and time, but has some disadvantages. The first is that we cannot use Unity’s API for game objects on a background thread, the following will throw an exception:
private async Awaitable WrongAwaitable()
{
await Awaitable.BackgroundThreadAsync();
transform.position = new Vector2(1f, 1f);
await Awaitable.MainThreadAsync();
}
The second is that using many Awaitables (hundreds of thousands or more) at the same time, can actually make our program slower than it would be, if we didn’t use them. A lot of that, depends on the target hardware.
Conclusion
Running loops inside the Update method, can significantly increase the time it takes to complete the frame the calculation runs. If precision is not of great importance, we can use the above ways to equally distribute the load of the calculation.
Although in the above examples, I do a single calculation each frame, that doesn’t have to be the case. We can refactor the code to perform some of the necessary calculations in each frame. How many depends on balancing the precision we need with the amount of time each frame will take to complete. The new Awaitable in Unity, can be of great help in those situations when we need a loop to perform those calculations.
This was just an example for avoiding looping within the update methods of Unity, and for this reason I didn’t cover the Awaitable in depth. This will happen in a future post. Until then, thank you for reading, and as always, if you have any questions or comments you can use the comments section, or contact me directly via the contact form or by email. Also if you don’t want to miss any of the new blog posts, you can always subscribe to my newsletter or the RSS feed.