Godot Basics We Used

Scene tree, CharacterBody2D, signals, exports, and other project-relevant fundamentals.

stablegodotgdscriptarchitecture
On this page
Clerk behind the front desk with clock HUD at top
Live scene: CharacterBody2D clerk, room art as Sprite2Ds, and a CanvasLayer clock HUD that does not move with the camera.

Grounded in what Night Audit actually touches — not a Godot tour.

Scene / node model

Everything is a tree of nodes. Gameplay lives in scenes (.tscn) that instance other scenes.

RoleNode type we used
Player / NPC bodyCharacterBody2D
Sprite animationAnimatedSprite2D
Physics shapeCollisionShape2D
Interact radiusArea2D
Screen-space UICanvasLayer + Control / PanelContainer
Following cameraCamera2D (child of the clerk)
PathfindingNavigationAgent2D + NavigationRegion2D

Implemented: scenes/game.tscn instances hotel.tscn, clerk.tscn, dialog, clock, midnight report, accusation.

$NodeName and node paths

$AnimatedSprite2D is shorthand for get_node("AnimatedSprite2D") relative to the script’s node.

$AnimatedSprite2D.play("walk_down")
$NavigationAgent2D.target_position = _stand_position

Nested paths appear in dialog UI via @onready:

@onready var _portrait: TextureRect = $MarginContainer/HBoxContainer/Portrait

If you rename or reparent a node, that path becomes a null instance. See Scene Tree and Node Paths.

Lifecycle callbacks

CallbackUsed for
_ready()Wire signals, apply guest data, bake walls/navmesh
_processPlayer input + move_and_slide (clerk)
_physics_processGuest steering on the navmesh
_unhandled_inputInteract (E), dialog advance, time-scale keys
_exit_treeRelease stand slots when a guest is freed

Prefer _physics_process for anything that collides; we kept player input in _process and it works because move_and_slide is still called every frame.

@export and inspector wiring

@export var guest_data: GuestData
@export_range(0, 1000) var speed := 120
@export var id: Id  # Station enum on a Node2D

Exports let stations pick an id in the editor and let guest speed be tuned without code edits.

Signals

We use signals for loose coupling:

  • dialog_closed — guest resumes after talk
  • arrived_at_station / checked_in — manager / clock
  • hour_changed / midnight_reached — HUD and day loop
  • Area2D.body_entered / body_exited — interaction range
dialog_box.dialog_closed.connect(_on_station_dialog_closed, CONNECT_ONE_SHOT)

CONNECT_ONE_SHOT avoids leaking connections across conversations.

Scene instancing

Guests are never hand-placed for the full roster. GuestManager loads JSON and instances one packed scene:

var packed: PackedScene = load("res://scenes/guest.tscn")
var guest: Node = packed.instantiate()
guest.guest_data = data
_parent.add_child(guest)

Autoloads

Registered in project.godot:

  • GameState — day, hour, possession flags, clock
  • GuestManager — stations, stands, spawn queue
  • Music

Gotcha: --script / --check-only do not load autoloads. See Debugging Godot.

Groups

  • "player" — clerk body; guests ignore collisions with it
  • "dialog_box" — find the UI without a hard scene path
  • "room" — camera collects Room areas
var dialog_box = get_tree().get_first_node_in_group("dialog_box")