Emitting particles dynamically

It is time to round out our laser beam’s code to use our new effects.

Go to the script editor and open the LaserBeam2D.gd script.

At the top of the code, add three new onready variables to reference our three particle nodes, CastingParticles2D, CollisionParticles2D, and BeamParticles2D.

Add these lines below the line onready var tween := $Tween.

onready var casting_particles := $CastingParticles2D
onready var collision_particles := $CollisionParticles2D
onready var beam_particles := $BeamParticles2D

To control the particles’ emission, we are going to use the is_casting property inside the set_is_casting() method.

func set_is_casting(cast: bool) -> void:
    # [...]

    set_physics_process(is_casting)
    casting_particles.emitting = is_casting
    beam_particles.emitting = is_casting

The complete function should look like this:

func set_is_casting(cast: bool) -> void:
    is_casting = cast

    if is_casting:
        cast_to = Vector2.ZERO
        fill.points[1] = cast_to
        appear()
    else:
        collision_particles.emitting = false
        disappear()

    set_physics_process(is_casting)
    beam_particles.emitting = is_casting
    casting_particles.emitting = is_casting

Next, we are going to make the CollisionParticles2D emit when the beam is colliding. We also have to move the node to the collision point. For that, inside the cast_beam() method, use the cast_point as the particles’ position.

Also, change the particles’ global rotation based on the collision normal. You can do so using the get_collision_normal() method from RayCast2d. It gives us a normalized vector that we can convert to an angle, using the method Vector2.angle().

func cast_beam() -> void:
    # [...]
    collision_particles.emitting = is_colliding()

    if is_colliding():
        cast_point = to_local(get_collision_point())
        collision_particles.global_rotation = get_collision_normal().angle()
        collision_particles.position = cast_point
    # [...]

We finally need to extend the BeamParticles2D’s emission box to cover the entire beam’s length. The box works with extents, that is to say, half the size of a rectangle that expands in two directions. We have to always keep the particle system in the middle of the beam. Set its position to half of the cast_point vector, and use half the beam’s length for the Emission Shape extents x value.

Add these lines at the bottom of the cast_beam() function.

beam_particles.position = cast_point * 0.5
beam_particles.process_material.emission_box_extents.x = cast_point.length() * 0.5

The function should look like this:

func cast_beam() -> void:
    var cast_point := cast_to

    force_raycast_update()
    collision_particles.emitting = is_colliding()

    if is_colliding():
        cast_point = to_local(get_collision_point())
        collision_particles.global_rotation = get_collision_normal().angle()
        collision_particles.position = cast_point

    fill.points[1] = cast_point
    beam_particles.position = cast_point * 0.5
    beam_particles.process_material.emission_box_extents.x = cast_point.length() * 0.5

And with that, you can pat yourself in the back: you’re done. You have a terrifying laser to blast your enemies… or the player! It’s your choice.

Here is the complete LaserBeam2d.gd class for reference:

extends RayCast2D

export var cast_speed := 7000.0
export var max_length := 1400
export var growth_time := 0.1

var is_casting := false setget set_is_casting

onready var fill := $FillLine2D
onready var tween := $Tween
onready var casting_particles := $CastingParticles2D
onready var collision_particles := $CollisionParticles2D
onready var beam_particles := $BeamParticles2D

onready var line_width: float = fill.width


func _ready() -> void:
    set_physics_process(false)
    fill.points[1] = Vector2.ZERO


func _physics_process(delta: float) -> void:
    cast_to = (cast_to + Vector2.RIGHT * cast_speed * delta).clamped(max_length)
    cast_beam()


func set_is_casting(cast: bool) -> void:
    is_casting = cast

    if is_casting:
        cast_to = Vector2.ZERO
        fill.points[1] = cast_to
        appear()
    else:
        collision_particles.emitting = false
        disappear()

    set_physics_process(is_casting)
    beam_particles.emitting = is_casting
    casting_particles.emitting = is_casting


func cast_beam() -> void:
    var cast_point := cast_to

    force_raycast_update()
    collision_particles.emitting = is_colliding()

    if is_colliding():
        cast_point = to_local(get_collision_point())
        collision_particles.global_rotation = get_collision_normal().angle()
        collision_particles.position = cast_point

    fill.points[1] = cast_point
    beam_particles.position = cast_point * 0.5
    beam_particles.process_material.emission_box_extents.x = cast_point.length() * 0.5


func appear() -> void:
    if tween.is_active():
        tween.stop_all()
    tween.interpolate_property(fill, "width", 0, line_width, growth_time * 2)
    tween.start()


func disappear() -> void:
    if tween.is_active():
        tween.stop_all()
    tween.interpolate_property(fill, "width", fill.width, 0, growth_time)
    tween.start()