Describe the bug
Described here: https://www.reddit.com/r/rust/comments/bvrkpx/a_guide_to_rust_2d_game_frameworks_2019/epv2f4q/?context=3
Additionally, I've encountered situations where I've been able to destabilize the main loop;
i.e. where the update method runs too slow and ggez keeps running update, failing to
draw or handle inputs in a timely manner.
To Reproduce
At a GUESS, you make a loop with event::check_update_time() that takes longer than the given update time to execute. I believe it is possible to make the main loop hang forever by doing that, and it won't ever get around to checking for events or handling quit requests. Need to double check.
Expected behavior
Not sure! But making everything hang forever is obviously wrong. Options:
Result from check_update_time so we can distinguish between "time is caught up", "time is not caught up", and "it doesn't look like time will ever catch up". Basically, incorporate a watchdog into it and force the user to handle the case where it takes too long.Screenshots or pasted code
todo
Hardware and Software:
check_update_time() shouldn't really be different between themEasiest (and I think typical) way to solve this is to have max number of times check_update_time returns true for every update. This is essentially how Unity solves the issue as well (source): An update step (== check_update_time iteration) has a fixed dt, but the ~sum~ real world computation time of those steps should never be over a given max timespan per update, otherwise one runs into a feedback loop with more and more update steps to catch up per update.
Alternatively check_update_time could clamp residual_update_dt to zero if "current time spend updating" + "last render timer" took already more than the available time budget... but sounds to me like this gets quite tricky and hard to control 馃
...That's honestly a great idea, and simple to implement. Thank you!
Possible link to add as a reference: https://joetsoi.github.io/fix-your-timestep-rust-ggez/
The way examples in ggez devel currently treat #774 , which is to use ? to unwrap check_update_time(), exiting the application on failure, behaves undesirably in two cases:
event::run will register a long delay between the context initialization and the first update, which the timer perceives as requiring a "catch-up" of over 20 frames, resulting in an immediate crashI have two possible ideas as to how to fix this, possibly both:
.unwrap_or(false) instead of ?. This way, the application doesn't crash if it's updating too slow, but it does yield time back to the event handler, allowing other events to process, such as quit events.I just removed the Result itself and undid the timer accumulator, since ? makes it wayyyyyyyy to easy for this to silently fail. Since failing means that the error gets caught by the event handler loop, a warning logged (iff you are capturing for log invocations), and the event loop stops cleanly, this is not desirable behavior. Moreover, apparently the accumulation of time can also occur when the game is running but not drawing anything... either 'cause it's off screen/minimized, or as seems to have happened to me with several of ggez's examples sometimes, everything's ready to go and running except the graphics context and it just displays the window for an instant and immediately exits.
This is currently what I use to deal with this problem in my own game's code, but as the comments say, it's not perfect:
impl event::EventHandler for MainState {
fn update(&mut self, ctx: &mut Context) -> GameResult {
let frame_duration: Duration = Duration::from_secs_f32(TARGET_FRAME_TIME);
let update_start = Instant::now();
// Run multiple updates if necessary to catch up
// ...but not too many
//
// TODO: If the window is off screen or such for a long time this will try to catch
// up on all the frames it missed
// Make it pause while not selected or something.
let mut max_updates = 5;
while update_start > (self.last_update_time + frame_duration) {
self.run_one_frame();
self.last_update_time += frame_duration;
let update_end = Instant::now();
self.update_duration = update_end - update_start;
max_updates -= 1;
if max_updates == 0 {
warn!("Did more than 5 updates in a row without drawing, reconsider your laggy life");
break;
}
}
}
I think in the end, solving this problem needs more sophisticated control flow than ggez can or should try to provide for you. Oh well.
Most helpful comment
Easiest (and I think typical) way to solve this is to have max number of times
check_update_timereturns true for everyupdate. This is essentially how Unity solves the issue as well (source): An update step (==check_update_timeiteration) has a fixed dt, but the ~sum~ real world computation time of those steps should never be over a given max timespan per update, otherwise one runs into a feedback loop with more and more update steps to catch up per update.Alternatively
check_update_timecould clampresidual_update_dtto zero if "current time spend updating" + "last render timer" took already more than the available time budget... but sounds to me like this gets quite tricky and hard to control 馃