2
\$\begingroup\$

In games like Europa Universalis or Hearts of Iron, there is a way to set game speed. This affects how long does e.g. one in-game day take in real-life time.

At first, I thought this mechanism was independent from framerate, i.e. measuring actual time passed, but that cannot be true: For example, speed 5 is faster on a faster machine than on a slower machine. Still, on both machines, the proportional difference between speed 5 and speed 1 is the same. And you can still take actions while the game is paused (speed = 0).

How is this implemented? In the context of the Godot engine (and gdscript), which provides _process and _physics_process methods, what is the right approach to achieve this?

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Do you want this to be frame dependent for your game? \$\endgroup\$
    – Theraot
    May 10, 2023 at 10:05
  • \$\begingroup\$ I don't currently understand the advantage in doing it the way the games I mentioned do it, so it is hard to decide whether that's what I need. Let's go with Yes. \$\endgroup\$
    – jakub
    May 10, 2023 at 12:47

3 Answers 3

2
\$\begingroup\$

Frame rate dependent

If you want to trigger events in a frame dependent way, then you are going to count frames.

That is, you will have code like this:

var frame_count := 0

func _process(_delta:float) -> void:
    frame_count += 1

And then based on the accumulated value, you trigger your event.

What advantage is there in doing it that way? I don't know. In fact, In Godot this is the approach that will require more code, as everything that would make this task easier is frame rate independent out of the box.


Frame rate independent

Please notice that you might instead of an counter int make an accumulator float and increment it by delta, making this frame independent. The result would be a poor-man's Timer.

In fact, you could use a Timer, or a SceneTreeTimer, or other time keeping solutions (e.g. Time.get_ticks_msec()) to trigger events.

You can also take advantage of Animations (using an AnimationPlayer), for example to change the sky and lights for a day-night cycle.


If you wanted a frame independent way to slow down or speed up your game, you can use Engine.time_scale.


Note: You probably don't want the time to keep going while the game is paused. Please refer to Pausing games and process mode. You can also handle NOTIFICATION_PAUSED and NOTIFICATION_UNPAUSED in the _notification virtual method.

\$\endgroup\$
3
  • \$\begingroup\$ Very useful! What happens if a bunch of calculations are supposed to take place in one in-game day, but end up taking longer than the expected time? My intuition (which could be wrong) is that fr dependent approach would make it easier to keep things in sync (even if a hiccup occurs). I am not sure how this would work using the fr independent approach. \$\endgroup\$
    – jakub
    May 10, 2023 at 17:20
  • \$\begingroup\$ @jakub What do you need to keep in sync? Instead of running a lot of stuff in a frame, you would want to run a bit of it per frame, so no hiccup. If you worry that eats your time, consider starting the next timer when the prior task ends. If we are talking about synchronizing with audio (e.g. rhythm games), it can get complicated because of audio delays. For the complicated cases you can start by having a background Thread sleeping for the desired time, and running the code when it awakes (and use deferred operations to interact). Consider also computing ahead of time what you need. \$\endgroup\$
    – Theraot
    May 10, 2023 at 17:36
  • \$\begingroup\$ Lots of good starting points. I will need to think this through a bit more, so for now I will just accept the answer. Thanks! \$\endgroup\$
    – jakub
    May 11, 2023 at 18:24
0
\$\begingroup\$

I think these kinds of games usually separate their simulation from their regular update and render loops. That allows them to rapidly update the sim while keeping a normal framerate for rendering.

Two primary approaches to increase game speed come to mind:

  1. Increase delta time passed to your sim_update()
  2. Increase the number of times you call sim_update()

The first tends to be easier (and it's likely similar to using Engine.time_scale). However, it can cause issues on fast mode where entities may make large leaps in their individual updates that modify the outcome of the sim.

This comment from Limeox explains:

Say some object goes invincible for 0.5 seconds and is about to take two damaging hits, one at 0.45s and one at 0.55s. If your delta is 0.01 seconds, the first hit will be blocked and the second one won't, as the effect has worn off by then. If it is 0.2 seconds, the effect may now block both, or neither, depending on when it is removed relative to the hits. Or you could have the "second" hit blocked, because it simulates first, then the effect is removed, then the "first" one hits?

By just simulating more ticks, this wouldn't happen. Of course, by pushing those occurrences closer together (hits at 0.499s and 0.501s), you can construct similar issues. But, and this is the important point, the behavior will be identical regardless of game speed. After all the result doesn't need to be 100% correct (especially in the case of physics, you'd go insane). It just needs to be 100% consistent at all speeds.

So instead, you can do #2 and update your sim more frequently to ensure the result of each update is the same, but occurs at a faster rate. Of course, this means your sim update must be fast enough that you can afford to run it multiple times in 33ms to still hit 30 FPS on fast mode. But by decoupling it from animations, fx, sound, and any other presentation elements, you can likely avoid a great deal of cpu time.

For gdscript, that means you'd have something like this:

@export_range(1, 5) var game_speed := 3
var sim

func _process(delta):
    for i in game_speed:
        sim.update(delta)
    sim.apply_to_world.emit()

Everything processed in sim wouldn't use _process or _physics_process. Likely, you wouldn't want to rely on a general physics simulation at all. apply_to_world pushes the sim changes out so everything can grab the latest state for presentation.

\$\endgroup\$
0
\$\begingroup\$

I assume you are from Poland like me but lets keep it in English, I hope you will understand my poor english :). I am making a strategy/managment game now and I had same questions like you. I came with Idea using Timer. So, I have one Timer that clicks every 0.05 second (lowest recommended by Godot documentation). I have global variable which keep value of 1440.0 which is the number of minutes of full day. I have following speed settings:

  • Normal - with every tick of TImer vaule of 2 (minutes) are substracted from 1440 - the day last 36 seconds
  • 2x speed - with every tick of TImer vaule of 4 (minutes) are substracted from 1440 - the day last 18 seconds
  • 4x speed - with every tick of Timer vaule of 8 (minutes) are substracted from 1440 - the day last 9 seconds
  • Pause

When 1440 counter reaches 0, I am adding +1 day and resetting the counter.

I have also global variable holding timescale (1, 2 and 4) which are used as multiplayers for all animations, speed variables, shaders and particle emmiting speed.

I wanted this approach as I wanted to separate speed of simulation from speed of GUI which also consist of animated windows/frames, floating labels etc, I also have animated drag and drop elements. I guess changing Engine.time_scale would affect whole game including GUI. Also when I was testing stuff with Engine.time_scale I noticed some stuttering of simulation, animations when time scale was set to 2 or 4.

I don't know if this approach is good one as I am pretty new to GameDev (around 3 years of hobby game making) but it suits my needs so far. Not that much of a code, I have one Singleton that holds variables (day, month, years counters, time of day, continuous timer which keep time passed from start of game) and one Node+script that is controlling time in my game.

I am open to other ideas :)

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .