Clashing signals are making upgrades for a weapon’s fire rate act strangely / not work

Godot Version

Godot 4.2.2

Question

Hi all,

I’ve been trying to make my first game (top-down shooter) for a few months and I’ve ran into a persistent issue. Basically, when I’ve used a Timer node and the set_wait_time() function to determine the fire rate of a weapon, a clash seems to happen between two signals that are meant to apply upgrades to the weapon and change the fire rate.

I believe the clash happens because set_wait_time() is called twice; once when any upgrades are activated, and again when other upgrades are deactivated:

func un_equip_change_fire_rate(value, shipcolor):
	var updated_fire_rate = fire_rate_stat.fire_rate_mult + value
	var fireraterefresh = 1 / updated_fire_rate
	fire_rate_timer.set_wait_time(fire_rate_timer.wait_time * fireraterefresh)
	print("Weapon fire_rate_timer: ", fire_rate_timer.wait_time, " on ", shipcolor, " side")


func equip_change_fire_rate(value, shipcolor):
	var updated_fire_rate = fire_rate_stat.fire_rate_mult + value
	var fireraterefresh = 1 / updated_fire_rate
	fire_rate_timer.set_wait_time(fire_rate_timer.wait_time * fireraterefresh)
	print("Weapon fire_rate_timer: ", fire_rate_timer.wait_time, " on ", shipcolor, " side")

Problems start to happen because the upgrades are supposed to add onto or decrease fire rate as floats (represented as ‘value’). When I want to apply multiple upgrades at once (using multiple signals), one set_wait_time() call tends to override the others due to the nature of set_wait_time(). Ideally, these values would be combined into one modified fire rate variable, but this doesn’t happen. This has caused fire rate upgrades to not work and act in strange ways (e.g. sometimes “equip_change_fire_rate” overrides “un_equip_change_fire_rate” and causes fire rate to permanently increase each time a weapon is activated)!

Is there a way to increase/decrease the wait_time value of a timer through code without using set_wait_time() and experiencing these kinds of signal clashes? If not, is there a better system I can use for modifying the fire rate of a weapon?

If anything’s unclear, feel free to ask and I can add more details.

Cheers!

Is there any difference between these two functions? Because I can’t spot any, then why do you have it as 2 separate functions?
Anyway, you can adjust the wait_time property directly with the += operator (you probably need to adjust this formula to match your game logic)

func un_equip_change_fire_rate(value, shipcolor):
	fire_rate_timer.wait_time += value

This way, when the function is called 10x even in the same frame, it will each time adjust the wait time accordingly taking into consideration previous changes without overwriting them. When the value is negative, it will decrease the time.

Hi @wchc,

I tried that solution and unfortunately it didn’t work; I think this is because of something I’ll go into more detail now. The initial fire_rate value starts as a multiplier, not a set value for the Timer’s wait_time:

extends Node


@export var fire_rate_mult: float = 1.00

I’ve done this because it makes the maths easier for making upgrades and for gameplay purposes (e.g. an upgrade is described as increasing fire rate by 20%, rather than decreasing time between shots by X seconds). It also makes it possible to apply the player’s fire rate stat across different weapon types (e.g. values don’t need to be adjusted for a weapon with an innately higher fire rate, like a minigun).

One difficult part of this system is converting fire_rate_mult into an actual value that I can add onto the Timer’s wait_time. That’s why simply adding ‘value’, in its current state, to wait_time isn’t working.

For example, if I had a weapon with a base wait_time of 2.00 seconds between each shot, and I wanted to increase its fire rate by 20% (e.g. fire_rate_mult goes from 1.00 → 1.20), the ‘value’ to add should eventually be converted to -0.4: the actual no. of seconds I want to take away from wait_time and decrease the time between each shot. That would be the ideal method but with the current system, that isn’t possible.

Sorry about this! If anything else needs to be clarified, please let me know!

(By the way, I’ve made two signals because they’re called at different times; one for activated upgrades and one for when they’re put away and become inactive. I’ve done this because my game’s logic treats activated and deactivated upgrades quite differently, but do let me know if this is bad practice!)

Ok, so if we’re talking percentages then I assume your value that you throw in this function is e.g. 0.20 as in 20%, which means you want to increase the fire rate by 20%, which means you want to decrease the wait time by 20%. Then the formula would be that:

func un_equip_change_fire_rate(value, shipcolor):
	fire_rate_timer.wait_time /= (1 + value)

What you then just need to be aware of is that decreasing the value by 20% and then increasing it by 20% doesn’t lead to the value returning to the same initial state, it will be lower.
Depending on what your goal is, this might or might not be what you want. Let me know if you need the value to return to the initial value, I can help you with that as well.

2 signals is ok, but if the logic what happens after the signal is triggered should be exactly the same in both cases, then they can point to the same function, e.g. just change_fire_rate(). No need to have 2 functions, because it makes it harder to maintain later - any change you do to one of them you need to remember to do twice.

