Monday, May 23, 2016

Video Game Design: Design Patterns

Design Patterns are common abstract patterns or methodologies used in programming, that tend to be useful for a large range of problems.  If you are not familiar with the concept, let me recommend spending a few minutes learning about the general idea of design patterns.

In video game development, there is a large collection of design patterns that are commonly used.  Many are used only in very large games, where problems can arise without careful management and organization of code.  There are a few that are used almost everywhere though.  This article is going to discuss the three most commonly used video game design patterns, and we will also briefly discuss one design pattern that is widely used in nearly every domain of software development.  Knowing these design patterns can make video game development much faster.

Before we start, there is one other thing to keep in mind: It is very rare when a design pattern will work for a specific problem in its pure state.  Design patterns are not careful recipes that must be followed perfectly for satisfactory results.  They almost always need to be tweaked and adjusted to fit any specific application.  Think of a design pattern as a template or starting position, and then customize it to fit your needs.

Game Loop

This is an essential design pattern in video game design.  It is so important that a vast majority of games end up using it, whether the developers know about the design pattern or not.  It may even seem so obvious that it does not need to be stated.  Understanding this design pattern can allow you much more flexibility in customizing it to your needs though, so there is significant value in discussing it.

In a nutshell, the Game Loop design pattern is implemented by making an infinite loop (with some means of escape) that executes all of the game operations within it.  This includes handling user input, updating the game state, and drawing graphics.  It may include other output, like playing audio, and it may include things like networking.  Once the game has started, nothing happens outside of the game loop, unless the user produces input telling the game to terminate.

The game loop has several responsibilities that may not be initially obvious.  The most important one is to decouple time from input.  By default, operating systems only support text input and they buffer program input.  This means that all input takes the form of text, and when the program requests input, it stalls and waits until the os provides it.  The os presents the user with a prompt, and it buffers the input until the user presses Return or Enter.  In most video games, this is unacceptable.  If the game is real-time, it needs to be updating the game state regularly, even when it is waiting for user input.  It also needs to be redrawing the graphics to the screen regularly, whether it is real-time or not.  If the program is waiting for the os to return with user input, the game cannot do this.  Decoupling time from input allows the game to keep running even when the user is not providing input.  The game loop must process input when it comes, but it should not wait for it to become available.

The second responsibility of the game loop is to decouple time from processor speed.  This can be more difficult, and it may be outright impossible with a high performance game on a very slow system.  Long ago, most games relied on processor speed for time regulation.  The game loop would do enough stuff that the processor could just keep up at a reasonable pace.  This lead to a serious problem as processor technology improved.  Games that ran at an acceptable pace on an old processor would run so fast that they were unplayable on newer, faster processors.  The initial solution was the addition of a "turbo button" to new machines.  This button could be used to reduce the clock speed of the processor to make it equivalent to the older machines, thus allowing the games to run at the right speed.  This worked, but it was a poor solution.  As machines improved and as new games were made for each new processor speed, turbo buttons would have to offer more and more clock speeds, and the processors would have to support those speeds.  This was an untenable long term solution, so game developers had to find a better solution.  Their solution was to explicitly regulate game speed in the game loop.  This is part of the responsibility of the game loop.

In a video game, the game loop is the most important part of the code.  It is the main bottleneck.  If it runs slowly, the whole game will suffer.  If it does its job well, the game will be able to run smoothly on any system fast enough to handle the game.  This design pattern is used in nearly every video game, but it is practically non-existent outside of video games.  There are some non-game applications that could benefit from it though.

 The typical outline for a game loop has several elements.  The first is a while loop.  This is the game loop, and it must run indefinitely.  It can either use true for its condition, or it can use a condition variable that the input processing can change if the user wants to exit the game.  The game loop should be expected to run for as long as the user wants it to run.  Inside the game loop, three are three more elements.  The first is input processing.  The second is updating the game state.  The third is redrawing the screen or rendering.  If these are the only thing in the game loop, order is not important, however, if there is also a network handler and an audio player, order can affect how the player perceives the responsiveness of the game.  If too much stuff happens between input processing and updating the game state, it will introduce input lag, which can be very frustrating.  If too much stuff happens between updating game state and rendering graphics, it can make it appear like there is input lag, which is generally just as bad.  The most important thing, though, is making sure the entire game loop runs at an acceptable speed.

Speed is essential for a game loop.  The speed of the game loop dramatically affects how the user perceives the game.  A slow game loop will result in choppy graphics and laggy input, both of which can make a game unplayable.  It is generally a good idea to know how fast your game runs on the systems you expect it to be played on.  With the default game loop configuration, the most common way of measuring performance is in frames per second (FPS).  Technically FPS refers exclusively to graphics frames rendered each second, but when the frame rate is tied to the game speed, it is an accurate measuring device.

The speed of your game loop will be affected by a number of things.  Input handling generally does not take much time.  Updating game state, however, can take a significant amount of time.  This is largely dependent on the complexity of the game physics and the number of objects that need to be updated.  Rendering graphics often takes even more time.  This depends mostly on the number of objects that must be rendered, though with 3D graphics, the complexity of the objects as well as things like shaders can play a big part.  In addition to all of this, there is also hardware.  The processor, memory, video card, sound card, and operating system all play a role in how fast your code runs.  Understanding these things can help you to determine the system requirements for your game, and it can also allow you to optimize your game to run on less powerful systems, by reducing or limiting complexity.

In games, it is common for the number and complexity of game elements to vary.  One area might have tons of objects, while another might be barren.  This will affect the speed of the game loop.  The game play needs to maintain a consistent speed, despite this.  The game play should not be faster when there are fewer game elements on the screen and slower when there are more.  It should appear to the player to have a consistent speed at all times.  Likewise, the game speed should be consistent across different hardware.  The game should not run at different speeds on different hardware.  This is especially important when it comes to networked games.  There are several ways to maintain consistent game speed, though they all have cases where they will fail.

Managing timing in a game loop is called scheduling.  There are many ways to handle scheduling, but three of them are most common.  The first is merely adding delay.  To add delay, you begin the game loop by getting the current system time.  At the end of the loop, you calculate the time elapsed, and then you subtract that from the amount of time you want each game loop to take, and you have the program sleep for that amount of time.  This will force the game loop to run at a specific rate, and it will allow the operating system to do other things while the program is sleeping.  The problem with merely adding a delay is that it only helps if the hardware is fast.  If the hardware is so slow that the processing in the game loop takes longer than the time you want each loop to take, the game will run slow.  If you never expect your game to run on hardware this slow though, just having a delay is fairly simple and efficient.

The second common scheduling method is called variable or fluid time.  Essentially, it scales game state updates to the time elapsed.  All updating functions in this method take the time elapsed as an argument, and this time is calculated at the beginning of each game loop.  All movement or progression calculations are scaled based on the time passed.  For example, if a character should move 5 pixels per second, then the movement distance each frame is 5 pixels divided by the time elapsed in that frame.  The benefit with fluid time is that the game play speed will never deviate.  Even if the game is running at only 5 frames per second, the speed of game play will be consistent (though, the game will not really be playable).  This can also allow for very accurate physics on very fast system.  There are two problems with this scheduling method.  The first is that it requires a lot more math, and all time dependent update math must be floating point math, which is generally slower than integer math.  This will likely make your game run at least slightly slower.  In most cases, the fact that the game uses fluid time will make up for this problem though.  The second problem is that fluid time makes keeping games synchronized over a network very difficult.  A fast system is likely to accumulate more floating point error over time than a slow one.  In most cases this won't make a noticeable difference, however when two players are playing over a network, it will.  Adjusting for the differences in floating point error will require significantly more network communication, which could result in unexpected behavior, lag, or other problems.  Fluid time is generally a bad idea for a game that is going to be networked, and it may have performance problems on slower systems due to the extra math.

