Welcome to my first blog post!
My name is Kelly, though I often use the name zigzag online. I started teaching myself C# and JavaScript about two years ago, and in doing so found my passion. Now, I'm enrolled at Flatiron School, and working hard on achieving my dreams of being a game developer!
Have you ever wondered how video games work behind the scenes? I know I did, it's why I learned programming in the first place. Games always seemed like magic, and I just had to find out the secret behind the trick. It turns out there are quite a lot of interesting things going on behind the scenes! While each game has numerous unique qualities, there are certain general patterns that pop up in many of them. Over the next few posts, I'm going to discuss a few of the programming patterns that make games what they are. Not every game uses every pattern I'll introduce, but most use at least some of them, and almost all of them use the first one I'm going to talk about - the Game Loop.
What are programming patterns anyway?
Programmers are constantly solving the problem of how to communicate with the computer to have it do what they want it to do. This problem solving can get quite complex, and certain features will always require unique solutions. While this is unavoidable, a developer regularly runs into problems that can be solved with the same basic solutions. These are referred to as programming patterns - generalized and reusable solutions that can be used repeatedly in different contexts. The patterns I'll discuss are most common in games, but these and other programming patterns aren't exclusive to games.
The Game Loop
One major problem that is nearly universal across all games is how to present the player with a smooth, continuously moving and progressing world in real time. While games aren't the only things that can have this feature, the use of the game loop pattern is used relatively little outside of game development.
While it may be rare for other programs, most of the games we know and love wouldn't exist without the use of this pattern! Don't believe me? Think of a game you like. Does this game completely stop and wait for your input to continue? Or do the characters or background scenes continue animating and moving whether you supply input or not? If your game can continue its various game behaviors, has a concept of time, allows you to pause, or plays music or sounds without your input, it's using the game loop in one way or another. If you don't pause, Mario can definitely still be killed by that Goomba whether you're at the controls or not! Games that use a game loop pattern can process user input, but don't stop and wait for it. Even turn-based games usually continue animating the scene or characters in some way and provide audio and sound effects while waiting for user input. The game loop is what allows these updates to happen continuously over time.
How quickly your game goes through a complete loop is something you may have heard of before- the frames per second, or FPS. If you're playing a game at 30 FPS, without complicating things, this would mean your game is completing 30 game loops per second (I'll discuss why that's not always quite the case later!). In each loop, it updates the scene- if you press a button to jump, each loop updates the position your character is in. Even if you let go of that jump button, your character may continue to jump, or begin to fall; these are all physics calculations that are updated by steps in each loop.
Even the most basic game loops include a function, often called Update
, or Tick
, which takes care of anything that needs to be updated in steps each frame. Think about the jumping character again- for it to look like itβs jumping, it must start on the ground, begin to rise, come to its apex, then fall back down to the ground. To achieve this, the jump action has to be updated in steps each frame; it has to take some time in order to create the appearance of a jumping character. That's the purpose of Update
. If you create your own game loop, it needs an update function. If you use someone else's game engine, such as Unity's, it provides you with an update function to use as needed. Other core features of a game loop include a function to process user input, which then influences the data in the update function, and a rendering function, which draws the game at the state it's in according to the most recent update function.
The simplest possible game loop looks something like this:
while (true)
{
processInput();
update();
render();
}
But most games won't have a simple loop like this, as it creates some pretty critical issues. With this loop, you have no control over how fast the loop runs; if you have a fast computer, updates may occur too quickly for you to see what's going on, while if you have a slow computer, updates may crawl along at a painfully slow rate. To deal with this variability, we spread loops out over a certain amount of real time, the duration of which is sometimes measured in frames per second. We must include a variable or fluid time step to our loop, like so:
double previousTime = getCurrentTime();
while (true)
{
double current = getCurrentTime();
double elapsed = current - previousTime;
processInput();
update(elapsed);
render();
previousTime = current;
}
In this game loop, before the loop begins, we get the current time and store it in a global variable that can be accessed within the loop. Then, in the loop, we get the current time again, and subtract the original time from the current time to determine how much time, in milliseconds, has elapsed.
This is used in an interesting way! Notice how this elapsed time is passed as an argument to the update function. This allows the update function to calculate how big of a "step" it has to take in order to keep up with where it should be. If we know how many frames we need to have per second, say, a target of 30 FPS, we can then use the real time since the loop last ran to determine if our computer is keeping up, and adjust the size of the step accordingly. If our computer is fast, it updates normally. If our computer is slow and can't update or advance the game quickly enough - in other words, if game time is lagging behind the real time -, we can use the real amount of time passed to tell our update function it needs to take a bigger step, and advance the game further in each call to update than our fast computer.
Seems easy enough, right? Well, unfortunately this loop presents its own problems. In a multiplayer game, for instance, if two characters fire a weapon from the same start location, it might end up in different locations depending on each computer being used! This is because game engines mostly use floating point numbers, which are subject to inaccuracies and rounding errors- the more calculations that are done with these slightly-off floating point numbers, the greater the error might be. In our loop, a fast computer may be calling update
at our desired 30 FPS (or more), while a slow computer may use fewer calls to update
to do the same thing. This means that with each of our 30 calls to update
, the calculations get more and more inaccurate, and will therefore produce a different end result than a slower computer that only uses a few calls!
To fix that, we might use something like this:
double previous = getCurrentTime();
double lag = 0.0;
while (true)
{
double current = getCurrentTime();
double elapsed = current - previous;
previous = current;
lag += elapsed;
processInput();
while (lag >= MS_PER_UPDATE)
{
update();
lag -= MS_PER_UPDATE;
}
render(lag / MS_PER_UPDATE);
}
In this version, we update lag at the beginning of the frame based on how much real time has passed. We then perform a series of calls to update
until we've caught up, and only then do we render the scene. Sometimes, the call to render
may happen in between two calls to update
, potentially causing an object to appear in the previous spot during an animation and not show any progression. But by passing lag / MS_PER_UPDATE
in to render
, we are telling the render function exactly how far in between the two updates we are, and the renderer can guess the position the object should be in based on interpolation. This method presents its own problems as well! For example, if our object is going to collide with something in the next frame, but our render function doesn't know that because the next update hasn't run yet, it may guess that the object is somewhere where it shouldn't be.
Professional level game loops are much more complicated and must account for many other factors, such as GPUs and multithreading, and even the different ways that user input is processed across different devices. The details of every game's game loop vary greatly, but the core pattern is always there- process user input, update the scene, and render the scene for the user to see.
Example Game Loop- Unity Engine
Now that we've gone over some of the essentials of a core game loop, it's time to see a real example of one that has been used by millions of people! I'll be discussing the core game loop that is used by Unity, a popular game engine for beginners and pros alike. It should be noted that while the core of Unity is written in C++, developers using Unity are generally expected to use C#, and therefore any examples I give from now on will be pseudocode based on C#.
When you download Unity and begin a new project, this is the core loop that each script you create can use:
Woah! There's a lot happening there! Our little loops seem inadequate next to Unity's. If it hasn't scared you away yet, bear with me and I'll explain what's going on in this loop.
Unity designed this loop to give game developers a lot of options as to which part of the loop they want their code to execute in, and a large part of learning to use Unity is learning where different parts of your code should go within this loop. Aside from the greyed-out lines, each line you see in the above loop is a different method or event that your scripts can have access to, so long as they are derived from Unity's MonoBehaviour. So, when creating a class that you want to be able to display and update within a scene, you would simply have it inherit from MonoBehaviour, like this:
class Example : Monobehaviour
{
*Example class stuff here*
}
By default, newly created scripts in Unity inherit from MonoBehaviour, and these scripts must be placed on a GameObject within the scene in order for them to be executed.
At the start of Unity's loop, you can see three functions, Awake
, OnEnable
and Start
; these are methods that should be used within your script for initializing variables, but because they are called in the order you see in the flowchart, there are subtle differences as to the type of variable initialization to use in each.
The very first method call we have access to is the Awake
method, which is called when Unity instantiates a GameObject with any MonoBehaviour-derived script attached to it; as Awake
is called first, you should initialize variables that will be needed by either the same script or other scripts, so that they can be accessible by the time your scripts reach the Start
method. Using Awake
to access other scripts variables will often result in a null reference exception, as the other scripts' Awake
methods may not have been executed yet.
OnEnable
is called right after instantiation and the Awake
method so long as the script is enabled, and is best used for subscribing to events; similarly, towards the end of Unity's game loop, OnDisable
is best used for unsubscribing from those events.
Lastly, Start
, the last of our initialization functions, is called just before the first call to Update
for that script. This is the best time to initialize those variables that are needed in the rest of the script, whether in an Update function or in other custom or Unity methods. If we initialized something in Awake
, on any script, we can access it in any scripts' Start
; the Awake
method for every script in the scene is called before any script's Start
method is called, at least for GameObjects that are created at the beginning of the scene.
It's important to note that Start
only ever executes once for any given script. This means that even if you disable and re-enable a game object within your scene, the Start
function on any attached scripts will not be run again, while Awake
and OnEnable
do get called again.
Unlike our simple game loop, you can see that Unity's Update methods are also split into multiple methods. The first one listed is called FixedUpdate
, and this function is mostly used for updating anything related to the physics in the scene, as right after FixedUpdate
is called, Unity performs its physics calculations and updates, as well as processing and updating animation states and properties. FixedUpdate
is called such because it is executed based on a timer that is independent of frame rate, meaning it is called at regular real-time intervals and it may be called more or less than once per frame.
After FixedUpdate
and all the physics updates are finished, all user input functions are called. On computers, these functions process any keys pressed on the keyboard or any mouse clicks; on consoles, any buttons pressed on the controller; and any interaction with the touch screen on mobile devices. This is represented by the line OnMouseXXX
in the function execution order flowchart shown above.
The next method we can utilize for updates is simply called Update
, which runs right after the user input events are fired. This update function handles game logic, and it's often the most frequently used update function. It is best to have our physics calculations done before reaching this method, as our Update
method should use the most recent physics information to accurately update the game's logic. If, for example, our physics calculations were not done beforehand, the Update
method could be applying game logic that doesn't take into account any changes in the physics, such as a collision that caused an object to stop its movement. After Update
, the loop again processes updates to the animated objects within the scene, though sometimes this update to animations coincides with the the ones after FixedUpdate
.
Finally, LateUpdate
is for any last minute updating that needs to happen before the scene is rendered (I know we skipped a few- I'll explain in more detail next!). The most common use for LateUpdate
is updating the camera's position; if we want the camera to always be looking at the user, it is best if we already processed our physics and game logic to know the location the player is going to be in before updating the camera's position!
Now, about the ones I skipped; as you can see, under Update
is a section of four that all start with the word 'yield'. These are used in Unity's Coroutines, which are a special kind of updating method. Normally, tasks within a method stop the execution of the rest of the loop until they have finished calculating, and these tasks will happen all at once in a single frame. In a Coroutine, you would include logic for something that needs to happen in order and in steps one frame at a time, but then stop once they have completed the task. With Coroutine's, you can pause the logic from executing until the next frame or for a certain number of real-time seconds, or even until a provided expression evaluates to true! Coroutine's return an enumerable so that it can pick up right where it left off in the sequence whenever you want.
Imagine you want to move a character from one position to another, and after a pause, make them move back to the first position, like they were pacing; one way to do this might be to lerp between the positions. Lerp means to go from one value to another value in a variable number of steps instead of all at once, such as going from 0 to 10 by counting 1 at a time. If you were to put the logic for using lerp to change positions in a regular method, even Update
, it will do all the steps of the lerp in a single frame, which would make our character appear to stay right where they started. We could use a Coroutine to go one step of the lerp at a time. Here's how that might look as a Coroutine in Unity:
IEnumerator ChangeCharacterPosition(Vector3 newPosition)
{
Vector3 startPosition = transform.position;
while(transform.position != newPosition)
{
transform.position = Vector3.Lerp(transform.position,
newPosition, t);
yield return null;
}
yield return new WaitForSeconds(1);
while(transform.position != startPosition)
{
transform.position = Vector3.Lerp(transform.position,
startPosition, t);
yield return null;
}
}
In this example, t
is whatever value of change you wish to happen on each step; in our counting to 10 example, t is equal to 1. The purpose of placing yield return null
in the while
loop is to tell Unity to pause the execution, which will pick up where it left off in the next frame. The rest of Unity's loop will then execute and it will start the loop all over again, resuming the Coroutine just after the Update
function completes. Similarly, we can yield return new WaitForSeconds(timeToWaitArg)
to tell Unity how long we want the pause to be before it resumes executing our code. Pretty neat, right? After handling any running Coroutines, LateUpdate
is called.
After LateUpdate
, with all the up-to-date calculations for each object in the scene, the loop goes through rendering the scene for us to see. This is also broken up into multiple callback functions. The naming of the functions pretty much sums up their purpose, so I won't go over each of them, but in case it's not clear, OnPreCull
is called before Unity culls, or hides, any parts of the scene that would be blocked by something in front of it to prevent unnecessary rendering of things we wouldn't even be able to see. OnRenderImage
is called just after the scene is fully rendered and it allows for post-processing, which applies effects to the entire image. However, each of these rendering callback functions are only available to use in Unity's standard, built-in render pipeline; these days, most developers choose to use Unity's Universal Render Pipeline, URP, or their High Definition Render Pipeline, HDRP, or even a custom pipeline, and it is likely that Unity will eventually completely remove their original built-in pipeline. Fortunately, Unity provides other ways of modifying the rendered image, and developers can still apply post-processing and other effects to the rendering of the scene.
And lastly, if a GameObject is going to be destroyed in the current frame, any logic placed in the OnDestroy
method will be executed after all updates have been called. Sometimes, this waiting is problematic, and Unity does provide a method to immediately destroy a GameObject rather than waiting until the end of the loop.
Unity's core game loop is just one example of many out there. Some developers don't like Unity's loop and claim other execution orders allow for more powerful game design, but others claim that Unity can be just as powerful when you know how to use it. In the end, whatever game engine is used, it is important to understand its' core loop and the order in which it processes different parts of the code. This can help avoid bugs, and allow developers to fully take advantage of the engine they are using to create their game.
I hope you found this overview of the game loop and the examples provided interesting and helpful. Whether you were just curious, or are aspiring to be a game developer yourself, understanding what a Game Loop is and some of the ways this pattern is used is an important part of learning about how games work!
Sources:
https://gameprogrammingpatterns.com/game-loop.html
https://docs.unity3d.com/Manual/ExecutionOrder.html
Top comments (0)