Slowly reducing a variable's value in a loop?

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By Robster
:warning: Old Version Published before Godot 3 was released.

Hi all,

I have a function being called that reduces my character’s boost as a button is pressed. When the boost runs out, he can no longer boost until it rebuilds.

Currently though, when I do something like:

boostRemaining = boostRemaining-boostUsageRate

The boost is all used before you can even blink.

How can I go about slowing it down so it isn’t reduced by boostUsageRate on every frame?

I’m sure this is a basic and once learned it will be a concept I’ll use everywhere so am keen to see what others have to say.

Thanks a tonne…

Rob

:bust_in_silhouette: Reply From: gonzo191

Use a timer or coroutine to represent the remaining time for boosting rather than decrementing from your value directly. The reason why your value is gone in a blink of an eye is because of how quickly the engine updates so you need to have it be limited by the ramerate.

So do something like this instead:

var max_boost_time = 5.0    # represents 5 secs
var boost_timer    = 0      # current boost count timer

_process(dt):
	if (boost_timer < max_boost_time):
		boost_timer += dt
		# handle boost logic here...

	if (boost_timer > max_boost_time):
		# reset boost timer and cool down

I think this is bending my brain. I can’t figure out what’s wrong with me today.

My understanding is the timer counts down from Wait Time to 0.

With the code example it appears to be saying:

If Timer < 5
Increase timer's Wait Time
#do boost

The problem with the above is, if I’m holding down the boost key, it will be recharging whilst it’s using the boost.

If Timer > 5 
#reset Timer.

With above, if somehow the timer ended up with a Wait Time larger than 5 then sure. But that would be a logic error and not normal.

I’m a little lost sorry. Thank you for your help though, maybe I’m just not here today fully in my head.

Robster | 2016-08-22 04:07

:bust_in_silhouette: Reply From: avencherus

Consider how much of an amount you want to be gone from one continuous second. Say it’s 100 boost fuel.

# Lose 100 units per second.
fuel_loss_per_second = 100

Then use the delta given by the _process(delta) function and multiply it into that amount, while the conditions are met.

What happens with this, is in a game operating at 60 frames per second, that will be 60 updates that take about (1 second / 60 frames) .016666~ seconds each.

Now if you’re multiplying 100 by .016, you’re expending 1.6 fuel points every update while holding your button down. After 60 frames that will total up to 100 (or super close) after 1 second of game time.

func _process(delta):
  if boosting:
    fuel = fuel - fuel_loss_per_second * delta

OK that’s a pretty cool and simple solution. What happens though if the game chugs down and frame rates decrease? I’d assume the boost would not work to plan? What are your thoughts on that?

Robster | 2016-08-22 04:10

I think the problem you’re worried about is what is going to happen if you don’t use delta, or game logic or nodes that are fed by delta time.

The game loop measures and works to keep timing synchronized. Delta is a value it reports as it works to do this from frame to frame.

Another benefit worth mentioning is that if you decide to change the frame rate of your game, and you’re using delta, you don’t have to manually adjust any of your code or values.

I should’ve pointed out too that you may want to use fixed_process() rather than process() for mechanics that need to be more accurately handled over time.

The process() function is more for rendering, and the delta will be variable. I haven’t tested it in Godot, but I think it still will give a value that tracks to the elapsed time. It tries to hit your frame rate, but it if it doesn’t it will adjust the delta it sends to reflect the difference since the last time it updated. Meaning if you lost a whole frame, the next update you will have a delta that is twice as large, which should keep the values accurate over time.

If you want to see the difference I would recommend just doing a print(delta) in each function, and watch the output console.

A game loop is generally split into two parts, the physics and the rendering. For the physics part, which is fixed_process(delta) in Godot, it will generally aim to keep time as best it can, and all timesteps are a fixed size. This is important, because of lost precision in rounding floating point numbers. If you have .01444 then .01788 next frame, you might lose some small amount of accuracy in the multiplications, even if it averages out to .01666 over 1 second.

When using a fixed time step, if the game loop falls behind, it will begin doing multiple updates per frame to catch up, and begin to defer the rendering. As long as it can do this, it will keep the simulation accurate. The game may hiccup visually, but it will do all the calculations in the meanwhile. So your amounts like distance traveled and fuel consumed will still be calculated 60 times a second. The loop will measure and check the time to make sure it is staying faithful to 60 updates a second, even if they end up being awkwardly spaced out because of performance problems.

If your game can never catch up, than there is only one solution for that, you have to reduce something or find some optimization solution, because the machine you’re using cannot handle what you’re asking it to do.

For the occasional spike in performance, things should be fine, this is what delta time is designed to handle. However, in the extreme cases, when things are so bad and physics frames have to be dropped to keep the game from locking up (spiral of death), then you have to consider the metrics of how much work is being done and how much the hardware can handle.

avencherus | 2016-08-22 05:54

That is the best description of how it works that I could have read. I really want to just stop and say thank you.

