Understanding the basics of a game architecture
In order to better understand the fundamentals of a typical game architecture at a high level, we can begin with a historical perspective with the first text-based adventure game Adventure.
The high-level game loop of Adventure
The following figure (Figure 1) describes the high-level game loop of Adventure type text-based games where the game would block all updates for its lifetime until it receives user-based text input on the command line:
Once the text was read from the command line (carriage returns were typically an indication that the player was finished inputting text), the game would then parse the entire string, breaking up the sentence into its component parts. The most basic system would have a dictionary of actions, or verbs, that the player can do. Each of the words in the sentence would be compared against that player's action list. The same would be for objects, or nouns.
For example, if the player wanted to take the fresh water from a flask and pour it over the dirty floor, they would type in the following command:
> pour water
The game would process the user input string, break it up into word chunks, and compare against its verb and noun dictionaries (key-value mappings). We would end up with two lists. The action list would contain one valid action, POUR. The object list would contain one valid object, WATER.
POUR would be the primary action in this sentence with a rule similar to the following:
POUR [OBJECT 1] IS SUCCESSFUL IF PLAYER HAS [OBJECT 1] IN INVENTORY AND [OBJECT 1] IS POURABLE
There would also be some extra data regarding certain properties of objects in the game, such as whether an object is able to be carried in an inventory and whether it is pourable, wearable, or throwable. These could be in the form of a subset list of objects for each of the actions. For example, POUR would verify that WATER is in the list POURABLE, while something such as FOOD would not. These edge case checks with object attributes would prevent awkward word combinations as follows:
> pour food
Checking the words against action and object lists would also have the side-effect of throwing out extraneous words that make English sentences complete. For instance, in some text-based adventure games, the following two commands would work the same way:
> go into the building > go building
This model is the basic concept behind a two-way communication subsystem that deals with NPC interaction, such as asking NPCs questions or viewing shop items. This will be discussed more in-depth when developing a dialog tree system.
The difficulty with a text-driven system is that because the syntax of the parser is so specific, and because of the complexities with equally valid variations of an English sentence, the player can lead down a rabbit hole of frustration, infamously referred to as guess-the-verb or guess-the-noun, where most of the player's time is spent trying to figure out why certain combinations of words do not work. One example of this problem is best demonstrated with a recent session I had with an online version of Adventure:
You are standing at the end of a road before a small brick building. Around you is a forest. A small stream flows out of the building and down a gully. > look stream I don't understand. stream what? > look at stream I don't understand that! > go into building I don't understand that! > go Where? > building You are inside a building, a well house for a large spring. There are some keys on the ground here. There is a shiny brass lamp nearby. There is tasty food here. There is a bottle of water here.
Interestingly enough, when distilled down to its essence, this model is also how event-driven systems such as user interfaces work today.
The high-level event-based loop
The primary target platform for this book is Microsoft Windows, even though compiling for the other target platforms isn't much more effort. There are special considerations to keep in mind when running your game on mobile devices, such as graphic rendering performance, smaller screen real estate to work with for UIs, touch screen controls instead of using a keyboard and mouse, limited access and size constraints to external save game profiles, and smaller overall package size. Mobile device optimizations are topics that deserve their own chapters, but will be beyond the scope of this book.
The following figure (Figure 2) refers to an event loop, for instance, in how Windows processes its graphical user interface events. This figure can even be generalized across most platforms, including how Java processes its own event loop within the Java Virtual Machine (JVM):
At a high level, a graphical user interface (GUI) application processes events from the event loop and only terminates when the application receives a quit message. The operating system (OS) will generate an event message based on such events as a mouse button click or keyboard key press and place the message in the message queue. The message queue is processed by the GUI application by processing the first element in the queue, on a first-in-first-out basis, with the newest event message at the end of the queue. The GUI application will then in turn check the message queue for a relevant message, process it if the event message is relevant to the window, and then dispatch the message. The message dispatch will forward the message to the registered callback procedure or message handler associated with that specific event. So, just like the loop for text-based adventure games we saw earlier, the event loop is really just a loop that runs indefinitely, responding to input type events.
The high-level game loop for a graphic-based video game
The following figure (Figure 3) demonstrates at a high level how a graphics-based video game loop actually functions at its core:
Figure 3 demonstrates at a high level how a graphics-based video game loop actually functions at its core. As previously discussed, a text-based game loop and a GUI event loop share a common methodology of blocking, an interrupt-based approach used for waiting on user input. This model would not work for graphics-based games today because there is always something that needs to be updated every cycle in the loop even if the player is idle, such as AI (NPC movements), physics, or particle effects. Instead of waiting for user input, a game loop polls for events, processing all user input available at that time. Once processed, the loop will then step into the game objects that need to be updated based on the current state of the game world, such as enemies moving and resolving collisions. Once these updates are complete, the loop will then draw the graphics based on the update calculations done previously, rendering them to the screen (or back buffer) when ready to display. This loop then starts again for the next cycle in the game loop.
One cycle of the game loop, represented by Figure 3, is generally referred to as a frame. The logical question is how fast can we cycle through each frame in the game loop?
The term used to gauge how many cycles we can complete in a fixed amount of time, measured in frames per second (FPS), is called the frame rate. The more frames we are able to process per second, the better the perceived experience will be for the player. The game will feel more responsive, there will be better collision detection and enemy movement, and the graphics rendering will be much smoother. This is because we are polling user input, updating, and rendering much more frequently. The lower the frames per second, the more degraded the game experience will be for the player. The player movement will be jerky and not as responsive, there may be collisions that never get detected with objects appearing to go through each other, enemies may appear to teleport across the screen, and the graphics will appear to be very choppy. This is usually caused by polling much less frequently than what is needed. In modern games today, for example, a frame rate of 30 FPS is standard for a good gameplay experience.
There are two primary factors that affect the frame rate of a game:
- The first factor is how fast the underlying target system can process each frame. This factor is influenced by the system's hardware such as the clock speed of the CPU, and whether there is a dedicated graphics processing unit (GPU) available to offload rendering. Another factor is the software of the target system, specifically how effective the OS is at parallelizing across multiple cores and how efficient the scheduler is.
- The second factor is determined by how much logic there is to process every frame. Calculations for physics (used in collision detection or velocity updates) and rendering high-fidelity graphics for lots of game objects can affect the amount of work that needs to be accomplished every frame, leading to a frame taking longer to render. Therefore, fewer frames are completed every second.
Given the myriad platforms that the game can run on, these two factors will cause very different experiences based on the platform it's running on. On a mobile phone, the game may not have access to a dedicated GPU, and so the CPU becomes the bottleneck for calculating all the user input, physics, AI, and rendering causing the game to run at a low frame rate. On the flip side, if you have been developing your game on a mid-range system for the last two years, when you release your game, your game may run at a much higher frame rate on newer hardware than what you are accustomed to in your testing. This not only can cause bugs you didn't expect, but also high battery consumption on mobile devices and laptops or hot CPUs and GPUs.
The typical, brute force solution for dealing with these factors that affect the frame rate is to lock the frame rate so that the experience on the various platforms is a consistent one. This is not an optimal solution though, because if the refresh rate of the monitor is not synced with the locked frame rate, then you can get visual artifacts such as screen tearing. Screen tearing usually occurs because multiple frames get updated to the screen during draw calls before the monitor finishes its current refresh cycle.
LibGDX addresses the problem of varying frame rates depending on the device, by passing in a deltaTime
value during each render call for a frame. This value represents the total time in seconds that the game took to render the last frame. By updating calculations using the deltaTime
value, the gameplay elements should be synchronized running more consistently across the different devices, using this time-based approach instead of just locking the game to a specific frame rate.