⭐ My First Video Game · Series 1 · Episode 7
🏎️ Ep 1 · Zoom Zoom 🍄 Ep 2 · Jump Jump 🔫 Ep 3 · Bang Bang 🧚 Ep 4 · Fairy Survivors 🦍 Ep 5 · Barrel Blast 🗡️ Ep 6 · Pixel Quest 🐻 Ep 7 · Night Watch 🐰 Ep 8 · Night Watch Part 2
🐻 Episode 7 · Intermediate · Godot 4

Night Watch! Build a FNAF-Style Game

Survive the night as the security guard! Watch the cameras, close the doors, and don't let the animatronics sneak into your office. Build cameras, doors, a power meter, animatronic AI, and a terrifying jump scare!

🧒 Ages 8+ ⏱️ 1.5-2.5 Hours 🎮 Godot 4 ✓ Free
📷 Camera Systems 🚪 Signals Power Drain 🤖 AI Movement 😱 Jump Scares Timer Nodes
0 XP
Level 1
🔥0
Your Progress 0 / 12 steps
1
🌟
Welcome, Night Guard!
A big hello before the lights go out...
Active
🎯
Goal for this step

Understand what we're building and get Godot open and ready!

🧒
We're going to build a SPOOKY game! You're a security guard at a pizza restaurant but at night, the animatronic animals (like robot bears and bunnies!) come to life and try to find you. You have to check cameras, close your doors, and SURVIVE until 6am. It's creepy, it's exciting, and YOU are going to build it!
👨‍👧
Parent note: Episode 7 of My First Video Game. This is the most complex episode yet — it introduces node signals, a SubViewport-based camera system, a state machine via Timer nodes, and AnimationPlayer for the jump scare. The spooky theme is age-appropriate for 8+ but as always, parents should judge. Five Nights at Freddy's is a major part of many kids' gaming culture — this is a great way to demystify how it works!
💡
Purple boxes are for grown-ups! The child does the typing — your job is to read the steps aloud and celebrate progress. This episode has more code than earlier ones, but each piece is clearly explained. Take it one step at a time — the payoff (a working jump scare!) is HUGE.
✏️
Fill in the Blanks
+15 XP
In this workshop we're building a game inspired by . The player is a who must survive until 6am.
🧠
Knowledge Check
+15 XP
What engine are we using to build Night Watch?
AUnity
BUnreal Engine
CGodot 4
2
📁
New Project — Night Watch!
Create a fresh spooky project!
Locked
🎯
Goal for this step

Create a new Godot 4 project called "Night Watch" and configure the window.

🧒
Every game starts as a blank project — like an empty notebook waiting for your story! Let's set ours up. We'll use a wider window this time because our security office is wide (you need room for both doors!).
👨‍👧
Parent note: Standard Godot 4 new project. Compatibility renderer. Set the window size to 960x540 in Project Settings → Display → Window (16:9, fits most screens). Also recommend setting Stretch Mode to "canvas_items" and Aspect to "keep" so it scales cleanly.

What to do

  • 1Open Godot 4 and click "New Project".
  • 2Name it Night Watch. Choose a folder on your Desktop.
  • 3Make sure Renderer is set to Compatibility.
  • 4Click "Create & Edit".
  • 5Go to Project → Project Settings → Display → Window. Set Width: 960, Height: 540.
🕹️
Try it! After creating the project, go to Project → Project Settings → Display → Window. Set Width: 960, Height: 540. This gives us a nice wide office!
🔢
Put It In Order
+15 XP
Click these steps in the correct order to set up a new Godot project:
Set Window Width to 960 and Height to 540
Click "New Project" in Godot
Name it "Night Watch" and click Create & Edit
🧠
Knowledge Check
+15 XP
What window size do we set for our Night Watch project?
A1920 x 1080
B960 x 540
C640 x 480
3
🏢
Build the Security Office!
Your home for the whole night...
Locked
🎯
Goal for this step

Create the main scene — a dark security office with a desk, monitor, and two door panels.

🧒
This is your office! It's small, dark, and a bit scary. You have a screen in the middle (that's the camera monitor), and two doors — one on the LEFT and one on the RIGHT. As long as those doors are closed, nothing can get in. As long as you have POWER.
👨‍👧
Parent note: Create main.tscn with a Node2D root named "NightWatch". Build the office using ColorRect nodes: a dark background, a centre monitor area, a left door panel, a right door panel. Use CanvasLayer for UI elements. Keep it simple — solid coloured rectangles work perfectly.