The third common scheduling method is frame skip.  Since rendering is typically the bottleneck in a single threaded game, skipping a frame when the game is behind can quickly allow the game state to catch up.  This scheduling method starts with the delay method.  The delay method caps the frame rate, but it cannot help if the speed is too slow.  The frame skip method keeps track of total time elapsed minus the time that should be taken by the game loop.  If this number ever exceeds the time expected for two frames, rendering is skipped until the game catches up.  There are a few ways to do this.  Either the rendering can be in an if statement that only runs the renderer if the accumulated time is not too large, or the game state update code can be put in a while loop that keeps running it and decrementing the counter until it is caught up.  Because this is more complex than the other two, I will provide some pseudocode:
elapsed = 0
t = time.time()
while:
    elapsed = elapsed + time.time() - t
    t = time.time()
    processInput()
    while (elapsed > 16):
        update()
        elapsed -= 16
    render()
The important thing with this is that we are not recalculating the elapsed time each loop.  We are adding the currently elapsed time to the leftover elapsed time from the last update.  You may also notice that this does not have a delay.  Ideally, there should be a delay for 16 - elapsed at the end, however, without it, the renderer and input handling are allowed to use extra time, which will make the game more responsive on a fast system.  It is a trade off.  With the delay, the game consumed less power and gives its extra time to the operating system.  Without the delay, it consumes more power, but it also is more responsive.

Frame skip comes with its own problems.  First, if the game state update takes longer than the expected time, each game loop will take longer than the last, because more and more time will be spent trying to catch up.  This effectively results in infinite lag.  If the update time is too long, we can get stutter in the graphics, because the game is not being updated frequently enough.  Multithreading can be used to put the renderer and input handling in a separate loop that runs concurrently with the game update, and this can mitigate a lot of the problems, even on a single core system.

There is a fourth option for scheduling.  Many game libraries and frameworks include scheduling mechanics that you can use.  Writing your own game loop gives you a lot of flexibility and the ability to customize the scheduling to your game, but it can be more work than using an existing system.  Using an existing system is generally easier, but it is also far less flexible.  That said, in some cases, there is no other option.  For example, JavaScript in browsers does not allow scripts to run indefinitely.  Instead, you use a callback system that handles the scheduling for you.  You can tell it to run your functions on a given schedule, but you are forced to rely on the browser to keep that schedule, and it is rarely accurate.  Generally you will get better results by writing your own scheduling system, but sometimes it is undesirable to spend the time on it, and in some cases it is even impossible.

The game loop pattern is probably the most essential design pattern for any video game.  It is the root of the game, where everything starts from.  It is also where scheduling will need to take place.  Getting your game loop right is often the most important step in making sure your game runs smoothly.

Update Method

The Update Method design pattern is commonly used in object oriented games.  Heavy object orientation is not always good for video games, because it comes with performance costs, but this design pattern is not very difficult to adapt.  The Update Method Design pattern makes handling a heterogenous mixture of game entities much easier.

In an object oriented game, game entities, including characters, in-game objects, and other parts of the game (like the map) are represented as objects (class instances).  Typically all of these objects need to be updated every game loop.  There are many ways to do this, but the Update Method design pattern is one of the most common.  Somewhere a list of game objects that need to be updated regularly is kept.  In the game loop, when the function is called that updates the game state, that function iterates through the list of game objects calling the update method on each of those objects.  Most commonly, this method is just called "update".  Every object that might end up in the list of updateable objects needs this update method.

Implementation of this is language specific.  In Java, it is common to use an interface.  In C++, inheritance will probably be used.  In Python, object type is not important, so long as all of them have an update function with the same name.  What is important is that anything that will need regular updates has this function.

This design pattern can even be adapted to non-OOP languages or games, with a bit of work.  Even in the worst case, and update function can be made for each entity type, and then the different entity types can have their own lists.  The main update function can then iterate through the lists, passing each element to the appropriate updater function.

