Please note that I don't update this web site anymore. You can visit my new personal blog at www.williamwilling.com.

A Closer Look At The Game Loop

Friday, July 22, 2005

When I presented a way to build a game engine last time, I mentioned the game loop, but I left out the implementation of the three steps. This time, I want to fill that gap.

The game loop needs to do three things: process the player's input, update the game world and render to the screen. It does this over and over.

Processing input

In this step you take the player's input and translate it to an action in the game world. For example, when the player presses the left arrow key, you might call the function MovePlayerLeft() and when the player presses escape, you translate it to GameEngine::Stop().

To process the player's input, you need to continuously check whether there is new input available. When the player presses a key or moves the mouse or something like that, his input gets stored in a queue, usually referred to as the event queue. Every time you process input, you need to retrieve the input events from the queue one by one and translate each of them to the appropriate action.

From now on, I will refer to the code that processes input as the controller.

Updating the game world

In all but the simplest games, something will happen inside the game world even if the player doesn't send us input. Enemies move around, bullets fly by, stuff like that. How you implement this step, completely depends on the game you are creating.

It's usually a good idea to make sure the game world runs indepently from everything else. This means that the game world doesn't know anything about input methods. It doesn't know whether it's controlled by keyboard, mouse or joystick. Also, the game world doesn't know about how it will look on screen. Bitmaps don't exist in the game world.

Rendering to the screen

Now that the game world is up-to-date, we can render it to the screen. The rendering code reads data from the game world and translates it to render calls. For example, you read the world coordinates of the player, translate those to screen coordinates and blit the appropriate bitmap to the screen at the right coordinates.

From now on, I will refer to the code that renders the game world to the screen as the renderer.

Schematic view of all steps that take place during a single iteration of the game loop, from the player providing input to rendering on the screen.

The steps that occur during a single iteration of the game loop. 1. The player sends input to the game, which is stored in the event queue. 2. The controller retrieves input from the event queue. 3. The controller issues commands to the game world based on the player's input. 4. All actions in the game world that don't rely on player input are performed. 5. The rendered reads the state of the game world and renders it to the screen. 6. The player can see the game world on the screen.

Learning by example

To illustrate how the game loop works, I'll present a simple maze game. There isn't much to it: you have to move your character through a maze from the start point to the end point. The code in this article that deals with reading input and drawing to the screen is pseudo-code, since I want this discussion to be library-independent. The finished example uses SDL for these things, though.

Keeping the engine clean

You'll recall that the game loop looks like this in code.

while (isRunning)
{
  Process();
  Update();
  Render();
}

The implementation of these three functions don't belong inside the game engine. They can even change during the execution of the game. So, we move them to a new class. I'll call this class State.

class State
{
public:
  void Process();
  void Update();
  void Render();
};

Since the game engine now needs to divert the steps of the game loop to another class, it needs to hold a State object and call member functions on it. This changes the code for the game loop slightly.

while (isRunning)
{
  state.Process();
  state.Update();
  state.Render();
}

Interfacing with the game world

The game world is completely seperate from the rest of the code, so it doesn't need to know how the controller and the renderer are implemented. However, the controller needs to send instructions to the game world, so it has to know which functions to call. And the renderer needs to retrieve information about the game world in order to do its job, so it has to know how to access the game world.

It's convenient to provide the interface to the game world through a single class. This way, the controller and the renderer always know where to turn to. If they need classes that live deeper inside the game world, they can get to them via this interface.

For our maze game, we'll put the game world interface in a class called simply GameWorld. The controller needs to tell the game world in which direction the player is moving, so we need functions for that: MoveLeft(), MoveRight(), MoveUp(), MoveDown(). In addition, the controller checks whether the player has reached the end of the maze using the function IsEndReached(). The renderer needs access to the structure of the maze as well as to the player's position.

class GameWorld
{
public:
  void IsEndReached() const;
  void MoveDown();
  void MoveLeft();
  void MoveRight();
  void MoveUp();
  
  const Maze& GetMaze() const;
  Location GetPlayerLocation() const;
};

For the implementation of this class, as well as the rest of the game world, check the sample code.

Controller

As explained before, the controller reads input from the event queue and translates the input to actions in the game world. How exactly you read the event queue depends on the library you use, but it usually goes something like this.

Event event = GetNextEvent();

switch (event)
{
  case KEYPRESS_LEFT:
    world.MoveLeft();
    break;
    
  case KEYPRESS_RIGHT:
    world.MoveRight();
    break;
    
  case KEYPRESS_UP:
    world.MoveUp();
    break;
  
  case KEYPRESS_DOWN:
    world.MoveDown();
    break;

  case KEYPRESS_ESCAPE:
  case WINDOW_CLOSE:
    GameEngine::Stop();
    break;
}

The switch block can become unwieldy when you have a lot of input to handle, but it's straight-forward and sufficient for our example.

Every time the controller is called, you should handle all events that are in the queue at that moment. It may seem like a good idea to handle only one event and then let the rest of the game loop run, but events tend to queue up pretty quickly. By the time the controller gets called again, a dozen or so messages may have been added to the queue, so if you only handle one at the time, you start lagging behind pretty badly very quickly.

