From godot-prompter
Covers Godot 4.3+ 2D systems including CanvasLayers, draw order, TileMaps, parallax scrolling, 2D lights/shadows, particles, custom drawing, and meshes with GDScript and C# examples.
npx claudepluginhub jame581/godotprompter --plugin godot-prompterThis skill uses the workspace's default tool permissions.
All examples target Godot 4.3+ with no deprecated APIs. GDScript is shown first, then C#.
Renders high-performance 2D graphics, particle effects, sprite animations, and interactive canvases using PixiJS with WebGL/WebGPU acceleration for games and UI overlays.
Guides implementation of particle effects in Godot 4.3+ using GPUParticles2D/3D, ParticleProcessMaterial, emission shapes, subemitters, trails, attractors, collision, and VFX recipes.
Outlines 2D game development principles covering sprites, animations, tilemaps, collision physics, camera systems, and genre patterns for platformers and top-down games.
Share bugs, ideas, or general feedback.
All examples target Godot 4.3+ with no deprecated APIs. GDScript is shown first, then C#.
Related skills: player-controller for CharacterBody2D movement patterns, animation-system for AnimatedSprite2D and sprite animation, physics-system for collision shapes and raycasting, camera-system for Camera2D follow and shake, shader-basics for 2D shaders and post-processing, godot-optimization for rendering and draw call tuning.
Within a single canvas layer, nodes draw in scene tree order — nodes listed lower in the Scene panel draw on top. Use z_index to override without rearranging the tree.
# Draw this node above siblings (default z_index is 0)
z_index = 10
# Make z_index relative to parent (default: false = global)
z_as_relative = true
CanvasLayer creates a separate rendering layer with its own transform, independent of the camera. Higher layer values draw on top.
| Layer | Typical Use |
|---|---|
| -1 | Parallax backgrounds |
| 0 | Default game layer (all Node2D without CanvasLayer) |
| 1 | HUD / UI overlay |
| 2 | Pause menu, screen transitions |
# Scene tree example
Main
├── ParallaxBackground (CanvasLayer, layer = -1)
│ └── Parallax2D
├── World (Node2D — default layer 0)
│ ├── TileMapLayer
│ └── Player
└── HUD (CanvasLayer, layer = 1)
└── Control
Note: CanvasLayers are NOT required to control draw order. For objects within the same game world, use
z_indexor scene tree ordering. CanvasLayers are for elements that should be independent of the camera (HUD, parallax, transitions).
Camera2D works by modifying the viewport's canvas_transform. For manual control:
# Scroll the canvas directly (equivalent to camera movement)
get_viewport().canvas_transform = Transform2D(0, Vector2(-200, 0))
# Local to canvas (world) coordinates
var world_pos: Vector2 = get_global_transform() * local_pos
var local_pos: Vector2 = get_global_transform().affine_inverse() * world_pos
# Local to screen coordinates (accounts for camera, stretch, window)
var screen_pos: Vector2 = get_viewport().get_screen_transform() * get_global_transform_with_canvas() * local_pos
// Local to canvas (world) coordinates
Vector2 worldPos = GetGlobalTransform() * localPos;
Vector2 localFromWorld = GetGlobalTransform().AffineInverse() * worldPos;
// Local to screen coordinates
Vector2 screenPos = GetViewport().GetScreenTransform() * GetGlobalTransformWithCanvas() * localPos;
Deprecation notice: The old
TileMapnode (with in-node layer management) is deprecated as of Godot 4.3. Use individualTileMapLayernodes instead — one node per layer. All examples in this skill use the currentTileMapLayerapproach. If you encounter legacy code using the oldTileMapnode, migrate by replacing it with separateTileMapLayernodes sharing the sameTileSetresource.
TileMapLayer nodeTileSet resource in its inspectorSave the TileSet as an external .tres resource (click dropdown → Save) to reuse across levels.
| Property | Purpose |
|---|---|
Tile Shape | Square (default), Isometric, Half-Offset Square, Hexagon |
Tile Size | Tile dimensions in pixels |
Rendering > UV Clipping | Clip tiles by their coordinates |
| Property | Purpose |
|---|---|
Margins | Edge margins in pixels |
Separation | Gap between tiles in pixels |
Texture Region Size | Size of each tile on the atlas |
Use Texture Padding | 1px transparent border to prevent bleeding (recommended ON) |
F for default rectangle collisionTerrains automatically select the correct tile based on neighboring tiles.
| Mode | Shortcut | Notes |
|---|---|---|
| Paint | Left-click | Shift = line, Ctrl+Shift = rectangle |
| Erase | Right-click | Also works with Paint/Line/Rect modes |
| Picker | Ctrl+click | Pick tile from placed tiles |
| Bucket Fill | — | Toggle Contiguous checkbox for fill-all behavior |
| Terrain Connect | — | Tiles connect to all neighbors |
| Terrain Path | — | Tiles connect only within current stroke |
Enable randomization to randomly choose from selected tiles when painting. Set Scattering > 0 for a chance of placing no tile (useful for non-repeating detail like grass tufts).
Use multiple TileMapLayer nodes for foreground/background separation. Only one tile per layer at a given position — overlap via multiple layers.
Level
├── TileMapLayer (Background — behind player)
├── TileMapLayer (Midground — player level)
├── Player
└── TileMapLayer (Foreground — in front of player, higher z_index)
damage: float, destructible: bool)# Read custom data from a tile at runtime
var tile_map: TileMapLayer = $TileMapLayer
var cell: Vector2i = tile_map.local_to_map(global_position)
var tile_data: TileData = tile_map.get_cell_tile_data(cell)
if tile_data:
var damage: float = tile_data.get_custom_data("damage")
var tileMap = GetNode<TileMapLayer>("TileMapLayer");
Vector2I cell = tileMap.LocalToMap(GlobalPosition);
TileData tileData = tileMap.GetCellTileData(cell);
if (tileData != null)
{
float damage = tileData.GetCustomData("damage").AsSingle();
}
Place entire scenes as tiles (e.g. doors, chests, spawn points with AudioStreamPlayer2D or particles). Greater performance overhead — each is instanced individually. Use for gameplay elements, not mass terrain.
Characters snagging on edges between adjacent tile colliders is a common issue:
TileMapLayer groups tiles into physics quadrants via Rendering Quadrant Size (default 16) which helps reduce edge snaggingStaticBody2D shapesCharacterBody2D with floor_snap_length > 0 also reduces edge catchingSee physics-system for more collision troubleshooting.
Parallax2D is the modern replacement for the deprecated ParallaxBackground/ParallaxLayer nodes.
Core Properties:
| Property | Purpose |
|---|---|
scroll_scale | Scroll speed multiplier per axis. 1 = camera speed, <1 = farther, >1 = closer, 0 = static |
repeat_size | Image size for infinite repeat. Set to texture dimensions for seamless tiling |
repeat_times | Extra repeats behind/in front (increase when zooming out) |
scroll_offset | Offset for the repeat canvas starting point |
Main
├── Camera2D
├── Parallax2D (scroll_scale = 0.1, 0) ← Sky (slowest)
│ └── Sprite2D (sky texture)
├── Parallax2D (scroll_scale = 0.2, 0) ← Clouds
│ └── Sprite2D (clouds texture)
├── Parallax2D (scroll_scale = 0.5, 0) ← Hills
│ └── Sprite2D (hills texture)
├── Parallax2D (scroll_scale = 0.7, 0) ← Trees
│ └── Sprite2D (trees texture)
└── World (scroll_scale = 1.0 — default)
├── TileMapLayer
└── Player
(0, 0) — the repeat canvas starts there and expands down-rightrepeat_size to match the texture dimensionsextends Parallax2D
func _ready() -> void:
scroll_scale = Vector2(0.3, 0.0)
repeat_size = Vector2(1920, 0) # Match texture width for horizontal repeat
repeat_times = 3 # Extra repeats for zoom-out safety
public partial class CloudLayer : Parallax2D
{
public override void _Ready()
{
ScrollScale = new Vector2(0.3f, 0.0f);
RepeatSize = new Vector2(1920, 0);
RepeatTimes = 3;
}
}
| Problem | Fix |
|---|---|
| Texture centered at (0,0) | Keep top-left at (0,0) — centering breaks infinite repeat |
| Gaps when zooming out | Increase repeat_times (e.g. 3) |
| repeat_size doesn't match | Set to actual texture/region size, not viewport size |
| Texture smaller than viewport | Scale the Parallax2D node, or use texture_repeat = ENABLED with region_enabled = true on Sprite2D |
For split-screen games, clone parallax nodes into each SubViewport. Use visibility_layer on parent nodes and canvas_cull_mask on SubViewports to isolate parallax per viewport.
| Node | Purpose |
|---|---|
CanvasModulate | Darkens the entire scene (sets ambient color) |
PointLight2D | Omnidirectional/spot light from a point |
DirectionalLight2D | Uniform light from a direction (sun/moon) |
LightOccluder2D | Casts shadows from light sources |
CanvasModulate to the scene → set color to dark (e.g. Color(0.1, 0.1, 0.15))PointLight2D → assign a Texture (light shape/gradient) → set Energy for intensityShadow > Enabled on the light, add LightOccluder2D nodes to shadow-casting objectsWarning: The background color (Project Settings → Rendering → Environment → Default Clear Color) does NOT receive 2D lighting. Use a full-screen
Sprite2DorColorRectas your background instead.
| Property | Purpose |
|---|---|
Texture | Light shape (size determined by texture size) |
Texture Scale | Multiplier for light size (larger = more expensive) |
Color | Light tint |
Energy | Intensity multiplier |
Height | Virtual height for normal mapping (0 = parallel to surface) |
Blend Mode | Add (default), Subtract (negative light), Mix (linear interpolation) |
| Property | Purpose |
|---|---|
Shadow > Enabled | Must be enabled for shadows |
Shadow > Color | Shadow tint (default: black) |
Shadow > Filter | None (pixel-art), PCF5 (soft), PCF13 (softest, expensive) |
Shadow > Filter Smooth | Softening amount (higher = softer, may cause banding) |
Shadow > Item Cull Mask | Which LightOccluder2D nodes cast shadows |
Both lights and occluders have cull masks:
CanvasItem nodes receive this lightLightOccluder2D nodes cast shadows for this lightLightOccluder2D node → click "+" to create polygon → draw pointsApply per-pixel lighting with normal maps using CanvasTexture:
CanvasTexture resourceDiffuse > Texture to the base color spriteNormal Map > Texture to the normal mapSpecular > Texture for reflectionsCanvasTexture as the Sprite2D's texture| Property | Purpose |
|---|---|
Specular > Shininess | Specular exponent (lower = diffuse, higher = focused) |
Specular > Color | Tint for reflections |
Tip: Use Laigter (free, open source) to generate normal maps from 2D sprites.
Standard 2D lighting renders at viewport pixel resolution, not texture texel resolution. For pixelated lighting that matches pixel-art aesthetics:
shader_type canvas_item;
uniform float pixel_size : hint_range(1.0, 16.0) = 4.0;
void fragment() {
LIGHT_VERTEX.xy = floor(LIGHT_VERTEX.xy / pixel_size) * pixel_size;
SHADOW_VERTEX = floor(SHADOW_VERTEX / pixel_size) * pixel_size;
COLOR = texture(TEXTURE, UV);
}
Apply this shader via ShaderMaterial on each lit sprite that should have pixelated lighting.
Set CanvasItemMaterial > Blend Mode to Add on a Sprite2D for a cheap glow effect. Faster than real lights but no shadows, no normal map support, and inaccurate blending in dark areas.
| Feature | GPUParticles2D | CPUParticles2D |
|---|---|---|
| Performance (many particles) | Better | Worse |
| Low-end GPU support | May have issues | More compatible |
| Material | ParticleProcessMaterial | Built-in properties |
| Shader customization | Yes | No |
Use GPUParticles2D by default. Convert between types via the toolbar menu.
GPUParticles2D nodeProcess Material, create new ParticleProcessMaterialTexture (particle sprite)| Property | Purpose |
|---|---|
Lifetime | Particle lifespan in seconds |
One Shot | Emit once then stop (explosions, impacts) |
Preprocess | Simulate N seconds before first draw (torches, ambient mist) |
Speed Scale | Global speed multiplier |
Explosiveness | 0 = steady stream, 1 = all particles at once |
Randomness | Random variation on initial values |
Local Coords | On = particles move with node, Off = world space (default) |
Emission:
| Property | Purpose |
|---|---|
Direction | Base emission direction (default: right) |
Spread | Angle variance from direction (180 = all directions) |
Initial Velocity | Emission speed (pixels/sec) — must be > 0 for Spread to work |
Motion:
| Property | Purpose |
|---|---|
Gravity | Applied to all particles |
Linear Acceleration | Per-particle acceleration |
Radial Acceleration | Positive = away from center, negative = toward |
Tangential Acceleration | Perpendicular to radial direction |
Damping | Friction — forces particles to slow down (sparks, explosions) |
Angular Velocity | Rotation speed (degrees/sec) |
Orbit Velocity | Orbital motion around emission center |
Appearance:
| Property | Purpose |
|---|---|
Scale | Particle scale over lifetime (Min/Max + Curve) |
Color | Static color or gradient (via Color Ramp) |
Hue Variation | Randomize hue on spawn |
Most properties support Min/Max range (randomized on spawn) and a Curve (multiplied over particle lifetime).
Select GPUParticles2D → Particles menu → Load Emission Mask:
CanvasItemMaterial in the GPUParticles2D's Material slotParticle Animation, set H Frames and V FramesThe AABB used for culling. Auto-generate via Particles menu → Generate Visibility Rect. If particles disappear when the emitter goes offscreen, increase this rect.
Fire:
Direction: (0, -1), Spread: 15°, Initial Velocity: 50-100
Gravity: (0, -20), Damping: 5
Scale: 1.0 → 0.0 (curve), Color Ramp: yellow → orange → transparent
Sparks:
Direction: (0, -1), Spread: 45°, Initial Velocity: 200-400
Gravity: (0, 200), Damping: 10, Explosiveness: 0.8
Scale: 0.5, Lifetime: 0.5, One Shot: true
Dust Trail:
Direction: (0, 0), Spread: 180°, Initial Velocity: 10-30
Gravity: (0, -5), Damping: 3, Local Coords: false
Scale: 0.5 → 1.5 (curve), Color Ramp: white → transparent
Override _draw() on any CanvasItem-derived node (Node2D or Control) for custom rendering.
extends Node2D
func _draw() -> void:
draw_circle(Vector2.ZERO, 50.0, Color.RED)
draw_rect(Rect2(-25, -25, 50, 50), Color.BLUE, false, 2.0)
draw_line(Vector2(-100, 0), Vector2(100, 0), Color.WHITE, 2.0, true)
public partial class CustomDraw : Node2D
{
public override void _Draw()
{
DrawCircle(Vector2.Zero, 50f, Colors.Red);
DrawRect(new Rect2(-25, -25, 50, 50), Colors.Blue, false, 2f);
DrawLine(new Vector2(-100, 0), new Vector2(100, 0), Colors.White, 2f, true);
}
}
_draw() is called once and cached. Call queue_redraw() to trigger a redraw.
@export var radius: float = 50.0:
set(value):
radius = value
queue_redraw() # Redraw when property changes
func _draw() -> void:
draw_circle(Vector2.ZERO, radius, Color.RED)
private float _radius = 50f;
[Export]
public float Radius
{
get => _radius;
set { _radius = value; QueueRedraw(); }
}
public override void _Draw()
{
DrawCircle(Vector2.Zero, _radius, Colors.Red);
}
For per-frame animation, call queue_redraw() from _process().
| Method | Description |
|---|---|
draw_line(from, to, color, width, antialiased) | Single line segment |
draw_multiline(points, color, width) | Multiple disconnected lines (better performance) |
draw_polyline(points, color, width, antialiased) | Connected line sequence |
draw_polygon(points, colors) | Filled polygon (auto-closes) |
draw_circle(center, radius, color) | Filled circle |
draw_arc(center, radius, start, end, segments, color, width, aa) | Arc / partial circle |
draw_rect(rect, color, filled, width) | Rectangle (filled or outline) |
draw_texture(texture, position) | Draw a texture |
draw_string(font, position, text, alignment, width, font_size) | Draw text |
draw_set_transform(position, rotation, scale) | Apply transform for subsequent draws (rotation/scale optional) |
var font: Font = ThemeDB.fallback_font
func _draw() -> void:
draw_string(font, Vector2(10, 30), "Score: 100", HORIZONTAL_ALIGNMENT_LEFT, -1, 16)
private Font _font = ThemeDB.FallbackFont;
public override void _Draw()
{
DrawString(_font, new Vector2(10, 30), "Score: 100", HorizontalAlignment.Left, -1, 16);
}
Add @tool (GDScript) or [Tool] (C#) to see custom drawing in the editor. Requires scene reload after adding/removing the annotation.
Odd-width lines (1px, 3px): offset start/end positions by 0.5 to center the line on a pixel boundary and avoid subpixel blurriness.
MeshInstance2D replaces Sprite2D when large transparent areas waste GPU fill rate. The GPU draws the entire texture quad including fully transparent pixels — a mesh eliminates those.
Sprite2DBest candidates:
Many drawing methods support an antialiased parameter:
draw_line(Vector2.ZERO, Vector2(100, 50), Color.WHITE, 2.0, true) # antialiased = true
// Equivalent in a CanvasItem subclass (e.g., a custom Control or Node2D):
public override void _Draw()
{
DrawLine(new Vector2(0, 0), new Vector2(100, 50), Colors.White, width: 2.0f, antialiased: true);
}
Line2D has an Antialiased property in the inspector — set it via line2D.Antialiased = true in C# or as an Inspector toggle in the editor. This works by generating additional geometry — no MSAA needed.
Available in Forward+ and Mobile renderers only (NOT Compatibility).
Project Settings → Rendering → Anti Aliasing → Quality → MSAA 2D
Levels: 2x, 4x, 8x.
| MSAA affects | MSAA does NOT affect |
|---|---|
| Geometry edges (lines, polygons) | Aliasing within nearest-neighbor textures |
| Sprite edges touching texture edges | Custom 2D shader output |
| Font rendering | |
| Specular aliasing with Light2D |
For pixel art: Do NOT enable MSAA 2D — it blurs intentionally sharp edges. Use the per-node
antialiasedparameter selectively.
Three-dot menu in the 2D toolbar:
For pixel-art games, enable pixel snapping to prevent subpixel jitter:
Snap 2D Transforms to PixelSnap 2D Vertices to PixelSnap Controls to Pixels.tres resource for reuse across levelsUse Texture Padding enabled to prevent texture bleedingrepeat_size matches actual texture dimensionsrepeat_times is increased if the camera can zoom outqueue_redraw() is called when custom drawing state changes