Allegro.cc - Online Community

Allegro.cc Forums » Programming Questions » Game Loops

This thread is locked; no one can reply to it. rss feed Print
Game Loops
wegstar
Member #5,653
March 2005
avatar

In the past I've been making simple games with a simple game loop where I handle input, do logic calculations, draw, and then cleanup. However, on my current, large 2d project, I realized that this method no longer applies well in the "larger games" arena. This method, when used, kills the game's FPS. So far the current rate is 25, and I haven't implemented all the features yet. Now some of you would argue that 25fps is fine for 2d games, however, I haven't implemented much of the game yet.. So I was wondering if you guys have any suggestions for optimizing this simple loop method or have a better method altogether...

Programmer's Drinking Song: 99 programming bugs in the code/99 programing bugs/Fix one bug/compile it again/now there's 100 bugs in the code! (repeat until bugs==0)

miran
Member #2,407
June 2002

You're doing something wrong with the drawing if you only get 25 FPS. A typical game loop looks like this:

while (!done) {
   while (timer_ticker > 0) {
      process_one_frame_of_logic();
      --timer_ticker;
   }

   if (need_to_redraw) {
      draw_one_frame_to_the_backbuffer();
      blit_buffer_to_screen();
   }
}

Any major deviation from this is likely to be wrong.

--
sig used to be here

TeamTerradactyl
Member #7,733
September 2006
avatar

wegstar, could you provide a sample of what your code's loop currently looks like?

int main(void)
{
  ...
  while (!exit_game)
  {
    get_userinput();
    update_animations();
    move_npcs();
    move_player();
    play_sound();
    blit_to_screen();
  }
  return 0;
}

This is a generic loop you might see in different peoples' code. Are you saying that this is where all of your FPS slowdown is coming from?

Paul whoknows
Member #5,081
September 2004
avatar

Quote:

Now some of you would argue that 25fps is fine for 2d games

NO! it should not run at less than 60fps.

Quote:

I realized that this method no longer applies well in the "larger games" arena

OOP is the only way to write complex games IMHO.

____

"The unlimited potential has been replaced by the concrete reality of what I programmed today." - Jordan Mechner.

Hard Rock
Member #1,547
September 2001
avatar

Or if you want to ruthlessly abuse the CPU: (This is so you can see how fast your CPU maxes out).

while (!done) {
   while (timer_ticker > 0) {
      process_one_frame_of_logic();
      --timer_ticker;
   }

   while (timer_ticker <=0) {
      draw_one_frame_to_the_backbuffer();
      blit_buffer_to_screen();
      fps++; //<--- For kicks you know.
   }
}

_________________________________________________
Hard Rock
[ Stars Dev Company ][ Twitter ][Global Warming: ARA My TINS 07 Entry][Pong Ultra Website][GifAllegS Ver 1.07]
"Well there's also coolwebsearch but we'll let that be an IE exclusive feature" - arielb on the New Browser Plugins "What's better, HTML or Variables?"

wegstar
Member #5,653
March 2005
avatar

My code roughly translates to this:

int main(void)
{
  ...
  while (!exit_game)
  {
    get_userinput();
    update_world();
    update_player();
    update_npcs(); // loop through a global list of npcs and run their update funcs
    //draw some extra things here
    blit_to_screen();
  }
  return 0;
}

When I removed a portion of the code where I did drawing in a for loop, I had a 5 fps increase...

for(int i = 0; i < int(player.bullets); i++){
     blit_bullets_to_buffer(x*i, y); // (x position, y position)
}

But normally, doing simple logic calculations involving setting variables shouldn't cause too much latency..correct?

Programmer's Drinking Song: 99 programming bugs in the code/99 programing bugs/Fix one bug/compile it again/now there's 100 bugs in the code! (repeat until bugs==0)

miran
Member #2,407
June 2002

Are you loading and unloading and data inside the loop? If so, then don't. Are you doing any drawing in the logic code? Don't do that either. How do you measure the framerate? Why aren't you using timers?

--
sig used to be here

Kibiz0r
Member #6,203
September 2005
avatar

