Creating a recurring cost for an Ability

Godot Version

4.6.2

Question

Ok so for a bit of context. I am trying to create a ChanneledAbility class for a game I’m working on. in this context, a ChanneledAbility is an Ability that can be turned on or off. Note that this is distinct from an ability that applies a lingering effect. Moreover, while a ChanneledAbility is active, every second it will attempt to pay its cost every second if possible. And its this last part that I’m having problems with.

So far Ive tried 2 methods of implementing this “cost-over-time” mechanic and unfortunately neither worked. the first method I tried was by creating the timer via code. However that ran into the problem of; if you turned off and then back on the ability within a second, it would end up creating two valid repayment Timers. which naturally caused things to break.

The second method I**‘ve tried is by using an @export var, something along the lines of. This would work however it would require me to use the _ready() function to connect the Timer’**s timeout signal. I don’t like this because doing it this way would mean that I can never create a ChanneledAbility that uses the _ready() function for any reason.

If anyone would mind helping me figure out how I should go about implementing this “cost-over-time” mechanic that would be greatly appreciated.

Subtract a small amount of cost each frame in _process().

I could but A) that has a similar problem to using a node for the timer where it overwrites a commonly used function, in this scenario _process() and B) that would mean having to check that every frame for every ChanneledAbility which feels really inefficient.

I might just have to go with the node option for now and figure out if there’s a better option later.

Feelings are often misleading :smile:

You can do it with tweens as well. If _process() performs ok, keep to that.

1 Like

Make your own simple timer. I use this kind of timing more than the actual Timer nodes.

var ability_timer: float = 0.0
var ability_lifetime: float = 3.0
	
func _physics_process(delta: float) -> void:
	if ability_timer > 0:
		ability_timer -= delta
		pay_ability_cost()
			
func use_ability() -> void:
	ability_timer = ability_lifetime

More complicated version for 1 second payment interval (code not tested…):

var payment_timer: float = 0.0
var payment_interval: float = 1.0
var payments_required: int = 3
var payment_counter: int = 0

func _physics_process(delta: float) -> void:
	if payment_timer > 0:
		payment_timer -= delta
		if payment_timer <= 0:
			pay_ability_cost()
			payment_counter += 1
			if payment_counter < payments_required:
				payment_timer = payment_interval

func use_ability() -> void:
	payment_counter = 0
	if payment_timer <= 0: # not yet paying, so reset the timer
		payment_timer = payment_interval

Maybe you could have a bool that checks if there’s a timer active. To prevent multiple timers from spawning.

Also, from my understanding, your ChanneledAbility sounds like a resource meter. Kinda like Phara’s jetpack from Overwatch.

image

If this is the case, you should tie the resource to a float variable. Then when the ability is active, subtract the variable by delta. And when it’s not active, stop the timer. And have that bool I mentioned.

You could just deduct the cost instantly on timer on. Whenever you turn it off part way through or reach zero, set it to full again.

I dont see a reason to create new timers when toggling on and off. Reusing the same makes more sense. It may seem wasteful to have some timers around when not active but checking if a timer is active has next to zero cost.

You could also just use one timer and set its reset value to whichever ability is active but then it starts to get kind of finicky and you have to do work arounds if you want to be able to have several active abilities.

I think using simple code timers in the process function would be the way to go here.

A lot of good suggestions here by others already.

This is a problem with how you implemented it, not with using a code created Timer. Simply assigning it to the same variable every time would solve this particular problem. It would also improve performance.

This is not true. You can call _super() in the inherited class in the _ready() function to get that functionality.


If I were going to implement this, I’d do it with a float and using delta as others have suggested. I’d do something like this:

class_name ChanneledAbility extends Ability

var timer: float = 0.0
var charges: int = 100:
	set(value):
		charges = value
		if charges <= 0:
			disable()


func _ready() -> void:
	set_process(false)


func _process(delta: float) -> void:
	timer += delta
	if timer >= 1.0:
		charges -= 1


func enable() -> void:
	if charges > 0:
		set_process(true)	


func disable() -> void:
	set_process(false)

The benefit of this approach is that you can track partial seconds used when the ability is turned off. If you don’t want to track partial usage, just add timer = 0.0 to the disable() function and it will reset whenever turned off. Turning _process() on and off means you don’t need an is_active variable, and if you do need to check if its active, you can just check if _process() is running. If your charges run out, you use the setter to turn the ability off. If you want to know when the ability fails to turn on,. you can just add a bool return value to enable().

1 Like

Technically, yes, but why would you want to do it like that?

2 Likes

The most elegant and performant way is likely doing it with a tween. It doesn’t require any script code to run on the per frame basis.

class_name Depletor
	
var tween: Tween
	
func _init(object, property: String, depletion_rate: float) -> void:
	tween = object.create_tween()
	tween.set_loops()
	tween.tween_property(object, property, -depletion_rate, 1.0).as_relative()
	tween.pause()
	
func _notification(what):
	if what == NOTIFICATION_PREDELETE:
		tween.kill()

func toggle():
	if tween.is_running():
		tween.pause()
	else:
		tween.play()

Usage:

extends Node

var depletable_property = 20.0
var depletor: Depletor

func _ready():
	depletor = Depletor.new(self, "depletable_property", 4.0)
	depletor.toggle()

func _process(_dt):
	if Input.is_action_just_pressed("ui_accept"):
		depletor.toggle()
	print(depletable_property)
3 Likes