2025/06/21

Type
Learning Resource
Format
Video
Version
Godot 4.x
  • Downloadable Demo
  • FAQ/Troubleshooting
  • Bonus
Code
Assets
All else
Copyright 2016-2025, GDQuest
Created
2025/05/13
Updated
2025/06/21

2D laser in Godot 4

This video and guide explain how to create a laser beam in 2D using Godot. The laser is a raycast node that extends and retracts, with a line drawn on top of the invisible ray to visualize it.

Code Reference: res://laser_2d/laser_2d.gd

Here is the complete code reference for the laser shown in the video, with just the raycast and the line. This is the foundation of the laser. The bulk of the logic is in the set_is_casting() and _physics_process() functions. The appear() and disappear() functions animate the laser's thickness with a tween.

@tool
extends RayCast2D

## Speed at which the laser extends when first fired, in pixels per second.
@export var cast_speed := 7000.0
## Maximum length of the laser in pixels.
@export var max_length := 1400.0
## Distance in pixels from the origin to start drawing and firing the laser.
@export var start_distance := 40.0
## Base duration of the tween animation in seconds.
@export var growth_time := 0.1
@export var color := Color.WHITE: set = set_color

## If `true`, the laser is firing.
@export var is_casting := false: set = set_is_casting

var tween: Tween = null

@onready var line_2d: Line2D = %Line2D
@onready var line_width := line_2d.width


func _ready() -> void:
	set_color(color)
	set_is_casting(is_casting)
	line_2d.points[0] = Vector2.RIGHT * start_distance
	line_2d.points[1] = Vector2.ZERO
	line_2d.visible = false

	if not Engine.is_editor_hint():
		set_physics_process(false)


func _physics_process(delta: float) -> void:
	target_position.x = move_toward(
		target_position.x,
		max_length,
		cast_speed * delta
	)

	var laser_end_position := target_position
	force_raycast_update()

	if is_colliding():
		laser_end_position = to_local(get_collision_point())

	line_2d.points[1] = laser_end_position


func set_is_casting(new_value: bool) -> void:
	if is_casting == new_value:
		return
	is_casting = new_value

	set_physics_process(is_casting)

	if not line_2d:
		return

	if is_casting:
		var laser_start := Vector2.RIGHT * start_distance
		line_2d.points[0] = laser_start
		line_2d.points[1] = laser_start
		appear()
	else:
		target_position = Vector2.ZERO
		disappear()


func appear() -> void:
	line_2d.visible = true
	if tween and tween.is_running():
		tween.kill()
	tween = create_tween()
	tween.tween_property(line_2d, "width", line_width, growth_time * 2.0).from(0.0)


func disappear() -> void:
	if tween and tween.is_running():
		tween.kill()
	tween = create_tween()
	tween.tween_property(line_2d, "width", 0.0, growth_time).from_current()
	tween.tween_callback(line_2d.hide)


func set_color(new_color: Color) -> void:
	color = new_color
	if line_2d == null:
		return
	line_2d.modulate = new_color

Changing the laser's look and feel with different colors

The Line2D node used for the laser has multiple features you can use to change its look. Most notably, you can assign a gradient to the Gradient property to add some shade or change the tint along the length of the beam. The gradient will be applied from the start to the end of the line.

If you use very bright colors, the engine's glow post-processing effect can pick up on them and give you a nice gradient within the glow.

Here are some color gradients to try:

Fiery sunbeam: red to bright yellow
Screenshot of the laser with a red to yellow gradient
Blue flame: deep to light blue or cyan
Screenshot of the laser with a blue gradient
Radioactive green: neon green to light green
Screenshot of the laser with a green gradient
Disco beam: light purple to neon pink
Screenshot of the laser with a disco beam gradient

Keeping the color consistent

All three particle effects use a white color by default. I use the modulate property to make their color match the laser color. Here's the code that updates the color of all three particle systems and the laser line. It's part of the script:

@export var color := Color.WHITE: set = set_color

@onready var line_2d: Line2D = %Line2D
@onready var casting_particles: GPUParticles2D = %CastingParticles2D
@onready var collision_particles: GPUParticles2D = %CollisionParticles2D
@onready var beam_particles: GPUParticles2D = %BeamParticles2D


func _ready() -> void:
	set_color(color)


func set_color(new_color: Color) -> void:
	color = new_color

	if line_2d == null:
		return

	line_2d.modulate = new_color
	casting_particles.modulate = new_color
	collision_particles.modulate = new_color
	beam_particles.modulate = new_color

It uses:

  1. A property named color to store the color value
  2. A setter function set_color, attached to the color property, to apply the color to the laser and all particle systems when changing the color

Paired with the tool annotation, the code runs in the editor and allows us to preview the color change.

Improving the laser visuals with particle effects

The laser uses 3 particle systems to make the beam look appealing. They add energy and movement to what would otherwise be just a boring, static line:

This clip shows the complete laser with all three particle systems
  1. The casting particles emit from the base of the laser and sprawl out in a single direction. They are the first particles emitted when the laser fires.
  2. The collision particles are a variation of the casting particles that emit where the laser hits something. They create a burst effect that looks like the laser is nibbling away at the target.
  3. The beam particles spawn energy bits that radiate from the entire beam and softly fly away.

All three particle systems have this in common:

As often with particles, it's mostly a few settings that really differentiate them. More on that below. But first, let's talk about how I use to keep the laser and the particle color in sync.

Casting particles breakdown

The casting particles use spread and linear velocity to fire in a cone, paired with a relatively high emission amount. Here are the most notable properties I set on their process material:

This clip shows the casting particles firing from the laser base, in isolation

Collision particles breakdown

The collision particles burst out from the point where the laser hits something. They're similar to the casting particles, but have a few differences to distinguish them. Here are the most notable properties I set on them:

This clip shows the collision particles bursting at the collision point, next to an asteroid

Beam particles breakdown

The beam particles emit along the entire length of the laser beam, creating the impression of energy radiating from it. They use a high emission amount, a box emission shape that resizes with the laser beam, and a lot of tangential acceleration to create a swirling effect.

Here are the most notable properties I set on their process material:

This clip shows the beam particles flowing along the entire laser, in isolation

Here's the code that resizes the beam particles emission shape to match the laser beam size. It's part of the script:

@onready var line_2d: Line2D = %Line2D
@onready var beam_particles: GPUParticles2D = %BeamParticles2D


func _physics_process(delta: float) -> void:
	# ...
	var laser_start_position := line_2d.points[0]
	beam_particles.position = laser_start_position + (laser_end_position - laser_start_position) * 0.5
	beam_particles.process_material.emission_box_extents.x = laser_end_position.distance_to(laser_start_position) * 0.5

Below you can find the complete laser code reference with the added particle effects. You can also download the project files to see the laser in action or modify it.

Code Reference: res://laser_2d/laser_2d.gd
@tool
extends RayCast2D

## Speed at which the laser extends when first fired, in pixels per seconds.
@export var cast_speed := 7000.0
## Maximum length of the laser in pixels.
@export var max_length := 1400.0
## Distance in pixels from the origin to start drawing and firing the laser.
@export var start_distance := 40.0
## Base duration of the tween animation in seconds.
@export var growth_time := 0.1
@export var color := Color.WHITE: set = set_color

## If `true`, the laser is firing.
## It plays appearing and disappearing animations when it's not animating.
## See `appear()` and `disappear()` for more information.
@export var is_casting := false: set = set_is_casting

var tween: Tween = null

@onready var line_2d: Line2D = %Line2D
@onready var casting_particles: GPUParticles2D = %CastingParticles2D
@onready var collision_particles: GPUParticles2D = %CollisionParticles2D
@onready var beam_particles: GPUParticles2D = %BeamParticles2D

@onready var line_width := line_2d.width


func _ready() -> void:
	set_color(color)
	set_is_casting(is_casting)
	line_2d.points[0] = Vector2.RIGHT * start_distance
	line_2d.points[1] = Vector2.ZERO
	line_2d.visible = false
	casting_particles.position = line_2d.points[0]

	if not Engine.is_editor_hint():
		set_physics_process(false)


func _physics_process(delta: float) -> void:
	target_position.x = move_toward(
		target_position.x,
		max_length,
		cast_speed * delta
	)

	var laser_end_position := target_position
	force_raycast_update()

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

	line_2d.points[1] = laser_end_position

	var laser_start_position := line_2d.points[0]
	beam_particles.position = laser_start_position + (laser_end_position - laser_start_position) * 0.5
	beam_particles.process_material.emission_box_extents.x = laser_end_position.distance_to(laser_start_position) * 0.5

	collision_particles.emitting = is_colliding()


func set_is_casting(new_value: bool) -> void:
	if is_casting == new_value:
		return
	is_casting = new_value
	set_physics_process(is_casting)

	if beam_particles == null:
		return

	beam_particles.emitting = is_casting
	casting_particles.emitting = is_casting

	if is_casting:
		var laser_start := Vector2.RIGHT * start_distance
		line_2d.points[0] = laser_start
		line_2d.points[1] = laser_start
		casting_particles.position = laser_start

		appear()
	else:
		target_position = Vector2.ZERO
		collision_particles.emitting = false
		disappear()


func appear() -> void:
	line_2d.visible = true
	if tween and tween.is_running():
		tween.kill()
	tween = create_tween()
	tween.tween_property(line_2d, "width", line_width, growth_time * 2.0).from(0.0)


func disappear() -> void:
	if tween and tween.is_running():
		tween.kill()
	tween = create_tween()
	tween.tween_property(line_2d, "width", 0.0, growth_time).from_current()
	tween.tween_callback(line_2d.hide)


func set_color(new_color: Color) -> void:
	color = new_color

	if line_2d == null:
		return

	line_2d.modulate = new_color
	casting_particles.modulate = new_color
	collision_particles.modulate = new_color
	beam_particles.modulate = new_color

Download files

Updates / Code patches

Bonus

How do you make the laser glow?

To make the laser glow, I'm using the engine's built-in glow environment effect. The scene has a world environment node with the glow post-processing effect turned on. This effect blankets the entire screen and makes any pixel above a certain brightness threshold glow.

By setting the laser to a very bright value, it triggers the glow effect.

When using this post processing effect, be careful that other assets in the game are not as bright or they will glow.

If your game uses the Vulkan rendering engine, you can also turn on the HDR 2D project setting to allow picking very bright colors.

Become an Indie Gamedev with GDQuest!

Don't stop here. Step-by-step tutorials are fun but they only take you so far.

Try one of our proven study programs to become an independent Gamedev truly capable of realizing the games you’ve always wanted to make.

Nathan

Founder and teacher at GDQuest
  • Starter Kit
  • Learn Gamedev from Zero
Check out GDSchool

You're welcome in our little community

Get help from peers and pros on GDQuest's Discord server!

20,000 membersJoin Server

Contribute to GDQuest's Free Library

There are multiple ways you can join our effort to create free and open source gamedev resources that are accessible to everyone!