The implementation of the update method will depend largely on your scheduling method.  If you are using any kind of fixed frame scheduling (the game loop is run at specific intervals), then the update methods can assume that the amount of time passed is always the same.  If you are using a variable time scheduling method, then the time elapsed since the last update will need to be passed into each update method, and the update methods will have to use that for scaling.

Update Method is a very useful design pattern for keeping your game organized and easy to maintain.  It makes adding new game entities much easier, and it helps to keep your code understandable.  It does come at the cost of a lot of function calls (a simpler system might loop through game entities, operating on them directly), but often it is worth the small performance cost.

Component

The Component design pattern is not as general purpose as the previous two, but it mitigates a major problem in game development.  That problem is coupling.  A program with high coupling has a lot of interdependent code, which means two things.  First, high levels of interdependent code means that if one piece of code breaks, it is likely to have wide reaching effects.  This can result in a lot of time spent finding and adjusting code scattered throughout the program, just to fix or modify something small.  Second, high levels of interdependent code also makes maintenance very difficult.  Unfortunately, in video games, this is very difficult to avoid.  A single character might need to interact with the input system, the graphics system, the physics system, and the sound system.  The menu system also needs to interact with all of those except the physics system.  The map system probably needs to interact with the graphics system, the physics system, and the sound system.  A small change to the sound API will require all three of these to be adjusted, and if the game has 20 or 30 different characters, this may require changes to 32 different pieces of code.  In a large video game, with characters, items, terrain, destructable and non-destructable terrain features, a menu system, and so on might end up having hundreds of places in code where a simple API change requires adjustments.  Further, a tightly coupled game can become a development nightmare, because a character designer might end up having to poke around in sound system code, and the sound system coder may end up having to figure out exactly who an API change affects, which can result in a lot of people working on the same pieces of code.  This can end very badly.  The Component design pattern helps to separate concerns by centralizing API usage and packaging components.

The Component design pattern also helps with one other thing.  While OOP tends to extol the virtues of class hierarchies, complex inheritance trees can cause all sorts of problems, including maintenance issues and performance issues.  The biggest problem though, is that inheritance tress are not a very good model for the kinds of objects often found in video games.  The Component design pattern generally uses only one class type for a given entity type (for example, all characters, including player character, NPCs, and so on, use the same object type).  Instead of an inheritance hierarchy, it composes entities out of components.  This results in a trivial inheritance hierarchy, it minimizes performance issues, and it allows for a much wider range of options.

The Component design pattern uses an object as a template.  That object has a collection of component containers, generally one for each component type.  These typically include an input component, a physics component, a graphics component, an audio component, and an AI component.  For a player controllable character, the input component might take input directly from the keyboard and mouse.  For an NPC, the input component will dynamically generate input for the object, probably based on the AI component.  The specific collection of components define the behavior, look, and sound of the object.

Components should not interact directly.  Sometimes this is unavoidable, but it should be avoided where possible.  To facilitate this, components should also contain their own data, except when sharing is necessary.  For example, the input, AI, and physics components might all need to access the location of a character.  If all of that type of component object need location data, the object might be a good place to store it.  Otherwise, it might be better to store it in physics, and either allow some direct interaction, or create a place for interaction to take place indirectly.  In some cases it may even be a good idea to create a special component specifically for communication between other components, though this can increase complexity.

The Component design pattern is generally used when high flexibility is necessary due to having a large number of necessary entities.  A database of hundreds of classes for different types of monsters can be very difficult to manage.  Often there are many different graphics, but there are 20 or fewer behaviors, and the other differences are trivial (max HP and such).  Instead of classes, you can create a collection of component classes, and then create factory functions that will compose the appropriate monsters from the different components.  Not only does this make things more manageable, it can also allow for combinations that would not have been possible otherwise without creating new classes for them.  For very small projects, the Component design pattern may be overkill, and because it requires OOP for easy implementation (there are non-OOP ways to implement it), it will affect performance, but when organization and manageability are serious concerns, it can be very valuable.

Observer

