Game Engine that conserves CPU
Myrdos

This is a continuation of the OT discussion from this thread, on how to write a game engine that uses less than 100% of the CPU on fast computers, but automatically uses more CPU time as needed on slower computers.

Here's a simple program that implements such a system. Try it out to convince yourself that it works.

 1 /* A simple program that demonstrates how to write a game engine that doesn't use 100% 2 of the CPU. The game will automatically use more CPU time on slower machines, and less 3 time on faster machines. 4 5 On a fast machine: the game uses less than 100% CPU (only as much as it needs) 6 7 On a slower machine: the game uses 100% CPU. The Frames Per Second are lowered until the 8 machine can keep up. (We redraw the screen less frequently). The number 9 of logic updates per second remains the same. So the game runs at the same 10 speed on slower machines, but the graphics get choppy. 11 12 On a very slow machine: if the machine can't even handle 1 Frame Per Second, we start to skip 13 logic updates. (The game starts running slower) At this point, the 14 machine is probably too slow to run this game, or the user has other 15 programs running. 16 */ 17 #include "allegro.h" 18 19 //Number of logic updates per second 20 #define CYCLES_PER_SEC 50 21 22 #include 23 #include 24 #include 25 26 using namespace std; 27 28 volatile int _logicTime = 0;//incremented CYCLES_PER_SEC times each second 29 30 //These variables let us calculate FPS: 31 int graphicsCounter = 0;//keeps track of how many times we drew to the screen 32 int logicCounter = 0;//keeps track of how many times we updated the logic 33 int currentFPS = 0;//the current frames per second 34 35 bool stopGame = false;//set to true to stop the game 36 37 //Display a bouncing ball to the screen: 38 int ballX = 0;//ball coordinates 39 int ballY = 0; 40 int horizDir = 0;//0 if moving left, 1 if moving right 41 int vertDir = 0;//0 if moving up, 1 if moving down 42 BITMAP *backBuffer = NULL;//lets us implement double-buffering 43 44 bool initAllegro(void); 45 void run(void); 46 void doLogic(void); 47 void drawGraphics(void); 48 string intToStr(int theValue); 49 50 void ticker(void) 51 { 52 //if logicTime is > CYCLES_PER_SEC, we start skipping logic updates 53 if (_logicTime <= CYCLES_PER_SEC) 54 _logicTime++; 55 }//ticker 56 END_OF_FUNCTION(ticker); 57 58 int main(void) 59 { 60 if (initAllegro()) 61 run(); 62 63 if (backBuffer != NULL) 64 destroy_bitmap(backBuffer); 65 }//main 66 67 void run(void) 68 { 69 int logicLimiter = 0;//if we aren't drawing anything to the screen and still can't 70 //keep up, draw graphics at least once per second anyways 71 bool logicChanged = false;//true if we updated the logic and should redraw the graphics 72 73 while (!stopGame) 74 { 75 logicLimiter = 0; 76 logicChanged = false; 77 78 while ((_logicTime > 0) && (logicLimiter < CYCLES_PER_SEC)) 79 { 80 doLogic(); 81 82 logicChanged = true; 83 logicCounter++; 84 _logicTime--; 85 86 logicLimiter++; 87 rest(0);//yield the CPU 88 }//while logicTime 89 90 if (logicChanged) 91 { 92 drawGraphics(); 93 rest(0);//yield the CPU 94 graphicsCounter++; 95 }//if logicChanged 96 else 97 rest(10);//sleep for 10 ms 98 }//while !stopGame 99 }//run 100 101 void doLogic(void) 102 { 103 //*** Calculate the FPS *** 104 if (logicCounter >= CYCLES_PER_SEC) 105 { 106 currentFPS = graphicsCounter; 107 graphicsCounter = 0; 108 logicCounter = 0; 109 }//if logicCounter 110 111 //*** Move the ball *** 112 if (horizDir == 0) 113 ballX -= 3; 114 else 115 ballX += 3; 116 117 if (vertDir == 0) 118 ballY -= 3; 119 else 120 ballY += 3; 121 122 //*** make the ball bounce when it hits the edge of the screen *** 123 if (ballX < 0) 124 horizDir = 1; 125 if (ballX > 640) 126 horizDir = 0; 127 128 if (ballY < 0) 129 vertDir = 1; 130 if (ballY > 480) 131 vertDir = 0; 132 133 if (key[KEY_ESC]) 134 stopGame = true; 135 }//doLogic 136 137 void drawGraphics(void) 138 { 139 string theFPS = "FPS: ";//display the frames per second to the screen 140 141 clear_bitmap(backBuffer); 142 143 //*** Draw a bouncing ball *** 144 circlefill(backBuffer, ballX, ballY, 145 20, makecol(100, 150, 200)); 146 147 //*** Display the frames per second *** 148 theFPS.append(intToStr(currentFPS)); 149 textout_ex(backBuffer, font, 150 theFPS.c_str(), 151 10, 5, 152 makecol(200, 200, 200), 0); 153 154 textout_ex(backBuffer, font, 155 "Hit ESC to quit", 156 10, 40, 157 makecol(200, 200, 200), 0); 158 159 //*** Draw the back buffer to the screen *** 160 draw_sprite(screen, backBuffer, 0, 0); 161 }//drawGraphics 162 163 bool initAllegro(void) 164 { 165 int gResult = 0;//result of graphics setup 166 bool result = true;//false if we couldn't initialize 167 168 set_uformat(U_ASCII); 169 allegro_init(); 170 171 //these are unlikely to fail 172 install_timer(); 173 install_keyboard(); 174 175 set_color_depth(24); 176 gResult = set_gfx_mode(GFX_AUTODETECT_WINDOWED, 177 640, 480, 178 0, 0); 179 180 if (gResult < 0) 181 result = false; 182 183 //*** Ensure the program will keep running even if it lost focus *** 184 if (result) 185 { 186 if (set_display_switch_mode(SWITCH_BACKGROUND) == 0) 187 cout << "Using SWITCH_BACKGROUND mode" << endl; 188 else 189 { 190 if (set_display_switch_mode(SWITCH_BACKAMNESIA) == 0) 191 cout << "Using SWITCH_BACKAMNESIA mode" << endl; 192 else 193 { 194 result = false; 195 cout << "Couldn't ensure that the program keeps running if it loses focus" << endl; 196 }//else set_display_switch_mode 197 }//if set_display_switch_mode 198 }//if result 199 200 //*** Initialize timer *** 201 if (result) 202 { 203 LOCK_VARIABLE(_logicTime); 204 LOCK_FUNCTION(ticker); 205 if (install_int_ex(ticker, BPS_TO_TIMER(CYCLES_PER_SEC)) < 0) 206 { 207 cout << "Couldn't start ticker function" << endl; 208 result = false; 209 }//if install_int_ex 210 }//if result 211 212 if (result) 213 backBuffer = create_bitmap(640, 480); 214 215 return result; 216 }//initAllegro 217 218 string intToStr(int theValue) 219 { 220 stringstream converter;//converts theValue to a string 221 string result;//the converted text 222 223 converter << theValue; 224 converter >> result; 225 226 return result; 227 }//showInt

