The Book of Nodes: 2D

Christine Coomans
41 min readAug 7, 2024

--

Below you can find a list of 2D nodes that can be used in Godot 4. This is part of my Book of Nodes series. If you want to see similar content on 3D or UI nodes, please refer to the parent page of this post for those links. 😊

Before we begin, if you need a base project to test these code snippets, feel free to download my FREE 2D and 3D templates here. I’ll be using these templates throughout this post.

*Please note that this list is not 100% complete yet, but I will be updating this list as time goes on.

  • AnimatedSprite2D
  • AnimationPlayer
  • AnimationTree
  • Area2D
  • AudioStreamPlayer2D
  • Camera2D
  • CharacterBody2D
  • CollisionShape2D
  • DirectionalLight2D
  • LightOccluder2D
  • MeshInstance2D
  • NavigationAgent2D, NavigationObstacle2D, NavigationRegion2D
  • Node2D
  • Path2D, PathFollow2D
  • PointLight2D
  • RayCast2D
  • RigidBody2D
  • Sprite2D
  • StaticBody2D
  • TileMap
  • TileMapLayer
  • Timer

Node2D

The Node2Dnode is the fundamental building block for all 2D scenes in Godot. It provides us with basic 2D spatial features like position, rotation, and scale. Almost all 2D nodes (like Sprite2D, Area2D, etc.) inherit from Node2D, which makes it the most essential node for any 2D game development.

Mechanic:

Move a group of 2D nodes collectively.

Implementation:

  • Add a Node2D to your scene to serve as a parent node. This could represent a game object like a vehicle or a character.
  • Add child nodes such as twoSprite2Dnodes. These children can represent visual components, collision areas, etc.
  • In the Inspector, assign a texture (image file) of your choosing to the texture property of the Sprite2D node. This image will be what is displayed in the scene.
  • Now if we manipulate the Node2D parent, it will affect all its children. For example, moving the Node2D will move all its children, whilst maintaining their relative positions and transformations.
  • We can also do this via code — say if we press SPACE on our keyboard, the node moves -100 pixels on the x-axis.
### Main.gd

extends Node2D

@onready var node_2d = $Node2D

func _process(delta):
if Input.is_action_pressed("ui_select"):
node_2d.position.x -= 100 * delta

Sprite2D

The Sprite2Dnode in Godot is used to display 2D images in your scenes. Since it inherits from the Node2Dnode, it can handle various transformations like scaling, rotation, and translation. It’s one of the most commonly used nodes for representing characters, objects, and other visual elements in a 2D space.

You can use either a singular image as the texture, or a tilesheet.

If you are using a Tilesheet(as I am in this example) as your texture, you will have to crop out your sprite using the HFramesand VFramesproperty in the Inspector Panel. Then, add a key at each frame. The Frame Coords x property determines which column you are trying to access, and the Frame Coords y property determines which row you are trying to access.

Mechanic:

Display and animate a character.

Implementation:

  • Create a Sprite2D node in your scene.
  • In the Inspector, assign a texture (image file) to the texture property of the Sprite2D node. This image will be what is displayed in the scene.
  • Adjust the position, scale, and rotation properties to position the sprite correctly within your game world.
  • If you want to animate the sprite, you can use an AnimationPlayer to animate properties like position, rotation_degrees, and scale. You can also swap out the texture of the Sprite2D if you have a sprite that has multiple animations, for example walking, idling, etc.
  • Add the code to play the animation on load.
### Main.gd

extends Node2D

@onready var animation_player = $AnimationPlayer

func _ready():
animation_player.play("move_character")
  • Run your project and your Sprite2D node should be in your scene. If you added an animation, it should play.

AnimatedSprite2D

The AnimatedSprite2D node utilizes a series of images (sprites) and displays them in a sequence to create an animation. Unlike a simple Sprite2D node that displays a single static image, AnimatedSprite2D can cycle through multiple frames to animate characters, objects, or effects within your 2D game. To create these frames, we can use either sprites or a spritesheet.

The AnimatedSprite2D node utilized the SpriteFrames Resource to create animations. This is a special resource in Godot that holds collections of images. Each collection can be configured as an animation by specifying the images (frames) that belong to it. You can create multiple animations within a single SpriteFrames resource, each with its own set of frames and playback properties like speed and loop settings.

Mechanic:

Create a character with walking animations.

Implementation:

  • Create an AnimatedSprite2D node in your scene.
  • Assign a SpriteFrames resource to the AnimatedSprite2D.
  • Add a new animation by clicking on the page+ icon.
  • Rename this animation by double-clicking on it.
  • Either drag in sprites into the frames box or click the spritesheet icon to add animations via an atlas.
  • Crop out the frames horizontally and vertically.
  • Select the frames you want. For instance, in my person atlas, I will choose frames 0–6 in row 5.
  • Then play the animation to see if you need to alter the FPS to make the character move faster/slower:
  • Repeat the process for all of your animations, for example walk_left, walk_up, walk_down, walk_up, idle_x, run_x, etc.
  • Play the animation in your code so that when your player moves during runtime the animation can play:
### Player.gd

extends CharacterBody2D

# Scene-Tree Node references
@onready var animated_sprite = $AnimatedSprite2D

# Variables
@export var speed = 100

# Input for movement
func get_input():
var input_direction = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
velocity = input_direction * speed

# Movement & Animation
func _physics_process(delta):
get_input()
move_and_slide()
update_animation()

