Now we get into the meat and potatoes of what makes the weapons system work. The firing configuration creates an emitter that shoots projectiles. Here, we create classes that future emitters and projectiles extend and specialize.
src/Projectile/ProjectileEmitter.gd
We use a class name so we can use its type name without a preload statement, then define some useful properties. Not every projectile type uses every property here, but enough of them do that they are in the base class.
class_name ProjectileEmitter extends Node2D # How much damage each collision causes on damageable objects. export var damage_per_collision := 5 # The number of projectiles that the emitter can shoot inside of one second. # It runs a timer with a duration of 1.0 / projectiles per second. export var projectiles_per_second := 1.0 # The amount of seconds before the projectile dies. export var projectile_lifetime := 1.0 # The modular weapons system, where signals for damage go and from which we pull information. var weapons_system: Node # The node to spawn new projectiles and effects into. It is separate from the shooter so projectiles move independent of the shooter and endure if they die. # It can be an Autoload, or a node that is in a group with I.E. `get_tree().get_nodes_in_group("spawned_objects").front()` # We use the base of the scene tree itself for simplicity. onready var spawned_objects: Node = get_tree().root
We split firing a projectile into two functions: one is a simple fire()
function. When you want to shoot, you call fire()
and everything else should fall into place without babysitting.
The other part is a specialized virtual function. Each projectile emitter shoots and acts differently, so _do_fire()
is the one they override and replace. fire
’s job is to pass information to the _do_fire
method such as the direction of travel, how to move, and the lifetime of the projectile.
func fire() -> void: _do_fire( Vector2.UP.rotated(global_rotation), weapons_system.projectile_motions, projectile_lifetime ) func _do_fire(_direction: Vector2, _motions: Array, _lifetime: float) -> void: pass
src/Projectile/Projectile.gd
The projectile data contains the parameters sent by fire
from the emitter, with the addition of _is_setup
. We add it for persistent projectiles. Projectiles that always exist can use the setup function to reset their position and orientation. Duplicating an array of motions every frame is terrible for performance, and those motions may also have an ongoing state (like time) we don’t want to overwrite.
A more thorough implementation would check if the motion type already exists in the list, add those that are missing, and remove those that have disappeared. I leave this as an exercise for you, dear reader.
class_name Projectile extends KinematicBody2D # The amount of seconds before the projectile dies. var lifetime := 1.0 # An array of motions; resources that control how a projectile moves each update frame. var motions := [] # The direction the projectile is moving in. var direction := Vector2.UP # Whether we've configured the projectile once already. This is to prevent persistent projectiles from duplicating the new motions each time we call setup. var _is_setup := false
In setup, we set the base data like position, direction, and lifetime. We iterate over the new motions and create a brand new copy of each motion for the projectile’s use. This ensures the projectile has the motion at the time it got shot out. Like we do with _do_fire
, we add a specialized _post_setup
function. This is a virtual function that each projectile can override, like finding nodes or starting timers.
func setup(_position: Vector2, _direction: Vector2, _motions: Array, _lifetime: float) -> void: position = _position direction = _direction lifetime = _lifetime if not _is_setup: for motion in _motions: var new_motion = motion.duplicate() new_motion.projectile = self motions.append(new_motion) _is_setup = true _post_setup() func _post_setup() -> void: pass