This is the bonus design pattern.  Unlike the previous ones, which are primarily limited to games, the Observer design pattern is very commonly used in nearly all domains of software development.  It is incredibly useful for handling communication between different parts of a program.  It is not as efficient as some other methods, but its centralization of communication can make development and maintenance much easier.

The Observer design pattern allows objects to registers as Listeners.  A Listener generally wants to know about a specific type of event.  The event handler keeps a dynamic list of registered Listeners for each type of event.  When an event occurs, all Listeners registered for that event are notified.

A Listener is generally an object that cares about an event.  In OOP languages, Listeners are implemented through interfaces or inheritance.  Each event type has its own Listener class or interface, and anything that cares about that event type inherits.  The Listener class or interface has a single virtual method that is the event handler for the appropriate event, and the child is expected to implement the listener.

The event handler has collections of Listener objects that care about various events.  It has a functions for registering Listeners and it has functions for unregistering.  When a Listener needs to start responding to events, it is registered.  When it needs to stop it is unregistered.  This allows any number of any type of object to listen for specific types of events.

The Listener objects are responsible for handling the events themselves.  Generally, they execute some kind of behavior in their event handling code, though in games this may not always be ideal.  In general, state updates should take place in the update methods, not in the event handling methods.  State updates in the event handling methods can interfere with proper scheduling, by making the event handler take longer than it should.  The Listener objects need to manage their event handling time wisely, otherwise it can interfere with performance.

There are a few options for event handling.  The first is to just do everything in the event handler.  As mentioned, this can cause problems, but if none of the event handling is especially intensive, it may be the best option.  For example, in a platformer, pressing a button might trigger an effect. If that effect just moves an object from one location to another by changing its coordinates, without any other effect, then doing it in the event handler is probably the most efficient way to do it.  On the other hand, if pressing the button starts the character moving in a specific direction, it would probably be better to just set a flag or a velocity variable and handle the actual movement in the update method.  In some cases, a Listener might even need its own internal event queue, and the event handler just adds events to the queue to be handled during updates.  This uses more memory, but it also minimizes the work done by the event handler.  In general though, if the event handler can just make small state changes that will affect the update behavior, that is all you need.

The Observer design pattern is not just useful for user input.  A well designed event handler can also be given internal events.  A great example is messaging the sound system from the physics system.  In an ideal system, the physics system should never play a sound directly.  If it did, it would be susceptible to problems from API changes to the sound system.  Instead, the physics system would send a message to the sound system saying what event happened, and the sound system would play the appropriate sound.  The problem is, if the physics system is calling a messaging function of the sound system, the API problem still exists.  Instead, the physics system can create a new event and send it to the event handler.  If the sound system is listening for physics events, it will be notified of things like the character hitting the ground, and it can play the appropriate sound.  This way, the sound system does not have to know about the physics system, and the physics system does not have to know about the sound system, but they can still communicate effectively.  The communication won't be quite as fast, but in most cases, it won't make any difference.

The Observer pattern can also be implemented in non-OOP languages, but it is much more difficult.  In languages that allow passing and storing functions like variables (Python, Haskell...), you can store listener functions instead of objects.  This is usually no more difficult than passing objects, and it may even allow for more targeted event handlers, since the name of the function is irrelevant.  In languages like C though, the only option is to pass function pointers, which requires function pointer arrays, and even arrays of function pointer arrays.  This is possible, and it works very well (I have done it), but it requires impressive pointer skills, and debugging it can be awful.

The Observer pattern is very useful in many applications.  In games it can make event handling much more organized, and more importantly, it can reduce coupling, making code much more manageable and maintainable.


These are just four design patterns commonly used in video games.  By themselves, these four can really make game development easier, but there are many more that can also be very useful.  This book is a great source of information about design patterns used in video games, and it even discusses the ones discussed here in more depth, including examples.  Anyone who is serious about learning game design and development would benefit greatly from reading this book: http://gameprogrammingpatterns.com/contents.html

No comments:

Post a Comment