Okay, as requested, here is a bad summary of Game Coding Complete's architecture.

You have three separate systems, that communicate via a message system that is controlled by the Game Application Layer.

  • Game Application Layer

  • Game Logic Layer

  • Game View*

*Game View is actually a list of Game View interface objects that can be local players, AI agents, or networked players.

Example of message system:
"A" Button is pressed on keyboard

Human View checks for button presses, translates button press to game message "Apply Emergency Brake" and sends it

Game Logic receives message and changes the state of the player's car, sending a message back to the Human View: "Car State: E-Brake On"

Human View responds to the message by maybe changing the visual representation of the car to have its tires lay down rubber or something

---Game Application Layer---
Connection and disconnection of devices like keyboards and mice
Files
RAM
Timing
Language DLLs
Threads
Network connection
Core libraries
The main loop (AppLayer->Run();)
Init & Shutdown

---Game Logic Layer---
**you may have different implementations of the Game Logic that you swap in and out depending on whether the player is in server or client mode, single player or multiplayer, whatever**
Game State & Data Structures
Physics
Events*
Process Manager**
Command Interpreter***

*Send messages such as "time bomb created" so Game View can play a ticking sound and make a visual representation of it and so AI agents can run from it, etc **Processes are anything that needs to run every loop; this is essentially your Update() function (they are esentially threads, but in a cooperative multitasking environment rather than this mutex locking, time-wasting jazz) ***Takes the messages from the Game View and does something with them inside the Game Logic, like applying the E-Brake and then sending a message back to the Game View so it knows to lay down the rubber trails

---Game View (Human)---
Game display
^- the scene
^- GUI
^- FLIC anims
Audio
^- sfx
^- music
^- speech
Input interpreter*
Process Manager** (yet again!)
Options

*Should be obvious what this does... **For button animations, little graphical tricks, anything that needs to have the equivalent of an Update() function

---Game View (AI agent)---
Stimulus interpreter
Decision system
Process Manager
Options

Rather self-explanatory

In addition, you may have a "Remote Game View" derived class that gets messages from a server game logic instead of a local game logic. If you need me to explain that in detail, ask me to.

That's the basic setup, here's the execution of it.

//entry point for the file
GameAppLayer g_app;

int main(int argc, char *argv[])
{
    if (g_app.Init())
    {
        g_app.Run();
    }
    g_app.Deinit();
    return g_app.GetExitCode();
}

1//GameAppLayer.cpp
2//This is actually a bit hackish because I just quickly adapted it from DirectX,
3//but the basic setup ought to be the same
4void GameAppLayer::Run()
5{
6 while (m_bIsRunning)
7 {
8 while (m_LogicTime == 0)
9 {
10 rest(0);
11 }
12 float renderTime = 0.0;
13 while (m_LogicTime > 0)
14 {
15 Update(m_TotalTime, m_LogicTime);
16 
17 //done this way in case TimerHandler() is called during Update()
18 m_LogicTime -= m_TimerResolution;
19 renderTime += m_TimerResolution;
20 }
21 Render(m_TotalTime, renderTime);
22 }
23}

There's a ton more to this architecture, but I'm not about to type up 900 pages.
;D

Edit: Corrected.

gillius
Member #119
April 2000

There's probably a few things to comment about the above, but I'll keep it to one thing.

The above is basically a model-view-controller architecture, except that the controller and view are combined. One suggestion might be that splitting the view and controller could give some benefits. In the above, the "human view" I assume is responsible for all rendering (graphics and sound), but it also processes input. If the input processing is split from the view, you can have different implementations of the controller. This may be useful for demo playback, or for spectating in an online game (where the input comes from a remote player).

Gillius
Gillius's Programming -- https://gillius.org/

Kris Asick
Member #1,424
July 2001

Beyond the common mistakes, such as loading data while in your game loop, allocating and deallocating memory frequently, adding/removing items from vectors or using large linked lists, there are a few things you need to watch out for when coding with Allegro:

