top of page

All My Update()s

Updated: Nov 15, 2023

Unity has a number of options as far as where you put code that is intended to get executed on a frame by frame basis. How do you decide which one to use?

The first step is to read and understand Unity's execution order. It lays out clearly which functions are called at what time. While that article is the foundation of understanding which functions to use, it does not lay out much in the way of best practices. The most crucial of these to understand is FixedUpdate, because in many ways, it's both the most tempting and the most dangerous.


FixedUpdate and exponential slowdown

FixedUpdate runs at a set, predictable time interval... or at least, it pretends to (we'll get to that below). By default, this fixed timestep is set to 0.02 seconds, meaning that FixedUpdate will be called 50 times every second. The physics engine also processes "frames" at this same speed (as you can see in the execution order page, the physics engine does its thing immediately after all your fixedUpdate code runs). This predictability may seem attractive to certain users; why wouldn't we want to use FixedUpdate all the time?

In a word, performance. In particular, adaptation of performance to both fast and slow machines. Let's break this down.

Code that's being executed in Update and LateUpdate will execute exactly once for every frame rendered, and Unity will pump out these frames more or less as fast as the computer is able. If you're running on a high-end machine, you can easily reach 90fps or 150fps, and Unity will just keep going as fast as your machine can run it, executing all Update and LateUpdate code right along the frame. This is the first problem with putting everything in FixedUpdate: people on high-end computers will see your game effectively stuttering along at 50fps, even when they dropped big bucks on a top of the line machine and a 144Hz monitor, even though Unity is still rendering 144 frames a second - it's just not changing anything for 94 of those frames when FixedUpdate isn't being run, and wasting a lot of rendering effort on things that aren't changing. And the sorts of players that drop that kind of cash on a computer tend to want the games they play to take advantage of it.

But the far worse scenario for overuse of FixedUpdate is on a slower machine, because FixedUpdate does not adapt at all to slowness, and forces the entire rest of the game to fill in the cracks. This is largely why you see games with buttery-smooth physics or games with super-choppy physics, and rarely do you see much in between - with FixedUpdate, the slowdown becomes exponential. Why?

Let's imagine you have a function, DoSomeWork(), that takes a consistent amount of time each time it's executed. Let's say it takes 5 ms on your main development desktop computer. (This is a pretty slow function, but we're pretending it's necessary for your game, so roll with it.) for comparison, FixedUpdate runs every 20 ms. So if this code is being executed in FixedUpdate, then 5 out of every 20 ms is being spent on DoSomeWork(), leaving the other 15 ms for rendering and other game processing. And this works out fine on your machine - performance is good, your framerate is smooth, so everything's fine, right?

Well, now let's take this project from your desktop to your laptop, which is not as powerful - let's say half as fast. On this machine, DoSomeWork() takes 10 ms to execute, but your game is still going to run it every 20 ms, so now there's around half as much time remaining for all the other game code and rendering, so your framerate halves. But this machine is half as powerful, so that's to be expected, right?

Well, don't count your chickens yet, because we're about to run this game on a phone, which is half again as powerful as your laptop. Now DoSomeWork() takes 20 ms to execute, but it's still being called every 20 ms, and we start running into real trouble, because this leaves no time for any other code to run in your game. Your game goes from having slight slowdowns as expected to total standstill much, much faster than you'd expect. In this case, Unity is smart enough to avoid a total standstill, which it does by slowing down time itself - the game's simulated time will no longer match the real-world passage of time. But before it does, your game is going to drop to incredibly unplayable framerates - in my experience, usually around 2-5 frames per second.

At this point I'll go into a bit about when the engine actually runs FixedUpdate, because as hinted at before, it's not actually running like clockwork every 20 milliseconds in the real world. What actually happens is that the engine checks to see how much time has passed between the last frame and this one, and as long as it's above 20 ms (cumulatively), it will repeatedly execute FixedUpdate (incrementing Time.time by 0.02 each time) until that time reaches the real time - take another look at Unity's flowchart to see what I mean.

So let's say that rendering on the above machines takes 2 ms, 5 ms, and 9 ms, respectively. On the desktop, you're going to have this pattern for each frame along the lines of (7 ms, 2 ms, 2 ms, 2 ms, 2 ms, 2 ms, 2 ms, 7 ms, 2 ms, 2 ms, etc) - it'll run FixedUpdate only every 20 ms, so it will only take a "long" frame every 8th frame or so, when the cumulative total crosses the next threshold of 20 ms. For the laptop, the pattern will look something like (10, 5, 5, 10, 5, 5), taking a longer frame more frequently relative to the frame rate. On the phone, it's going to try to keep up, meaning that it'll be running FixedUpdate many times for each rendered frame, resulting in a pattern looking more like (29, 49, 89, etc) until the time-slowing algorithm kicks in and limits it.

Even if you don't expect to deploy your game on a phone, it's important to remember that the exact same problem applies to increased workloads as opposed to reduced capacity. If you add a few more objects to your game that need to DoSomeWork(), it can easily grind your top-of-the-line machine to terrible framerates, too. This is a natural consequence of something being run at specific intervals, and there's not much you can do about it as long as your code is being run in FixedUpdate().

Update & LateUpdate - proportional slowdown

So what can you do? Move your code out of FixedUpdate. How does that help? Let's run the same game function, but with DoSomeWork being executed in Update() instead. It takes the same 5 ms to run on the desktop, but now, it's just going to do a constant cycle of run once, render once, and then move on to the next frame, without regard for how much time has passed. so on your desktop, it's going to DoSomeWork() (5 ms), then render (let's say 2 ms on your desktop), pumping out a frame every 10 ms. That's a respectable, even admirable, 144 fps.

