tl;dr: There are a lot of different ways to track elapsed time in a game. I describe many useful techniques in the article, and I've made an open-source Time utility class for Godot that implements these.
What even is time??
You may not be aware, but there are many different ways of tracking elapsed time in a game, and they are all broken.
Fixed time steps
The first thing to know, is what a "fixed time step" is and why it's important. The tl;dr: is that if you don't use a fixed time step, you won't have determinism; if you run the same program twice, you could see different results. This is because physics engines are iterative, and depend on the elapsed time since the last frame—give a different time delta, get a different result. Most game engines use a fixed time step for their physics engine for this reason. Godot uses this fixed timestep in the `_physics_process(delta)` function.
Render frame rate
However, graphical updates aren't rendered to the display at a constant rate. This is simply because things run more slowly when the computer has more to compute. You typically want to interpolate between the previous and next physics frames in order to determine the state of the current render frame. Most game engines provide a separate callback for hooking into the render-frame cycle separately from the physics-frame cycle. How frequently the display is actually updated is usually what we mean when we say "frame rate" or "fps." Godot uses this variable render timestep in the `_process(delta)` function.
Problems with fixed time steps
In general, unless you have a reason not to, it's usually better to track elapsed time according to the fixed-rate physics cycle, since otherwise you will introduce non-determinism into your game, and this tends to cascade.
However, there is a big problem with the fixed-time approach: what if the calculations for a given frame actually take longer than the fixed-time interval? Well, the physics engine keeps chugging along happily, but you will see a slow-down in your render steps, and the total-elapsed time according to the physics engine will fall behind true clock-on-the-wall time!
Wall-clock time
Fortunately, the operating system always provides some sort of separate utility for tracking an approximation of real clock time. I'm sure it's complicated for the OS to get right, but, fortunately, I don't have to worry about that!
What happens when you pause?
In addition to the three ways of tracking time above—physics time, render time, wall-clock time—we also need to consider whether we need to pause our time tracking when the game is paused! Sometimes we do, sometimes we don't. For example, we don't want the core game to keep running when paused—otherwise we're definitely going to lose! However, we still want to be able to at least interact with and animate the GUI of the pause screen. And sometimes you could still want to show some animations in the underlying game—for example, you could keep animating some pretty background effects that don't actually affect the gameplay.
So we also need to track elapsed "app time" separately from elapsed "play time." And this combines exponentially with our other dimensions, so we now might need to consider six separate time scales: app-physics, app-render, app-wall-clock, play-physics, play-render, play-wall-clock.
Scaling time (e.g., cool slow-mo effects!)
Also, since we're so time-savvy, and we want our game to look super-cool, we obviously want to support dynamically turning on and off slow-motion effects. Or maybe we want to support fast-forwarding through long-winded dialog or cut-scenes when the user is bored. To support this, we need to be able to track elapsed time after scaling it up or down.
Since our time-scaling is dynamic—it can speed-up or slow-down at different times—we need our running totals to keep careful track of the elapsed time for each frame, according to the current time scale.
It's important to note that this time scale doesn't actually affect the "frame rate." The same number of frames should be rendered per second, the updates between them should just be smaller or larger.
This "scaled time" also combines exponentially with our previous six separate time scales, giving us a grand total of twelve different types of time that we might care about.
What does Godot support?
Godot does have various utilities for manipulating and tracking time, but they all have limitations on their own.
Engine.time_scale
Godot's Engine.time_scale is a multiplier for the elapsed time provided to most of Godot's APIs. So if you set `Engine.time_scale = 0.5` you should see _process and _process_physics callbacks being called with a delta_time argument that is half as much what it would normally be, but the callbacks themselves should be called the same number of times. That is, Engine.time_scale does not affect the frame rate.
Problem: Nondeterminism
Since Engine.time_scale changes the delta used in Godot's physics engine, the physics calculations are no longer deterministic when changing Engine.time_scale. That is, trajectories will be different for different Engine.time_scale values.
Also, even though you want the core gameplay to slow-down or speed-up, you probably want your GUI interactions and animations to stay consistent. Engine.time_scale will affect all of these equally.
So we shouldn't use Engine.time_scale for our cool dynamic slo-mo effects!
Engine.target_fps
Godot's Engine.target_fps does affect the frame rate, but only for the render-cycle frames (I think). So, if you lower Engine.target_fps, you will see _process callbacks being called less often and with larger deltas, but you will see _physics_process callbacks being called at the same rate and with the same deltas.
Engine.iterations_per_second
Godot's Engine.iterations_per_second changes the fixed-time physics engine frame rate. So, this will affect how often _physics_process callbacks are called, but will not affect _process callbacks.
Querying time
The main way of tracking time in Godot is by looking at the delta argument passed to the render-frame and physics-frame callbacks—_process and _physics_process. Additionally, Godot's OS class provides many ways of querying the current time, such as OS.get_system_time_msecs and OS.get_ticks_usec.
A Time utility for Godot
In my games I need to be able to track elapsed time across all of the different dimensions discussed above: physics-time vs render-time vs clock-time, play-time vs app-time, and scaled-time vs unscaled-time. So I created my own time utility to track these twelve different views of time.
I also created some other convenience utilities in my time class. If you are at all familiar with JavaScript development, you may miss using setTimeout and setInterval! I've added both of them, as well as a throttle function, a debounce function, and a custom Tween system so that you can easily configure interpolation to consider these different views of time.
My time class is on GitHub, in case you're interested in how I've implemented this!
🎉 Cheers!
Comments
Post a Comment