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 | |
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 |
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.
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).
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
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.
vsync() is a busy wait on win98.
edit: I rechecked: mingw executable of "exmouse" uses vsync for speed control : I get 100%CPU.
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..
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
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...
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
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 |
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 <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 |
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(); |
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 <pthread.h> |
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.
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...
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...
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.
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.