So, since I recently decided to start playing around with A5, I again decided to tackle the problem of the perfect game loop for fixed timestep timing system (the only deterministic system there is).
I've came up with two approaches, which roughly operate to give the same resulting logic per second and frame per second rates.
This first approach uses A5 events and timers in a very hacky fashion. I don't really like it, although it takes the fewest number of lines, and perhaps is easier to understand. Basically, we use a timer only event queue to control our logic updates, and at the same time rest when possible.
1 | int refresh_rate = 60; |
2 | float max_logic_time = 0.5f / refresh_rate; |
3 | ALLEGRO_EVENT timer_dummy; |
4 | |
5 | ALLEGRO_TIMER *timer = al_install_timer(ALLEGRO_BPS_TO_SECS(refresh_rate)); |
6 | al_start_timer(timer); |
7 | ALLEGRO_EVENT_QUEUE *timer_queue = al_create_event_queue(); |
8 | al_register_event_source(timer_queue, (ALLEGRO_EVENT_SOURCE*)timer); |
9 | |
10 | bool done = false; |
11 | while (!done) |
12 | { |
13 | al_wait_for_event(timer_queue, &timer_dummy);//rest at least the fixed time step |
14 | double start_time = al_current_time(); |
15 | do |
16 | { |
17 | DoLogic() |
18 | |
19 | if(al_current_time() - start_time > max_logic_time)//break if we are taking too long |
20 | break; |
21 | } while(al_get_next_event(timer_queue, &timer_dummy)); |
22 | |
23 | al_flush_event_queue(timer_queue);//remove excess overflow |
24 | |
25 | al_wait_for_vsync(); |
26 | al_clear(black); |
27 | |
28 | DrawStuff(); |
29 | |
30 | al_flip_display(); |
31 | } |
The second approach takes more lines, but it does not use A5 timers or events, making it a little more elegant in that sense. It's operation is a little more complicated, but I am sure you all will be able to figure it out. Another advantage of this method is that it maps a little more closely to the abortive delta timing method, making it a little more tractable.
1 | int refresh_rate = 60; |
2 | double fixed_dt = 1.0f / refresh_rate; |
3 | double old_time = al_current_time(); |
4 | double game_time = al_current_time(); |
5 | |
6 | bool done = false; |
7 | while (!done) |
8 | { |
9 | double dt = al_current_time() - old_time; |
10 | al_rest(fixed_dt - dt); //rest at least fixed_dt |
11 | dt = al_current_time() - old_time; |
12 | old_time = al_current_time(); |
13 | |
14 | if(old_time - game_time > dt)//eliminate excess overflow |
15 | { |
16 | game_time += fixed_dt * floor((old_time - game_time) / fixed_dt); |
17 | } |
18 | |
19 | double start_time = al_current_time(); |
20 | while(old_time - game_time >= 0) |
21 | { |
22 | game_time += fixed_dt; |
23 | |
24 | DoLogic(); |
25 | |
26 | if(al_current_time() - start_time > fixed_dt * 0.5)//break if we start taking too long |
27 | break; |
28 | } |
29 | |
30 | al_wait_for_vsync(); |
31 | al_clear(black); |
32 | |
33 | DrawStuff(); |
34 | |
35 | al_flip_display(); |
36 | } |
So yeah... comments/suggestions? I think the second one can be improved by hiding some of its functionality behind functions, for one.
EDIT:
Notably, if you promise that your DoLogic() will never ever ever ever take longer than the fixed time step to complete, you can simplify the versions as follows:
A5 Timer/Event:
1 | int refresh_rate = 60; |
2 | ALLEGRO_EVENT timer_dummy; |
3 | |
4 | ALLEGRO_TIMER *timer = al_install_timer(ALLEGRO_BPS_TO_SECS(refresh_rate)); |
5 | al_start_timer(timer); |
6 | ALLEGRO_EVENT_QUEUE *timer_queue = al_create_event_queue(); |
7 | al_register_event_source(timer_queue, (ALLEGRO_EVENT_SOURCE*)timer); |
8 | |
9 | bool done = false; |
10 | while (!done) |
11 | { |
12 | al_wait_for_event(timer_queue, &timer_dummy);//rest at least the fixed time step |
13 | do |
14 | { |
15 | DoLogic() |
16 | } while(al_get_next_event(timer_queue, &timer_dummy)); |
17 | |
18 | al_wait_for_vsync(); |
19 | al_clear(black); |
20 | |
21 | DrawStuff(); |
22 | |
23 | al_flip_display(); |
A5 cur_time:
1 | int refresh_rate = 60; |
2 | double fixed_dt = 1.0f / refresh_rate; |
3 | double old_time = al_current_time(); |
4 | double game_time = al_current_time(); |
5 | |
6 | bool done = false; |
7 | while (!done) |
8 | { |
9 | double dt = al_current_time() - old_time; |
10 | al_rest(fixed_dt - dt); //rest at least fixed_dt |
11 | dt = al_current_time() - old_time; |
12 | old_time = al_current_time(); |
13 | |
14 | while(old_time - game_time >= 0) |
15 | { |
16 | game_time += fixed_dt; |
17 | |
18 | DoLogic(); |
19 | } |
20 | |
21 | al_wait_for_vsync(); |
22 | al_clear(black); |
23 | |
24 | DrawStuff(); |
25 | |
26 | al_flip_display(); |
27 | } |
... for fixed timestep timing system (the only deterministic system there is).
Don't tell Newton!
My only useful comment: if al_rest can at any point rest a little too short (e.g. if it resnts to within ±2ms) then following:
al_rest(fixed_dt - dt); //rest at least fixed_dt dt = al_current_time() - old_time;
dt may be less than fixed_dt. Is that what you want? If al_rest has a stated accuracy then you should round down to th enearest multiple of that and then add one more multiple, if it doesn't then I guess there's little you can do.
Re:
al_wait_for_vsync(); al_clear(black); DrawStuff(); al_flip_display();
Would it not be more sensible to do:
al_clear(black); DrawStuff(); al_wait_for_vsync(); al_flip_display();
Vsyncing before your draw and then flipping after whatever portion of a frame it takes you to draw is likely to lead to tearing.
Other than that, I've yet to fully digest the code. And it's slightly past work o'clock.
Aha, thanks for the vsync tip. It doesn't really work at all for me, so I couldn't tell the difference between different locations for it. Your reasoning makes sense though.
Is that what you want? If al_rest has a stated accuracy then you should round down to th enearest multiple of that and then add one more multiple, if it doesn't then I guess there's little you can do.
The idea is that we rest until the time passed since we last measured time is at least fixed_dt. dt is usually smaller than fixed_dt, so we usually rest for some period of time.
al_clear(black); I believe that should be in DrawStuff();
Maybe this was with AllegroGL (and by extension, OpenLayer) only (and I haven't looked at Allegro5's docs yet), but doesn't calling al_flip_display(); imply al_wait_for_vsync(); on platforms that support vsync?
No, it doesn't.
doesn't calling al_flip_display(); imply al_wait_for_vsync(); on platforms that support vsync?
It probably should (or at least be a parameter).
It's not clear to me that al_wait_for_vsync() is useful by itself.
How would it make any difference? The user calls al_wait_for_vsync then al_flip_display.
What happens if al_flip_display() waits for vsync anyway (eg: an OpenGL backend)? Do we wait for vsync twice? If we ignore al_wait_for_vsync() on those platforms, apps lose the ability to time things using vsync (which is still useful for many reasons).
The opengl backend doesn't wait for vsync AFAIK unless you call al_wait_for_vsync yourself.
Yes it does. At the end of the day, it calls wglSwapBuffers(), which is allowed to (and most of the time does) wait for vsync, by default.
We should remove al_wait_for_vsync and make it a display flag.
We should remove al_wait_for_vsync and make it a display flag.
I was thinking the same thing. And a HINT at that. We can't guarantee its enabled or disabled since the user can force the issue in the driver settings.
Okay, I looked around some other forums, and Bullet source code, and it seems that this is the optimal solution. I probably will put it up on the wiki eventually...
1 | int refresh_rate = 60; |
2 | double fixed_dt = 1.0f / refresh_rate; |
3 | double old_time = al_current_time(); |
4 | double game_time = al_current_time(); |
5 | |
6 | bool done = false; |
7 | while (!done) |
8 | { |
9 | /* |
10 | Standard rester: |
11 | Forces the drawing to take at least one fixed_dt interval, reducing CPU usage |
12 | in lighweight programs. |
13 | */ |
14 | double dt = al_current_time() - old_time; |
15 | al_rest(fixed_dt - dt); |
16 | dt = al_current_time() - old_time; |
17 | old_time = al_current_time(); |
18 | |
19 | /* |
20 | Time overflow eliminator: |
21 | Makes sure that when game time gets too far behind, |
22 | the logic while loop does not enter a period of |
23 | hyperactivity. Note that this may need to be eliminated |
24 | for networked games, where game_time should be synchronized |
25 | across machines. |
26 | */ |
27 | if(old_time - game_time > dt) |
28 | { |
29 | game_time += fixed_dt * floor((old_time - game_time) / fixed_dt); |
30 | } |
31 | |
32 | double start_time = al_current_time(); |
33 | while(old_time - game_time >= 0) |
34 | { |
35 | game_time += fixed_dt; |
36 | |
37 | DoLogic(); |
38 | |
39 | /* |
40 | Logic time limiter: |
41 | It ensures that the logic does not spiral out of control when it starts |
42 | taking too long to complete. This measures the time for all of the logic |
43 | steps, not just one. I find that this leads to a smoother operation than |
44 | when limiting each step individually. This favours FPS over LPS. |
45 | */ |
46 | if(al_current_time() - start_time > fixed_dt) |
47 | break; |
48 | } |
49 | |
50 | /* |
51 | Various drawing operations go here |
52 | */ |
53 | al_clear(black); |
54 | |
55 | DrawStuff(); |
56 | |
57 | al_wait_for_vsync(); |
58 | al_flip_display(); |
59 | } |
Sorry, this is absolutely more my day job editor instincts coming out than my programmer instincts, but maybe chuck an extra comment in above 'double start_time = al_current_time();' to the effect that the main logic call loop follows, so that it's more obvious to newbies where the section of code that "may need to be eliminated for networked games" ends?
My unnecessary, tedious pedantry also makes me comment again on:
fixed timestep timing system (the only deterministic system there is).
To the effect that my argument for disagreeing with your assertion is that real life isn't run on a fixed timestep system and you cannot build a deterministic system from a non-deterministic system.
Sorry, this is absolutely more my day job editor instincts coming out than my programmer instincts, but maybe chuck an extra comment in above 'double start_time = al_current_time();' to the effect that the main logic call loop follows, so that it's more obvious to newbies where the section of code that "may need to be eliminated for networked games" ends?
Ah, good point. I'll be more blatant about it, and why that actually works in the actual article.
To the effect that my argument for disagreeing with your assertion is that real life isn't run on a fixed timestep system and you cannot build a deterministic system from a non-deterministic system.
What I mean by deterministic is that, given identical starting conditions, when you run a game it will run identically in every case. This is impossible (pretty much) to do with a variable time step, especially one dependent on framerate. There are other benefits to using fixed step timing of course, but that's irrelevant in this case.