Godot Version
Godot 4.4
Question
Hey everyone !
I’m working on a top-down shooter with an upgrade system that’s giving me some headaches. I’d appreciate advice on a cleaner architecture.
Current Implementation
My upgrade system uses callbacks that are stored in arrays in the ShootingManager and passed to bullet instances:
- Base Upgrade Class:
class_name Upgrade
extends Resource
@export var upgrade_name: String = "Base Upgrade"
@export var upgrade_description: String = "Base upgrade description"
func on_apply_amelioration(shooting_manager : ShootingManager, health_manager : HealthManager):
pass
- Bullet Function Arrays:
# Function collections for shooting and bullet events
var bullet_on_spawn_functions = []
var bullet_on_hit_functions = []
var bullet_on_bounce_functions = []
var bullet_on_expiry_functions = []
Upgrade Flow Explained
Here’s how the flow works in my current system:
-
When an upgrade is applied, it calls
on_apply_amelioration()
which:- Adds callback functions to the ShootingManager’s function arrays
-
When a player shoots:
shooting_manager.shoot()
creates a bullet- The bullet is initialized with the function arrays for different events
-
During bullet lifecycle:
- Callbacks from
on_spawn_functions
run when bullets are created - Callbacks from
on_hit_functions
run when bullets hit enemies
…
- Callbacks from
Example: Splittig Bullets Upgrade
The Explosive Bullets upgrade demonstrates the problem:
func on_apply_amelioration(shooting_manager: ShootingManager, health_manager: HealthManager):
var explode_func = func(bullet):
print("explode")
for i in range(6):
var angle = i * (2 * PI / 6)
var direction = Vector2(cos(angle), sin(angle))
var new_bullet = shooting_manager.bullet_scene.instantiate()
bullet.get_tree().root.add_child(new_bullet)
# Add explosion function to bullet hit events
shooting_manager.bullet_on_hit_functions.append(explode_func)
In my bullet script, when a hit is detected:
func _on_area_entered(hurtbox: HurtBox) -> void:
# ... existing collision handling ...
spawn_effects()
print("hit")
for funct in on_hit_functions:
funct.call(self) # This calls explode_func when bullet hits
queue_free()
# ... more code ...
The Problem
The error occurs because:
- Bullet collides with an enemy during physics processing
_on_area_entered
is called- The
explode_func
tries to create and add new bullets to the scene tree - This happens during the physics step when queries are being flushed
- Godot throws an error:
E 0:00:04:409 explosive_bullets_upgrade.gd:21 @ <anonymous lambda>(): Can't change this state while flushing queries. Use call_deferred() or set_deferred() to change monitoring state instead.
<Erreur C++> Condition "body->get_space() && flushing_queries" is true.
<Source C++> modules/godot_physics_2d/godot_physics_server_2d.cpp:663 @ body_set_shape_as_one_way_collision()
<Pile des appels>explosive_bullets_upgrade.gd:21 @ <anonymous lambda>()
bullet.gd:104 @ _on_area_entered()
I could fix this specific case with call_deferred
, but I feel like the whole design is problematic. Passing functions arrays like this seems wrong and making new upgrades will probably be as hard to debug as this one.
Also physics timing issues seems hard to manage.
I’m looking for a more robust architecture for upgrades that modify bullet behavior an patterns for organizing complex upgrade interactions like this. It seems really hard to do with Godot.
Has anyone implemented a similar system that works well? Any design patterns that would work better for this kind of feature?
Thank you for any insights!