Refining the beam

Our laser works, but it doesn’t look anything like a laser. Let’s make it glow. To do so, we’re going to use Godot’s post-processing glow effect, which works both in 2D and in 3D.

We need to:

  1. Use color values above 100% in the red, green, and blue channels to make the pixels shine.
  2. Set up a world environment with the glow post-processing effect active.

Create a new 2D scene for your final laser beam demo. Click and drag LaserBeam.tscn from the FileSystem tab to your scene. Add a WorldEnvironment node as a sibling of the laser and select it.

In the Inspector, click on the Environment property and create a new “Environment” resource. Expand that resource. Change its Background Mode to “Canvas” and enable the Glow property.

Expand the Glow category and change the Blend Mode property to “Additive”. Doing so adds the values generated by the glow shader to the rendered frame, making the effect look bright. The “Soft Light” blending mode is a good alternative for a more nuanced and realistic result.

Then, head back to the LaserBeam2D scene. Select the FillLine2D and change the Default Color to a lighter blue. In the Capping category, change Joint Mode, Begin Cap, and End Cap to “Round”.

Select FillLine2D, BeamParticles2D, CollisionParticles2D, and CastingParticles2D and change their Modulate color property to a tone beyond 100% white. To do so, click on the color slot to open the color picker, and turn on the “Raw” mode. The raw mode allows you to increase color values beyond 100% intensity in each channel, triggering effects such as the glow shader.

For more information, see the official documentation on Glow.

Coding a fire mechanic

If you try the game, the beam is continuously active. Let’s add an interactive weapon fire mechanic.

Head back to the script editor and open the LaserBeam.gd file.

In the _ready callback, deactivate the _physics_process callback and set the FillLine2D last point to Vector2.ZERO. Doing so makes the line invisible at the start of the game.

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

Add a new property, is_casting, to be able to toggle the beam’s cast on and off. At the top of the class, above the fill onready variable, add a new variable named is_casting, set it to false by default, and give it a setter function, set_is_casting.

var is_casting := false setget set_is_casting

This setter function is going to control what happens when we start and finish casting the laser beam.

Add the set_is_casting() setter method after cast_beam().

There, don’t forget first to assign the argument the function receives to the is_casting variable. If is_casting is true, we are going to reset the laser beam. Set cast_to to Vector2.ZERO and do the same for the last point in the fill line.

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

    if is_casting:
        cast_to = Vector2.ZERO
        fill.points[1] = cast_to

After that, use the value of is_casting to enable or disable the _physics_process callback.

set_physics_process(is_casting)

You can use the _unhandled_input callback to test the shooting mechanic by calling set_is_casting(). Something like this:

func _unhandled_input(event: InputEvent) -> void:
    # Turn on casting if Enter or Space is pressed.
    if event.is_action_pressed("ui_accept"):
        set_is_casting(true)
    # Stop casting the beam upon releasing the key.
    elif event.is_action_released("ui_accept"):
        set_is_casting(false)

But make sure to remove that input handling function after running the test as input is not the responsibility of the LaserBeam2D.

Animating the beam

If you test the scene now, the beam has its full width whenever you fire. We are going to animate it, so the laser grows and shrinks. That’s where the Tween node we created in the first section comes in.

We need a new exported variable to control how fast the line’s width increases or decreases when animating the beam.

Add a new variable below max_length and name it growth_time.

export var growth_time := 0.1

Then, add a reference to the Tween node, under the onready var fill statement.

onready var tween := $Tween

We need to store the FillLine2D’s initial width to animate to that value. Add a new onready var below the tween variable to store that value.

onready var line_width: float = fill.width

At this point, the top of your script should look like this:

extends RayCast2D

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

onready var fill := $FillLine2D
onready var tween := $Tween

onready var line_width: float = fill.width

Create two methods below cast_beam() to animate the bar’s width. Call them appear() and disappear().

We are going to use Tween.interpolate_property() to animate our beam’s width.

When you tween properties, you might want to stop running animations before starting a new one, so they don’t conflict. That’s our case with appear() and disappear()

You can use Tween.is_active() and Tween.stop_all() to do so:

func appear() -> void:
    if tween.is_active():
        tween.stop_all()


func disappear() -> void:
    if tween.is_active():
        tween.stop_all()

Let’s focus on the appear() method for now. We need to:

  1. Interpolate the fill.width from 0 to line_width for a duration of growth_time.
  2. Start the tween.
func appear() -> void:
    if tween.is_active():
        tween.stop_all()
    tween.interpolate_property(fill, "width", 0, line_width, growth_time * 2)
    tween.start()

Do the same in the disappear(), but interpolate from the current fill.width to 0.

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

Let’s call appear() and disappear() inside set_is_casting(). If is_casting is true, call appear, otherwise, call disappear():

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

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

    set_physics_process(is_casting)

If you test the scene now, with the input code set up, you should see the beam animate.