Interestingly I’ve gone and made up a whole set of logic which I’ll paste below to help others in the future but the description you gave is priceless. It’s a shame there’s no “sticky” feature here.

Thanks again friend. It’s really cleared up a lot of things for me.

Robster | 2016-08-22 13:14

You’re quite welcome Robster. X)

I’m glad it was sensible enough that you got value from it.

avencherus | 2016-08-22 14:33

:bust_in_silhouette: Reply From: Sojan

As the others said, simplly use a timer.

var timer = 100			#initiall length of the timer

if true:			#replace true with whatever your trigger
	if timer <= 200
		#end your speed boost here
	else:
		timer = 0	#this resets the timer whene trigger is false
else:
	pass

#note, i am not sure what units this uses, and hence how long the boost will hold. But you can just multiply it by delta to get seconds.

:bust_in_silhouette: Reply From: Robster

I’ve gone through a heap of options in my head and thank you to all who have helped. This image shows what I’m leaning towards at the moment. Let’s see if it works in code and if I’ve missed anything.

Boost Logic Workflow

On Key Just Pressed
    Allow Timer to be Active
On Key Held down
    IF Timer is Active
        IF Timer > 0
            USE UP BOOST! Go FAST!
        ELSE IF Timer == 0
            Make Timer Inactive (pause boost)
            Don't allow Timer to be Active
    ELSE if Timer is not Active
        IF allowed to Make Timer Active
            Make Timer Active (start/resume boost)
On Key released
    Make Timer inactive (pause boost)
    Start recharging the Timer to its full ammount

OK I have regenerative boost working now.

You can Boost, then when not boosting it rebuilds itself back up. Here’s the code for some inspiration for someone one day in the future:

Variables being used:

#boost usage
var boostTimer     = null  # the actual  boost timer
var boostUse       = false # is the boost being used or not?
var boostAllow     = true  # are we allowing the player to boost or not?
var boostRegenRate = 0     # progress of regeneration of boost (when in use)

I also have this one in a globals file but probably can be local rather than global:

var playerBoostGrowRate = 1			#how fast does the boost regenerate

In the func _ready()

#setup the boost bits	
boostTimer = get_node("BoostTimer") 								#create boost usage timer
boostTimer.connect("timeout", self, "on_boost_timer_timeout") 		#connect a function (on_boost_timer_timeout) to the boost timer's timeout signal
boostTimer.set_wait_time(globals.get("playerMaxBoost")) 			#setup the boost timer with the amount of time it should run for
boostTimer.start()													#start up the timer countdown
boostTimer.set_active(false)										#make the timer inactive at the start of the scene (pause it)

Main boost code called from func _fixed_process(delta):

func boost_use(delta):
	#if we're using boost somewhere
	if boostUse :  
		if boostTimer.is_active():
			if boostTimer.get_time_left() > 0:   #if there's still time on the timer 
				print("BOOSTING!")
				boostRegenRate = boostTimer.get_time_left()   #reduce the regenRate so it matches the amount of time left, this way when it regenerates it starts regenerating at the same place that the boost has reduced to.
			elif boostTimer.get_time_left() == 0:  	#we're out of boost
				boostTimer.set_active(false)  		#turn off timer / pause boost
				boostTimer.stop()
				boostAllow = false					#don't allow boost anymore 
		else:
			if boostAllow == true:
				boostTimer.set_active(true)		#turn on the timer (unpause boost)
				boostTimer.start()
			
	else:
		#turn off the timer (this pauses it at its current count value)		
		boostTimer.set_active(false)
		boostTimer.stop()

		#now increment the boost timer back up to its maximum time
		#we need If Time left is less than wait time
		if debug_boostUse.check() == 0:  #key not pressed
			if boostTimer.get_time_left() <= globals.get("playerMaxBoost"):  							#only increase boost if we've used some
				if boostRegenRate <= globals.get("playerMaxBoost"):
					boostRegenRate = boostRegenRate + globals.get("playerBoostGrowRate") * delta		#do math increase at a rate determined in globals.gd
					boostTimer.set_wait_time(boostRegenRate)  												#change the timer value with each parse through the loop

To use it I simply turn on boostAllow = true and to turn it off I do the opposite (boostAllow = false).

In my HUD I have a progress bar that shows the boost as shown here:

onready var player = get_node("/root/world/player")
onready var boostTimer = get_node("/root/world/player/BoostTimer")

func _ready():
	set_fixed_process(true)

func _fixed_process(delta):

	#get the maximum boost value for the boost bar
	get_node("BoostBar").set_max(globals.get("playerMaxBoost"))

	#show the player boost in the boost bar
	get_node("BoostBar").set_value(player.boostRegenRate)

I hope this helps someone in the future. I don’t know if I made that over complicated but it works really well so I’m happy.

I can now tap into it with dashing, using weapons, using jetpacks, etc. Anything that needs boost I can use the above code to restrict how long the boost is used.

Robster | 2016-08-24 11:20