6.4 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. It operates in two modes: CameraMode.FREE (stays where placed) and CameraMode.CENTERED (eases to orbit behind the player using a velocity-based approach).
| 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 |
7.0 |
Orbit radius |
minDistance:float |
2.5 |
Minimum camera distance (collision won't push closer) |
pitchMin/Max:float |
-80° / 70° |
Vertical clamp |
orbitSensitivity:float |
144.0 |
Degrees/sec at full controller input |
orbitAcceleration:float |
600.0 |
Controller ramp speed (deg/s²) |
orbitFriction:float |
10.0 |
Controller/centering velocity decay rate |
mouseSensitivity:float |
0.3 |
Right-click drag sensitivity |
centeredDelay:float |
1.0 |
Seconds of walking + no camera input before switching to CENTERED |
centeredFollowRate:float |
0.75 |
Base centering rate; scales up with angular distance from target |
centeredMaxFollowRate:float |
3.0 |
Cap on the dynamic follow rate |
centeredAcceleration:float |
180.0 |
How fast _centerVelocity ramps up toward target (deg/s²) |
centeredMaxYawDiff:float |
120.0 |
If yaw offset exceeds this, centering is suppressed (e.g. player walking toward camera) |
centeredPitch:float |
30.0 |
Target pitch in CENTERED mode |
collisionMask:int |
1 |
Physics layers the camera avoids; entities are on layer 2 and excluded by default |
Mode transitions:
- FREE → CENTERED: after
centeredDelayseconds of player movement with no camera input, or immediately viacenter_camera(G / LB) - CENTERED → FREE: any manual camera input (controller stick or right-click drag)
CENTERED mode behaviour:
- "Behind" direction is derived from the player's velocity vector (not facing direction), so centering tracks actual movement and eases out naturally as the player decelerates
- Yaw uses a velocity model:
_centerVelocity(deg/s) accelerates toward the target rate viacenteredAccelerationand friction-decays viaorbitFrictionwhen the player stops — no sudden snap - Pitch recenters at 3× the yaw rate via direct lerp
- If
abs(yawDiff) > centeredMaxYawDiff, centering is suppressed and_centerVelocitydecays instead
Collision resolution uses a sphere-plane circle intersection so the camera satisfies both minDistance and terrain clearance simultaneously.
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", "")