Edit: There is a good practice that you can remember - DRY - Don’t Repeat Yourself. Whenever you see yourself repeating the same pattern, there probably is a better way to do it. You don’t need to stick to it perfectly, but it helps to identify potential issues early on.

1 Like

Hi @wchc,

Thank you for your great replies! After I followed your code…

func un_equip_change_fire_rate(value, shipcolor):
	fire_rate_timer.wait_time /= (1 + value)

…the fire rate upgrades are working much better! It seems that this is part of the problem is solved. Thank you!

However, the fact that the value doesn’t return to the same initial state is a small problem - if you could offer me some help on how to implement this, that would be fantastic!

As for the DRY rule, I’ve realised there are a few instances of this in other parts of my code, so I’ll get to work on fixing those and making my code easier to work with in future. Thank you for this advice!

1 Like

Alright. Just so you understand where this issue comes from, let me briefly explain. Let’s assume your initial value is 100 for simplicity. You increase it by 20%, and then decrease it by 20%.
100 * (1 + 20%) = 100 * 1.2 = 120
120 * (1 - 20%) = 120 * 0.8 = 96
The same the other way around - first decrease by 20%, then increase by 20%
100 * (1 - 20%) = 100 * 0.8 = 80
80 * (1 + 20%) = 80 * 1.2 = 96
Maths can be weird :upside_down_face:

In order to mitigate the issue, your initial instinct might have been correct - to make a separate variable and adjust it instead of going directly for the wait_time. You already have the fire_rate_mult, which we can use here - this is assuming it always start as 1.0, otherwise it might behave incorrectly. And if it’s always 1.0 - then you don’t need the @export keyword.

extends Node

var fire_rate_mult: float = 1.00

We need to additionally introduce the base_wait_time variable that holds the original value of the timer. Then, in your change_fire_rate() function you can first adjust the fire_rate_mult variable and then apply it to the wait_time by multiplying the base_wait_time with fire_rate_mult.

@onready var base_wait_time: float = fire_rate_timer.wait_time

func change_fire_rate(value, shipcolor):
	fire_rate_stat.fire_rate_mult += value
	fire_rate_timer.wait_time = base_wait_time * fire_rate_stat.fire_rate_mult

What you might want to be on the lookout now is that when you have an upgrade with e.g. +20% increased fire rate, and you apply it 5x, your fire_rate_mult variable will go down to exactly 0.0, which will break your Timer. Not sure if that situation will ever happen in your game, but if so - you might want to safeguard against it somehow.

Remember, multiplication of scalars is commutative, so you can apply these in any order. If you’ve got a set of upgrades/downgrades, it might be worth keeping an explicit array of them:

const BASE_FIRE_RATE: float = 2.0 # One shot every two seconds.

var fire_rate_mods: Array = []
var current_fire_rate: float = BASE_FIRE_RATE

func add_fire_rate_mod(title: String, icon: String, value: float, expiry: int) -> void:
    fire_rate_mods.append({"name": title, "icon": icon, "value": value, "expiry": expiry})
    recalc_fire_rate()

func expire_fire_rate_mods(time: int) -> void:
    var rem_vals: Array = []

    # Make a list of elements that have expired.
    for i in fire_rate_mods.size():
        var mod: Dictionary = fire_rate_mods[i]
        if mod["expiry"] <= time:
            rem_vals.append(i)

    # Walk the list backwards, removing them.  Backwards because the list collapses on removal.
    for i in rem_vals.reverse():
        fire_rate_mods.remove_at(i)

    recalc_fire_rate()

func recalc_fire_rate() -> void:
    var rate: float = BASE_FIRE_RATE

    for mod: Dictionary in fire_rate_mods:
        rate *= mod["value"]

    current_fire_rate = rate
1 Like

Hi @wchc,

I’ve managed to combine both functions into one change_fire_rate() function, and it’s going through successfully. However, when I ran this code…

@onready var base_wait_time: float = fire_rate_timer.wait_time

[...]

func change_fire_rate(value, shipcolor):
	fire_rate_stat.fire_rate_mult += value
	fire_rate_timer.wait_time = base_wait_time * fire_rate_stat.fire_rate_mult

…it sadly didn’t seem to work. The actual value for when the upgrade becomes active is 0.5 (becomes negative when deactivated), but the code seems to decrease base_fire_rate to 0.75 instead of the expected 0.5.

It might be something to do with the fact that the line number for @onready var base_wait_time: float = fire_rate_timer.wait_time is grey instead of green (I’ve taken a screenshot since preformatted text might not show it):

I’m not entirely sure what this means, or if it has anything to do with the problem, but if you have any ideas let me know!

You can try adding print() statements in your code to see what all values actually are at the time of running the code. Hopefully it will help you find the rootcause. If not - check back in here so we can figure out together.

Hi @wchc,

I’ve added some print() functions like this:

