4.6 KiB
Overworld System
Scene structure
OverworldScene (Node3D) ← always resident, managed by SCENE singleton
└─ Map (Node3D) ← emptied and repopulated on each map change
TestMap (Node3D) ← typical map root, extends Node3D
├─ Player (Entity) ← movementType = PLAYER, entityId = "player"
├─ NPC/object (Entity) ... ← interactType drives what happens on interact
├─ TestMapBase (StaticBody3D) ← reusable terrain plane (200×200, collision layer 1)
└─ Camera3D (OverworldCamera) ← targetNode → Player
Map transitions
Use OVERWORLD.mapChange(path, destinationNodeName) to switch maps.
OVERWORLD.mapChange("res://overworld/map/SomeMap.tscn", "SpawnPoint")
Flow: fade-out begins → map loads on a background thread → when both complete, OVERWORLD.mapChanged fires → OverworldScene clears Map children and instances the new map → fade-in begins. The destinationNodeName is passed with the signal for the new map to use as a spawn point (not yet wired to player placement — see stubs).
Entity
All overworld objects (player, NPCs, items, triggers) are instances of entity/Entity.tscn with different export values.
| Export | Purpose |
|---|---|
entityId:String |
UUID; use the Inspector button to regenerate |
movementType:MovementType |
NONE (static), DISABLED, or PLAYER (input-driven) |
interactType:InteractType |
What happens when the player presses Interact nearby |
dialogueResource:DialogueResource |
.dialogue file — required for CONVERSATION |
dialogueTitle:String |
Dialogue section to start from (default "start") |
oneTimeItem:ItemResource |
Item granted on interact — required for ONE_TIME_ITEM |
cutscene:CutsceneResource |
Cutscene to run — required for CUTSCENE |
Interaction types
InteractType |
Behaviour |
|---|---|
NONE |
Not interactable |
CONVERSATION |
Runs dialogueResource from dialogueTitle via DialogueAction |
ONE_TIME_ITEM |
Grants oneTimeItem, then frees the entity |
CUTSCENE |
Queues and starts cutscene |
BATTLE_TEST |
Starts a test battle (hardcoded enemy, for dev use) |
To add a new interaction type: add a value to Entity.InteractType, then add the matching match branch in EntityInteractableArea.onInteract() (entity/EntityInteractableArea.gd).
Collision layers
| Area | Layer | Mask | Purpose |
|---|---|---|---|
EntityInteractingArea |
0 | 2 | Player's reach — detects nearby interactables |
EntityInteractableArea |
2 | 0 | Entity's surface — detected by other reaches |
The asymmetric setup means entities never trigger themselves.
Movement
EntityMovement (a child Node under Components) handles all physics each frame:
- Apply gravity if airborne
- Apply friction (
velocity.x/z *= delta * FRICTION) - If
_canMove()andmovementType == PLAYER: read input, compute camera-relative direction, set velocity move_and_slide()
Movement is blocked (_canMove() → false) when UI.dialogueActive, UI.TEXTBOX is open, or UI.GAME_MENU.isOpen().
Camera-relative direction is derived from the active Camera3D's basis — the camera's Y-zeroed and renormalized X/Z axes map input axes to world axes. The entity faces (look_at) the movement direction each frame.
Camera
OverworldCamera orbits around targetNode using yaw/pitch angles driven by camera_orbit_* inputs.
| Export | Default | Purpose |
|---|---|---|
targetNode:Node3D |
— | Node to orbit (assign Player in scene) |
pivotOffset:Vector3 |
(0, 1.2, 0) |
Orbit point above entity origin |
distance:float |
10.0 |
Orbit radius |
pitchMin/Max:float |
-10° / 70° |
Vertical clamp |
orbitSensitivity:float |
120.0 |
Degrees/sec at full input |
collisionMask:int |
— | Layers the camera avoids (terrain = layer 1) |
If the ray from pivot to desired camera position hits collisionMask, the camera is pulled in to just in front of the hit point (with COLLISION_MARGIN = 0.3), clamped to minDistance.
Adding a new map
- Create a new scene (
Node3Droot) inoverworld/map/ - Add
Entityinstances, setinteractTypeand relevant exports in the Inspector - Instance
TestMapBase(or your own terrain) as a child - Add a
Camera3DwithOverworldCamerascript; settargetNodeto the Player entity - Add a
Playerentity withmovementType = PLAYERandentityId = "player" - Switch to it with
OVERWORLD.mapChange("res://overworld/map/YourMap.tscn", "")