Basic · Build main.tscn
30 minYou are building: main.tscn, the scene that runs from launch to quit. Menus will show and hide inside it. The map will be added inside it. It never unloads.
The shape you are aiming for
Main (Node) runs main.gd — screen flow + spawning
├── World (Node3D) everything that exists in 3D
│ ├── MapRoot (Node3D) the map scene gets added here
│ ├── Players (Node3D) spawned player bodies land here
│ └── PlayerSpawner (MultiplayerSpawner) spawn_path = ../Players
└── UI (CanvasLayer) draws on top of the 3D view
├── MainMenu instance of main_menu.tscn
├── CharacterSelect instance of character_select.tscn
└── HUD instance of hud.tscnThe three UI scenes do not exist yet. You will build them in lessons 13 to 15 and drag them in then.
1 · The nodes
- Scene → New Scene, then Other Node →
Node. Rename it Main. - Add a child
Node3Dnamed World. - Inside World, add two more
Node3Ds: MapRoot and Players. - Still inside World, add a
MultiplayerSpawnernamed PlayerSpawner. - Add a
CanvasLayernamed UI as a sibling of World — a child of Main, not of World. - Save as
res://scenes/main.tscn.
2 · The one property that matters
Select PlayerSpawner. In the Inspector, set Spawn Path to ../Players.
Leave the Auto Spawn List empty
That array is for the simple case where the spawner makes a scene with no arguments. Ours cannot use it: each player needs a name, a character, an authority and a position. In lesson 18 you will set spawn_function in code instead.
3 · Make it the main scene
- Project → Project Settings → Application → Run.
- Set Main Scene to
res://scenes/main.tscn. - Press F5. You get an empty grey window. That is correct — there is nothing in the world yet.
4 · Why each node is there
- MapRoot and Players are separate so leaving a world can free the map and the players independently — and so a future map change never disturbs the spawn path.
- PlayerSpawner creates a node locally and tells every connected machine to create it too. Someone who joins late gets every player already there, without you writing catch-up code.
- UI is a CanvasLayer, not a plain Control. A CanvasLayer draws independently of the 3D camera, so the HUD stays put wherever the player looks.
5 · The reason it never unloads
Godot replicates by node path. When the host says “spawn a player at World/Players”, that path has to exist on every machine at that moment.
If you called change_scene_to_file() the old tree would be destroyed and rebuilt. For a few frames the path would be missing, and arriving players would have nowhere to land. So: nothing in this project ever changes scene.
Done when: F5 opens a grey window with no errors, the Scene dock shows all six nodes in the right nesting, and PlayerSpawner's Spawn Path reads ../Players.
Challenge 1 · Prove the path 🟢 Easy
8 minAttach a script to Main and save it as res://scripts/main.gd. Write:
extends Node
@onready var players_root: Node3D = $World/Players
@onready var spawner: MultiplayerSpawner = $World/PlayerSpawner
func _ready() -> void:
print("players path: ", players_root.get_path())
print("spawn path: ", spawner.spawn_path)Run it. Both lines should describe the same node, written two different ways.
- Copy both printed lines into your notes.
- Rename Players to Bodies and run again. Write down what breaks, then undo it.
Done when: you have both lines, and you can say in one sentence what the rename broke.
Challenge 2 · Something to look at 🟡 Medium
12 minA grey window is hard to test against. Put a temporary scene in MapRoot so you can see the world exists.
- New scene, root type
Node3D, saved asres://scenes/maps/arena.tscn. - Add a
WorldEnvironmentand aDirectionalLight3D. Without both, the map renders black. - On the WorldEnvironment, create a new Environment and set its Background mode to Sky, with a new ProceduralSkyMaterial.
- Add a
CSGBox3Dnamed Ground. Size it 40 × 1 × 40 and tick Use Collision. - Back in
main.tscn, dragarena.tscninto MapRoot. - Add a
Camera3Dunder World, raised and tilted down, so F5 shows the ground.
The camera is temporary
In lesson 9 every player brings their own camera. Delete this one then, or you will have two cameras arguing about the view.
Done when: F5 shows a lit ground plane under a sky, with no errors in the Output panel.
Challenge 3 · Show and hide, do not change 🟠 Harder
14 minScreens in this game are shown and hidden, never loaded and unloaded. Prove to yourself that this works.
- Under UI, add three
ColorRectnodes named MainMenu, CharacterSelect and HUD. Give each a different colour and set Layout to Full Rect. - In
main.gd, add:
@onready var main_menu: Control = $UI/MainMenu
@onready var character_select: Control = $UI/CharacterSelect
@onready var hud: Control = $UI/HUD
func _show_only(screen: Control) -> void:
for s in [main_menu, character_select, hud]:
s.visible = s == screen
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("ui_accept"):
# Cycle the three screens so you can see it working.
if main_menu.visible:
_show_only(character_select)
elif character_select.visible:
_show_only(hud)
else:
_show_only(main_menu)- Call
_show_only(main_menu)at the end of_ready(). - Run, and press Enter three times. The colour changes and the 3D world behind never reloads.
Then answer in one or two sentences: what would a joining player see if each screen were a separate scene loaded with change_scene_to_file(), while spawns were arriving?
Done when: Enter cycles all three screens, the world stays put, and your answer names the missing node path.
Extra · Explore the spawner 🔴 Open-ended
no limit- Open the Godot docs page for MultiplayerSpawner. Write down what
spawn_functiondoes, in your own words, before you meet it in lesson 18. - Make your arena less plain: add a few CSG boxes as walls and pillars. Tick Use Collision on each, and keep them all on collision layer 1.
- Draw your scene tree on paper and add a note beside each node saying what would break without it.
- Find one game you play that clearly loads a new scene between menu and level. What does the player see while it happens?