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 <iostream>
23#include <string>
24#include <sstream>
25 
26using namespace std;
27 
28volatile int _logicTime = 0;//incremented CYCLES_PER_SEC times each second
29 
30//These variables let us calculate FPS:
31int graphicsCounter = 0;//keeps track of how many times we drew to the screen
32int logicCounter = 0;//keeps track of how many times we updated the logic
33int currentFPS = 0;//the current frames per second
34 
35bool stopGame = false;//set to true to stop the game
36 
37//Display a bouncing ball to the screen:
38int ballX = 0;//ball coordinates
39int ballY = 0;
40int horizDir = 0;//0 if moving left, 1 if moving right
41int vertDir = 0;//0 if moving up, 1 if moving down
42BITMAP *backBuffer = NULL;//lets us implement double-buffering
43 
44bool initAllegro(void);
45void run(void);
46void doLogic(void);
47void drawGraphics(void);
48string intToStr(int theValue);
49 
50void 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
56END_OF_FUNCTION(ticker);
57 
58int main(void)
59{
60 if (initAllegro())
61 run();
62 
63 if (backBuffer != NULL)
64 destroy_bitmap(backBuffer);
65}//main
66 
67void 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 
101void 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 
137void 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 
163bool 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 
218string 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 <allegro.h>
5//for posix threading
6#include <pthread.h>
7//for printing seconds since 1970 or whatever
8#include <stdlib.h>
9 
10//number of cycles per second
11#define BPS 60
12 
13//create the mutex
14pthread_mutex_t timer_mutex = PTHREAD_MUTEX_INITIALIZER;
15 
16void ticker(void)
17{
18 pthread_mutex_unlock(&timer_mutex);
19}
20END_OF_FUNCTION(ticker);
21 
22 
23int 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}
46END_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 <allegro.h>
2//for windows threading
3#include <winbase.h>
4//for printing seconds since 1970 or whatever
5#include <stdlib.h>
6 
7//number of cycles per second
8#define BPS 60
9 
10//create the mutex
11HANDLE timer_mutex; //pthread_mutex_t
12 
13void ticker(void)
14{
15 ReleaseMutex(timer_mutex);
16}
17END_OF_FUNCTION(ticker);
18 
19 
20int 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}
45END_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. :P
DyMutex.hpp:

1#ifndef DY_MUTEX
2#define DY_MUTEX
3 
4#ifndef DY_WINDOWS_VERSION
5 #include <pthread.h>
6#endif
7 
8class 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 
3using namespace std;
4 
5DyMutex::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 
17DyMutex::~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.

http://www.geisswerks.com/ryan/FAQS/timing.html

Thread #589969. Printed from Allegro.cc