Skip to main content

CanvasLayer Overview

CanvasLayer nodes render their children on separate rendering layers, independent of the camera and scene hierarchy. They’re primarily used for:
  • UI overlays (HUD, menus)
  • Parallax backgrounds
  • Foreground effects
  • Multi-layered scenes

Basic CanvasLayer

Creating a UI Layer

extends CanvasLayer

func _ready():
    # Set layer index (higher = drawn on top)
    layer = 1
    
    # Make visible
    visible = true

Layer Ordering

Layers are drawn in order of their layer property:
# Background layer
background_layer.layer = -1

# Game layer (default is 0)
game_layer.layer = 0

# UI layer
ui_layer.layer = 1

# Debug overlay
debug_layer.layer = 2
Embedded windows are on layer 1024. Layers 1025+ appear above embedded windows.

UI with CanvasLayer

Basic HUD Setup

Game (Node2D)
├── Level (Node2D)
│   ├── Player
│   └── Enemies
└── HUD (CanvasLayer)
    ├── HealthBar
    ├── ScoreLabel
    └── PauseButton

Example HUD Script

extends CanvasLayer

@onready var health_bar = $HealthBar
@onready var score_label = $ScoreLabel

func _ready():
    layer = 1  # Above game content

func update_health(value: int):
    health_bar.value = value

func update_score(value: int):
    score_label.text = "Score: %d" % value

Following the Viewport

# Layer stays fixed on screen (default)
follow_viewport_enabled = false

# Layer follows camera in world space
follow_viewport_enabled = true
follow_viewport_scale = 1.0  # Adjust scale for parallax effect

Transform Properties

CanvasLayer has its own transform, independent of the scene:
extends CanvasLayer

func _ready():
    # Offset the entire layer
    offset = Vector2(10, 10)
    
    # Rotate the layer
    rotation = deg_to_rad(5)
    
    # Scale the layer
    scale = Vector2(1.5, 1.5)

func shake():
    # Shake effect for the whole layer
    var tween = create_tween()
    tween.tween_property(self, "offset", Vector2(randf_range(-5, 5), randf_range(-5, 5)), 0.05)
    tween.tween_property(self, "offset", Vector2.ZERO, 0.05)

Custom Viewport

# Assign specific viewport
custom_viewport = $SubViewport

# Use default viewport
custom_viewport = null

ParallaxBackground

ParallaxBackground creates parallax scrolling effects for backgrounds.
ParallaxBackground is deprecated in favor of Parallax2D. However, it’s still widely used and functional.

Basic Setup

Level (Node2D)
├── ParallaxBackground
│   ├── ParallaxLayer (Sky)
│   │   └── Sprite2D
│   ├── ParallaxLayer (Clouds)
│   │   └── Sprite2D
│   └── ParallaxLayer (Mountains)
│       └── Sprite2D
├── TileMap
└── Player

ParallaxBackground Properties

extends ParallaxBackground

func _ready():
    # Scroll offset (usually controlled by camera)
    scroll_offset = Vector2(0, 0)
    
    # Base offset for all layers
    scroll_base_offset = Vector2(0, 0)
    
    # Base scale for all layers
    scroll_base_scale = Vector2(1, 1)
    
    # Ignore camera zoom
    scroll_ignore_camera_zoom = false
    
    # Scroll limits
    scroll_limit_begin = Vector2(0, 0)
    scroll_limit_end = Vector2(0, 0)

Manual Scrolling (No Camera)

extends ParallaxBackground

func _process(delta):
    # Manual scroll control
    scroll_offset.x += 50 * delta

ParallaxLayer

ParallaxLayer must be a child of ParallaxBackground. Each layer moves at a different rate.

Motion Scale

extends ParallaxLayer

func _ready():
    # Move at half speed (far background)
    motion_scale = Vector2(0.5, 0.5)
    
    # Move at double speed (foreground)
    # motion_scale = Vector2(2.0, 2.0)
    
    # No horizontal movement
    # motion_scale = Vector2(0, 1)

Motion Offset

# Initial offset
motion_offset = Vector2(100, 0)

Infinite Scrolling

extends ParallaxLayer

func _ready():
    # Repeat texture every 512 pixels horizontally
    motion_mirroring = Vector2(512, 0)
    
    # No vertical mirroring
    # motion_mirroring.y = 0
