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.
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()
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)
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")
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