# Animation
func update_animation():
if velocity == Vector2.ZERO:
animated_sprite.play("idle")
else:
if abs(velocity.x) > abs(velocity.y):
if velocity.x > 0:
animated_sprite.play("walk_right")
else:
animated_sprite.play("walk_left")
else:
if velocity.y > 0:
animated_sprite.play("walk_down")
else:
animated_sprite.play("walk_up")
  • Run your project and move around:

AnimationPlayer

Unlike the AnimatedSprite2D which is specifically designed for sprite animations, the AnimationPlayer can animate virtually ANY node within a Godot scene. Instead of animating a simple sprite, you can animate the node’s properties — including but not limited to positions, rotations, scales, colors, and even variables.

The AnimationPlayer can hold a set of animations on a singular timeline, each containing keyframes that define the start and end points of any property that changes over time. You can create complex sequences and control animations in a non-linear fashion.

This node can be used to animate 2D, 3D, and even UI nodes!

Mechanic:

Animate a Sprite2D node of a potion that pulses in size to capture player's attention.

Implementation:

  • Add a Sprite2D node and an AnimationPlayer node to your scene. The Sprite2D node is the node we want to animate, and the property we want to animate of this node is its scale.
  • Assign a sprite to the Sprite2D node. I’ll assign a potion.
  • Select the AnimationPlayer node.
  • In the animation panel, click “New Animation” and name it something descriptive like “pulse”.
  • Set the animation length to the duration you want for one pulse cycle (e.g., 1 second).
  • Enable the “Loop” option to make the animation repeat continuously.
  • Go to the beginning of the animation timeline (0 seconds).
  • Select the Sprite2D node, and in the Inspector, set the scale property to its initial value (e.g., Vector2(1, 1)).
  • Right-click the scale property in the Inspector and select "Key" to add a keyframe.
  • Move to the middle of the timeline (e.g., 0.5 seconds), change the scale to a larger value (e.g., Vector2(1.2, 1.2)), and add another keyframe.
  • At the end of the timeline (1 second), set the scale back to the initial value (Vector2(1, 1)) and add a final keyframe.
  • You can control when the animation starts or stops via script, or let it run continuously since it’s set to loop.
### Main.gd

@onready var animation_player = $AnimationPlayer

func _ready():
animation_player.play("pulse")
  • Start your project and observe the potion sprite pulsing in size.

MeshInstance2D

The MeshInstance2D node is used for displaying a Meshin a 2D space. In Godot, a mesh is used as a resource that can be applied to MeshInstance nodes to render geometry in a scene.

It can be particularly useful for achieving effects or visual styles that are difficult with standard 2D sprites or animations, such as deformations or complex shading that reacts to lighting conditions.

Mechanic:

Dynamically deform a 2D mesh (Sprite2D conversion).

Implementation:

  • Create a Sprite2D node in your scene. Assign it with a texture of your choice.
  • Use the editor’s conversion tool to convert it to MeshInstance2D node.
  • A dialog will appear, showing a preview of how the 2D mesh will be created. The yellow lines are the mesh polygons — and this will make up your 2D mesh shape. The default values are fine.
  • The Sprite2Dnode should be converted into a MeshInstance2Dnode.
  • Use scripts to deform the mesh dynamically.
### Main.gd

extends Node2D

@onready var sprite_2d = $Sprite2D

func _process(delta):
var scale_factor = sin(Time.get_ticks_msec() / 1000.0) * 0.1 + 1.0
sprite_2d.scale = Vector2(scale_factor, scale_factor)
  • Run the scene to observe the 2D mesh dynamically deform itself!

AnimationTree

The AnimationTree node enhances the capabilities of the AnimationPlayer by providing advanced features for animations, such as blending, transitions, and states. This makes it extremely easy to make detailed character animations and interactive scene elements in 2D and 3D environments.

We usually use blending to create smooth transitions between animations, for example, smoothly transitioning between walking and running depending on the player’s speed.

We use state machines to switch our animations dynamically depending on the conditions, for example, switching between idle and attack animations if the player presses a key.

Mechanic:

Animate a 2D character with multiple actions (e.g., walking, idle).

Implementation:

  • Add an AnimationPlayernode to your scene and create animations for it like "walk_x”, "idle". You’ll need to create these animations from a Sprite2Dnode, or else it won’t work.
  • Now, add an AnimationTree node, linking it to the AnimationPlayer.
  • Configure the tree_root as an AnimationNodeStateMachine for managing states.
  • Also assign the AnimationPlayeras the Anim Player property because this is where our AnimationTree will call the animations from, and the Advanced Expression as the root node because this is where our animation coding can be found (our script).
  • If you open your AnimationTree, you will see if you right-click you can add animations, blend trees, and state machines. Add your ‘idle’ animation and a BlendSpace2Dso that we can play our walk animations depending on our player's Vector2() coordinates. Rename the BlendSpace2D to ‘walk’.
  • Add a transition between start -> idle. The transition type should be immediatebecause we want the animation to play immediately.
  • Click the pencil icon to edit your walk BlendSpace2D. Then add a point with an animation at each coordinate. Up (0, -1). Down (0, 1). Left (-1, 0). Right (1, 0). Idle (0,0). Also change the blend mode to “Discrete”.
  • Add transitions between idle -> walk and vice versa. The transition type for both should be syncbecause we want to blend the animation. Also set the mode to enabledbecause we will activate this animation via the code, and not automatically.
  • Now in our code, we can play our animations based on our input.
### Player.gd

extends CharacterBody2D

# Scene-Tree Node references
@onready var animation_tree = $AnimationTree
@onready var animation_state = animation_tree.get("parameters/playback")

