Prototyping the laser

When designing visual effects, you always want to have the underlying mechanic or interactions in place. That’s why we’re going to prototype our laser’s fire mechanic first.

For this effect, we need:

  1. A RayCast2D node to detect collisions and limit the length of the laser.
  2. a Line2D node to draw our laser beam, following the raycast.
  3. Three Particle2D nodes. We are going to emit particles from the ship, along the beam, and where the laser hits. Name them respectively CastingParticles2D, BeamParticles2D, and CollisionParticles2D.
  4. A Tween node to animate the laser’s width, when we start or stop casting the beam.

Create these nodes in a new scene, with the raycast as the scene’s root node. Rename the RayCast2D node LaserBeam2D.

Your scene should look like that:

We are going to use the glowing_circle.png image for all the particle systems. You can find it in the FileSystem dock under the assets/ folder. Select all the Particle2D nodes at the same time and drag the image to the Texture property in the Inspector. Changing a property with multiple nodes selected updates them all at the same time.

Save your scene as LaserBeam.tscn

Coding the interactive beam

Next, we have to write some code to fire the beam.

Let’s start by making the beam cast towards a direction until it reaches its maximum length. Add a new GDScript file to the LaserBeam2D.

Open the script and add two exported properties so you can configure the beam’s cast speed and its maximum length.

class_name LaserBeam2D
extends RayCast2D

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

We need to make the raycast extend up to max_length at a speed of cast_speed. We are then going to use it to limit the length of the Line2D node, where it intersects with other colliders.

We are going to use the _physics_process callback for that. Inside the _physics_process method, let’s use the cast_to property of RayCast2D`` to extend the ray. Clamp the length tomax_length`.

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

The calculation above extends the laser to the right, which corresponds to an angle of 0 in Godot. Doing so ensures we won’t run into problems when we rotate the laser later.

Designing the base visuals

We have the raycast set, now let’s create the laser. Back to the scene, in the FillLine2D, create 2 points. The line should:

  1. Only have two points.
  2. The first point should be at the origin of the world, Vector2(0, 0).

You can use the Grid Snap to snap the points to precise locations or expand the Points property in the Inspector to set their coordinates by hand.

Back to the script, store a reference to the FillLine2D node. You can place it right below the exported variables you already have:

onready var fill := $FillLine2D

Now, we need to make the FillLine2D extend with the ray we cast in _physics_process. For that, create a cast_beam() method and call it in _physics_process:

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


func cast_beam() -> void:
    pass

In the cast_beam() method, we are going to calculate how far the laser should extend:

  1. If the raycast collides with something, the laser should stop there.
  2. Otherwise, the laser should extend to the length of the ray.

In the cast_beam() method, add a cast_point variable and have it default to the cast_to value. If the raycast is colliding, then assign the collision point’s coordinates to the cast_point variable.

The physics engine updates the collision information on raycasts and other physics nodes all together at the end of the physics frame. We need to update the raycast manually to get correct collision information. Call force_raycast_update() before checking for collisions to do so.

Finally, set the fill line’s last point to the value of cast_point to extend the laser beam. Your cast_beam function should look like this:

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

    force_raycast_update()
    if is_colliding():
        cast_point = to_local(get_collision_point())

    fill.points[1] = cast_point

Note that we need to convert the collision point’s coordinates to local ones because the function get_collision_point() returns a global position, and our fill line uses local coordinates for drawing.

To test your changes, in _physics_process, you can add a call to look_at(get_global_mouse_position()). This function rotates your node towards the mouse. Be sure to remove the extra line after testing at the end of the tutorial.