Setting Up A Basic Projectile

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.

Creating a scene we can spawn from the emitter

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.

Firing our new projectile

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)

Creating the cannon scene

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