# Variables
@export var speed = 100

# Input for movement
func get_input():
var input_direction = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
velocity = input_direction * speed

# Movement & Animation
func _physics_process(delta):
get_input()
move_and_slide()
update_animation()

# Animation
func update_animation():
var blend_position = Vector2.ZERO
if velocity == Vector2.ZERO:
animation_state.travel("idle")
else:
blend_position = velocity.normalized()
animation_state.travel("walk")

animation_tree.set("parameters/walk/blend_position", blend_position)
  • Run the scene and control the character to observe the transitions and movement based on our state.

CollisionShape2D

The CollisionShape2D node allows you to specify the boundaries of an object for collision detection, which is essential for handling interactions between objects in your game.

Mechanic:

Add a collision area to block the character from passing.

Implementation:

  • Create a CollisionShape2D node as a child of a CharacterBody2D, RigidBody2D, orStaticBody2D. These nodes will block other collisions. To have a node pass through collisions, use an Area2D.
  • In the Inspector, assign a Shape2D resource to the shape property of the CollisionShape2D. The shape you choose will depend on the shape of your entity. For example, a player might have a capsule shape, a pickup a circle, an area a box.
  • Let’s enable debugging to see our collisions in action.
  • Run your scene to see how your player interacts with the collision shape. Since we used a StaticBody2D node, they should be blocked and unallowed to go through the collision.

CharacterBody2D

The CharacterBody2D node is a specialized class for physics bodies that are meant to be controlled or moved around by the user. Unlike other physics bodies such as the RigidBody2Dor StaticBody2Dnode, CharacterBody2Dis not affected by the engine’s physics properties like gravity or friction by default. Instead, you have to write code to control its behavior, giving you precise control over how it moves and reacts to collisions.

Mechanic:

Move a character with arrow keys, including handling gravity and jumping.

Implementation:

  • Add a CharacterBody2D node to your scene.
  • You’ll see it has a warning icon next to it. This is because it needs a collision shape to be able to interact with the world. Add a CollisionShape2D as a child of the CharacterBody2D and set its shape to match your character.
  • Add a Sprite2D node to this scene so that we can see our character.
  • Attach a script to the CharacterBody2D to handle movement and jumping.
### Player.gd

extends CharacterBody2D

# Variables
@export var speed = 200
@export var jump_force = -400
@export var gravity = 800

# Input for movement
func get_input():
velocity.x = 0
if Input.is_action_pressed("ui_right"):
velocity.x += speed
if Input.is_action_pressed("ui_left"):
velocity.x -= speed
if Input.is_action_pressed("ui_down"):
velocity.y -= jump_force
if is_on_floor() and Input.is_action_just_pressed("ui_up"):
velocity.y = jump_force

# Movement & Gravity
func _physics_process(delta):
get_input()
velocity.y += gravity * delta
move_and_slide()
  • Run the scene and use the arrow keys to move the character and make it jump.

StaticBody2D

The StaticBody2D node is used to represent objects that do not move. This node is ideal for creating static elements in your game, such as walls, floors, and other immovable objects such as chests.

Mechanic:

Create an obstacle.

Implementation:

  • Create a StaticBody2D node in your scene. Add a CollisionShape2Das a child of the StaticBody and set its shape to match the obstacle.
  • Give it a Sprite2D of your choice so that we can see the item.
  • Run your scene to see how your player interacts with the collision shape. They should be blocked and unallowed to go through the obstacle.

RigidBody2D

The RigidBody2D node is used for objects that are affected by the engine’s physics. These bodies can move, rotate, and respond to forces and collisions. They are ideal for creating dynamic objects that need realistic physics interactions, such as balls, bullets, moveable obstacles, etc.

Mechanic:

Create a moveable obstacle.

Implementation:

  • Create a RigidBody2D node in your scene. Add a CollisionShape2Das a child of the RigidBody and set its shape to match the obstacle.
  • Give it a Sprite2D of your choice so that we can see the item.
  • Since we want to move this item when our player collides with it, we should disable its gravity so that it doesn’t get pulled downwards (unless you are making a 2D platformer).
  • Use GDScript to apply forces or impulses to the rigid body if the player pushes it.
### Player.gd

extends CharacterBody2D

# Scene-Tree Node references
@onready var animation_tree = $AnimationTree
@onready var animation_state = animation_tree.get("parameters/playback")

# Variables
@export var speed = 100
@export var push_force = 80.0

# Input for movement
func get_input():
var input_direction = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
velocity = input_direction * speed

# Movement & Animation
func _physics_process(delta):
get_input()
move_and_slide()
update_animation()
handle_collisions()

# Animation
func update_animation():
var blend_position = Vector2.ZERO
if velocity == Vector2.ZERO:
animation_state.travel("idle")
else:
blend_position = velocity.normalized()
animation_state.travel("walk")

animation_tree.set("parameters/walk/blend_position", blend_position)

# Handle Collisions
func handle_collisions():
for i in range(get_slide_collision_count()):
var collision = get_slide_collision(i)
if collision.get_collider() is RigidBody2D:
var collider = collision.get_collider() as RigidBody2D
var impulse = -collision.get_normal() * push_force
collider.apply_central_impulse(impulse)
  • Run the scene and observe how the obstacle moves when the player pushes against it.

Area2D

The Area2D node is used to detect when objects enter or exit a defined area. They do not represent physical bodies but are useful for triggering events such as cutscenes or map transitions, detecting overlaps, and creating zones for things such as enemy or loot spawning.