What to do

  • 1Click Scene → New Scene. Root: Node2D. Rename it NightWatch. Save as main.tscn.
  • 2Add a ColorRect child. Set it to cover the full screen (Width: 960, Height: 540). Colour: very dark grey or near-black.
  • 3Add another ColorRect for the monitor in the centre — make it dark green/teal. Roughly 400x260, centred.
  • 4Add a ColorRect on the far left for the left door panel — tall and dark. About 120px wide.
  • 5Add a ColorRect on the far right for the right door panel — matching size.
  • 6Press Ctrl+S then F5 to test!
💡
The "office" in FNAF is entirely 2D UI-style. There is no player movement — the player is static. Everything is about switching what you SEE (cameras) and what you PRESS (doors).
✏️
Fill in the Blanks
+15 XP
The root node type for our main scene is . We save it as .
🧠
Knowledge Check
+15 XP
What node type do we use for the coloured rectangles in our office scene?
AColorRect
BSprite2D
CPanel
4
📷
Build the Camera System!
See the whole building from your desk!
Locked
🎯
Goal for this step

Create multiple camera "views" and let the player switch between them by clicking.

🧒
The coolest part of FNAF is the cameras! You click a button and suddenly you can see a DIFFERENT room — the kitchen, the stage, the hallway. We're going to build that. Each camera is just a picture of a different room, and we swap which one is showing on our monitor.
👨‍👧
Parent note: Camera approach: create a CanvasLayer containing the monitor. Inside it, create multiple "room" panels as ColorRect children — one per camera location (Stage, Hallway Left, Hallway Right, Kitchen). Only one is visible at a time. A row of camera buttons swaps which panel is visible. This is conceptually identical to FNAF's actual camera approach.

What to do

  • 1Inside the monitor ColorRect, add 4 ColorRect children — name them Cam1_Stage, Cam2_HallLeft, Cam3_HallRight, Cam4_Kitchen.
  • 2Give each panel a different colour. Only show Cam1_Stage at start (set the others to Visible: off).
  • 3Add a row of Button nodes below the monitor — one for each camera.
  • 4Attach the script below to your NightWatch root node.
  • 5Connect each button's pressed signal to the _on_cam_button function.
📝 GDScript — Camera Switching
extends Node2D # Drag each camera panel into these in the Inspector @export var cam_panels: Array[ColorRect] = [] var current_cam = 0 func show_cam(index: int): for i in cam_panels.size(): cam_panels[i].visible = (i == index) current_cam = index func _on_cam_button(index: int): show_cam(index)
🕹️
Try it! Click each camera button. Does the picture on the monitor change? You should see a different room name/colour for each camera.
💻
Code Challenge
+20 XP
Fill in the blanks to show and hide camera panels:
func show_cam(index: int): for i in cam_panels.size(): cam_panels[i]. = (i == )
Hint: We set each panel's visibility property, comparing i to the parameter.
🧠
Knowledge Check
+15 XP
How does the camera system work in our FNAF-style game?
AIt moves the player to different rooms
BIt shows and hides different panels on the monitor
CIt uses a real 3D camera to render different angles
5
🚪
The Doors & Lights!
Your only defence against the dark...
Locked
🎯
Goal for this step

Add left and right door buttons — clicking them slams the door shut (and costs power)!

🧒
The doors are your best friends in this game! Click a door button and SLAM — the door closes and nothing can get past it. But here's the catch: keeping a door closed uses up your POWER. So you have to be smart about when you close them!
👨‍👧
Parent note: Add a Button below each door panel. A boolean tracks door state. When the door is closed, draw a thick ColorRect over the doorway. The power drain rate increases while a door is closed. Door state is checked by the animatronic AI later.

What to do

  • 1Add a Button node below your left door panel. Label it "Close Left Door". Do the same on the right.
  • 2Add a dark ColorRect over each doorway (named LeftDoorBlock and RightDoorBlock). Set them Visible: off.
  • 3In your NightWatch script, add the door variables and functions below.
  • 4Connect each door button's pressed signal to the matching function.
  • 5In the Inspector, drag LeftDoorBlock and RightDoorBlock into the @export fields.
📝 GDScript — Door Toggle
var left_door_closed = false var right_door_closed = false @export var left_door_block: ColorRect @export var right_door_block: ColorRect func _on_left_door_pressed(): left_door_closed = not left_door_closed left_door_block.visible = left_door_closed func _on_right_door_pressed(): right_door_closed = not right_door_closed right_door_block.visible = right_door_closed
True or False?
+15 XP
Closing a door uses a boolean variable to track its state.
You connect the door button's "clicked" signal to your function.
The not keyword in GDScript flips a boolean from true to false (or vice versa).
🧠
Knowledge Check
+15 XP
What Godot feature do we use to link a button click to our door function?
ASignals
BGroups
CAnimationPlayer
6
The Power Meter!
When the power runs out... the doors open.
Locked
🎯
Goal for this step

