NPC Architecture

One guest scene, GuestData, stations, stands, navmesh steering, schedules.

stablegodotnpcarchitecture
On this page
Reusable guest scene with name label from GuestData
One guest.tscn: name label and portrait path come from GuestData, not a per-NPC script.

Goal

One reusable scenes/guest.tscn + guest.gd. Identity, art, dialog, and route come from data — not per-NPC scripts.

Separation

ConcernWhere
Who / say / lookGuestData (+ JSON)
Where / stateGuest instance vars
Where stations areStation nodes in hotel.tscn
Spawning / standsGuestManager autoload
Day / hour / possession flagGameState

Possession is a bool (is_possessed), not a movement state. Orthogonal on purpose.

Swapping art per character

$AnimatedSprite2D.sprite_frames = guest_data.sprite_frames
$AnimatedSprite2D.scale = Vector2.ONE * guest_data.sprite_scale
$InteractionHighlight/Name.text = guest_data.guest_name

Stations

Station is a Node2D with enum Id and self-registers in _ready:

GuestManager.register_station(self)

JSON schedules name stations by enum string ("FRONT_DESK"), never by NodePath.

Watch-out: Renaming / inserting enum values shifts integer ids stored in .tscn. Keep scene id = N in sync or registration silently overwrites (dictionary keyed by id).

Stand slots

Multiple guests at one station get horizontal offsets (STAND_SPACING = 60). reserve_stand / release_stand prevent stacking on one pixel.

Movement

$NavigationAgent2D.target_position = _stand_position
# in _physics_process:
velocity = _steer_direction() * speed
move_and_slide()

Steering uses get_next_path_position(), falling back to a straight line if the agent is not ready.

While walking, guests clear their collision layer so other walkers pass through, but still mask the guest layer to queue behind people already standing.

Avoid per-NPC scenes

Do: add resources/guests/name.json + roster entry + art paths.

Don’t: duplicate guest.gd for each character.

See also Data-Driven Content and State Machines.