func change_fire_rate(value, shipcolor):
	#fire_rate_timer.wait_time /= (1 + value)
	print("Value at change_fire_rate(): ", value)
	fire_rate_stat.fire_rate_mult += value
	print("fire_rate_mult at change_fire_rate(): ", fire_rate_stat.fire_rate_mult)
	fire_rate_timer.wait_time = base_wait_time * fire_rate_stat.fire_rate_mult
	print("base_wait_time at change_fire_rate(): ", base_wait_time)
	print("Weapon fire_rate_timer at change_fire_rate(): ", fire_rate_timer.wait_time, " on ", shipcolor, " side")

There’s another print() print("Fire rate value: ", value) from an earlier signal, just to check if value changes between signals being called. There’s also a print() function for each weapon under ready() that tells me their initial base_wait_times:

func _ready():
	muzzle_flash.visible = false
	var fireraterefresh = 1 / fire_rate_stat.fire_rate_mult
	fire_rate_timer.set_wait_time(fire_rate_timer.wait_time * fireraterefresh)
	#base_wait_time = fire_rate_timer.wait_time
	print("BASE WAIT TIME: ", base_wait_time)

They gave me this output, which is correct for what I’m going for:

BASE WAIT TIME: 0.5
BASE WAIT TIME: 0.2

However, the first time I activated the upgrade (initially equipping it), the print() functions under change_fire_rate gave me this output (this weapon should have a base_wait_time of 0.5):

Fire rate value: 0.5
Value at change_fire_rate(): 0.5
fire_rate_mult at change_fire_rate(): 1.5
base_wait_time at change_fire_rate(): 0.5
Weapon fire_rate_timer at change_fire_rate(): 0.75 on 0 side

This is where the initial weirdness seems to come from. It’s using the correct base_wait_time, but I think the line fire_rate_timer.wait_time = base_wait_time * fire_rate_stat.fire_rate_mult (0.5 * 1.5) is turning wait.time into 0.75. I’m not sure if making value negative instead of positive would help (and vice-versa for when the upgrade is de-activated)? Let me know what you think.

When I switched to a different weapon with a base_wait_time of 0.2 (un-equipping and deactivating the upgrade), I got this output:

Fire rate value: -0.5
Value at change_fire_rate(): -0.5
fire_rate_mult at change_fire_rate(): 1
base_wait_time at change_fire_rate(): 0.5
Weapon fire_rate_timer at change_fire_rate(): 0.5 on 1 side

Again, the line fire_rate_timer.wait_time = base_wait_time * fire_rate_stat.fire_rate_mult (1 * 0.5) seems to be turning wait_time into 0.5.

When I switched back to the first weapon (re-activating it), I got this:

Fire rate value: 0.5
Value at change_fire_rate(): 0.5
fire_rate_mult at change_fire_rate(): 1.5
base_wait_time at change_fire_rate(): 0.2
Weapon fire_rate_timer at change_fire_rate(): 0.3 on 0 side

Strangely, the base_wait_time now seems to match that of the second weapon. 1.5 x 0.2 = 0.3, hence the new wait_time. I’m not sure why base_wait_time would change like this. The actual firing speed of the weapon on-screen also changes after this; to my eyes, it looks like it’s firing a bullet every 0.5 seconds, rather than every 0.3 seconds as the code might suggest.
I have a feeling that all the print() functions after the upgrade is initially activated are called right before the weapon actually switches, which means they output the variables for the previous weapon… This doesn’t explain the strange behaviour, though, so I might be wrong.

Switching to the second weapon again, I got this:

Fire rate value: -0.5
Value at change_fire_rate(): -0.5
fire_rate_mult at change_fire_rate(): 1
base_wait_time at change_fire_rate(): 0.5
Weapon fire_rate_timer at change_fire_rate(): 0.5 on 1 side

What’s really weird is that on-screen, this time the weapon looks like it fires a bullet every 0.3 seconds, rather than every 0.5 seconds as the fire_rate_timer would have us believe. This makes me more suspicious that it’s outputting the variables for the previous weapon, but they’re still acting strangely. It might need to go back and check when exactly the switch_weapon() signal is called elsewhere in the code…
Another theory is that there might be another function somewhere else that changes the wait_time just after change_fire_rate is called. I don’t think this is the case because change_fire_rate() doesn’t call any global signals that would change the , but I’ll double-check this if it becomes necessary to do so.

The wait_time for re-activating the first weapon seemed to stabilise after this, because switching back to it a couple more times still gave me 0.3:

Fire rate value: 0.5
Value at change_fire_rate(): 0.5
fire_rate_mult at change_fire_rate(): 1.5
base_wait_time at change_fire_rate(): 0.2
Weapon fire_rate_timer at change_fire_rate(): 0.3 on 0 side

The values from switching back to the second weapon also stabilised in the same way.

Very strange behaviour and output going on here; let me know what you think!