while (IsEventAvailable())
{
  event = GetNextEvent();

  switch (event)
  {
    // etc...
  }
}

Updating the game world

The game world of our maze game doesn't contain any creature besides the one controlled by the player. Consequently, there isn't much updating to do in this game. In fact, the only thing that takes place during this step, is to check whether the player has reached the end of the maze and has thus won the game.

The controller is responsible for initiating the proper response when the player has won. In the sample code, the game kind of brutally quits at this point. It would be nicer if there'd be a victory message of some sort, but I'll leave that as an exercise for the reader.

Renderer

In essence, the rendering of the game world is a rather straight-forward affair: read the game world, render the bitmaps. Often, though, you want to render efficiently, since rendering takes a lot of time relative to the other parts of the game loop. So, you don't want to render stuff that won't be visible, either because it gets clipped or because other bitmaps are drawn on top of it. Nor do you have to render bitmaps that are still on screen from the last time the renderer was called.

There are a lot of things to consider when optimizing your renderer and I won't discuss them now. You can read my article on static render lists to get an idea about this topic. For now, my advice to you is to just re-render the entire game world on each iteration of the game loop and worry about optimizing later.

Sample code

To see how all the concepts discussed in this article add up to a complete game, check out the sample code (.zip, 15KB). It uses SDL for rendering and reading the event queue, so if you want to rebuild the code, you need to have SDL installed. I tried to keep the code portable, but I only tested it with my Microsoft Visual C++.NET compiler; just so you know. I've released the code under the zlib license, so you can pretty much do with it as you please.

Questions, suggestions, comments and criticism, all are welcome.

Back to blog index

Comments

GBGames says:

I'm trying to compile the code on my Debian system using G++, but I've hit a few snags: - some files complain about there being no newline at the end. I was able to get around this by editing the last line in vim on my system, as dos2unix didn't seem to work. - you include in Error.h, which is definitely not portable. B-) - FatalException.h apparently has a problem with the definition of the constructor: ****** In file included from Main.cpp:26: FatalException.h: In constructor `FatalException::FatalException(const std::string&)': FatalException.h:33: error: no matching function for call to `std::exception:: exception(const char*)' /usr/include/c++/3.3/exception:53: error: candidates are: std::exception::exception(const std::exception&) /usr/include/c++/3.3/exception:55: error: std::exception::exception() ****** Regarding your post, I think it is very informative. I will continue to tinker with the code to see if I can get it to compile under Debian Gnu/Linux.

Sunday, July 24, 2005 11:40 PM


Joost Ronkes Agerbeek says:

Windows.h should of course only be included on Windows machines. I wrapped the include in #if WIN32 ... #endif. Appearently, the std::exception-constructor that accepts a C-string isn't standard. I resolved the problem by making FatalException derive from std::runtime_error instead of from std::exception. Should work okay now. I added end-of-line characters at the end of each file. Please, let me know if everything works now.

Monday, July 25, 2005 1:29 PM


GBGames says:

Another thing I found is that when you include SDL, it is apparently suggested that you do so in the following way: #include "SDL.h" rather than #include Filenames are case sensitive on platforms like Gnu/Linux, and for apparently "" as opposed to is just more portable, according to the libsdl.org site.

Monday, July 25, 2005 7:24 PM


GBGames says:

Well, I finally got it to compile on my Debian system. I had to make a few changes, although some changes might not be necessary and I just didn't know how to compile it otherwise. I created a Makefile for people to use as well. You can find the .zip and the .tar.gz files here: http://www.gbgames.com/downloads/ronkes/

Monday, July 25, 2005 8:02 PM


Joost Ronkes Agerbeek says:

Thanks for taking the trouble to make the code more portable. According to Stroustroup, you use "" for header files that belong to your current project and for external libraries. That's the convention I stick to, but I'll try to use #include "SDL.h" whenever I post code. You are probably right about the int 3h statement being x86-specific. I realized that, but somehow I couldn't be bothered to change it. Maybe because I don't know a cross-platform way to generate a user breakpoint... From The C++ Standard Library by Nicolai Josuttis: "The base class exception and class bad_exception are defined in . Class bad_alloc is defined in . Classes bad_cast and bad_typeid are defined in . Class ios_base::failure is defined in . All other classes are defined in ." He's talking about exception-classes, of course. So, you are right: now that I changed the base class from exception to runtime_error, you should include . The code I provided passed Microsoft's compiler, though, so I didn't notice. Of course, I could just have said, "you are right on all accounts"; I just wanted to clarify all points. :-)

Friday, July 29, 2005 1:22 PM


GBGames says:

I also thought that it made sense to use #include , but it is apparently more portable, and it is suggested by libsdl.org. I asked about this same issue in one of my "Learning Kyra" posts, and someone on the Kyra forums explained it. Glad I could help!

Monday, August 1, 2005 7:56 PM

Tell me what you think

Since I'm not updating this site anymore, I disabled comments. You can visit me at my new site: www.williamwilling.com.