Bloog Bot
The Game Loop
"Game loops are the quintessential example of a “game programming pattern”. Almost every game has one, no two are exactly alike, and relatively few programs outside of games use them." - Robert Nystrom, Game Programming Patterns
Game Engines are a fascinating topic. Designing a game engine poses considerable challenges. Rendering millions of polygons often times at 60 frames per second is an incredible performance implication. The floating point math involved in physics calculations can introduce non-deterministic behavior if not considered carefully. Synchronizing one server with multiple clients makes multiplayer game design its own beast. If you're interested in learning more, I strongly encourage you to check out Robert Nystrom's book "Game Programming Patterns" linked above (he's been kind enough to release it for free online).
If you're anything like me and you found the Gang of Four book a chore to get through, this book explores the same patterns from the perspective of game programming. Most of the nitty gritty isn't relevant to our discussion, however, we do need to understand the basics of how game engines work before we can continue our discussion about bot development. Specifically, we need to understand the game loop.
Consider the following Hello World program in C#:
using System; class Program { static void Main(string[] args) { Console.WriteLine("Hello World!"); Console.WriteLine("Press any key to exit."); Console.ReadKey(); } }
Consider how this program works. There's no looping or branching. It will run from top to bottom:
- Print "Hello World" to the console
- Prompt the user to enter a key
- Receive input from the user and throw it away
- The main method returns, and the program exits
A game wouldn't be very fun if this is all it did. Instead, games typically involve the player influencing the world by interacting with it using some sort of input device (mouse, keyboard, controller, joystick, etc). Most games also have some simulation running in the background. Enemies are controlled by artificial intelligence and will act of their own free will. Most games also have a graphical component, where the world is rendered to a display. And most importantly for our discussion, games continue running until the player turns them off. The pattern that marries all of these interrelated concerns is the game loop.
As I mentioned at the beginning of this section, game loops can be very complicated. For the purposes of this discussion, I'll be using a very rudamentary game loop as an example to demonstrate a few important ideas. The skeleton of our game loop looks like this:
int main() { while (true) { // handle user input // update simulation // render graphics } }
Unlike our Hello World example above, this program is going to run indefinitely until we close the window. Many of the applications you're familiar with using every day work like this. The program loops over and over again, listening for user input and acting accordingly. For our purposes, we aren't particularly concerned with rendering graphics, so we'll keep things simple and just print text to the console. We also don't need any NPCs with AI, and we won't be interacting with a physics engine, so we don't have a simulation to update. We do want to handle user input though:
#include <windows.h> int main() { INPUT_RECORD event; HANDLE hStdIn = GetStdHandle(STD_INPUT_HANDLE); DWORD count; // required by ReadConsoleInput, we can ignore it while (true) { // handle user input if (WaitForSingleObject(hStdIn, 0) == WAIT_OBJECT_0) { ReadConsoleInput(hStdIn, &event, 1, &count); // check for input events if (event.EventType == KEY_EVENT && !event.Event.KeyEvent.bKeyDown) // was there a KeyUp event? { switch (event.Event.KeyEvent.wVirtualKeyCode) { case VK_SPACE: // we'll do something here in a bit> case VK_ESCAPE: // exit on ESC return 0; } } } // update simulation // render graphics } }
Running the code above isn't very interesting. You'll get a console window, and hitting escape will kill the program. Let's add a Player we can interact with, and a Game object to hold a pointer to our Player. Normally you'd use a Game object to hold more than just a Player, but again, we're trying to keep this as simple as possible.
#include <iostream> #include <windows.h> struct Player { int level; int health; }; struct Game { Player *player; }; Game *pGame; int main() { pGame = new Game; pGame->player = new Player; pGame->player->level = 1; pGame->player->health = 100; INPUT_RECORD event; HANDLE hStdIn = GetStdHandle(STD_INPUT_HANDLE); DWORD count; std::cout << "Welcome to Bloog's Quest!" << std::endl; std::cout << "Player is level 1 and has 100 health." << std::endl << std::endl; while (true) { // handle user input if (WaitForSingleObject(hStdIn, 0) == WAIT_OBJECT_0) { ReadConsoleInput(hStdIn, &event, 1, &count); if (event.EventType == KEY_EVENT && !event.Event.KeyEvent.bKeyDown) { switch (event.Event.KeyEvent.wVirtualKeyCode) { case VK_SPACE: pGame->player->health--; std::cout << "After taking 1 damage, Player's remaining health is: " << pGame->player->health << std::endl; break; case VK_ESCAPE: return 0; } } } // update simulation // render graphics } }
And we're done! Let's take a look at what was added. First. we added two struct declarations, Player and Game. We added a global variable to hold a pointer to our Game object. Then immediately in the main function we initialize our Game pointer to point to a new Game object, and we initialize the Player pointer on the Game object to point to a new Player object. Then we initialize the level and health of our Player. We can also interact with our Player object. Pressing space will damage the Player, decreasing his health by 1.
It may not be particularly compelling, but this is a working game loop. We'll be referencing the code here throughout the rest of this discussion, but for now, let's set it aside and shift gears.