Now, let's run it on your laptop. DoSomeWork now takes 10 ms, and rendering now takes 5 ms. This means each frame will take 15 ms, giving you a framerate of around 60-65 - still pretty good, but at this point, not much has changed from the FixedUpdate scenario.

Now let's run it on your phone. DoSomeWork now takes 20 ms, and rendering now takes 9 ms, which puts your framerate around 30 fps. This is a predictable, proportional slowdown, and acceptable performance for a mobile device.

When is FixedUpdate appropriate?

You may come away from the analysis above with the conclusion that I'm telling you never to use FixedUpdate. This isn't the case. There are two major rules for using FixedUpdate effectively:

1) Use it only when working with physics.

2) Any heavy calculations that can be moved elsewhere, should be.


LateUpdate

We haven't gone over LateUpdate much yet. Superficially, it may seem obvious when to use LateUpdate: when a function on script A needs to run each frame before script B, use LateUpdate on script B. But that's not necessarily the right practice, for one reason: what happens if script C depends on the functionality you ran in script B? These aren't talk shows; there's no LateLateUpdate to take advantage of.

In some cases, rather than using LateUpdate, you should call script A's function directly from script B. You might use an event for this. If you have a particularly intricate sequence of event that needs to happen each frame, you may even want to have a manager class, and not use Update() at all except for in that class.

So where does this leave LateUpdate? The practice I use is this: If a script is doing something that's strictly visual in nature, use LateUpdate. Everything else goes into Update. Camera movement is by far the most common, but things like dynamic mesh updates can fit in here, too. This will ensure that anything that these visuals might be reacting to will have been processed already.

Edge case: Smooth following a physics-controlled object

Let's say that your player-controlled object is a marble, controlled by physics, and your camera uses something along the lines of a standard smooth follow script. You've taken my advice from above, and put all your camera movement into LateUpdate. If you watch this in the Scene view, everything looks fine - the camera follows the ball around as you expect, and the ball's movement is likewise perfectly smooth. But in the game view, the ball is jittery, and jumps all over the place. What's going on?

You're falling prey to the inconsistent lineup between FixedUpdate cycles and LateUpdate cycles. Remember, for every LateUpdate, you may experience 0, 1, or more FixedUpdates. That means that the ball can be moving an unpredictable amount of distance with each rendered frame - and more importantly, with each LateUpdate, when your camera moves. If your camera placement is absolute, this isn't an issue, because it'll always be at the same spot relative to the ball anyway. But a smoothfollow camera depends on its previous position, and thus will move a little each frame.

If you want a smooth follow script, the answer here is actually to change your SmoothFollow to use FixedUpdate; that way, it'll be executed on a consistent timetable as the player's ball is moved.

コメント


bottom of page