Add a draining power bar — doors drain it faster. When power hits 0, all doors open!

🧒
Here is the scariest part of the whole game — the POWER. You start with 100% power, but it slowly drains away as time passes. And if you keep a door closed, it drains EVEN FASTER. If the power hits zero... all the doors fly open and you're totally defenceless!
👨‍👧
Parent note: Power is a float from 100.0 to 0.0. In _process(delta), drain at a base rate (e.g. 2.0 per second). Each closed door adds +3.0 per second. Display it as a ProgressBar node. When power reaches 0.0, force both doors open.

What to do

  • 1Add a ProgressBar node to your UI — position it at the bottom left.
  • 2Add a Label showing "Power:" and a percentage number.
  • 3Add the power logic to your NightWatch script (see code below).
  • 4Call _update_power(delta) from inside _process(delta).
📝 GDScript — Power System
var power = 100.0 var power_drained = false @export var power_bar: ProgressBar @export var power_label: Label func _process(delta): _update_power(delta) func _update_power(delta): if power_drained: return var drain = 2.0 # base drain per second if left_door_closed: drain += 3.0 if right_door_closed: drain += 3.0 power -= drain * delta power = max(power, 0.0) power_bar.value = power power_label.text = str(int(power)) + "%" if power <= 0.0: _power_out()
💻
Code Challenge
+20 XP
Fill in the blanks for the power drain calculation:
var drain = 2.0 if left_door_closed: drain += power -= drain *
Hint: Each door adds 3.0 to the drain rate, and we multiply by the time step.
🧠
Knowledge Check
+15 XP
What happens when the power reaches 0%?
AThe game automatically wins
BThe cameras stop working but doors stay closed
CBoth doors are forced open and you're defenceless
7
🐻
Create Your Animatronic!
Meet Freddy — he's... friendly. Probably.
Locked
🎯
Goal for this step

Build an animatronic scene with a sprite, a position tracker, and a movement timer.

🧒
Here comes the star of the show — our animatronic robot bear! We'll call him Freddy. Right now he just sits on the stage and looks cute. But in the next step, we'll make him start MOVING towards you. Tick tock, tick tock...
👨‍👧
Parent note: Animatronic scene: Node2D → Sprite2D + Timer node. The animatronic has a "room" variable (integer: 0=Stage, 1=HallLeft, 2=HallRight, 3=LeftDoor, 4=RightDoor). The Timer fires every few seconds. Save as freddy.tscn and instance it in main.tscn.

What to do

  • 1Click Scene → New Scene. Root: Node2D. Rename it Freddy.
  • 2Add a Sprite2D child. Make it big and bearlike.
  • 3Add a Timer child. Set Wait Time: 5.0 and tick Autostart: on.
  • 4Attach the script below. Save as freddy.tscn.
  • 5In main.tscn, drag freddy.tscn into the scene tree.
📝 GDScript — Freddy Setup
extends Node2D # 0 = Stage, 1 = Hall Left, 2 = Hall Right, 3 = Left Door, 4 = Right Door var room = 0 signal reached_door(side) @export var move_timer: Timer @export var markers: Array[ColorRect] = [] func _ready(): move_timer.timeout.connect(_on_move_timer) func _on_move_timer(): _move_closer() func _move_closer(): pass # We'll fill this in next step! func _update_display(): for i in markers.size(): markers[i].visible = (i == room)
✏️
Fill in the Blanks
+15 XP
The animatronic starts in room which represents the . We use a node to control when it moves.
🧠
Knowledge Check
+15 XP
What does the signal reached_door(side) line do?
AIt makes Freddy visible on the camera
BIt moves Freddy to the next room
CIt defines a custom event that Freddy can emit when reaching a door
8
👣
Animatronic Movement AI!
He's getting closer... slowly...
Locked
🎯
Goal for this step

Make Freddy move through rooms on a timer, appearing on the correct camera panel each time.

🧒
Freddy is going to creep towards you one room at a time! He moves on a timer — every few seconds he shuffles one step closer to your office. Check the cameras and you might see him moving around! When he reaches your DOOR, that's when you need to slam it shut FAST.
👨‍👧
Parent note: Movement path: Stage (0) → Hall Left (1) → Left Door (3) OR Hall Right (2) → Right Door (4). Randomise which side. When room == 3 or 4, emit reached_door. In main.tscn, if the corresponding door is closed, reset Freddy; if not, trigger jump scare.

What to do

  • 1Add a ColorRect marker for Freddy inside each camera panel — link them via @export.
  • 2Fill in the _move_closer() function in Freddy's script.
  • 3Update _update_display() to show Freddy only on the correct camera.
  • 4In main.tscn, connect Freddy's reached_door signal to _on_freddy_at_door.
📝 GDScript — AI Movement
func _move_closer(): match room: 0: # Stage pick a side room = [1, 2].pick_random() 1: # Hall Left → Left Door room = 3 2: # Hall Right → Right Door room = 4 3, 4: # At a door — emit signal reached_door.emit("left" if room == 3 else "right") return _update_display()
🔢
Put the Movement Path In Order
+15 XP
Click Freddy's movement path in the correct order (left side):
Hall Left (room 1)
Left Door (room 3) — emit signal
Stage (room 0)
🧠
Knowledge Check
+15 XP
What happens when Freddy reaches a door and it's closed?
AHe breaks through the door
BHe gets sent back to the Stage (room 0)
CThe game ends immediately
9
😱
The Jump Scare!
...BOO!
Locked
🎯
Goal for this step

When the animatronic reaches an open door, trigger a full-screen jump scare and game over!

🧒
This is the most exciting step — the JUMP SCARE! If Freddy reaches your door and it's open, he JUMPS at the screen! A huge spooky face fills everything, there's a loud noise, and it's game over. It sounds scary to build but actually it's just showing a big coloured rectangle really fast!
👨‍👧
Parent note: Jump scare: a CanvasLayer with a high layer number (e.g. 10) containing a full-screen ColorRect and a large Label. Hidden normally. On trigger: set visible = true, play audio, wait 1.5 seconds, then show Game Over screen.

What to do

  • 1Add a CanvasLayer to main.tscn. Set Layer to 10. Name it JumpScare.
  • 2Inside it, add a full-screen ColorRect (red/dark) and a huge Label saying "BOO!". Set the CanvasLayer Visible: off.
  • 3Optionally add an AudioStreamPlayer for a scary sound.
  • 4Add the _trigger_jumpscare() function to your main.gd script.
📝 GDScript — Jump Scare
@export var jumpscare_layer: CanvasLayer @export var scare_sound: AudioStreamPlayer func _trigger_jumpscare(): jumpscare_layer.visible = true if scare_sound: scare_sound.play() await get_tree().create_timer(1.8).timeout _game_over()
True or False?
+15 XP
CanvasLayer lets us draw UI elements on top of everything else.
The jump scare in FNAF lasts about 30 seconds.
The await keyword pauses the function until the timer finishes.
🧠
Knowledge Check
+15 XP
Why do we set the CanvasLayer's Layer property to 10?
ASo it renders on top of all other UI elements
BSo it runs 10 times faster
CSo it only shows on camera 10
10
🌅
Win & Lose Conditions!
Survive until 6am or face the consequences!
Locked
🎯
Goal for this step

Add a night timer counting from 12am to 6am. Survive all the way and you win!

🧒
Here's how you WIN: survive the whole night! The clock starts at midnight (12am) and slowly ticks towards 6am. If you make it to 6am without getting caught, you did it — you survived the night! Each hour takes about a minute of real time, so 6 hours = about 6 minutes.
👨‍👧
Parent note: Night timer: a float from 0.0 to 1.0 mapping to 12am-6am. Increment in _process by delta / (6 * 60) to make 6 minutes = a full night. Display as "12 AM", "1 AM" etc. At night_time >= 1.0, call _win().

What to do

  • 1Add a Label for the clock — top centre. Name it ClockLabel.
  • 2Create WinScreen and GameOverScreen CanvasLayers (both hidden). Each has a message and "Play Again" button.
  • 3Add the night timer logic and win/lose functions to main.gd.
  • 4Connect "Play Again" buttons to get_tree().reload_current_scene().
📝 GDScript — Night Timer
var night_time = 0.0 var game_active = true const NIGHT_DURATION = 360 # 6 minutes @export var clock_label: Label @export var win_screen: CanvasLayer @export var game_over_screen: CanvasLayer func _update_clock(delta): night_time += delta / NIGHT_DURATION var hour = int(night_time * 6) var display = ["12 AM","1 AM","2 AM","3 AM","4 AM","5 AM","6 AM"] clock_label.text = display[min(hour, 6)] if night_time >= 1.0: _win()
🔮
Predict What Happens
+15 XP
The player has 5% power left and the clock shows 5 AM. Both doors are closed. What will happen first?
AThe player wins because 6 AM arrives first
BThe power runs out and the doors fly open
CFreddy breaks through the closed door
🧠
Knowledge Check
+15 XP
How long does one in-game night last in real time?
A60 seconds
B30 minutes
CAbout 6 minutes
11
🎚️
Difficulty & Night 1 Tuning!
Just hard enough to be scary, just easy enough to survive!
Locked
🎯
Goal for this step

Tune Freddy's speed, power drain, and movement chance so Night 1 feels fair but tense.

🧒
Right now the game might be too easy or too hard. Let's tune it — that's what real game designers do! We'll slow Freddy down at the start and speed him up over time, and make sure the power lasts just long enough.
👨‍👧
Parent note: The real FNAF uses an "AI level" system from 0-20 per animatronic, checked each movement tick. An AI of 0 never moves, AI of 20 always moves. This simple random check creates emergent behaviour that feels intelligent. Start ai_level at 3 for Night 1.

What to do

  • 1In freddy.gd, add @export var ai_level: int = 3 and wrap movement in a random check.
  • 2Set Freddy's Timer Wait Time to 7.0 in the Inspector.
  • 3Adjust power drain — try base drain of 1.5 and +2.5 per door.
  • 4Play test! Tweak until Night 1 is winnable but tense.
📝 GDScript — AI Level
@export var ai_level: int = 3 func _on_move_timer(): # Only move if the random roll beats the AI level if randi() % 20 < ai_level: _move_closer()
💻
Code Challenge
+20 XP
Fill in the AI level check — Freddy only moves if the random number is less than his level:
if randi() % < : _move_closer()
Hint: The max AI level is 20, and we compare against the ai_level variable.
🧠
Knowledge Check
+15 XP
If Freddy's AI level is 3, what are the chances he moves each tick?
A100% (always moves)
B15% (3 out of 20)
C50% (coin flip)
12
Polish & Celebrate!
You built a horror game. Legend!
Locked
🎯
Goal for this step

Add final touches — atmosphere, sound, and your own creative spin!

🧒
Your game WORKS! Now let's make it feel really spooky and special. We can add flickering lights, eerie sound effects, a better looking office, and anything else YOU want. This is the fun part — make it your own!
👨‍👧
Parent note: Remind the child: they just built one of the most mechanically complex games in the whole series. Camera systems, AI, signals, power management, jump scares — that's real game development. They should be incredibly proud!

Polish ideas

  • 1Flickering lights: add a Timer (0.15s) that randomly toggles a dark ColorRect.
  • 2Camera static: briefly show a white ColorRect when switching cameras.
  • 3Ambient sound: add an AudioStreamPlayer with a looping dark tone.
  • 4Second animatronic: duplicate Freddy, give it a different movement path!
  • 5Celebrate! Save everything, press F5, and survive the night!
📝 GDScript — Flickering Lights
func _on_flicker_timer_timeout(): if randf() < 0.05: # 5% chance to flicker light_flicker.visible = true await get_tree().create_timer(0.08).timeout light_flicker.visible = false
✏️
Fill in the Blanks
+15 XP
To add a flickering light, we use a node and the function to create random chance.
🧠
Final Knowledge Check
+15 XP
Which Godot concepts did you learn in this workshop? (Pick the best answer)
AOnly how to make coloured rectangles
BOnly how to use Timer nodes
CSignals, CanvasLayer, Timers, AI movement, @export, and await
🎉🐻😱🎊👻
You Built a Horror Game!

Incredible — you've made a working FNAF-style game with cameras, doors, power, AI movement, and a jump scare. That's serious game development!

0
Total XP
1
Level
0
Best Streak
0%
Accuracy
🐰 Episode 8: Night Watch Part 2 → ⭐ View My Progress & Certificates

This workshop was free and took many hours to build. If it helped you learn something new, consider supporting the project.

☕ Support on Ko-fi
⭐ My First Video Game · Series 1
You survived the night! 🎉
You built a HORROR game that is seriously impressive. Go back and play the earlier episodes if you want more!
← Ep 6 · Pixel Quest Ep 8 · Night Watch Part 2 →

🛠️ Free In-House Dev Tools

Make It Yours

Use these free browser tools alongside this workshop to create custom sprites, sounds, levels and colour schemes for your game. No installs. Free forever.

🎨
Pixel Studio
Draw sprites & animations
🗺️
Level Designer
Build 2D tile maps
🎵
SFX Studio
Create custom sound effects
🎨
Colour Palette
Build a game-ready colour scheme
🎲
Game Idea Gen
Random game concepts & prompts
🛠️ See All 20 Free Tools →