We can use the Area2D node’s on_body_entered() and on_body_exited() signals to determine whether or not a PhysicsBody has entered this zone.

Mechanic:

Create a trigger zone that detects when the player enters a specific area.

Implementation:

  • Create an Area2D node in your scene. You also need to add a CollisionShape2D as a child of the Area and set its shape to define the trigger zone. Adjust the collision shape's properties to fit the dimensions of your trigger zone.
  • Attach the Area2D node’s on_body_entered() and on_body_exited() signals to your script.
  • Use GDScript to notify us when the Player enters or exits the area.
### Main.gd

extends Node2D

func _on_area_2d_body_entered(body):
if body.name == "Player":
print("The player has entered the area!")

func _on_area_2d_body_exited(body):
if body.name == "Player":
print("The player has exited the area!")
  • Enable debugging so we can see when our Player enters/exits our area.
  • Run the scene and observe how the area detects when the Player enters or exits the defined zone. Each time the player enters/exits the zone, the game should be notified.

RayCast2D

The RayCast2D node is used to cast a ray in a 2D space to detect objects along its path. This is useful for various purposes such as line-of-sight checks, shooting and attacking mechanics, and collision detection.

It can collide with bodies such as StaticBody2D (useful for detecting loot and quest items), CharacterBody2D (useful for detecting interactions with enemies and NPCs), and RigidBody2D (useful for detecting interactions with moveable objects. It can also collide with areas, such as Area2D (useful for interactions with trigger zones).

Mechanic:

Cast a ray from the player and detect what it hits.

Implementation:

  • Add a RayCast2D node to the Player in your scene.
  • Set the target_position property to define the direction and length of the ray. I usually leave mine at its default values. You can also enable its collision with areas.
  • In the code, let’s update our raycast to always face the player’s last direction. We will also print the colliders it’s hitting.
### Player.gd

extends CharacterBody2D

# Scene-Tree Node references
@onready var animation_tree = $AnimationTree
@onready var animation_state = animation_tree.get("parameters/playback")

# Variables
@export var speed = 100
var direction = Vector2()
@onready var ray_cast_2d = $RayCast2D

# Input for movement
func get_input():
var input_direction = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
direction = input_direction
velocity = input_direction * speed

# Raycast hit detection
func _process(delta):
if ray_cast_2d.is_colliding():
var collider = ray_cast_2d.get_collider()
print("Raycast hit: ", collider.name)

# Movement & Animation
func _physics_process(delta):
get_input()
move_and_slide()
update_animation()

# Update raycast to face player direction
if direction != Vector2.ZERO:
ray_cast_2d.target_position = direction.normalized() * 50

# Animation
func update_animation():
var blend_position = Vector2.ZERO
if velocity == Vector2.ZERO:
animation_state.travel("idle")
else:
blend_position = velocity.normalized()
animation_state.travel("walk")

animation_tree.set("parameters/walk/blend_position", blend_position)
  • Run your scene and interact with objects that have colliders. The raycast should detect the objects and notify the game.

Camera2D

The Camera2D node is used to control the view of a 2D scene. It allows the screen to follow the player or other objects. Only one Camera can be active per viewport, and it registers itself in the nearest Viewport node.

In 2D games, we usually attach the Camera2D node to either the Player or our World scene. Attach it to the Player if you want to follow the player around. Attach the Camera to your World (Main) scene if you want a birds-eye view of the environment. This usually requires a bit more configuration and coding, as you have to make the camera able to move, rotate, scroll, or zoom around the scene.

Mechanic:

Create a “God Mode” 2D camera that can move, zoom, and rotate based on user input.

Implementation:

  • Add a Camera2D node to your Main (World) scene. Make sure this camera is enabled, and all other cameras are disabled.
  • Add the inputs to zoom and rotate your camera. The default up, down, left, and right inputs should be fine to move the camera.
  • Use GDScript to handle the camera’s zoom, movement, and rotation. You can do this in a custom Camera.gd script (preferred), or directly in your root script.
### Main.gd

extends Node2D

@onready var camera_2d = $Camera2D
@export var zoom_speed = 0.5
@export var move_speed = 200
@export var rotate_speed = 0.5
@export var min_zoom = 0.5
@export var max_zoom = 2.0

func _process(delta):
handle_zoom(delta)
handle_movement(delta)
handle_rotation(delta)

# Zooming
func handle_zoom(delta):
if Input.is_action_pressed("zoom_out"):
camera_2d.zoom -= Vector2(zoom_speed, zoom_speed) * delta
if Input.is_action_pressed("zoom_in"):
camera_2d.zoom += Vector2(zoom_speed, zoom_speed) * delta
camera_2d.zoom.x = clamp(camera_2d.zoom.x, min_zoom, max_zoom)
camera_2d.zoom.y = clamp(camera_2d.zoom.y, min_zoom, max_zoom)

# Moving
func handle_movement(delta):
var direction = Vector2.ZERO
if Input.is_action_pressed("ui_right"):
direction.x += 1
if Input.is_action_pressed("ui_left"):
direction.x -= 1
if Input.is_action_pressed("ui_down"):
direction.y += 1
if Input.is_action_pressed("ui_up"):
direction.y -= 1
camera_2d.position += direction.normalized() * move_speed * delta

# Rotating
func handle_rotation(delta):
if Input.is_action_pressed("rotate_left"):
camera_2d.rotation -= rotate_speed * delta
if Input.is_action_pressed("rotate_right"):
camera_2d.rotation += rotate_speed * delta
  • Run the scene and use the defined input actions to move, zoom, and rotate the camera.

DirectionalLight2D

The DirectionalLight2D node is used to simulate sunlight or moonlight. It emits light in a specific direction, affecting all objects in the scene equally, regardless of their distance from the light source. This type of light is useful for outdoor scenes where you need consistent lighting across the entire scene.

This node’s two main properties are the energy and color properties. The energy property determines how bright/dim the light is, and the color is the shading of the light.

By default, I prefer NOT to use this node without shaders because its features are a bit unfinished (and the shadows are not good). If you’d like an introductory tutorial on how to use this node with shaders ( by making a day and night cycle), please check out my YouTube video.

Mechanic:

Illuminate a scene with sunlight.

Implementation:

  1. Create a DirectionalLight2D node in your scene.
  • As you can see, this is crazy bright. Let’s adjust our properties like energy andcolorto customize the light's appearance and behavior.
  • Now let’s do something fun. I recommend using shaders with this node, but just for testing sake, let’s have it randomize its color each second.
  • Add a Timer node to your scene. In the Inspector Panel, enable autostart, and connect its timeout signal to your script
  • In your code, let’s randomize our light’s color every time the timer times out (every second).
### Main.gd

extends Node2D

@onready var directional_light_2d = $DirectionalLight2D

func _on_timer_timeout():
directional_light_2d.color = Color(randf(), randf(), randf()
  • Run your scene and enjoy the overstimulation.

PointLight2D

The PointLight2D node emits light in all directions from a single point. This type of light is useful for creating focused lighting effects, such as flashlights, lamps, or fires.

This node’s two main properties are the energy , color, texture scale, and textureproperties. The energy property determines how bright/dim the light is. The color is the shading of the light. The textureproperty allows us to give our light a shape. The texture scale property determines the size of our light.

Mechanic:

Create a flickering torch.

Implementation:

  • In your scene, add a PointLight2D node. We’ll need to give it a shape, so download this texture, and drag it into your texture property.
  • Play around with the energy, color, andtexture scalevalues.
  • Just for fun, let’s give it a flicker effect. We’ll do this via an AnimationPlayer node.
  • Create a new animation in the AnimationPlayer.
  • Add a track for the energy property of the PointLight2D.
  • Add keyframes to the energy track to simulate flickering.
  • Enable looping, and change the blend mode to discrete.
  • Then play this animation via the code when the game loads.
### Main.gd

extends Node2D

@onready var animation_player = $AnimationPlayer

func _ready():
animation_player.play("flicker")
  • Run the scene and observe the player’s color changes when they go into the flickering lights.

LightOccluder2D

The LightOccluder2D node is used to cast shadows from a light source that hits it. This light source can come from a DirectionalLight2D or PointLight2D. It requires an OccluderPolygon2D to define the shape of the occlusion.

Mechanic:

Cast a shadow from our player.

Implementation:

  • Create a LightOccluder2D node to the source you want to cast your shadows from.
  • You’ll see it has an error. This is because we need to draw a OccluderPolygon2D shape to define the shape of our shadow. Give your LightOccluder2D a new OccluderPolygon2D resource, and draw the polygon around your player. Make sure that you complete your polygon by connecting all of your points.
  • Now in our Main scene, we will need a light source to cast this shadow shape. Let’s add a PointLight2D node and put it above our player. Make sure its shadows are enabled so that it can cast this shadow.
  • Run your scene and watch the shadow move depending on where the light hits the occluder.

AudioStreamPlayer2D and AudioStreamPlayer

These nodes are used to play audio in our games. We use the AudioStreamPlayer to play audio equally across our scene (such as background music or ambient sounds), and the AudioStreamPlayer2D to play audio positionally (such as from our players or NPCs).

Mechanic:

Play ambient music in the background, and sounds from the player when they move.

Implementation:

  • Download your sound effects. You can find free ones on Pixabay. Look for ones that work well in the background (they loop), and ones that are short effects, such as jumping sounds.
  • Add an AudioStreamPlayer node to play background audio. Add an AudioStreamPlayer2D node to play positional audio.
  • You will need to reimport your audio that is supposed to loop. Double-click it, and enable looping.
  • Set the stream property to the desired audio file.
  • Adjust properties like volume_db, and pitch_scale if needed. We’ll enable autoplay on our AudioStreamPlayer node since that is our background music.
  • We will play our sound effect audio (AudioStreamPlayer2D) when our player enters a certain area. To do this, add an Area2D node to your scene with a collision body, and attach its on_body_entered() signal to your script.
  • Now play the audio when the player enters the area.
### Main.gd

extends Node2D

@onready var audio_stream_player_2d = $AudioStreamPlayer2D

func _on_area_2d_body_entered(body):
if body.name == "Player":
audio_stream_player_2d.play()
  • Run your scene. The background music should play, and the sound effect should play when your player enters the area.

NavigationAgent2D, NavigationObstacle2D, NavigationRegion2D

The NavigationAgent, NavigationObstacle, and NavigationRegion nodes are used to manage navigation and pathfinding in both 2D and 3D environments. These nodes help create dynamic and realistic movement for characters and objects, allowing them to navigate around obstacles and follow paths.

  • The NavigationAgent2Dnode is used to move characters along a path while avoiding obstacles.
  • The NavigationObstacle2Dnode is used to create obstacles that navigation agents will avoid.
  • The NavigationRegion2D node defines areas where navigation is allowed or restricted.

These three nodes combined allow us to create a more immersive world through mechanics such as NPC and Enemy roaming, particle movements, and controlled entity spawning.

Mechanic:

Create an NPC that roams around a certain area on the map.

Implementation:

  • In your Main scene, add a NavigationRegion2D to your scene to define the roaming area.
  • Create a new NavigationPolygonresource for this node so that we can define our region. Draw the polygon to where you want your region to be.
  • Now all we need to do is select our NavigationRegion node and select “Bake Navigation”. You’ll see a blue-colored polygon get drawn over our floor, that is our navigation region!
  • In a new scene, create your NPC using a CharacterBody2Dnode as the root node. Add the collisions and animations for this entity just as you did for your player.
  • To your NPC scene, add a NavigationAgent2D node. The NPC will be assigned to this agent so that they can roam in the region. Enable avoidance for this NPC so that they can avoid obstacles.
  • Attach a script to your NPC. We will then need to connect our signals from our NavigationAgent2D node to 1) compute the avoidance velocity of our NPC, and 2) redirect our NPC when that target is reached. For moving the NPC whilst avoiding obstacles, attach the velocity_computed signal to your script. For redirecting the NPC, attach the navigation_finished signal to your script.
  • We also want our NPC to pause before redirecting. To do this, we will add a Timer node to our scene. Enable its one_shot property, and change its wait_time to however long you want the NPC to wait before roaming again.
  • Also attach its timeout() signal to your script.
  • Now add your roaming functionality.
