We created the base class for an emitter and a projectile. Now it’s time to use them to create a simple projectile. The basic projectile shoots one plasma pellet at a time.
We use a timer to keep track of the lifetime. When it runs out, then the projectile should vanish. Note the use of _post_setup
before starting the timer. setup
is the one that sets the lifetime which we need here.
src/Projectile/Basic/BasicProjectile.gd
extends Projectile onready var timer: Timer = $Timer func _post_setup() -> void: if not is_inside_tree(): yield(self, "ready") timer.start(lifetime) func _on_Timer_timeout() -> void: queue_free()
The if not is_inside_tree(): yield(self, "ready")
block is a common coroutine trick in Godot. It makes the function stop executing until the node is in the tree and has called _ready
and onready
. In this case, it’s to make sure we’ve found the timer.
We have the projectile we want to shoot, but we also need a scene to hold it. Projectiles in this tutorial are kinematic bodies with a collision shape. We know we need a timer for the lifetime and we also add a sprite for graphics. You could add an AudioStreamPlayer2D for a low volume crackling or whizzing noise, or maybe some particles.
src/Projectile/Basic/BasicProjectile.tscn
The sprite is a simple circle, but with a properly configured WorldEnvironment
node and a HDR modulate color, we can get a pretty lovely glowing effect.
We have the projectile; now we need the emitter.
src/Projectile/Basic/BasicEmitter.gd
extends ProjectileEmitter # The base projectile scene that this emitter fires. Since it lives right next to the emitter script, we can use a relative path. var projectile := preload("BasicProjectile.tscn") # One Shot timer to control the firing rate. onready var timer := $Timer
I’m not a fan of games where you have to tap the fire buttons constantly, so instead of an input event. We determine when to shoot based on the Input singleton. If we’re holding down the button and the timer isn’t running, then we fire and start the timer. If you want to tap the button, do this in _unhandled_input
instead and use the event
it provides.
func _physics_process(_delta: float) -> void: if Input.is_action_pressed("fire") and timer.is_stopped(): fire() timer.start(1.0 / projectiles_per_second)
For the tutorial’s simplicity, we call fire based on input in the emitter. A better solution is to have a custom signal that comes from higher up, like the ship it’s attached to, or the weapon system. That way, AI and networked games can use the system too by emitting a fire
signal!
The specialized function for the emitter is where we create and set up the projectile to fire. We give it the position of the cannon and the data we got from the fire
function.
func _do_fire(_direction: Vector2, _motions: Array, _lifetime: float) -> void: if not spawned_objects: return var new_projectile := projectile.instance() new_projectile.setup(global_position, _direction, _motions, _lifetime) spawned_objects.add_child(new_projectile)
The emitter is the Node2D that uses the basic emitter script. We add the timer with One Shot enabled. It’s not complicated, but it is the resource that ends up in the weapons system, so it goes into the /Projectiles
folder instead of /src
.
Projectiles/BasicEmitter.tscn