PIXEL QUEST! 🗡️Build Your Own Zelda-Style Adventure
Explore a top-down world, swing your sword at slimes, find a hidden chest, and unlock the exit! Your very own legend begins right here.
Explore a top-down world, swing your sword at slimes, find a hidden chest, and unlock the exit! Your very own legend begins right here.
Understand what we're building and get Godot 4 open and ready!
We're going to build a game like the Legend of Zelda — one of the most famous adventure games EVER! Your tiny hero will walk around a dungeon, swing a sword at slimes, find a chest with a key, and escape through a locked door. You're going to make every single piece of it. Let's go, hero! ⚔️
Episode 6 introduces top-down movement — no gravity at all this time. The hero moves in four directions using a velocity vector. New concepts across the 12 steps: 4-directional movement, sword as a temporary Area2D child, slime AI with velocity bouncing, chest state machine, key pickup, and a locked door that watches for a signal.
Create a new Godot 4 project called "Pixel Quest" and set it up.
Time to open Godot and start fresh! Every adventure begins somewhere — ours begins with a brand new project. Let's name it and get it ready!
Standard Godot 4 setup, Compatibility renderer. A square window works well for top-down: try 512×512 or 576×576 in Project Settings → Display → Window. This gives a classic Zelda dungeon-room feel.
Create a Hero scene that moves up, down, left, and right — no jumping, no gravity!
This time our hero doesn't jump at all! Instead of running and jumping, our Zelda hero walks in ALL FOUR directions — up, down, left, right. It's a completely different kind of movement, and it feels really cool!
Hero scene: CharacterBody2D → Sprite2D + CollisionShape2D (CircleShape2D works well for top-down). No gravity variable needed. Movement uses Input.get_vector("ui_left","ui_right","ui_up","ui_down") which returns a Vector2 — multiply by SPEED. Add to "player" group. Save as hero.tscn.
get_vector() is the cleanest way to handle 4-directional movement in Godot 4 — it handles diagonals gracefully and returns a properly normalised vector when two keys are held. No if/elif chains needed.extends CharacterBody2D const SPEED = 180 func _physics_process(delta): # get_vector gives us left/right/up/down as one Vector2 var dir = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down") velocity = dir * SPEED move_and_slide()
get_vector reads the arrow keys, you multiply the direction by your SPEED constant, and move_and_slide actually moves the body.Create a Main scene with a TileMapLayer dungeon floor and solid walls the hero bumps into.
Our hero needs somewhere to explore! We're going to build a dungeon room — a floor made of tiles and thick walls all around the edge. It's like building a room out of blocks. You get to be the dungeon designer!
Main scene: Node2D root. Add a TileMapLayer with a TileSet containing a floor tile (passable) and a wall tile (with Physics Layer collision). Paint a full floor, then a border of wall tiles. Instance hero.tscn in the middle and add a Camera2D to the hero.
Make a Slime enemy that bounces around the dungeon room all by itself.
What's a dungeon without a monster?! We're going to make a little slime that bounces around the room on its own. It doesn't chase you — it just bounces. But if it touches you... look out! 🟢
Slime scene: CharacterBody2D → Sprite2D + CollisionShape2D. Script: a constant velocity (e.g. Vector2(120, 90)). In _physics_process call move_and_slide(), then check collisions and reflect velocity using the collision normal: velocity = velocity.bounce(normal). Add to "enemy" group.
velocity.bounce(normal) is a lovely one-liner that handles wall reflection perfectly. The asymmetric starting velocity (120,90 rather than 100,100) prevents the slime getting stuck bouncing perfectly horizontally — a classic gotcha.extends CharacterBody2D # Start moving at an angle so bouncing looks natural var velocity_dir = Vector2(120, 90) func _physics_process(delta): velocity = velocity_dir move_and_slide() # Bounce off anything we hit for i in get_slide_collision_count(): var col = get_slide_collision(i) velocity_dir = velocity_dir.bounce(col.get_normal())
bounce the velocity off the surface, using the collision's normal (the direction the wall faces).velocity.bounce(normal) do?Show three hearts on screen. When a slime touches the hero, they lose a heart!
In Zelda, the hero has HEARTS for health! We're going to show three hearts at the top of the screen. If a slime touches you, you lose a heart. Lose all three — game over! Guard those hearts! 💚💚💚
Add a CanvasLayer → Label showing ❤️❤️❤️ for the HUD. Track lives = 3 in the hero script. Add an Area2D (HurtBox) child of Hero. On body_entered with "enemy": decrement lives, update the label, add a brief invincibility timer (1s), then reload the scene if lives == 0.
is_hurt + await get_tree().create_timer(1.0).timeout achieves this cleanly.# ADD to hero.tscn script: @export var hearts_label: Label var lives = 3 var is_hurt = false func _on_hurt_box_body_entered(body): if body.is_in_group("enemy") and not is_hurt: is_hurt = true lives -= 1 _update_hearts() if lives <= 0: get_tree().reload_current_scene() # Invincibility frames — 1 second safe time await get_tree().create_timer(1.0).timeout is_hurt = false func _update_hearts(): var h = "" for i in lives: h += "❤️" hearts_label.text = h
Press Space to swing a sword that destroys slimes it touches!
HERE IT IS — THE SWORD! 🗡️ Press the Space bar and your hero swings their sword. If a slime is in the way — BONK! It's gone. This is the moment our game really feels like Zelda. Get ready to fight!
Add a child Area2D (Sword) to the Hero, with a small RectangleShape2D in front of the sprite. Default: monitoring = false. On Space press, enable it for 0.2s then disable. On body_entered, if the body is in "enemy", queue_free() the slime.
$Sword.monitoring = true/false to toggle the hitbox cleanly.# ADD to hero.tscn script: @onready var sword = $Sword var attacking = false func _physics_process(delta): # ... keep your existing movement code ... # Swing the sword on Space! if Input.is_action_just_pressed("ui_accept") and not attacking: _swing_sword() func _swing_sword(): attacking = true sword.monitoring = true # Turn the hitbox ON await get_tree().create_timer(0.2).timeout sword.monitoring = false # Turn it OFF again attacking = false func _on_sword_body_entered(body): if body.is_in_group("enemy"): body.queue_free() # Slime destroyed!
true turns the hitbox on. queue_free removes the slime from the game.Place a chest in the dungeon. When the hero touches it, it opens and gives a key!
Every Zelda game has a treasure chest hiding something important! We're going to make a chest that sits in the dungeon. When you walk up to it, it OPENS and gives you a key. That key opens a special locked door — but first, we need to find the chest! 📦
Chest scene: StaticBody2D → Sprite2D + CollisionShape2D (blocks movement) + Area2D (detect overlap). Two states: closed and open. On Area2D body_entered with "player": if closed, switch to open, set has_key = true on the hero, and emit a key_collected signal.
key_collected) from the Chest to the Door keeps the two objects loosely coupled — a great pattern to introduce even at this level. Connect it in main.tscn via the editor.extends StaticBody2D signal key_collected # Broadcast when opened! var is_open = false func _on_open_zone_body_entered(body): if body.is_in_group("player") and not is_open: is_open = true body.has_key = true # Give the hero the key $Sprite2D.modulate = Color.YELLOW # Glow to show it's open! key_collected.emit() # Tell the door!
if body.is_in_group("player") and not is_open before opening.not is_open part do?key_collected signal used for?Place a locked door that opens only when the hero has collected the key from the chest.
Now we use that key! We're going to put a locked door on one side of the dungeon. It's solid as a wall until you have the key — then it disappears and you can escape! Can you fight through the slimes and find the chest in time? 🔐
Door scene: StaticBody2D → Sprite2D + CollisionShape2D + Area2D (detect hero). On body_entered with "player": check body.has_key — if true, hide the sprite and disable the collision so the hero can walk through.
extends StaticBody2D var is_open = false func _on_detect_zone_body_entered(body): if body.is_in_group("player") and body.has_key and not is_open: is_open = true $Sprite2D.hide() # Door disappears! $CollisionShape2D.disabled = true # Hero can walk through
Place an exit zone — once the door is open, step through it to win!
The door is open — now we need something waiting on the other side! We'll put an invisible exit zone just outside the door. Step through the open door and into it — YOU WIN! You've escaped the dungeon! 🏆✨
Exit scene: Area2D → CollisionShape2D positioned just outside the door gap. On body_entered with "player": show a CanvasLayer "You Escaped! 🎉" win label. Same win-condition pattern used across the whole series.
extends Area2D @export var win_label: Label func _on_body_entered(body): if body.is_in_group("player"): win_label.show()
if body.is_in_group("player")?Place a potion in the dungeon — touching it restores one heart!
What if the slimes get you and you lose a heart? That's what potions are for! We're going to hide a little green bottle somewhere in the dungeon. Find it and touch it — a heart comes back! Healing magic! 🧪💚
Potion scene: Area2D → Sprite2D + CollisionShape2D. On body_entered with "player": if the hero's lives < 3, increment lives, call _update_hearts(), and queue_free() the potion. Place it where the player must navigate around slimes to reach it.
extends Area2D func _on_body_entered(body): if body.is_in_group("player"): # Only heal if not already at full health if body.lives < 3: body.lives += 1 body._update_hearts() queue_free() # Potion is used up!
queue_free removes the used-up potion.if body.lives < 3 before healing?Customise everything — make this YOUR quest!
YOUR DUNGEON! YOUR RULES! 🎉 Add more rooms, more slimes, more potions! Give your hero a name. Make the walls a spooky colour. Design secret passages! You've built a real top-down adventure game. You are officially a video game developer. Go wild! 🗡️⚔️🏰
Great stretch ideas: add facing direction to the sword (flip $Sword.position.x based on last horizontal input); add a second room using change_scene_to_file(); add an AnimationPlayer for a sword swing; add AudioStreamPlayer for sound effects; add a second enemy that follows the player using (player_pos - position).normalized() * speed.
velocity.x and flip $Sword.position.x)A top-down adventure game just like the original Legend of Zelda! Your hero walks in all four directions, fights bouncing slime enemies with a sword, collects a chest, and escapes through a locked door.
Have you ever played Zelda? It's a game where a little hero explores a world full of puzzles and monsters. We're going to make our OWN version with our own hero, our own slimes, and our own dungeon to explore. You'll even make a SWORD to fight with!
Episode 4 of My First Video Game. Biggest new concepts: top-down 4-directional movement (no gravity!), a sword hitbox using a short-lived Area2D, bouncing slime AI using velocity reflection, a chest with an open/close state, and a locked door that opens when a key is collected. All new patterns, no prior episodes required.
🛠️ Free In-House Dev Tools
Use these free browser tools alongside this workshop to create custom sprites, sounds, levels and colour schemes for your game. No installs. Free forever.