Game State and Game Loops (transcript)
Any game can be thought of as a series of discrete states, momentary snapshots of its progress through time. Consider, for instance, tic tac toe, a simple game in which the state consists of the visible board but also the invisible state of whose turn it is. If we model the state of this game as data, we can do it like so: a string denoting who gets to move next and a 9-element list for the 9 cells of the board. Defining Tic Tac Toe as code, then, is simply a matter of modeling its possible states as data and defining the rules of how exactly one state changes to another as the game progresses.
The same can be said of any game, including video games. Unlike board games, though, video games generally progress through many states quickly, typically 30 or more per second. In Super Mario, for example, when Mario and his enemies move, the position changes occur in each new discrete state. And like in tic tac toe, some state is visible, such as Mario’s position, but other state is not, such as how much time Mario’s invincibility powerup has left.
If we breakdown some of the game state in Super Mario, we get something like this. The state of Mario himself includes an x and y coordinate denoting his current position in the world, an indicator of his current momentum (such as if he’s currently still traveling up from a jump or falling back down), his frame of animation i.e. the sprite to display when he is drawn, what powerup he currently has (if any), how many lives he has left, how many coins he’s collected, and how many points he’s accumulated. The state of the fireballs Mario shoots likewise each have an x and y coordinate, a current momentum, and an animation frame. And the game itself can be thought to have some high-level state, such as whether the game is paused or still at the title screen, or whether the game is over.
When modeling these sorts of things in code, the obvious approach is to model each entity type as a class, e.g. a Mario class and a Goomba class. And in proper object-oriented fashion, we arrange most of these types in a hierarchy of inheritance such that common functionality gets bundled into ancestor classes, with the most generic class at the top. Most commonly this will be something like WorldObject, which here represents anything that can get placed in the world with x and y coordinates. (And note that the Game class stands outside the hierarchy because it is not a kind of WorldObject, nor is WorldObject a kind of Game. So while most visible entities will fit into the one big hierarchy, some other entities, the non-visible ones especially, may not.)
Now, as is always the case in object-oriented programming, it’s important to distinguish between the class inheritance hierarchy and the object composition hierarchy, or more accurately, object composition graph. When we instantiate our classes, the instances themselves typically contain references to other instances, and this graph of references commonly resembles another hierarchy. For example, our Game object might itself be composed of all the things which make up the game world, including a Mario object, a WorldGeometry object, and an Enemies object. The WorldGeometry and Enemies objects themselves would likely contain one or more lists of other objects that make up the world, such as blocks and koopatroopers. Again, in this case, we have a neat hierarchy of composition, but composition might form a graph of any shape, because any object can reference any other (which is not the case with class inheritance because inheritance cannot be circular).
An alternative to creating an elaborate inheritance hierarchy, is to create what we can call component classes. These component classes represent attributes, which they provide as services to other classes. For example, both a Goomba class and a FlyingKoopaTrooper class would contain an instance of the Animated class because both represent animated entities. So, to animate, a Goomba or FlyingKoopaTrooper instance would use the methods of its Animated instance. The main virtue of this component-based approach is that it’s more flexible: we can define new entities by simply taking from a grab-bag of features.
Another alternative to big inheritance hierarchies is to store entity properties in tables, almost as if in a database. For example, for every entity with xy coordinates, an entry is made in the XY coords table mapping each entity by an id number to its xy value. Likewise, for every entity with hitpoints, an entry in the RemainingHitpoints table maps an entity id to a number of hitpoints. In this arrangement, the behaviors, the rules of how this data changes, might be defined simply in plain old functions, or possibly in classes representing each property. For example, each entry of the xy coords table might be an instance of an XY coords class that defines methods for manipulating the property. For behaviors that depend upon multiple properties, properties would be wired to send messages to other properties and trigger their methods. An entity, then, would be the aggregate behavior of all its independent properties.
Whatever approach is taken–either a straight-forward class hierarchy, a component-based design, or a property-based design–allowance must be made in most games for entities to trigger changes in disparate, unrelated entities. For example, in Super Mario, when a goomba gets killed by a thrown turtle shell, Mario gets points. The trouble is that, most likely, the goomba instance contains no reference to the Mario object or vice versa, so they have no direct means to communicate or watch what is happening to the other. The general solution is what’s called an ‘event’ or ‘message passing’ system. With such a system in place, the Goomba object, upon the Goomba’s death, can broadcast a message notifying any object that cares about the death. By this mechanism, a Goomba’s death can trigger Mario getting points.
As we go through examples of actual games, we’ll see these state modeling and event system approaches in action. For now, let’s consider the general outline of how we progress from one game state to the next, what’s traditionally called the game loop.
In the simplest possible arrangement, we start out by loading the game’s resources, mainly the sound and image data files. We then create the variables that will hold the game state. Here, we simply have the one object, GameState, which will hold everything: the state of the game menus and the state of the game world and all its entities. Next, we initialize the game state; in other words, we put everything the way it should be in the first moment of the game. Once everything is in place, we enter the game loop and the game begins. In each iteration of the loop, we first get user input, then update the game state to the next tic, the next moment of time in the game, and then once we have the new state, we render that state on screen and start and stop sounds as the game state dictates.
So note that we get the user input first so that our state update can reflect the user’s input. For example, if the user is pushing right on the d-pad, Mario in the update should start moving right. Also note that the sound and screen output is all done after the update is complete: we can’t render the world until we figure out what the world should look like for this moment–which part of the game world are we looking at, where should the characters be drawn, and what score should be at the top of the screen? All of that is drawn from the newly updated state in each iteration of our game loop. And the game loop keeps iterating indefinitely until the user exists the game, say by selecting the ‘exit’ item in the main menu, which triggers a code path in updateState that sets isGameStillRunning to False, thereby triggering the end of the loop.
So that’s the simplest possible game loop, but it has a problem: our loop will blindly iterate through states as fast as possible with no regard for time. This means the action of our game would run faster on faster systems and slower on slower systems, and the action may run erratically, seeming to speed up and slow down at various times. Many early games actually used this naive approach and so, on newer faster hardware, require ‘clockspeed’ hacks that artificially make them run slower. Early games also were generally simple enough that each iteration of the loop took about as much time as any other, so erratic speed ups and slow downs often weren’t a problem. The same cannot be said of most of today’s games: it’s one thing for the framerate of a game to fluctuate, but another for the apparent action speed to fluctuate. So how do we get a consistent flow of time?
Well consider the solution in an ideal world where we have an infinitely fast computer. Assuming we want a consistent 60 frames per second, we need a new game state and rendered frame approximately every 16.7 milliseconds (because 1000 divided by 60 is about 16.7). With our infinitely fast computer, each iteration takes only an instant to run, so after each iteration, we simply wait for 16.7 milliseconds until running the next iteration. This gives us 60 evenly spaced updates per second. Now of course, there’s no such thing as an infinitely fast computer, so the next best thing would be if each iteration consistently took less than 16.7 milliseconds. Our code would then simply wait out the remaining time until the next frame. For example, if an update takes 10 milliseconds to run, we would wait 6.7 milliseconds before starting the next iteration.
The problem with this arrangement is that, while our game action would never speed up, it may effectively slow down when iterations take longer than our target 16.7 milliseconds. If the game assumes only 16.7 milliseconds have passed when more have actually passed, the game will start running behind real time. So we’ve only fixed half the problem.
A better and more common solution is to iterate as fast as we can, just like in the naive approach, but update state based on the actual time elapsed since the last update. So here when we compute state s1, we take into account that 9.3 milliseconds have elapsed since the start of the last update. Time especially affects how we compute movements. For example, when we define a movement speed in coordinates per second, we can compute how many coordinates are traveled at that speed in some number of seconds by a simple multiplication. If an object moves a certain distance in one second, then it will travel 0.4 times that distance in 0.4 seconds.
Other parts of game logic also rely upon accounting for time. Mario’s invincibility powerup, for instance, is supposed to last a fixed amount of time. Accounting for the actual elapse of time when we update state allows us to turn off Mario’s invincibility at the right moment.
To get the time elapsed since the last iteration, our loop might look something like this. First suppose a function timeNow() which returns a timestamp of the current time with high resolution, let’s say accurate to a microsecond. We first record the time before our first iteration as the ‘previous time’, and then at the start of each iteration, get the time again, find the difference between this new time and the previous time, and update previous time to the new time for the sake of the next iteration. We now have an elapsed time, presumably a measure of seconds or milliseconds, which we can pass to updateState.
So now our game can account for irregular intervals, right? Well maybe, but in many cases game logic can only account for so much irregularity: outside a certain range of intervals, our game logic might break. Really small or large time values might thwart the most carefully written code. The obvious solution is to simply put a cap on the range of elapsed time values. When the elapsed time value is very small, it’s probably because our loop is simply running fast; there’s no point in running the game at excessively high frame rate anyway, so in such cases we can simply have our game wait a small amount of time before starting on the next iteration. On the other hand, when the elapsed time value is very large, like say 100 milliseconds or greater, either the system is incapable of running the game at an adequate frame rate, or something else in the system is interfering with our game, stealing CPU time that our game needs. In either case, the best we can generally do is just cap the elapsed time value and live with the resulting action slowdown. If the framerate is terrible, slow action isn’t the worst of our problems, and if something else on the system is interfering with the game, it’s out of our control: hopefully, the interference is just temporary or intermittent, and so any slowdowns are just momentary annoyances. It’s better to cap the elapsed time interval and avoid bugs: you don’t want your game to crash just because another program in the background hung the system for a few full seconds.
However, there are some systems of game logic that may require perfectly regular intervals to function properly. The most notable examples are physics and collision systems, which are notorious for time-related bugs. If you’ve ever randomly fallen through the floor in a game, chances are good that the root cause was a timing issue in the collision system.
So a common approach is to update physical state to fixed intervals, such that the time represented by successive states are all evenly spaced by a fixed time step. For example, each state represents the physical state of the world at precisely 16.7 milliseconds after the previous state. Here the white vertical lines are the states of our physical system, updated at regular 16.7 milliseconds. The red vertical lines here are the updates to the rest of our game state. Note that zero or more game states may lie in between any two physical states.
Now, our game logic could simply use the nearest-in-time physical state. For example, the game state represented by the first red line would use physical state s1, as would the game state of the second red line, because that is the closest matching physical state. This nearest-neighbor approach, however, can produce noticeably jerky motion of physically simulated objects.
To fix this, we use interpolation. For each game state, we compute a best matching (if not perfectly accurate) physical state by interpolating in between the two surrounding states computed at fixed intervals. For example, for the game state represented by the first red line, we compute a physical state for that moment by interpolating between the two physical states that surround it, s0 and s1.
To interpolate the position of objects between two states, we simply assume that their coordinate values change linearly between those two states. For example, if an object moves from x coordinate 30 in one state to x coordinate 50 in the next state, we assume it traveled those 20 units along the x axis at a constant rate; so at the time exactly in between those two points, the x coordinate of the object would be 40, exactly halfway in between the two states.
So, given two physical states separated by an interval of 16.7 milliseconds, we can interpolate a physical state at 10.1 milliseconds after the first using the ratio 10.1 divided by 16.7. To calculate an x coord, for example, we get the difference between the x coord in both states, multiply by our ratio, and add to the first state, yielding the interpolated x coordinate.
So now let’s look at how Pyglet handles game loops. First, though, you need to know about a Python feature called decorators. The decorator syntax is a line preceeding a function or class starting with the @ symbol and specifying a function. This is simply a syntactical convenience for invoking the function with the function or class below as argument, and the return value assigned to the variable of the same name as the function or class. So here, @foo above the definition of a function bar is the same as, immediately after the function definition, invoking foo with bar as its argument and assigning its return value to bar.
The idea of this is that we sometimes want to do something immediately with a class or function, such as wrap it in another class or function. The decorator syntax is simply a more convenient and more visible way of accomplishing this.
In Pyglet, decorators are most commonly used in connection with the EventDispatcher class in the module pyglet.event. The idea of the event dispatcher is that we can register event types by name, then register handler functions on those names, and then invoke the handlers by ‘dispatching’ the event under which they are registered. So here, we create an event dispatcher, register and event type we’ll call ‘end_of_the_world’, create a function foo that simply prints a message, register foo with the end_of_the_world event, and then dispatch the event, thereby invoking foo.
We can also include arguments when we dispatch events, in which case the invoked handlers must have parameters to receive the arguments (or else an exception is raised). Here we pass to foo the argument 5, which denotes the minuteslefttolive and thereby determines the level of panic.
In these artificial examples, it seems like we just went through a lot of work just to invoke a single function. In practice, though, a message passing system like EventDispatcher is useful for tying together disparate objects. Think back to our example of a Goomba giving Mario points when it gets killed. Rather than have the Goomba and Mario objects interact directly, we can have them interact indirectly through an EventDispatcher: Mario registers a function to respond to ‘point’ events, and when the Goomba dies, it dispatches a ‘point’ event, passing the number of points as argument. This gives us complex interactions while keeping our code relatively organized.
As a convenience, EventDispatcher has a method event() which attaches an event handler, inferring the event from the function name. So here, we pass the function named end_of_the_world to event, which registers that function for the event of the same name.
Taking this one step further, we can invoke the event method with the Python decorator syntax, which not only saves a slight bit of typing, but also stands out more in code.
Understand that EventDispatcher also has methods for attaching multiple handlers for each event, so a single event dispatch can trigger multiple handlers. The push_handlers method, for example, adds additional handlers to those already registered for the event. In our simple examples, though, we’ll usually use the event method, which sets just one handler for an event, replacing whatever other handlers might already be registered for that event.
Now, the Pyglet EventDispatcher class is actually used most commonly not directly but as a parent to other classes. The inheriting class usually defines the types of events it may dispatch. The pyglet Window class, for example, inherits from EventDispatcher, and defines events related to our application windows.
This pattern will become clear when look at a reductively simple Pyglet application that does nothing but display a blank window with the dimensions 640 by 480. The first thing you’ll notice is that there’s no game loop! That’s because the loop is simply hidden from us. We start by of course importing pyglet. We then create and configure the window to display, and when ready to run, the last thing we do is invoke pyglet.app.run(). It’s this run method which contains the loop of our application. This loop does three things:
First, the loop reads the events sent to our program from the windowing system, mainly mouse and keyboard events. For each windowing system event, the run loop dispatches a corresponding Pyglet event on the window object. So a windowing system keyboard event becomes a Pyglet keyboard event dispatched on the window object. So to respond to keyboard events sent to our application window, we attach a keyboard event handler on the Pyglet window object. Just be very clear we’re talking about two different kinds of events: a windowing system event is a message coming from the system, while a Pyglet event is simply an invocation of the EventDispatcher method called ‘dispatch_event’.
Aside from consuming and dispatching system events, the run loop also updates the timers of the pyglet.clock module. Without the run loop invoking their update methods, the timers would be inert.
And third, in each iteration, the run loop dispatches a draw event via the window objects. To draw in a window, we make our drawing calls in a handler registered for the draw event on the window object.
For example, here we use the pyglet.text module’s Label class, which is a simple abstraction over opengl that represents a piece of text to display. We create a Label object by simply specifying the text, a font size, and coordinates. By default, a text label is drawn anchored at its bottom left corner, so this text will be drawn such that its bottom left corner is at coordinate 0, 0 in our window, which is the bottom left corner of the window.
To actually render the text, we invoke its draw method, but we must do so in a draw handler. The draw event is called ‘on underscore draw’, so our handler function must have the same name when we register it with the window’s event method (which, again, the window object inherits from EventDispacher).
The reason you want to do your drawing in an on_draw handler is because, firstly, we generally want to draw on a regular basis, and unlike other events, the on_draw event is dispatched on every iteration of the run loop. Secondly, after dispatching the on_draw event, the run loop invokes the page flip of the buffers; if we did our drawing in other handlers, our drawing of a single frame might get improperly split across the double buffers.
Now let’s make things slightly more interesting with keyboard input. As mentioned, the run loop processes the windowing system events, then dispatches their Pyglet equiavlents as events on the window object. So, for example, when a key is pressed, the windowing system sends our program a key press event containing the information of which key was pressed. The pyglet run loop reads this and dispatches a on the window object an event called ‘on_key_press’, passing the keyboard symbol and modifier keys as arguments. (Modifiers are the keys ctrl, alt, and shift that might be held down when a key is pressed.) So to respond to key presses, we register on our window a hander called on_key_press.
In our example here, we’re going to have the space bar toggle the displayed text from hello, world to goodbye, cruel world. So we create two Label objects and assign the hello label to our label variable. When a key is pressed, on_key_press gets invoked. They keyboard symbols are represented by values in pyglet.window.key, and in this example, we’re checking if the symbol equals pyglet.window.key.SPACE for the spacebar. If so, we toggle which label object is stored in label. If hellolabel is already in label, we assign goodbyeLabel to label; otherwise we assign helloLabel to label. So each time we hit space key, the label object in the label variable gets swapped.
Meanwhile, at the end of each iteration of the run loop, the on_draw event gets dispatched, so our draw handler will run each time, drawing anew whatever label object is currently in the label variable. The important extra detail here is that, before drawing the text, we clear the window, that is we black out the framebuffer. Without using clear, we’d just be drawing over the current contents of the framebuffer and so would end up with text overlapping text instead of text replacing text.
So far, we have a roundabout facsimile of a game loop: we can get user input, change state based on this input, and then render this state on screen. But so far we’re only updating state as the immediate response to user input. Games generally need to update their state on a regular basis whether or not the user gives any input. So we need a timer, which is what the pyglet.clock module provides. Here we create a function update, and then register the function to get invoked on a regular basis, every 1/60th of a second, with the pyglet.clock.schedule_interval function. For reasons we discussed earlier, the timer can’t invoke our function at perfectly regular 60th of a second intervals because, for one, sometimes our update function might itself take longer than a 60th of a second to run, and secondly, because other things going on in our program or the system may interfere. So schedule_interval makes no guarantee, but tries its best, and so passes to our update function the actual elapsed time since the last run. Our parameter dt (short for delta time) receives this elapsed time as a floating-point value representing seconds. Usually, dt will be about 1/60th of a second, but it may sometimes be longer and occasionally slightly shorter.
In any case, what our update function does here is increment the y coordinate of our label every second. When the y coordinate goes past our max value, 400, it reset to the min value. So we should see the label start at coordinate y=200, rise to coordinate y=400, then loop back down to 200. And this looping will happen indefinitely.
Now, in our extremely simple code, assuming nothing aberrant is going on elsewhere in the system, the update function should run 60 times per second fairly consistently, so the label will rise 60 coordinate units every second. It should then take 3 and a third seconds for the label to go from the min y value to the max y value. But if we want to have the label move at a consistent speed no matter how quickly and consistently our update function actually runs, we should express the movement in terms of dt. To do so, we simply define a rate of movement per second, here COORDS_PER_SECOND = 100, and then we multiply dt by COORD_PER_SECOND to get how many coord units to move the label in each update. Now no matter how quickly or consistently the update function is run, the label should rise at 100 units per second.
Actually, this code has a problem because the Label object does funny things if you assign it floating-point coords. To get around this, we simply introduce an intermediate variable, ycoord, to store the floating-point value, and then convert its value to an integer when assigning to the label coord.
In any case, now we have the outline in Pyglet for a game. In our main module, we start out by of course importing pyglet and any other modules we might need, then loading resources and initializing the game state. Then we create our window and register handlers for the window events. We handle user input in the input handlers and draw the current game state in the draw handler. Then to update game state, we register an update function to run at regular intervals, say once every 60th of a second. It’s in this same update function that we generally start and stop any sounds to play. Finally, with our game state, windows, and handlers in place, we start the app with the app run method. And that’s how we can write a game in Pyglet.