1. If you're doing more than one blit to video bitmaps per frame, including the screen, you have to lock that bitmap first before you draw to it using the acquire commands, then unlock with the release commands. (Doesn't apply for blitting video bitmaps to video bitmaps.) If you don't, locks are engaged and disengaged for every draw you make to a video bitmap, causing an enormous amount of slowdown. If you only ever draw to the screen or a video bitmap once every frame then there's nothing to worry about.

2. If you're working in 8-bit colour, be careful when you call the palette setting commands as some of them request a vsync() operation first, thus if you call those commands more than once in a frame you'll kill your framerate. (And you should thus never call more than one palette command which will vsync() per frame and you should always call it right before blitting to the screen.) Also, palette altering and page flipping don't mix harmoniously as both of them request a vsync() operation.

3. Since Allegro does not take advantage of hardware acceleration (excepting video to video blits) 32-bit colour will run nearly twice as slow as 16-bit colour. If you're working in 32-bit, trying scaling down to 16-bit.

4. If you run your program in a window, try not to run a 16-bit application with a 32-bit desktop or vice versa as you'll get some additional slowdown. (The amount of which will vary depending on how good your video card is.) 15-bit and 24-bit are very strange to work with anymore and you should avoid them. 8-bit should run fine in a window at any bit-depth desktop.

--- Kris Asick (Gemini)
--- http://www.pixelships.com

--- Kris Asick (Gemini)
--- http://www.pixelships.com

Kibiz0r
Member #6,203
September 2005
avatar

Quote:

One suggestion might be that splitting the view and controller could give some benefits. In the above, the "human view" I assume is responsible for all rendering (graphics and sound), but it also processes input. If the input processing is split from the view, you can have different implementations of the controller.

Controllers ought to be abstracted so the game view can't even tell what kind of controller the player is using. That allows for the different implementations.

Quote:

This may be useful for demo playback, or for spectating in an online game (where the input comes from a remote player).

The architecture already allows this, since input from remote players goes through the same channels as local players. Demo playback is essentially a dummy AI agent that sends messages from a game record file. As for spectating, you would just have to disallow input for the spectator and use the regular client game logic.

DanielH
Member #934
January 2001
avatar

The only problem with Kibiz0r's main I see is that init usually allocates some memory such as creating buffers, ...

This way you can still call deinit to free any allocated memory.

Here is my typical app. main creates a buffer, loads a datafile, ...
I don't want to worry about freeing them in init if the something fails.