Quote:

if you're computer is fast enough, and it is sitting in a busy wait loop (pushing cpu usage to 100%) you should yield/rest then... no?

That is exactly what this does! If you're redrawing the graphics but there was no logic update since the last redraw, you're "caught up". You have time to kill. So sleep.

But if there was a logic update, your game is still chugging away. So yield. Yielding is optional, but it helps ensure smooth multitasking if there are other programs running, or if your program is multi-threaded.

Goalie Ca

I honestly believe the best way to do such a game loop is to have a timer that can wake up a blocking (sleeping) main loop thread. All the rest still boils down to polling.

But in order to have such a timer you can't use the allegro timer. A library would have to be written or os specific code would have to be used.

edit:
strong hack but.. i suppose since the timer runs in a seperate thread it could send some sort of message to the blocking main thread to basically wake it up. A semaphore mechanism could work here perhaps where timer unlocks and main thread locks (at end of loop).

Kris Asick

And this is all presuming you do fixed frame calculations. For almost three years now I've been programming real-time instead of fixed-frame and the concept of frame dropping has sort of eluded me as a result.

My real-time timers give 1 ms of time back to the CPU every frame to prevent I/O issues with Windows 98 and recent Allegro versions. This also means that at lower framerates, less time is given back to the CPU, and at higher framerates, more time is given up. Also, with vsyncing enabled, the time spent waiting for a vsync also gives the CPU a break.

My recent game, PixelShips Retro, at 120 FPS, measures only 30%~40% CPU usage on my 2 GHz, Windows 98 system. Granted, it's also running at a mere 320x240x8bpp graphics mode.

Though on some systems the CPU usage barely registers for some reason, even with vsyncing off... shrugs

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

Michael Jensen

My apologies for derailing the other thread...

Audric PM'd me a longer explination,

I think Goalie Ca is on to a neat idea...

Does vsync() actually cause the main thread to sleep? I thought it busy waited also...

KA: could you elaborate? It doesn't sound like you're using allegro timers.

Audric

vsync() is a busy wait on win98.

edit: I rechecked: mingw executable of "exmouse" uses vsync for speed control : I get 100%CPU.

Albin Engström

woah, hold on to your horses! , you mean that using vsync would cause a 100% cpu usage on win98 systems?!?! that's insane :S and i'm confused..

Kris Asick

Audric: Wasn't sure of that. Thanks.

Albin Engström: Based on what Audric said, yes it would, but only if you never gave any time back to the system with rest() or Sleep() commands.

Michael Jensen: I was using the Allegro timers at first, then I found a way to use the Windows high performance timers thanks to other forum goers here. As a result, my routines are now able to use either. (But prefers my own since it's more reliable than the Allegro ones and only uses the Allegro ones if the high performance timer is mysteriously unavailable.)

The way I do it is to run a timer at 2400 ticks a second, since this rate compares the most easily with the multitude of screen refresh rates out there, and I time my logic to this using floating point values where 1.0f = 1 second. (Thus one tick is 4.166... microseconds.) I also pick 2400 because the Allegro timers handle it better than higher values I've tried.

Thus every frame I update and check the timers to see how much time has passed since the last frame, and update my logic by that amount, and since everything is floating point, everything is updated at the proper rate, whether the framerate is 10, 100, or more. In the extremely unlikely event of a zero-frame (not enough time has passed to increment the tick counter) then the game logic is processed with a multiplier of 0.0f, and thus goes nowhere until the next frame.

I also do frame interpolation in PixelShips Retro, but that's another topic entirely.

(Side Note: The Allegro timers ALSO use the Windows high performance timers when they are present... but because they work on an interrupt basis they don't always give the exact value they should. Directly accessing the Windows high performance timers can thus be more accurate when done right.)

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

Audric

Albin: yeah, it was a surprise to me too. Maybe I disabled vsync in the DirectX settings for the graphics card ? It's possible I did, but I can't remember nor find the parameters right now...

Albin Engström

er.. i think i'm even more confused now.. would'nt the cpu reach 100% even without vsync? in the case that the program used no rest or sleep?? :S

Goalie Ca

I decided to implement some code. This is for linux/mac only at the moment. Windows support should be trivial to add though. Someone could please add it? ATM this uses absolutely 0% of the CPU time.

This isn't the best solution out there but its simple for most people. If your code runs slower than BPS this code will slow it down even more as it waits an extra tick for a release. I haven't tested that but you guys shouldn't have a problem. DO post any revisions to allegro.cc

I would like to make this an add-on library or something for people or something. I suppose if we went the c++ route we could use boost/mutex for cross platform. If not it'd be best to do a bunch of #ifdef WINDOWS etc. and add a few threading utilities. I believe SDL has threading

 1 /* 2 (c) 2007 by Ryan Dickie 3 */ 4 #include 5 //for posix threading 6 #include 7 //for printing seconds since 1970 or whatever 8 #include 9 10 //number of cycles per second 11 #define BPS 60 12 13 //create the mutex 14 pthread_mutex_t timer_mutex = PTHREAD_MUTEX_INITIALIZER; 15 16 void ticker(void) 17 { 18 pthread_mutex_unlock(&timer_mutex); 19 } 20 END_OF_FUNCTION(ticker); 21 22 23 int main(int argc, char** argv) 24 { 25 allegro_init(); 26 27 LOCK_FUNCTION(ticker); 28 29 install_timer(); 30 install_keyboard(); 31 set_color_depth(24); 32 set_gfx_mode(GFX_AUTODETECT_WINDOWED, 640, 480, 0, 0); 33 install_int_ex(ticker, BPS_TO_TIMER(BPS)); 34 35 while(!key[KEY_ESC]) 36 { 37 //do stuff here 38 rectfill(screen, 1, 1, 200, 20, makecol(0,0,0) ); 39 textprintf_ex(screen, font, 10, 10, makecol(255, 100, 200),-1, "Time: %d", time(NULL) ); 40 //end of loop! 41 pthread_mutex_lock(&timer_mutex); 42 } 43 44 return 0; 45 } 46 END_OF_MAIN();

edit: for those of you interested this is potential windows code. I believe though that by including windows.h you require a winmain function etc. This should be fixable by just including what's needed. This one hasn't been tested..

edit2: appears its in winbase.h , maybe that helps maybe it doesn't

 1 #include 2 //for windows threading 3 #include 4 //for printing seconds since 1970 or whatever 5 #include 6 7 //number of cycles per second 8 #define BPS 60 9 10 //create the mutex 11 HANDLE timer_mutex; //pthread_mutex_t 12 13 void ticker(void) 14 { 15 ReleaseMutex(timer_mutex); 16 } 17 END_OF_FUNCTION(ticker); 18 19 20 int main(int argc, char** argv) 21 { 22 timer_mutex = CreateMutex(0, FALSE, 0); 23 allegro_init(); 24 25 LOCK_FUNCTION(ticker); 26 27 install_timer(); 28 install_keyboard(); 29 set_color_depth(24); 30 set_gfx_mode(GFX_AUTODETECT_WINDOWED, 640, 480, 0, 0); 31 install_int_ex(ticker, BPS_TO_TIMER(BPS)); 32 33 while(!key[KEY_ESC]) 34 { 35 //do stuff here 36 rectfill(screen, 1, 1, 200, 20, makecol(0,0,0) ); 37 textprintf_ex(screen, font, 10, 10, makecol(255, 100, 200),-1, "Time: %d", time(NULL) ); 38 //end of loop! 39 WaitForSingleObject(timer_mutex, INFINITE); 40 } 41 42 CloseHandle(timer_mutex); 43 return 0; 44 } 45 END_OF_MAIN();

Myrdos

Here's the C++ version of a windows/linux friendly mutex, should be easy to convert to plain C. Should be, but I'm just too tired.
DyMutex.hpp:

 1 #ifndef DY_MUTEX 2 #define DY_MUTEX 3 4 #ifndef DY_WINDOWS_VERSION 5 #include 6 #endif 7 8 class DyMutex 9 { 10 private: 11 //the cross-platform mutex 12 #ifdef DY_WINDOWS_VERSION 13 HANDLE theMutex; 14 #else 15 pthread_mutex_t theMutex; 16 #endif 17 18 public: 19 //constructor 20 DyMutex(void); 21 22 //destructor 23 ~DyMutex(void); 24 25 //restrict access to critical areas 26 #ifdef DY_WINDOWS_VERSION 27 void enterLock(void); 28 void exitLock(void); 29 #else 30 void enterLock(void); 31 void exitLock(void); 32 #endif 33 };//DyMutex 34 35 #endif

DyMutex.cpp:

 1 #include "DyMutex.hpp" 2 3 using namespace std; 4 5 DyMutex::DyMutex(void) 6 { 7 #ifdef DY_WINDOWS_VERSION 8 theMutex = CreateMutex(NULL, FALSE, NULL); 9 if (theMutex == NULL) 10 cout << "DyMutex::DyMutex() CreateMutex " << GetLastError() << endl; 11 #else 12 if (pthread_mutex_init(&theMutex, NULL) != 0) 13 cout << "DyMutex::DyMutex() pthread_mutex_init" << endl; 14 #endif 15 }//constructor 16 17 DyMutex::~DyMutex(void) 18 { 19 #ifdef DY_WINDOWS_VERSION 20 if (!CloseHandle(theMutex)) 21 cout << "DyMutex::~DyMutex() CloseHandle " << GetLastError() << endl; 22 #else 23 if (pthread_mutex_destroy(&theMutex) != 0) 24 cout << "DyMutex::~DyMutex() pthread_mutex_destroy" << endl; 25 #endif 26 }//desctructor 27 28 #ifdef DY_WINDOWS_VERSION 29 void DyMutex::enterLock(void) 30 { 31 if (WaitForSingleObject(theMutex, INFINITE) != WAIT_OBJECT_0) 32 cout << "DyMutex::enterLock()" << endl; 33 }//enterLock 34 35 void DyMutex::exitLock(void) 36 { 37 if (!ReleaseMutex(theMutex)) 38 cout << "DyMutex::exitLock()" << endl; 39 }//exitLock 40 #else 41 void DyMutex::enterLock(void) 42 { 43 if (pthread_mutex_lock(&theMutex) != 0) 44 cout << "DyMutex::enterLock()" << endl; 45 }//enterLock 46 47 void DyMutex::exitLock(void) 48 { 49 if (pthread_mutex_unlock(&theMutex) != 0) 50 cout << "DyMutex::exitLock()" << endl; 51 }//exitLock 52 #endif

[EDIT]Looks like you've got it already.

Goalie Ca
Quote:

Looks like you've got it already.

Not really. You put it together in a single class. I also need to put error handling etc.

The only question I really have though is about including files. I noticed you don't include any windows header files.

The little i do know about windows.h and winapi is that you can't really mix and match it with anything else without issues. I tried to include windows base but i'm not sure if that requires winmain like everything else. Also.. say it does... and a user needs to call winmain.. how does that play with allegro!?

edit:
I booted to windows.. made some minor changes to the allegro init and found that winalleg.h is what contains all that is needed. The only problem is that it doesn't actually seem to block at all. It's always failing. How could a mutex not actually block!? only on windows...

Myrdos
Quote:

The only question I really have though is about including files. I noticed you don't include any windows header files.

It's been forever since I've looked at this... the original class is from my networking lib, viewable here.

My includes are in another class:

```   31   #include <ws2tcpip.h>
32   #include <winsock2.h>
```

...Probably not what you want to use, since you aren't doing networking.

And of course this code in my Allegro programs that use the lib:

```#include "allegro.h"
#ifdef DY_WINDOWS_VERSION
#include "winalleg.h"
#endif
```

And it just seems to work. I've never had any problems with it not blocking. (I use MinGW) However, it's been a long time since I wrote this code, and there might be something I've forgotten...

BAF
Quote:

edit: for those of you interested this is potential windows code. I believe though that by including windows.h you require a winmain function etc. This should be fixable by just including what's needed. This one hasn't been tested..

I think you only need WinMain if you are creating a Windows (Windows, menus, etc), and even then I'm not sure. I know I have used windows.h and winsock.h with just plain main before.

KnightWhoSaysNi

This guy has an interesting timing tutorial with many methods. I think he shows how to do a timer that doesn't use 100% cpu. I only read it for timeGetTime() though.