We’re ready to build the last object before we can shoot something: the actual weapons system. It doesn’t have a complicated job. It exposes properties in the inspector and places cannons according to the template it’s given.
src/ModularWeapon.gd
class_name ModularWeapon extends Node2D # The scene template for the position of cannons in space export var emitter_configuration: PackedScene # The emitter that controls what projectile to shoot export var projectile_emitter: PackedScene # An array of motion resources that control how the projectiles move. export(Array, Resource) var projectile_motions := []
We’ll cover the motions next!
When you change firing configuration, you should also be removing the old ones, so we need functions to clean up and to add new emitters.
To clean up, we remove any that are ProjectileEmitter
s for each child under the weapons system.
It takes a frame before the object is removed from the scene tree when using queue_free
, so we also call remove_child
because we want to replace the old emitters with the new ones right away. Otherwise, for one frame, you’d have both the old and the new cannons, and this could lead to undesired results. It could also not, but it’s better to be safe.
func _clear_emitters() -> void: for child in get_children(): if child is ProjectileEmitter: remove_child(child) child.queue_free()
To configure the cannon load-out, we grab the template, analyze it, and then create emitters in their place. Since it’s a PackedScene, we have access to instance
and get_children
to do both. For each weapon location, add a new emitter, position it, and rotate it.
Note the manual call to free
at the end to remove the configuration. We never add it as a child of any node, so there is no parent to clean up when it leaves the scene tree. It falls to us to clean up the node.
We could use queue_free
instead, but since it’s not in the scene tree (we didn’t call add_child
), nothing depends on it.
func _add_new_emitters() -> void: var configuration := emitter_configuration.instance() for weapon_position in configuration.get_children(): var new_emitter := projectile_emitter.instance() new_emitter.position = weapon_position.position new_emitter.rotation = weapon_position.rotation new_emitter.weapons_system = self add_child(new_emitter) configuration.free()
Right now, the weapons get configured once at the start. If you replace either the emitter or the configuration in the middle of the game, it won’t change the cannon load-out. A setter using setget
solves that problem: when you set emitter_configuration
or projectile_emitter
, Godot calls the setter. In those setters, we call clear and add emitters to redo all the instancing.
export var emitter_configuration: PackedScene setget _set_emitter_configuration export var projectile_emitter: PackedScene setget _set_emitter func _set_emitter_configuration(value: PackedScene) -> void: emitter_configuration = value if not is_inside_tree(): yield(self, "ready") _clear_emitters() _add_new_emitters() func _set_emitter(value: PackedScene) -> void: projectile_emitter = value _set_emitter_configuration(emitter_configuration)
The actual scene is a resource, the first resource you add to anything that should shoot projectiles with this system. It’s one Node2D with the weapons system script.
ModularWeaponsSystem.tscn
And now, after this hard work creating base classes with virtual functions for emitters and projectiles, we can finally add the modular system onto the ship, drag the firing configuration and emitter into its properties, and shoot a plasma pellet.
Glorious.