Adding Special Impact Events

The next topic is the side effects of the impact. In this chapter, we create a base class and create an explosion from it that can damage other objects nearby.

Like projectile, emitters and motions, we start with a base class which all impact events extend. It’s a Resource, like motions, and has a public-facing trigger and a specialized _do_trigger function like projectile emitters and projectiles.

The spawn parent can be the spawned_objects node we’re putting projectiles in, but it could also be a separate node, Autoload or grouped node altogether.

src/ProjectileEvents/ProjectileEvent.gd

class_name ProjectileEvent
extends Resource


# Whether the event should trigger when missing
export var triggers_on_misses := false


func trigger(_spawn_location: Vector2, _spawn_parent: Node, _weapons_system, _missed: bool) -> void:
    if _missed and not triggers_on_misses:
        return

    _do_trigger(_spawn_location, _spawn_parent, _weapons_system, _missed)


func _do_trigger(_spawn_location: Vector2, _spawn_parent: Node, _weapons_system, _missed: bool) -> void:
    pass

Note how _weapons_system doesn’t have a type. This is deliberate to prevent Godot 3 from referring to the weapons system which refers to the event in an infinite cycle.

Explosion on impact

We create the resource script for an explosion. In its _do_trigger function, we instance the explosion scene and make it play at the point of origin. We’ll add damage in the next chapter!

src/ProjectileEvents/Events/ExplosionEvent/ExplosionEvent.gd

class_name ExplosionEvent
extends ProjectileEvent

# How far-reaching the explosion should be
export var explosion_radius := 130.0

# How much damage the epicentre of the explosion causes
export var explosion_damage := 30.0

# Whether the damage reduces in damage the further away you are, with 0 at the very edges and max in the centre.
export var damage_scales_with_distances := true

# The layer mask of objects that the explosion can affect, like if you want the player to be immune to their own explosions.
export (int, LAYERS_2D_PHYSICS) var collision_mask: int

# The explosion scene to instance and play.
var explosion_effect := preload("Explosion.tscn")


func _do_trigger(_spawn_location: Vector2, _spawn_parent: Node, _weapons_system, _missed: bool) -> void:
    var explosion := explosion_effect.instance()

    explosion.position = _spawn_location

    _spawn_parent.add_child(explosion)
    explosion.shape.radius = explosion_radius
    explosion.area.collision_mask = collision_mask

    explosion.trigger()

The Explosion scene I’m using here comes from GDQuest’s Godot 2D Visual Effects repo, though I removed the smoke and added a script to play the explosion animation.

extends Node2D


# The animation player that controls the explosion.
onready var player := $AnimationPlayer

# The area object. We use this to detect objects that the explosion overlaps and damages.
onready var area := $Area2D

# The circle that the area uses. We change its radius to match the event's with this.
onready var shape: CircleShape2D = $Area2D/CollisionShape2D.shape


func trigger() -> void:
    player.play("Explode")
    yield(player, "animation_finished")
    queue_free()

Adding events to the weapons system

We add a new array of resources in ModularWeapons and a new helper function to add them without duplicates. The code is otherwise the same.

export (Array, Resource) var projectile_impact_events := []


func add_impact_event(new_event: ProjectileEvent, allows_duplicates := false) -> void:
    if not allows_duplicates:
        var has_event := false
        for event in projectile_impact_events:
            has_event = new_event.get_script() == event.get_script()
            if has_event:
                return
    projectile_impact_events.append(new_event)

Connecting emitters and projectiles

Now we have the task of causing those events to trigger based on hitting or missing. We’ll either send a missed signal or a collided signal along with relevant information.

Projectile.gd

signal collided(target, hit_location)
signal missed(miss_location)

The base projectile emitter has functions to react to those two signals. It iterates over each of the weapons system’s events and triggers each one.

ProjectileEmitter.gd

func _on_projectile_collided(target: Node, hit_location: Vector2) -> void:
    for event in weapons_system.projectile_impact_events:
        event.trigger(hit_location, spawned_objects, weapons_system, false)


func _on_projectile_missed(miss_location: Vector2) -> void:
    for event in weapons_system.projectile_impact_events:
        event.trigger(miss_location, spawned_objects, weapons_system, true)

When the basic projectile collides, it emits the collided signal with the data it got from the move_and_collide call and emits missed when it times out with its position in the world.

BasicProjectile.gd

func _physics_process(delta: float) -> void:
    var movement := _update_movement(delta)

    var collision := move_and_collide(movement)

    if collision:
        emit_signal("collided", collision.collider, collision.position)
        _impact()


func _miss() -> void:
    emit_signal("missed", global_position)
    collision_layer = 0
    collision_mask = 0
    tween.interpolate_property(self, "scale", scale, Vector2.ZERO, 0.25)
    tween.start()
    yield(tween, "tween_all_completed")
    queue_free()

And at the end of the chain is the emitter. When it shoots a projectile, it connects to the collided and missed functions in the new projectile.

BasicEmitter.gd

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)

    new_projectile.connect("collided", self, "_on_projectile_collided")
    new_projectile.connect("missed", self, "_on_projectile_missed")

Adding the explosion resource

All that’s left is to create the explosion resource in the resource part of the weapons system and drag it into the array of events on the weapons system.

ProjectileEvents/ExplosionEvent.tres