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