### NPC.gd

extends CharacterBody2D

@onready var navigation_agent_2d = $NavigationAgent2D
@onready var navigation_region = $"../NavigationRegion2D"
@onready var animation_tree = $AnimationTree
@onready var animation_state = animation_tree.get("parameters/playback")
@onready var timer = $Timer

# Variables
@export var movement_speed: float = 50.0
var roaming_area: Rect2
var target_position: Vector2

func _ready():
# Add a delay to ensure the navigation map is loaded
await get_tree().create_timer(1).timeout
set_roaming_area()
set_random_target()

func _physics_process(delta):
# Move NPC towards the target
var next_path_position = navigation_agent_2d.get_next_path_position()
var new_velocity = (next_path_position - global_position).normalized() * movement_speed
if navigation_agent_2d.avoidance_enabled:
navigation_agent_2d.velocity = new_velocity
else:
_on_navigation_agent_2d_velocity_computed(new_velocity)

# Update the NPC's position
move_and_slide()

# Play walking animation
update_animation(velocity)


func set_roaming_area():
# Set the roaming area
var navigation_polygon = navigation_region.get_navigation_polygon()
if navigation_polygon.get_outline_count() > 0:
var outline = navigation_polygon.get_outline(0)
# Calculate the bounding rect
var min_x = INF
var min_y = INF
var max_x = -INF
var max_y = -INF
for point in outline:
min_x = min(min_x, point.x)
min_y = min(min_y, point.y)
max_x = max(max_x, point.x)
max_y = max(max_y, point.y)
roaming_area = Rect2(min_x, min_y, max_x - min_x, max_y - min_y)
else:
print("No outlines found in the navigation polygon.")


func set_random_target():
# Set next roaming position within the roaming area
target_position = Vector2(
randf_range(roaming_area.position.x, roaming_area.position.x + roaming_area.size.x),
randf_range(roaming_area.position.y, roaming_area.position.y + roaming_area.size.y)
)
navigation_agent_2d.set_target_position(target_position)

func update_animation(velocity: Vector2):
if velocity.length() == 0:
animation_state.travel("idle")
else:
animation_state.travel("walk")
animation_tree.set("parameters/walk/blend_position", velocity.normalized())

func _on_navigation_agent_2d_velocity_computed(safe_velocity):
# Move NPC
velocity = safe_velocity

func _on_timer_timeout():
# Move NPC again
set_random_target()

func _on_navigation_agent_2d_navigation_finished():
# When path reached, redirect NPC
velocity = Vector2.ZERO
animation_state.travel("idle")
timer.start()
  • Instance your NPC in your Main scene. Move them into your region.
  • Optionally, add NavigationObstacle2D nodes to create obstacles. Add this node to a StaticBody2D with a collision shape.
  • Run your scene and see your NPC randomly roam. They should avoid your obstacles.

Path2D and PathFollow2D

The Path2D and PathFollow2D nodes work together to create and follow paths in a 2D space. The Path2D node is used to define a path using a sequence of points. I like this more than using a NavMesh because it allows you to create and visualize a path in the Godot editor, instead of randomizing it. The PathFollow2D node is used to make an object follow a path defined by a Path2D node.

Mechanic:

Create an NPC that roams on a defined path on the map.

Implementation:

  • Create a Path2D node in your scene.
  • Add a PathFollow2D node as a child of the Path2D.
  • Ensure the rotatesvalue of this path is disabled since our NPC should only move diagonally.
  • In a new scene, create your NPC using a CharacterBody2Dnode as the root node. Add the collisions and animations for this entity just as you did for your player.
  • Attach your NPC to your PathFollow2D node. This will tell the game that this is the object that should follow this Path.
  • Now we can draw our path. In the Godot editor, select the Path2D node. Use the “Add Point” button in the toolbar to add points to draw the path shape that your NPC has to follow. Select the point to move it on your map.
  • Add more points to complete your path.
  • With your path created, attach a script to your NPC. Then, add the logic for them to move along the path.
### NPC.gd

extends CharacterBody2D

@onready var animation_tree = $AnimationTree
@onready var animation_state = animation_tree.get("parameters/playback")
@onready var path_follow = get_parent()

# Variables
@export var speed = 50.0
var current_offset = 0.0
var path_length = 0.0
var direction = 1

func _ready():
# Get the total length of the path
path_length = path_follow.get_parent().curve.get_baked_length()

func _physics_process(delta):
# Update the progress along the path
update_path_progress(delta)
update_animation(Vector2(direction, 0))
move_and_slide()

func update_path_progress(delta):
# Update the progress along the path
current_offset += speed * delta * direction
# Reverse direction if the end or start of the path is reached
if current_offset >= path_length or current_offset <= 0:
direction *= -1
current_offset = clamp(current_offset, 0, path_length)
path_follow.progress = current_offset

func update_animation(velocity: Vector2):
if velocity.length() == 0:
animation_state.travel("idle")
else:
animation_state.travel("walk")
animation_tree.set("parameters/walk/blend_position", velocity.normalized())
  • Run your scene and see your NPC roam. They should follow your path in a zig-zag (back-and-forth) motion.

TileMap

The TileMap node allows us to create 2D grid-based levels. With it, we can place our objects directly onto a grid to draw our world.

The TileMap is composed of cells. Each cell has the same dimensions.

The TileMap uses a TileSet Resource that contains an array of 2D tiles that can be placed on cells on the grid. Each tile is comprised of a sprite, either from an atlas (tilesheet) or an individual image.

We can also add these tiles on different layers, which are added on top of other objects on the grid. These tiles can also have their own collision, navigation, and physics properties.

In my 2D base template project, you will see that my entire (very basic) world was made using several layers and tilesheets on the TileMap node.

Mechanic:

Create a tile-based map.

Implementation:

  • Let’s start by adding a TileMap node to our scene.
  • In the Inspector Panel, give this node a new TileSet resource.
  • You’ll see a panel open at the bottom. This is where we can add the sprites and tilesheets that we want to draw on our TileMap. To make it clear, we create tiles in the TileSet panel, and we draw these tiles onto our world in the TileMap panel.
  • Now, find a tilesheet or a sprite that you like, and drag it into this panel. Say yes to the prompt if you are using a tilesheet so that it can separate the tiles into grid cells.
  • Here we can give our tileset a name and change its margins (if necessary, usually it’s not).
  • In the Select and Paint panels, we can draw properties such as navigation, collision, or other physics properties onto our tiles. We’ll get to that in a bit.
  • Now, go to your TileMap panel. You will see that we can paint our tiles onto our screen. Using the tools, you can draw, erase, or select tiles that you have added.
  • You can also erase tiles by hovering over them and holding down your right mouse button.
  • You can rotate tiles via the X and Z keys on your keyboard.
  • You’ll see that we are drawing all the tiles on one layer. If you draw a tile over an existing one, it will replace that tile.
  • We don’t want this, so let’s add more layers. In the Inspector Panel, underneath Layers, press the “Add Element” button to add more layers.
  • Now we can draw on our different layers.
  • But what about collisions? For this, we need a Physics Layer. Click on the TileSet resource in your Inspector Panel, and navigate to Physics Layers.
  • Click “Add Element” to add a layer. You usually only need one layer for collisions.
  • Now go back to your TileSet panel, and navigate to the Paint pane. Select the Physics property, and select the layer you just created.
  • Now we can draw collisions wherever we want the player and other entities to be blocked. We usually only do this on objects such as trees, buildings, or the outer boundaries of our map.
  • To delete collisions, just clear your polygon on the left and click on your existing cells.
  • You can add more tilesheets and draw collisions on there too.
  • The last thing I want to show you is autotiling. Drawing the ground tile for tile, with all of its edges is extremely tedious. We can make use of autotiling, also called TERRAINS, to speed up this process. This feature automatically selects and places the appropriate tile based on the surrounding tiles, ensuring that the tiles blend seamlessly together.
  • Click on the TileSet resource in your Inspector Panel, and navigate to Terrain Sets.
  • We will add an Element for each resource we want to “autotile”. So if you added a tilesheet for dirt, grass, water, and mud in your TileSet panel, you will create an element for each of those resources. Since we only have the “Dirt” tilesheet (we don’t want to autotile foliage or buildings), we will only add one element.
  • If we had a “Grass” TileSet, we would add another element.
  • Now go back to your TileSet panel, and navigate to the Paint pane. Select the Terrains property, and select the layer you just created.
  • Now we will draw in our bits. This is what the engine will use to check the tiles for the appropriate variant to use. This might be confusing to you, but just remember to select all the areas of your tilemap that ARE NOT on weird shapes or corners.
  • You will have to play around with the tiles to find the bits that give you the best results.
  • Navigate back to your TileMap node, and go to the terrains property. You’ll see that your autotiles have been created.
  • Select your terrain and draw it onto the screen.
  • Go ahead and create your mini world. You can also select and drag commonly used tiles into the Patterns panel for easy access.
  • Run your scene and test your creation!

TileMapLayer

In Godot 4.3, the TileMap node has been replaced by TileMapLayer nodes. This node works exactly like the TileMap node, except each TileMapLayer node represents a single layer of tiles, making it easier to handle specific types of tiles separately (e.g., background, obstacles, interactive elements). This structure is meant to enhance our code clarity and allow for more granular control over the behaviors and properties of different tile layers.

Personally, I don’t like this way of TileMap creation because it reminds me too much of the way it was handled back in Godot 3. Now, instead of using one single TileMap node to create a map, we have to create many separate TileMapLayer nodes to achieve the same result. I guess I can’t complain because unless I make my own engine, this is the node that we will have to use going forward!

The way that we create collisions, autotiles, and tilesets is exactly the same as it was with the TileMap node — hence I left the details in for the TileMap node.

The TileMapLayer is composed of cells. Each cell has the same dimensions.

The TileMapLayer uses a TileSet Resource that contains an array of 2D tiles that can be placed on cells on the grid. Each tile is comprised of a sprite, either from an atlas (tilesheet) or an individual image.

To be able to create multiple layers of different tiles that can overlap with one another, we would have to create a TileMapLayer node for each layer.

In my 2D base template project, you will see that my entire (very basic) world was made using several TileMapLayer nodes. I added all of my layers to a Node2D node for better organization.

Mechanic:

Spawn an item on a TileMapLayer.

Implementation:

  • Let’s start by adding a TileMapLayernode to our scene. We will add this node to a Node2D node, renamed as “Map”.
  • In the Inspector Panel, give this node a new TileSet resource.
  • You’ll see a panel open at the bottom. This is where we can add the sprites and tilesheets that we want to draw on our TileMap. To make it clear, we create tiles in the TileSet panel, and we draw these tiles onto our world in the TileMap panel.
  • Now, find a tilesheet or a sprite that you like, and drag it into this panel. Say yes to the prompt if you are using a tilesheet so that it can separate the tiles into grid cells.
  • Here we can give our tileset a name and change its margins (if necessary, usually it’s not).
  • Now when we select the TileMap panel, we can select tiles from this TileSet resource and draw it onto our screen.
  • Let’s draw a large piece of dirt on our screen.
  • Now, let’s add another TileMapLayer node to our scene. This is the layer we want our item to spawn on. Also give it a TileSet resource.
  • Give it a tilesheet of your choice, and draw it on the screen.
  • Now, in our code, let’s add the functionality to spawn an item on our second (grass) TileMapLayer nodes only. In our script, we use these TileMapLayer methods to manage where items can spawn in a game:
  • get_used_rect(): This is used to find out the area of the TileMapLayer where tiles have been placed so that you can limit where items might appear to just these areas.
  • map_to_local(): This converts the position from the tile map's grid (which might be in big numbers like tile coordinates) to the actual position in the game world (which is in pixels), so you know exactly where to put an item.
  • get_cell_source_id(): This checks if a specific tile exists at a given position in the TileMapLayer. In our script, it's used to determine if a position is valid for spawning an item based on whether there's a tile from the sand or grass layer at that position.
### Main.gd

extends Node2D

# Node refs
var item_texture = load("res://Assets/Icons/icon1.png")
@onready var sand_layer = $Map/TileMapLayer
@onready var grass_layer = $Map/TileMapLayer2

var rng = RandomNumberGenerator.new()

func _ready():
# Spawn between 5 and 10 items
var spawn_item_amount = rng.randf_range(5, 10)
await spawn_item(spawn_item_amount)

# Valid item spawn location
func is_valid_spawn_location(layer, position):
var cell_coords = Vector2(position.x, position.y)
# Check if there's a tile on the sand layer
if sand_layer.get_cell_source_id(cell_coords) != -1:
return false
# Check if there's a tile on the grass layer
if grass_layer.get_cell_source_id(cell_coords) != -1:
return true
return false

# Spawn item
func spawn_item(amount):
var spawned = 0
var attempts = 0
var max_attempts = 1000 # Arbitrary number, adjust as needed

while spawned < amount and attempts < max_attempts:
attempts += 1
var random_position = Vector2(randi() % grass_layer.get_used_rect().size.x, randi() % grass_layer.get_used_rect().size.y)
var layer = randi() % 2
if is_valid_spawn_location(layer, random_position):
var item_instance = Sprite2D.new()
item_instance.texture = item_texture
item_instance.position = grass_layer.map_to_local(random_position) + Vector2(16, 16) / 2
add_child(item_instance)
spawned += 1
  • Run your scene and test to see if the items spawn on the green (grass) tiles which is the TileMapLayer2 node.

Timer

The Timer node is used to create countdown timers that can trigger events after a specified period. The Timer node provides several properties to control its behavior, including wait_time, autostart, and one_shot.

  • wait_time: The duration in seconds that the timer will count down before emitting the timeout signal.
  • autostart: If set to true, the timer will start automatically when the scene is loaded.
  • one_shot: If set to true, the timer will stop after emitting the timeout signal once. If false, the timer will restart automatically after each timeout.

It comes with a timeout signal, which is emitted when the timer reaches zero. This signal can be connected to a function to perform specific actions when the timer completes its countdown. The timeout signal is a crucial part of the Timer node's functionality, allowing you to trigger events at precise intervals.

Mechanic:

Spawn an enemy every 5 seconds.

Implementation:

  • Add a Timer node to your scene. Set its wait_time to 5 seconds. Since we want the enemy to “spawn” as soon as the game loads, we should enable its autostart property.
  • Connect the timer node’s timeout signal to your script. This will execute our logic to spawn our enemy every 5 seconds.
  • In your code, let’s “spawn” an enemy. Since we don’t have an actual enemy scene, we will just print the amount of enemies we have spawned.
### Main.gd

extends Node2D

@onready var timer = $Timer
var enemy_count = 0

func _ready():
if not timer.is_stopped():
timer.start()

func _on_timer_timeout():
spawn_enemy()

func spawn_enemy():
enemy_count += 1
print("An enemy has spawned!")
print("Current enemy count: ", enemy_count)
  • Run your game and your enemy should spawn each time the timer reaches 0!

--

--

Christine Coomans

Just a *redacted* trying to human. 👾Visit my website for more cool resources: https://christinecdevs.site