Below you can find a list of 3D 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 2D 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.
- AnimatedSprite3D
- AnimationPlayer
- AnimationTree
- Area3D
- AudioStreamPlayer3D
- Skeleton3D, Bone3D, and BoneAttachment3D
- Camera3D
- CharacterBody3D
- CollisionShape3D
- DirectionalLight3D
- GridMap
- MeshInstance3D
- NavigationAgent3D, NavigationObstacle3D, NavigationRegion3D
- Node3D
- OmniLight3D
- Path3D, PathFollow3D
- RayCast3D
- RigidBody3D
- Sprite3D
- Spotlight3D
- StaticBody3D
- Timer
- VehicleBody3D
- WorldEnvironment
Node3D
The Node3Dnode is the fundamental building block for all 3D scenes in Godot. It is the base node for all 3D-related functionalities, providing 3D spatial features like position, rotation, and scale. Almost all 3D nodes (like CharacterBody3D, Area3D, etc.) inherit from Node3D, which makes it the core of any 3D scene structure in Godot.
Mechanic:
Rotate a group of 3D nodes collectively.
Implementation:
- Add a Node3D to your scene to serve as a parent node. In an actual project, this could be used to represent a complex object like a spacecraft or a robot.
- Add child nodes such as twoMeshInstance3Dnodes. These children can represent visual components, collision areas, etc.
- Give the MeshInstance3Dnodes a shape of your choosing. I’m going to choose a BoxMeshshape.
- Now if we manipulate the Node3D parent, it will affect all its children. For example, rotating the Node3D will rotate all its children, whilst maintaining their relative positions and transformations.
- We can also do this via code — say every second the game runs the node should rotate around the y-axis pivot point.
### Main.gd
extends Node3D
@onready var node_3d = $Node3D
func _process(delta):
node_3d.rotate_y(1.0 * delta)
MeshInstance3D
The MeshInstance3D node in Godot is used to display 3D geometry. It takes a Meshresource and instances it in the current scene, which is essential for rendering 3D models.
In Godot, a mesh is used as a resource that can be applied to MeshInstance3D nodes to render 3D geometry in a scene. Meshes in Godot can be created directly in the engine or imported from external 3D modeling tools such as Blender. Godot supports several 3D model formats for importing meshes, including .fbx, .gltf and .glb (glTF), .obj, and others.
You can use this node to display characters, objects, or even simple shapes for prototyping, such as cylinders, planes, cubes, etc.
Mechanic:
Display a 3D model in a game scene.
Implementation:
- Create a MeshInstance3D node within your 3D scene.
- In the Inspector, assign a mesh resource to the mesh property of the MeshInstance3D.
- This is great for prototyping, but what if you created a Mesh in Blender and want to show that instead of a shape? Well, you can drag your imported objects directly into your scene.
- You’ll see that it is added to your scene underneath a Node3Dnode which has been imported as a scene. To access the MeshInstance3Dchild node from this parent, you’ll have to localize the scene. Do this by right-clicking on the node, and selecting “Make Local”.
- You can now add collisions to this node, or even materials and shaders.
Sprite3D
The Sprite3D node in Godot is used to display 2D images in a 3D environment. This node can be set to always face the camera, which makes it useful for billboards, decals, NPC headlines, and other elements that need to be visible from all angles in a 3D space.
Mechanic:
Display a billboard that always faces the player.
Implementation:
- Create a Sprite3D node in your 3D scene.
- Assign a texture to the texture property of the Sprite3D node in the Inspector. This texture will appear as a flat image in the 3D space.
- Currently the image is just flat. To change this, enable the billboard_mode property of the Sprite3D to ensure it always faces the camera, making it visible from any angle.
- Run the scene and navigate around the 3D world to see the sprite always facing towards the camera, behaving like a billboard.
AnimatedSprite3D
The AnimatedSprite3D node utilizes a series of images (sprites) and displays them in a sequence to create an animation in a 3D space. Unlike a simple Sprite3D node that displays a single static image in our world, AnimatedSprite3D can cycle through multiple frames to animate characters, objects, or effects within your 3D game. To create these frames, we can use either sprites or a spritesheet.
The AnimatedSprite3D 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:
Animate a 3D spinning coin.
Implementation:
- Download the spinning coin sheet.
- Create an AnimatedSprite3D node in your scene.
- Assign a SpriteFrames resource to the AnimatedSprite3D.
- 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 our coin atlas.
- Crop out the frames horizontally and vertically.
- Select the frames you want. For instance, I will choose frame 0–4 in row 1.
- Scale the coin down and move it to where you want it.
- Then play the animation to see if you need to alter the FPS to make the coin rotate faster/slower.
- Also enable billboard settings so that our image retains its 3D look and feel (instead of looking like a flat 2D coin).
- Play the animation in your code so that when your player moves during runtime the animation can play:
### Main.gd
@onready var animated_sprite_3D = $AnimationSprite3D
func _ready():
animated_sprite_3D.play("rotating_coin")
- Start your project and observe the coin rotating around in your world.
AnimationPlayer
Unlike the AnimatedSprite3D which is specifically designed for image 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:
Create a platform that moves up and down.
Implementation:
- Add a MeshInstance3Dnode and an AnimationPlayernode to your scene. The MeshInstance3Dnode is the node we want to animate, and the property we want to animate of this node is its position.
- Assign a shape to the MeshInstance3Dnode. I’ll assign a simple box shape.
- Change its scale to be more similar to a platform — Vector3(2, 0.2, 2).
- Select the AnimationPlayer node.
- In the animation panel, click “New Animation” and name it something descriptive like “move_platform”.
- 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 MeshInstance3D node, and in the Inspector, set the position property to its initial value (e.g., Vector3(0, 0, 0)).
- Right-click the position 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 position to a larger value (e.g., Vector3(0, 1, 0)), and add another keyframe.
- At the end of the timeline (1 second), set the position back to the initial value (Vector3(0, 0, 0)) 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("move_platform")
- Start your project and observe the platform move up and down.
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 3D character with multiple actions (e.g., running, jumping, idle).
Implementation:
- Add an AnimationPlayernode to your scene which has your 3D model, and create animations for it like "run", "attack", and "idle".
- You can also download animations from Mixamo if you don’t want to manually create these. If you want to download an animation from Mixamo, you will have to upload your 3D character (MAKE SURE IT’S THE SAME CHARACTER AS THE ONE IN YOUR GAME) to the dashboard, and then download the skin along with your desired animation.
- Then you’ll have to import the character with the animation into your project, add it to your scene, localize the scene, extract the animation (by saving it as a .res), and then you’ll have to load it into your original AnimationPlayer.
- Now, add an AnimationTree node.
- Configure the tree_root as an AnimationRootNode 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 a new animation for ‘idle’, ‘walk’, ‘run’, and ‘jump’.
- Add a transition between start -> idle. The transition type should be immediatebecause we want the animation to play immediately.
- 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.
- Select your transitions from walk <-> idle, and in the Inspector panel, change the x-fade time to a value such as 0.3. This is the blending time of the transitions, which smoothens out our transition from one animation to the next.
- Do the exact same for the transitions between your walk <-> sprint.
- Do the same for your jump animation, but it should transition back to all of your other animations (jump -> idle, jump -> walk, jump -> sprint). The transition from your Jump to other animations should be of type At End, because we want to transition back to running, walking, or jumping when the player has finished jumping.
- Then, we only want to JUMP if our player is jumping by pressing the ui_jump key. For this we can make use if an expression. We can enter boolean values in the expressionpanel, which will then check our code for the conditions to play the animation based on this boolean. So, if in our code we say is_jumping = true when we press our jump button, this part of the AnimationTree will execute.
- Add an expression is_jumping to all of the transitions that go towards your jump animation (idle -> jump, walk -> jump, sprint -> jump).
- Now in our code, we can play our animations.
### Player.gd
extends CharacterBody3D
# Vars
const run_speed = 3.0
const sprint_speed = 5.0
const jump_speed = 3.0
const gravity = 10
@onready var animation_tree = $AnimationTree
@onready var animation_state = animation_tree.get("parameters/playback")
@onready var camera = $ThirdPersonCamera/Camera
var is_jumping = false
func _ready():
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
func _process(delta):
handle_animation()
func _physics_process(delta):
handle_movement(delta)
func handle_animation():
var input_dir = Input.get_vector("ui_right", "ui_left", "ui_down", "ui_up")
if is_jumping:
if animation_state.get_current_node() != "jump":
animation_state.travel("jump")
elif input_dir.length() > 0:
if Input.is_action_pressed("ui_sprint"):
if animation_state.get_current_node() != "sprint":
animation_state.travel("sprint")
else:
if animation_state.get_current_node() != "walk":
animation_state.travel("walk")
else:
if animation_state.get_current_node() != "idle":
animation_state.travel("idle")
func handle_movement(delta):
# Check if the player has landed
if is_on_floor() and velocity.y == 0:
is_jumping = false
# Apply gravity
velocity.y += -gravity * delta
# Get input direction
var input_dir = Input.get_vector("ui_left", "ui_right", "ui_down", "ui_up")
if input_dir.length() > 0:
# Calculate movement direction based on camera orientation
var forward = -camera.global_transform.basis.z
var right = camera.global_transform.basis.x
forward.y = 0
right.y = 0
forward = forward.normalized()
right = right.normalized()
var movement_dir = (forward * input_dir.y + right * input_dir.x).normalized()
# Rotate player to face movement direction
var target_rotation = atan2(movement_dir.x, movement_dir.z)
rotation.y = lerp_angle(rotation.y, target_rotation, 0.1)
# Apply movement
if Input.is_action_pressed("ui_sprint"):
velocity.x = movement_dir.x * sprint_speed
velocity.z = movement_dir.z * sprint_speed
else:
velocity.x = movement_dir.x * run_speed
velocity.z = movement_dir.z * run_speed
else:
velocity.x = move_toward(velocity.x, 0, run_speed)
velocity.z = move_toward(velocity.z, 0, run_speed)
# Handle jumping
if Input.is_action_just_pressed("ui_jump") and is_on_floor():
velocity.y = jump_speed
is_jumping = true
move_and_slide()
- Run the scene and control the character to observe the transitions and movement based on our state.
CollisionShape3D
The CollisionShape3D 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 CollisionShape3D node as a child of a CharacterBody3D, RigidBody3D, orStaticBody3D. These nodes will block other collisions. To have a node pass through collisions, use an Area3D.
- In the Inspector, assign a Shape3D resource to the shape property of the CollisionShape3D. The shape you choose will depend on the shape of your entity. For example, a player might have a capsule shape, a pickup a sphere, an area a box.
- Let’s enable debugging to see our collisions in action. You can also change the color of your debug lines to make it more visible.
- Run your scene to see how your player interacts with the collision shape. Since we used a StaticBody3D node, they should be blocked and unallowed to go through the collision.
CharacterBody3D
The CharacterBody3D 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 RigidBody3D or StaticBody3D node, CharacterBody3D is 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:
- Create a CharacterBody3D node in 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 CollisionShape3D as a child of the CharacterBody3D and set its shape to match your character.
- Add a MeshInstance3D node to this scene so that we can see our character.
- You’ll also need to attach a camera to your character so we can see them.
- Attach a script to the CharacterBody3D to handle movement and jumping.
# Character.gd
extends CharacterBody3D
# Variables
@export var speed = 5.0
@export var jump_force = 10.0
@export var gravity = 10.0
# Input for movement
func get_input():
velocity.x = 0
velocity.z = 0
if Input.is_action_pressed("ui_up"):
velocity.z -= speed
if Input.is_action_pressed("ui_down"):
velocity.z += speed
if Input.is_action_pressed("ui_left"):
velocity.x -= speed
if Input.is_action_pressed("ui_right"):
velocity.x += speed
if is_on_floor() and Input.is_action_just_pressed("ui_accept"):
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.
StaticBody3D
The StaticBody3D 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 StaticBody3D node in your scene. Add a CollisionShape3Das a child of the StaticBody and set its shape to match the obstacle.
- Give it a MeshInstance3Dof 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.
RigidBody3D
The RigidBody3D 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 RigidBody3D node in your scene. Add a CollisionShape3Das a child of the RigidBody and set its shape to match the obstacle.
- Give it a MeshInstance3Dof your choice so that we can see the item.
- Since we don’t want the object to fly into space when we collide with it, we will need to increase its mass value.
- Use GDScript to apply forces or impulses to the rigid body if the player pushes it.
### Player.gd
extends CharacterBody3D
# Vars
const run_speed = 3.0
const sprint_speed = 5.0
const jump_speed = 3.0
const gravity = 10
@export var push_force = 80.0
@onready var animation_tree = $AnimationTree
@onready var animation_state = animation_tree.get("parameters/playback")
@onready var camera = $ThirdPersonCamera/Camera
var is_jumping = false
func _ready():
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
func _process(delta):
handle_animation()
func _physics_process(delta):
handle_movement(delta)
handle_collisions()
func handle_animation():
var input_dir = Input.get_vector("ui_right", "ui_left", "ui_down", "ui_up")
if is_jumping:
if animation_state.get_current_node() != "jump":
animation_state.travel("jump")
elif input_dir.length() > 0:
if Input.is_action_pressed("ui_sprint"):
if animation_state.get_current_node() != "sprint":
animation_state.travel("sprint")
else:
if animation_state.get_current_node() != "walk":
animation_state.travel("walk")
else:
if animation_state.get_current_node() != "idle":
animation_state.travel("idle")
func handle_movement(delta):
# Check if the player has landed
if is_on_floor() and velocity.y == 0:
is_jumping = false
# Apply gravity
velocity.y += -gravity * delta
# Get input direction
var input_dir = Input.get_vector("ui_left", "ui_right", "ui_down", "ui_up")
if input_dir.length() > 0:
# Calculate movement direction based on camera orientation
var forward = -camera.global_transform.basis.z
var right = camera.global_transform.basis.x
forward.y = 0
right.y = 0
forward = forward.normalized()
right = right.normalized()
var movement_dir = (forward * input_dir.y + right * input_dir.x).normalized()
# Rotate player to face movement direction
var target_rotation = atan2(movement_dir.x, movement_dir.z)
rotation.y = lerp_angle(rotation.y, target_rotation, 0.1)
# Apply movement
if Input.is_action_pressed("ui_sprint"):
velocity.x = movement_dir.x * sprint_speed
velocity.z = movement_dir.z * sprint_speed
else:
velocity.x = movement_dir.x * run_speed
velocity.z = movement_dir.z * run_speed
else:
velocity.x = move_toward(velocity.x, 0, run_speed)
velocity.z = move_toward(velocity.z, 0, run_speed)
# Handle jumping
if Input.is_action_just_pressed("ui_jump") and is_on_floor():
velocity.y = jump_speed
is_jumping = true
move_and_slide()
# Handle Collisions
func handle_collisions():
for i in range(get_slide_collision_count()):
var collision = get_slide_collision(i)
if collision.get_collider() is RigidBody3D:
var collider = collision.get_collider() as RigidBody3D
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.
VehicleBody3D
TheVehicleBody3D node is used to simulate a 3D vehicle with realistic physics. It requires VehicleWheel3D nodes for each wheel to function correctly.
On our VehicleBody3D node, the most important properties are the engine_force, steer_angle, and brake_force properties. The engine_force allows our vehicle to move forward/backward. The steer_angle/steering properties allow our vehicle to move left/right. The brake_force property allows our car to stop.
Mechanic:
Create a controllable vehicle with wheels.
Implementation:
- For this part, you will need to download a car and import it into your project. If you’re using my base template, I recommend downloading the Car Kit from Kenney and importing it into your project.
- Create a VehicleBody3D node in your scene. You will need to give it a CollisionShape3D, and a mesh (use the one you imported).
- Add VehicleWheel3D nodes as children of the VehicleBody3D node for each wheel. Name them FrontLWheel, BackLWheel, FrontRWheel, BackRWheel. Also move them into the positions of your wheels.
- You’ll need to set your front wheels as the steering wheels, and your back wheels as your traction wheels.
- Also attach a camera to your vehicle so you can follow it around.
- Attach a script to your vehicle to handle the vehicle’s movement and control. We’ll move our car with our arrow keys, and brake with our spacebar.
# VehicleBody.gd
extends VehicleBody3D
@onready var front_l_wheel = $FrontLWheel
@onready var front_r_wheel = $FrontRWheel
@onready var back_r_wheel = $BackRWheel
@onready var back_l_wheel = $BackLWheel
# Variables
@export var new_engine_force = 1000.0
@export var brake_force = 500.0
@export var steer_angle = 0.5
func _process(delta):
handle_input()
func handle_input():
var engine = 0.0
var brake = 0.0
var steer = 0.0
if Input.is_action_pressed("ui_up"):
engine = new_engine_force
if Input.is_action_pressed("ui_down"):
engine = -new_engine_force
if Input.is_action_pressed("ui_left"):
steer = steer_angle
if Input.is_action_pressed("ui_right"):
steer = -steer_angle
if Input.is_action_pressed("ui_accept"):
brake = brake_force
front_l_wheel.engine_force = engine
front_r_wheel.engine_force = engine
front_l_wheel.brake = brake
front_r_wheel.brake = brake
front_l_wheel.steering = steer
front_r_wheel.steering = steer
- Run your scene and try to control your vehicle. It should move around your map when controlled!
Area3D
The Area3D 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 Area3D 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 Area3D node in your scene. You also need to add a CollisionShape3D 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 Area3D 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 Node3D
func _on_area_3d_body_entered(body):
if body.name == "Player":
print("The player has entered the area!")
func _on_area_3d_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.
RayCast3D
The RayCast3D node is used to cast a ray in a 3D 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 StaticBody3D (useful for detecting loot and quest items), CharacterBody3D (useful for detecting interactions with enemies and NPCs), and RigidBody3D (useful for detecting interactions with moveable objects. It can also collide with areas, such as Area3D (useful for interactions with trigger zones).
Mechanic:
Cast a ray from the player and detect what it hits.
Implementation:
- Add a RayCast3D node to the Player in your scene.
- Set the target_position property face the direction of your player (z: 1). You can also enable its collision with areas.
- Now we can print what we are colliding with in our code.
### Player.gd
extends CharacterBody3D
# Vars
const run_speed = 3.0
const sprint_speed = 5.0
const jump_speed = 3.0
const gravity = 10
@onready var animation_tree = $AnimationTree
@onready var animation_state = animation_tree.get("parameters/playback")
@onready var camera = $ThirdPersonCamera/Camera
@onready var ray_cast_3d = $RayCast3D
var is_jumping = false
func _ready():
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
func _process(delta):
handle_animation()
if ray_cast_3d.is_colliding():
var collider = ray_cast_3d.get_collider()
print("Raycast hit: ", collider.name)
- Run your scene and interact with objects that have colliders. The raycast should detect the objects and notify the game.
Camera3D
The Camera3D node is used to control the view of a 3D 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 third-person games , we usually attach the Camera3D node to a SpringArm3D node. The SpringArm node reacts to collisions in the environment. It ensures that the camera moves closer to the player when there are obstacles, preventing the camera from clipping through objects.
In first-person games , we usually attach the Camera3D directly to our Pivot — such as our Player head. This way we “see” from the player’s perspective.
In “God-Mode” , we attach the Camera to our World scene so that we can get 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, and zoom.
Mechanic:
Create a “God Mode” 3D camera that can move, zoom, and rotate based on user input.
Implementation:
- Add a Camera3D node to your Main (World) scene. Make sure this camera is set to ‘current’, and all other cameras are disabled.
- Also move the camera up on the y-axis (10), and rotate it slightly on the x-axis (-45) to point down at your world.
- Add the inputs to zoom, move, and rotate your 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 Node3D
@onready var camera_3d = $Camera3D
@export var zoom_speed = 50.0
@export var move_speed = 5.0 # Adjusted to a more reasonable value
@export var rotate_speed = 0.5 # Adjusted to a more reasonable value
@export var min_zoom = 10.0
@export var max_zoom = 100.0
var target_fov = 70.0
var camera_drag = Vector2.ZERO
var camera_movement = Vector3.ZERO
var sensibility = 0.001
func _process(delta):
handle_zoom(delta)
handle_movement(delta)
handle_rotation(delta)
func handle_zoom(delta):
if Input.is_action_pressed("zoom_in"):
camera_3d.fov -= zoom_speed * delta
if Input.is_action_pressed("zoom_out"):
camera_3d.fov += zoom_speed * delta
camera_3d.fov = clamp(camera_3d.fov, min_zoom, max_zoom)
func handle_movement(delta):
if Input.is_action_just_pressed("camera_drag"):
camera_drag = get_viewport().get_mouse_position()
if Input.is_action_pressed("camera_drag"):
camera_movement.x += (get_viewport().get_mouse_position().x - camera_drag.x) * sensibility
camera_movement.z += (get_viewport().get_mouse_position().y - camera_drag.y) * sensibility
camera_3d.position += camera_movement * move_speed * delta
camera_movement = lerp(camera_movement, Vector3.ZERO, 0.2)
func handle_rotation(delta):
if Input.is_action_pressed("rotate_left"):
camera_3d.rotate_y(rotate_speed * delta)
if Input.is_action_pressed("rotate_right"):
camera_3d.rotate_y(-rotate_speed * delta)
- Run the scene and use the defined input actions to move, zoom, and rotate the camera.
DirectionalLight3D
The DirectionalLight3D node is used to simulate sunlight or moonlight in our 3D environment. 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. You can also change the Mode of the shadows, which will change the way your shadows are rendered. By default, PSSM 2 is a good option to choose.
In 3D environments, your light will also be influenced by your WorldEnvironment node, which contains the sky of your world.
Mechanic:
Illuminate a scene with sunlight.
Implementation:
- Create a DirectionalLight3D node in your scene.
- By default, the light and energy looks pretty good. We can change these values if you want. For instance, we can make the energy brighter and the color darker to simulate night time.
- 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 Node3D
@onready var directional_light_3d = $World/DirectionalLight3D
func _on_timer_timeout():
directional_light_3d.color = Color(randf(), randf(), randf()
- Run your scene and enjoy the overstimulation.
SpotLight3D
The SpotLight3D node emits light in a cone shape, illuminating objects within the cone’s range. This type of light is useful for creating focused lighting effects, such as flashlights or stage lights.
This node’s two main properties are the energy , color, specular, and range, and projectorproperties. The energy property determines how bright/dim the light is. The color is the shading of the light. The specular property affects how reflective surfaces appear under the spotlight. The range property defines the maximum distance the light can reach. The projector property allows us to give our light a texture, such as stained glass or a vignette effect.
Mechanic:
Create a flashlight attached to your player.
Implementation:
- To your player, attach a Spotlight3D node. Move this node into one of their hands, and rotate it on the y-axis (180) so that it faces the direction that the player is facing.
- Play around with the energy, range, angle, color, and specular values.
- This light looks too bright. It needs to look more like a flashlight. Download this texture, and drag it into your projector property.
- Now, let’s move this flashlight when we move our camera.
### Player.gd
extends CharacterBody3D
# Vars
const run_speed = 3.0
const sprint_speed = 5.0
const jump_speed = 3.0
const gravity = 10
@onready var animation_tree = $AnimationTree
@onready var animation_state = animation_tree.get("parameters/playback")
@onready var camera = $ThirdPersonCamera/Camera
@onready var flashlight = $SpotLight3D
var is_jumping = false
func _ready():
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
func _process(delta):
handle_animation()
func _physics_process(delta):
handle_movement(delta)
func handle_animation():
var input_dir = Input.get_vector("ui_right", "ui_left", "ui_down", "ui_up")
if is_jumping:
if animation_state.get_current_node() != "jump":
animation_state.travel("jump")
elif input_dir.length() > 0:
if Input.is_action_pressed("ui_sprint"):
if animation_state.get_current_node() != "sprint":
animation_state.travel("sprint")
else:
if animation_state.get_current_node() != "walk":
animation_state.travel("walk")
else:
if animation_state.get_current_node() != "idle":
animation_state.travel("idle")
func handle_movement(delta):
# Check if the player has landed
if is_on_floor() and velocity.y == 0:
is_jumping = false
# Apply gravity
velocity.y += -gravity * delta
# Get input direction
var input_dir = Input.get_vector("ui_left", "ui_right", "ui_down", "ui_up")
if input_dir.length() > 0:
# Calculate movement direction based on camera orientation
var forward = -camera.global_transform.basis.z
var right = camera.global_transform.basis.x
forward.y = 0
right.y = 0
forward = forward.normalized()
right = right.normalized()
var movement_dir = (forward * input_dir.y + right * input_dir.x).normalized()
# Rotate player to face movement direction
var target_rotation = atan2(movement_dir.x, movement_dir.z)
rotation.y = lerp_angle(rotation.y, target_rotation, 0.1)
# Apply movement
if Input.is_action_pressed("ui_sprint"):
velocity.x = movement_dir.x * sprint_speed
velocity.z = movement_dir.z * sprint_speed
else:
velocity.x = movement_dir.x * run_speed
velocity.z = movement_dir.z * run_speed
else:
velocity.x = move_toward(velocity.x, 0, run_speed)
velocity.z = move_toward(velocity.z, 0, run_speed)
# Handle jumping
if Input.is_action_just_pressed("ui_jump") and is_on_floor():
velocity.y = jump_speed
is_jumping = true
move_and_slide()
# Rotate flashlight to match camera orientation
flashlight.global_rotation = camera.global_rotation
- Run your scene and move your camera. Your flashlight should shine in that direction.
OmniLight3D
The OmniLight3D node emits light in all directions from a single point, similar to a light bulb. This type of light is useful for creating ambient lighting or point light sources.
This node’s two main properties are the energy , color, specular, and range, and projectorproperties. The energy property determines how bright/dim the light is. The color is the shading of the light. The specular property affects how reflective surfaces appear under the spotlight. The range property defines the maximum distance the light can reach.
Mechanic:
Create a flickering torch.
Implementation:
- In your scene, add a OmniLight3D node. Move it to where you want your light to shine from.
- Play around with the energy, range,color, and specular values.
- 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 OmniLight3D.
- Add keyframes to the energy track to simulate flickering.
- Enable looping.
- Then play this animation via the code when the game loads.
### Main.gd
extends Node3D
@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.
BoneAttachment3D
When importing a 3D model with a skeleton from 3D software such as Blender, the Skeleton3Dnode should automatically be populated with bones if the model is correctly rigged, exported, and imported. Bone3Dare the parts of the body — such as the hands, arms, legs, etc.
The BoneAttachment3Dnode is used to attach nodes to specific bones in a Skeleton3D. This allows you to dynamically attach objects to bones, which will follow the bone’s transformations. For example, we can attach a flashlight to our player’s hand, or a bow on our player’s back.
If we look at the Player character in the base template, we can see that their Skeleton3D node from is already populated with bones. This came from the model (.glb) itself — we didn’t have to manually create this in Godot.
Mechanic:
Attach a weapon to the right hand of our player.
Implementation Steps
- In your Player scene, add aBoneAttachment3D node as a child of the Skeleton3D node.
- Set the bone_name property of theBoneAttachment3D to the name of the bone you want to attach to. In our case, it should be the arm-right.
- Add the objects you want to attach as children of the BoneAttachment3D nodes. In our case, we want to add a weapon. Since we don’t have weapons in the base template, we will use the cane instead.
- Move the cane into the players hand.
- In our script, let’s handle the attachment and detachment of this “weapon”. We also need to add an input to handle this action.
### Player.gd
extends CharacterBody3D
# Vars
const run_speed = 3.0
const sprint_speed = 5.0
const jump_speed = 3.0
const gravity = 10
@onready var animation_tree = $AnimationTree
@onready var animation_state = animation_tree.get("parameters/playback")
@onready var camera = $ThirdPersonCamera/Camera
@onready var bone_attachment_3d = $"character-female-b2/character-female-b/Skeleton3D/BoneAttachment3D"
## Older code
func _input(event):
if event.is_action_pressed("ui_equip"):
equip_object()
func equip_object():
bone_attachment_3d.visible = !bone_attachment_3d.visible
- Run your scene. Your weapon should equip/unequip if you press TAB. When you move, the weapon should also move naturally with your bone.
AudioStreamPlayer3D 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 AudioStreamPlayer3D 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 AudioStreamPlayer3D 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 also want to enable the emission property on our AudiostreamPlayer3D so that the audio can sound more distanced.
- We will play our sound effect audio (AudioStreamPlayer3D) when our player enters a certain area. To do this, add an Area3D 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 Node3D
@onready var audio_stream_player_3d = $AudioStreamPlayer3D
func _on_area_3d_body_entered(body):
if body.name == "Player":
audio_stream_player_3d.play()
- Run your scene. The background music should play, and the sound effect should play when your player enters the area.
WorldEnvironment
The WorldEnvironment node configures the default environment settings for a scene, including lighting, post-processing effects, and background settings. This node allows you to add a bunch of environmental effects, such as a skybox, fog, shadows, etc.
Everything you see in the screenshot below (the sky, light, fog) was done with the help of the WorldEnvironment node.
The most common properties you will change on this node will be:
Fog
- Purpose: Fog makes distant objects fade into a uniform color, simulating atmospheric effects.
- Key Properties:
- Light Color: The color of the fog.
- Density: The ‘thickness’ of the fog until it is fully opaque.
- Sky Affect: Adjusts the fog color based on the sun’s energy.
Volumetric Fog
- Purpose: Volumetric fog provides a more realistic fog effect by interacting with the lights in the scene.
- Key Properties:
- Density: The base exponential density of the volumetric fog.
- Albedo: The color of the fog when interacting with lights.
- Emission: The emitted light from the fog, useful for establishing ambient color.
- Emission Energy: The brightness of the emitted light.
- Anisotropy: The direction of scattered light through the fog.
- Length: The distance over which the fog is computed.
- Detail Spread: The distribution of detail in the fog.
- Ambient Inject: Scales the strength of ambient light used in the fog.
- Sky Affect: Controls how much the fog affects the background sky.
Glow
- Purpose: Glow adds bloom effects to bright areas, enhancing the visual appeal.
- Key Properties:
- Intensity: The strength of the glow effect.
- Strength: The brightness for the glow effect.
- Blend Mode: The blending mode for the glow effect.
Tonemapping
- Purpose: Tonemapping adjusts the color balance and contrast of the scene.
- Key Properties:
- Mode: The tone-mapping algorithm (e.g., Linear, Reinhard, Filmic).
- Exposure: Adjusts the overall exposure of the scene.
Ambient Light
- Purpose: Ambient light provides a base level of illumination for the scene, ensuring that no part of the scene is completely dark.
- Key Properties:
- Color: The color of the ambient light.
- Energy: The intensity of the ambient light.
- Sky Contribution: The amount of light contributed by the sky.
Sky
- Purpose: The sky provides the background for the scene, which can be a solid color, a skybox, or a custom shader.
- Key Properties:
- Sky: The type of sky (e.g., PanoramaSky, ProceduralSky).
- Custom FOV: The intensity of the sky’s contribution to the scene lighting.
Background
- Purpose: The background sets the visual backdrop for the scene, which can be a solid color, a sky, or a custom shader.
- Key Properties:
- Mode: The background mode (e.g., Clear Color, Sky, Custom Color).
- Energy Multiplier: The intensity of the background’s contribution to the scene lighting.
Mechanic:
Create a spooky atmosphere.
Implementation:
- Add a WorldEnvironment node to your scene. To this node, add an Environment resource and configure it.
- Set the background to be a color. Set the color to be black.
- Change the ambient light color and energy.
- Everything is too dark. Let’s enable our fog and change our properties.
- Let’s lighten the mood. Enable the glow property, and change its properties.
- Run your scene. Your game should look a little bit more spooky now!
NavigationAgent3D, NavigationObstacle3D, NavigationRegion3D
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 NavigationAgent3Dnode is used to move characters along a path while avoiding obstacles.
- The NavigationObstacle3D node is used to create obstacles that navigation agents will avoid.
- The NavigationRegion3D 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 NavigationRegion3D to your scene to define the roaming area.
- Create a new NavigationMesh resource for this node so that we can define our region. Also move it to where you want your region to be.
- Now to actually add our region, we’ll need to add our “floor” as a child of this NavigationRegion node. Since I’m not sure if you are using Terrains or GridMaps, we’ll use a MeshInstance3D for this.
- Add a MeshInstance3D node as a child to this node. Change its size to something like 10 x 10 m.
- Move your region so the floor is below the grass.
- 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 CharacterBody3Dnode 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 NavigationAgent3D 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 NavigationAgent3D 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 CharacterBody3D
@onready var navigation_agent_3d = $NavigationAgent3D
@onready var navigation_region = $"../NavigationRegion3D/MeshInstance3D"
@onready var animation_tree = $AnimationTree
@onready var animation_state = animation_tree.get("parameters/playback")
@onready var timer = $Timer
# Variables
@export var movement_speed: float = 1.0
var roaming_area: AABB
var target_position: Vector3
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: Vector3 = navigation_agent_3d.get_next_path_position()
var new_velocity: Vector3 = global_position.direction_to(next_path_position) * movement_speed
if navigation_agent_3d.avoidance_enabled:
navigation_agent_3d.velocity = new_velocity
else:
_on_navigation_agent_3d_velocity_computed(new_velocity)
# Rotate NPC to face the direction of movement
if velocity.length() > 0:
var target_rotation = atan2(velocity.x, velocity.z)
rotation.y = lerp_angle(rotation.y, target_rotation, 0.1)
# Play walking animation
if velocity != Vector3.ZERO:
animation_state.travel("walk")
else:
animation_state.travel("idle")
move_and_slide()
func set_roaming_area():
# Set the roaming area based on the MeshInstance3D's bounding box
roaming_area = navigation_region.get_aabb()
print("Roaming area: ", roaming_area)
func set_random_target():
# Set next roaming position
target_position = Vector3(
randf_range(-roaming_area.position.x, roaming_area.position.x),
randf_range(-roaming_area.position.y, roaming_area.position.y),
randf_range(-roaming_area.position.z, roaming_area.position.z)
)
navigation_agent_3d.set_target_position(target_position)
func _on_navigation_agent_3d_velocity_computed(safe_velocity):
# Move NPC
velocity = safe_velocity
func _on_timer_timeout():
# Move NPC again
set_random_target()
func _on_navigation_agent_3d_navigation_finished():
# When path reached, redirect NPC
velocity = Vector3.ZERO
animation_state.travel("idle")
timer.start()
- Instance your NPC in your Main scene. Move them into your region.
- Optionally, add NavigationObstacle3D nodes to create obstacles. Add this node to a mesh with a collision shape.
- Run your scene and see your NPC randomly roam. They should avoid your obstacles.
Path3D and PathFollow3D
The Path3D and PathFollow3D nodes work together to create and follow paths in a 3D space. The Path3D 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 PathFollow3D node is used to make an object follow a path defined by a Path3D node.
Mechanic:
Create an NPC that roams on a defined path on the map.
Implementation:
- Create a Path3D node in your scene.
- Add a PathFollow3D node as a child of the Path3D.
- Enable its use_model_front property so that our NPC will always move whilst facing the front.
- In a new scene, create your NPC using a CharacterBody3Dnode as the root node. Add the collisions and animations for this entity just as you did for your player.
- Attach your NPC to your PathFollow3D 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 Path3D 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.
- Make sure your path is on the ground, otherwise your NPC will fly!
- With your path created, attach a script to your NPC. Then, add the logic for them to move along the path.
### NPC.gd
extends CharacterBody3D
@onready var animation_tree = $AnimationTree
@onready var animation_state = animation_tree.get("parameters/playback")
@onready var path_follow = get_parent()
# Vars
@export var movement_speed: float = 2.0
var current_offset: float = 0.0
var path_length: float = 0.0
var direction: int = 1
var previous_position: Vector3 = Vector3.ZERO
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)
# Calculate the velocity based on the change in position
var current_position = path_follow.global_transform.origin
var velocity = (current_position - previous_position) / delta
previous_position = current_position
# Update the animation based on the velocity
update_animation(velocity)
# Update the NPC's position to follow the PathFollow3D node
global_transform.origin = current_position
# Rotate NPC to face the direction of movement
if velocity.length() > 0:
var target_rotation = atan2(velocity.x, velocity.z)
rotation.y = lerp_angle(rotation.y, target_rotation, 0.1)
move_and_slide()
func update_path_progress(delta):
current_offset += movement_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 # Reverse the direction
current_offset = clamp(current_offset, 0, path_length)
# Update the progress of the PathFollow3D node
path_follow.progress = current_offset
func update_animation(velocity: Vector3):
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.
GridMap
The GridMap node allows us to create 3D grid-based levels. With it, we can place our 3D models (tiles) on a grid interactively, similar to how TileMap works in 2D. If you have suitable models, you can use them to create, design, and manage large, repetitive environments like dungeons, cities, or landscapes.
The Gridmap is composed of cells. Each cell has the same dimensions.
These cells are formed using floors (horizontal grid), and planes (vertical grid). Each floor represents a different height level, whereas each plane represents a different depth level.
The GridMap uses a MeshLibrary Resource that contains an array of 3D tiles that can be placed on cells on the grid. Each tile is a mesh with materials, and it can also include optional collision and navigation shapes.
In my 3D base template project, you will see that my entire world was made using several GridMap nodes.
All of these tiles that you see on the screen were originally .glb models.
We add all of these models to a new scene, add whatever collisions we want, and then we convert this entire scene into MeshLibrary Resources so that they can be used by our GridMap as tiles.
Mechanic:
Create a grid-based map.
Implementation:
- To begin, we need a MeshLibrary, which is a collection of individual meshes that can be used in the GridMap.
- Create a new scene with a Node3D node as the root node. Rename this node to “GridTiles”.
- Import any .glb asset that you want to use, and then drag it into this new scene.
- If you added meshes that need to block the player or the player should be able to walk on it (such as the ground), you’ll have to localize these scenes and add a collision shape to them.
- After this is done, you can navigate to Scene > Export As and export this scene as a MeshLibrary.
- Now we can add these to a GridMap. In your Main scene, add a new GridMap node.
- Assign the library that you just created as its MeshLibrary resource. The tiles should now be available to be placed down.
- If your meshes look odd, you can change the cell size to fit your mesh, as well as disable centering on a certain axis so it “snaps” better.
- You can alter between floors and planes by pressing the C, X, and Z keys on your keyboard.
- You can delete tiles by hovering over them and holding down your right mouse button.
- You can rotate meshes via the D and S keys on your keyboard.
- You will have to use multiple GridMaps with different settings to achieve a good result. Unless you’re using tiles that are all the same size, you won’t be able to do everything using only one GridMap.
- Go ahead and create your mini world.
- Run your scene and test your creation!
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 Node3D
@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!
Top comments (0)