For pixel-perfect mirroring, set motion_mirroring to match your background sprite’s width.

Complete Parallax Example

# ParallaxBackground.gd
extends ParallaxBackground

func _ready():
    layer = -100  # Behind everything

# Sky Layer (ParallaxLayer)
# motion_scale = Vector2(0.1, 0.1)
# motion_mirroring = Vector2(1024, 0)

# Cloud Layer (ParallaxLayer)
# motion_scale = Vector2(0.3, 0.3)
# motion_mirroring = Vector2(768, 0)

# Mountain Layer (ParallaxLayer)
# motion_scale = Vector2(0.6, 0.6)
# motion_mirroring = Vector2(512, 0)

With Camera2D

When using Camera2D, the parallax updates automatically:
Player (CharacterBody2D)
├── Camera2D
└── Sprite2D

Level (Node2D)
└── ParallaxBackground
    ├── SkyLayer (ParallaxLayer)
    ├── CloudLayer (ParallaxLayer)
    └── MountainLayer (ParallaxLayer)

Multi-Layer Scene Example

# Game structure with multiple canvas layers
extends Node2D

func _ready():
    # Background parallax
    var bg = $BackgroundLayer
    bg.layer = -1
    
    # Main game layer (default, layer 0)
    # Contains player, enemies, etc.
    
    # UI overlay
    var ui = $UILayer
    ui.layer = 1
    
    # Pause menu
    var pause = $PauseLayer
    pause.layer = 2
    pause.visible = false

func _input(event):
    if event.is_action_pressed("pause"):
        toggle_pause()

func toggle_pause():
    var pause_layer = $PauseLayer
    pause_layer.visible = !pause_layer.visible
    get_tree().paused = pause_layer.visible

Advanced: Dynamic Parallax

extends ParallaxLayer

var base_motion_scale = Vector2(0.5, 0.5)

func _process(delta):
    # Change parallax speed based on player state
    var player = get_tree().get_first_node_in_group("player")
    if player:
        if player.is_running:
            motion_scale = base_motion_scale * 1.5
        else:
            motion_scale = base_motion_scale

Visibility Control

# Hide entire layer and children
visible = false

# Show layer
visible = true

# Toggle visibility
func toggle_layer():
    visible = !visible

# Fade layer
func fade_out():
    var tween = create_tween()
    tween.tween_property(self, "modulate:a", 0.0, 1.0)

func fade_in():
    var tween = create_tween()
    tween.tween_property(self, "modulate:a", 1.0, 1.0)

Canvas Layer for Screen Effects

extends CanvasLayer

@onready var color_rect = $ColorRect

func _ready():
    layer = 10  # Very high layer
    color_rect.color = Color(0, 0, 0, 0)

func flash_white():
    var tween = create_tween()
    tween.tween_property(color_rect, "color", Color.WHITE, 0.1)
    tween.tween_property(color_rect, "color", Color(1, 1, 1, 0), 0.1)

func fade_to_black():
    var tween = create_tween()
    tween.tween_property(color_rect, "color", Color.BLACK, 1.0)

Split-Screen with CanvasLayers

# Each viewport needs its own CanvasLayer setup
# Player 1 Viewport
var viewport1 = SubViewport.new()
var canvas1 = CanvasLayer.new()
canvas1.custom_viewport = viewport1

# Player 2 Viewport  
var viewport2 = SubViewport.new()
var canvas2 = CanvasLayer.new()
canvas2.custom_viewport = viewport2

Best Practices

Always put UI elements in a CanvasLayer to keep them independent of the game camera.
Organize layers logically: backgrounds (-1), game (0), UI (1+), overlays (high numbers).
Too many parallax layers can hurt performance. Use 3-5 layers maximum.
For seamless scrolling, set motion_mirroring to exactly match your sprite width.
Each viewport needs its own ParallaxBackground for split-screen games.

Troubleshooting

Check that layer is higher than game content and visible is true.
Ensure there’s a Camera2D in the scene, or manually update scroll_offset.
Verify motion_mirroring matches sprite width exactly, and sprite edges tile seamlessly.
Set follow_viewport_enabled = false to fix UI in screen space.

Next Steps

2D Overview

Revisit 2D fundamentals and camera systems

Sprites and Textures

Learn more about displaying graphics

Build docs developers (and LLMs) love