1// app.h
2class App
3{
4public:
5 App();
6 ~App();
7 static int main( int argc, char **argv );
8 
9private:
10 int init();
11 void kill();
12 void loop();
13 void draw();
14 void logic();
15 
16 static void callback();
17 
18 static bool cbPressed;
19 static bool shutDown;
20};
21 
22// app.cpp
23#include "alleg.h"
24#include "globals.h"
25#include "defines.h"
26#include "app.h"
27#include "timer.h"
28#include "log.h"
29#include "gfx.h"
30#include "input.h"
31 
32bool App::cbPressed = false;
33bool App::shutDown = false;
34 
35int main( int argc, char **argv )
36{
37 return App::main( argc, argv );
38}
39END_OF_MAIN()
40 
41App::App()
42{
43}
44 
45App::~App()
46{
47}
48 
49int App::main( int argc, char **argv )
50{
51 App app;
52 
53 log.open( logFilename );
54 
55 if ( app.init() == 0 )
56 {
57 app.loop();
58 }
59 
60 app.kill();
61 
62 log.close();
63 
64 return 0;
65}
66 
67int App::init()
68{
69 char fullPath[ 1024 ] = "";
70 char workingDirectory[ 1024 ] = "";
71 
72 log << "Game startup begun" << endl;
73 
74 if ( allegro_init() < 0 )
75 {
76 log << "Allegro Library could not be initialized" << endl;
77 return -1;
78 }
79 log << "Allegro Library initialized" << endl;
80 
81#ifndef _DEBUG
82 get_executable_name( fullPath,
83 sizeof( fullPath ) );
84 
85 replace_filename( workingDirectory,
86 fullPath,
87 "",
88 sizeof( workingDirectory ) );
89 
90 chdir( workingDirectory );
91#endif
92 
93 
94 set_config_file( cfgFilename );
95 set_window_title( gameTitle );
96 set_display_switch_mode( SWITCH_BACKGROUND );
97 set_close_button_callback( App::callback );
98 
99 if ( Gfx::setMode() < 0 )
100 {
101 return -1;
102 }
103 else
104 {
105 clear_bitmap( screen );
106 textout_ex( screen, font, "Loading ...", 8, 8, makecol( 255, 255, 255 ), -1 );
107 }
108 
109 
110 if ( install_keyboard() < 0 )
111 {
112 log << "Keyboard could not be installed" << endl;
113 return -1;
114 }
115 log << "Keyboard installed" << endl;
116 
117 if ( install_timer() < 0 )
118 {
119 log << "Timer could not be installed" << endl;
120 return -1;
121 }
122 log << "Timer installed" << endl;
123 
124 if ( Timer::install() < 0 )
125 {
126 log << "Timers could not be installed" << endl;
127 return -4;
128 }
129 log << "Timers installed" << endl;
130 
131 if ( install_mouse() < 0 )
132 {
133 log << "Mouse could not be installed" << endl;
134 return -1;
135 }
136 log << "Mouse installed" << endl;
137 
138 if ( !( Global::buffer = create_bitmap( bufferWidth, bufferHeight ) ) )
139 {
140 log << "Double buffer could not be created" << endl;
141 return -1;
142 }
143 log << "Double buffer created" << endl;
144 clear_bitmap( Global::buffer );
145 
146 if ( !( Global::datafile = load_datafile( datFilename ) ) )
147 {
148 log << "Datafile '" << datFilename << "' could not be loaded" << endl;
149 return -1;
150 }
151 log << "Datafile '" << datFilename << "' loaded" << endl;
152 
153 input.enable();
154 
155 srand( (int)time( NULL ) );
156 
157 log << "Game startup complete" << endl << endl;
158 
159 return 0;
160}
161 
162void App::kill()
163{
164 log << endl << endl << "Game shutdown begun" << endl;
165 
166 if ( Global::datafile )
167 {
168 log << "Datafile unloaded" << endl;
169 unload_datafile( Global::datafile );
170 Global::datafile = NULL;
171 }
172 
173 if ( Global::buffer )
174 {
175 log << "Double buffer destroyed" << endl;
176 destroy_bitmap( Global::buffer );
177 Global::buffer = NULL;
178 }
179 
180 log << "Text mode set" << endl;
181 set_gfx_mode( GFX_TEXT, 0, 0, 0, 0 );
182 
183 allegro_exit();
184 log << "Allegro Library uninitialized" << endl;
185 
186 log << "Game shutdown complete" << endl;
187}
188 
189void App::loop()
190{
191 while ( !App::shutDown )
192 {
193 this->draw();
194 
195 while ( Timer::logicTimer > 0 )
196 {
197 Timer::logicTimer--;
198 this->logic();
199 }
200 
201 if ( key[ KEY_ESC ] ||
202 App::cbPressed )
203 {
204 App::shutDown = true;
205 }
206 }
207}
208 
209void App::draw()
210{
211 clear_bitmap( Global::buffer );
212 
213 Gfx::flip( Global::buffer );
214}
215 
216void App::logic()
217{
218}
219 
220void App::callback()
221{
222 log << "Close button pressed" << endl;
223 App::cbPressed = true;
224}

His run loop I don't like. What's up with the rest()?

Use Miran's. Do all logic, this might include multiple sets of logic.
Then draw the updated graphics once.

Kibiz0r
Member #6,203
September 2005
avatar

If you do it Miran's way, don't use a message system, because it will process all messages that have been queued up in multiple logic frames inside the equivalent of one logic frame.

Quote:

What's up with the rest()?

I was told that rest(0) yields to other processes. We ought to not use the CPU as long as logic doesn't need to be updated, yeah?

Edit: Your code looks eerily similar to mine, it gave me chills, heh. Your loop, though... it will draw the current frame again if logic time == 0.

Edit 2:

Quote:

The only problem with Kibiz0r's main I see is that init usually allocates some memory such as creating buffers, ...

This way you can still call deinit to free any allocated memory.

Yeah, it's in the book that way, I don't know why I noobed it up. :'(

Tobias Dammers
Member #2,604
August 2002
avatar

Quote:

4. If you run your program in a window, try not to run a 16-bit application with a 32-bit desktop or vice versa as you'll get some additional slowdown. (The amount of which will vary depending on how good your video card is.) 15-bit and 24-bit are very strange to work with anymore and you should avoid them. 8-bit should run fine in a window at any bit-depth desktop.

15-bit IS 16-bit, it just uses a different color format (RGB-555 vs. RGB-565). It ignores one of the bits, but it's still there, and you could safely use it for storage if you wanted - just like 32-bit ignores a full byte. 24-bit is strange to work with if you need direct access to pixel data, otherwise it's exactly the same as 32-bit. And since some GFX cards prefer 24-bit over 32-bit (yes, some still do), I'd support it anyway, if only for the final display (leaving all sprites at 16 / 32). 15 <-> 16 and 24 <-> 32 conversions are relatively cheap.

---
Me make music: Triofobie
---
"We need Tobias and his awesome trombone, too." - Johan Halmén

Kitty Cat
Member #2,815
October 2002
avatar

Quote:

Controllers ought to be abstracted so the game view can't even tell what kind of controller the player is using. That allows for the different implementations.

But it also makes it a bit more difficult to make a good control scheme if you have to work on an abstracted layer with no assumptions about the control method (eg. using a keyboard will work differently than using a joystick, which will work differently than a gamepad, etc). Sure th ey can be abstracted to properly work on them all, but then the controls can't be as tailored to the control type the player is using, which can make things feel clunky.

Quote:

I was told that rest(0) yields to other processes. We ought to not use the CPU as long as logic doesn't need to be updated, yeah?

rest(0) only yields the CPU, but returns ASAP. rest(1) will give away the CPU for at least 1 millisecond and return ASAP after that. The former will reduce CPU usage, while rest(0) takes full CPU usage.

--
"Do not meddle in the affairs of cats, for they are subtle and will pee on your computer." -- Bruce Graham

Goalie Ca
Member #2,579
July 2002
avatar

I prefer the semaphore mechanism myself.
Literally the same thing as the tick variable but goes to sleep in the main loop if the value is 0. It will wake up when the value goes back up above 0.

There are various portable libraries, and standard OS implementations.

-------------
Bah weep granah weep nini bong!

piccolo
Member #3,163
January 2003
avatar

This is good stuff i think i am having the same problem. i do music and sound effects in the logic loop how much of a slow down are we talking about.
and for 32bit thingy are you talking about changing the graphics mode to 16 or the sprites?

wow
-------------------------------
i am who you are not am i

Kibiz0r
Member #6,203
September 2005
avatar

Quote:

But it also makes it a bit more difficult to make a good control scheme if you have to work on an abstracted layer with no assumptions about the control method (eg. using a keyboard will work differently than using a joystick, which will work differently than a gamepad, etc). Sure th ey can be abstracted to properly work on them all, but then the controls can't be as tailored to the control type the player is using, which can make things feel clunky.

What I meant to say was that the "Input Interpreter" ought to serve the same function and send the same messages regardless of the control scheme, but can have completely different ways of going about it, depending on what kind of input we're talking about.

For example, Up on a d-pad might only result in a message like Accelerate 100%, whereas on an analog stick, it could send anything from Accelerate 1% to Accelerate 100%.

As for clunkiness arising from supporting multiple methods of input... It's a problem that exists no matter what setup you use, really. As game designers, we try to set it up so that it feels right to us, but another player might have a completely different idea of what feels right. But that's the strength of this architecture: you can specialize the way input is interpreted for each device without changing the way the game itself is played.

I do have a question, though: Should dead zones be applied in the application layer, or the game view?
It alters the input itself, not how it is interpreted as a game command... yet, the amount of dead zone may have to do with personalized options, which are associated with the game view and really shouldn't be within a stone's throw of the application layer.

Go to: