Generated reference. This page mirrors
Code/Movement/README.md, this module's source-of-truth documentation (Standards/02_DOCUMENTATION_STANDARDS.md §1). Edit it there, not here -- this file is regenerated on every docs build and any local edit is silently overwritten.
Movement¶
Purpose. The Character↔pawn Transform binding named as a gap since Milestone 6 and decided in ADR-0007. Owns the physical player presence itself — the codebase's first — and the host-only lookup everything else (real distance checks, admin teleport/spectate, the player-list panel) is built on. See Documentation/Architecture/22_PLAYER_PRESENCE_AND_ADMIN_TOOLS.md §1 and ROADMAP.md Milestone 11.
Written before implementation, per Documentation/Standards/02_DOCUMENTATION_STANDARDS.md §1 — nothing under this folder exists yet.
Responsibilities.
- PawnComponent — attached alongside S&box's own stock movement/character-controller
component on the GameObject a Character's connection spawns. Registers itself against
IPawnLocator on OnAwake/OnDestroy. Since the ADR-0007 Addendum (2026-07-31), also
polls its sibling CharacterNetworkComponent every OnUpdate and re-registers under the
real CharacterId once one is selected/created — closes the gap where CharacterId never
left the prefab's Guid.Empty default, which had made every IPawnLocator.Find/PositionOf
lookup miss. Carries IsNoclipping
([Sync(SyncFlags.FromHost)], per ADR-0005 rule 1) and the plain Teleport(Vector3)/
SetNoclip(bool) methods admin commands call into — not its own [Rpc.Host] surface; see
22_PLAYER_PRESENCE_AND_ADMIN_TOOLS.md §3 for why. Since Milestone 12
(ADR-0012) also carries
IsInVehicle (same [Sync(FromHost)] pattern as IsNoclipping, deliberately) and the plain
EnterVehicle(GameObject, Vector3)/ExitVehicle(Vector3) methods Applejack.Vehicles'
VehicleComponent calls into — this module's first non-admin, non-teleport real consumer,
the exact trigger ADR-0007 named for revisiting this design. PawnComponent's own identity
never changes for this: only its GameObject's parent does, via the confirmed
GameObject.SetParent API (see this file's Notes below) — there is still no "target
GameObject" to rebind, deliberately, per ADR-0007's original reasoning.
- IPawnLocator — host-only, in-memory CharacterId → PawnComponent lookup. Not persisted;
position is ephemeral.
- The reference player prefab and one spawn point — a capsule plus the stock movement
component plus PawnComponent, spawned when a connection becomes active.
Assets/Prefabs/PlayerPawn.prefab and Assets/scenes/main.scene's spawn point marker are a
hand-authored, unverified stand-in for this — see
Assets/Prefabs/README.md's FORMAT UNCERTAINTY section.
PlayerSpawnerComponent is the spawn-on-connect wiring itself (Component.INetworkListener
.OnActive, cloning PlayerPawnPrefab at the spawn_point-tagged GameObject and calling
NetworkSpawn) — spawns a pawn with no character selected yet (PawnComponent.CharacterId
stays Guid.Empty until the sibling CharacterNetworkComponent reports a real one, per the
ADR-0007 Addendum above); there is still no automatic character-creation-on-connect flow, and
the character-select UI that would let a player trigger RequestCreateCharacter/
RequestSelectCharacter is not yet scene-wired (Code/Characters/Ui/
CharacterSelectRootComponent.cs — see Code/Ui/README.md's own open scene-wiring gap). And
it requires a GameObject in the scene with the component attached and PlayerPawnPrefab set
in the Inspector, since a prefab-asset reference isn't a format worth hand-authoring blind.
- PlayerMovementComponent — drives the sibling CharacterController from local input
(Input.AnalogMove/AnalogLook), and this GameObject's own body yaw / the Camera child's
pitch from mouse look. Attached alongside CharacterController/PawnComponent on
Assets/Prefabs/PlayerPawn.prefab. Suspends movement (zeroes CharacterController.Velocity)
while PawnComponent.IsNoclipping/IsInVehicle is true. Gated by GameObject.IsProxy --
this codebase's first ownership/authority check -- so only the owning client reads its own
input; see this component's own header comment for the full engine citations. Out of scope
deliberately: jumping, sprinting, crouching, noclip's own actual collision behaviour. Since
ADR-0017
(2026-07-31), also looks up a sibling Sandbox.Citizen.CitizenAnimationHelper (nullable --
presentation, not part of this component's movement contract) and feeds it
WithWishVelocity/WithVelocity/IsGrounded every fixed tick, so the pawn's new
models/citizen/citizen.vmdl body actually animates instead of standing in a T-pose.
- MovementModule — registers IPawnLocator.
Not responsible for.
- Movement itself, or its replication. Whatever S&box's stock controller already networks
on the pawn GameObject's Transform is read, not reinvented — see ADR-0007's Alternatives
considered.
- Admin commands that act on a pawn (/goto, /bring, /tp, /noclip, /spectate) —
those are Applejack.Admin's job, calling into this module's public API, the same
direction Admin already depends on Characters rather than the reverse.
- A real distance-check abstraction. IPawnLocator.PositionOf is the primitive; each
consumer (DoorComponent, future admin commands) computes its own range check inline,
reviewed by reading — see ADR-0007's Decision section for why this isn't lifted into a
shared engine-agnostic rule type.
- A textured 2D minimap. No per-map coordinate-calibration convention exists anywhere in
this codebase; the admin player-list panel (Applejack.Admin) is a sortable list, not a map,
for this pass.
Dependencies. Core (IServiceRegistry, IServiceResolver), Characters (CharacterId, and,
since the ADR-0007 Addendum, CharacterNetworkComponent itself via sibling lookup — the same
GameObject.Components.Get<T>() direction Applejack.Interaction/Applejack.Economy already
use), the S&box engine (Component, GameObject, Sync, whatever the stock movement/controller
component turns out to be — unverified, see Notes).
Public API.
- IPawnLocator — Register, Unregister, Find(Guid), PositionOf(Guid), All.
- PawnComponent — CharacterId, IsNoclipping, Teleport(Vector3), SetNoclip(bool),
IsInVehicle, EnterVehicle(GameObject, Vector3), ExitVehicle(Vector3).
- PlayerMovementComponent — WalkSpeed, GroundFriction (both [Property], Inspector-
tunable).
- Player body — Sandbox.SkinnedModelRenderer (models/citizen/citizen.vmdl),
Sandbox.Citizen.CitizenAnimationHelper, Sandbox.Dresser (Source = OwnerConnection),
all siblings on PlayerPawn.prefab's root -- see
ADR-0017.
- PlayerSpawnerComponent — PlayerPawnPrefab (Inspector-set); spawns it on OnActive.
Events published. None.
Persistence. None — position is ephemeral, per ADR-0007.
Future improvements.
- A real textured 2D minimap, once a per-map coordinate-calibration convention exists.
- Surfacing a pawn's history (a path over time), not just its current position, if a second
consumer ever needs one — IPawnLocator returning only the latest position is deliberate for
this pass, not a ceiling; see ADR-0007's Notes.
- Cleaning up a PawnComponent that never calls OnDestroy cleanly (a crash, not a graceful
disconnect) — a real risk IPawnLocator's in-memory design doesn't solve yet; see ADR-0007's
Consequences.
Not unit-tested. PawnComponent and IPawnLocator's concrete implementation — thin,
engine-facing, no independent business rule to test, the same treatment Code/Admin/Ui/ and
AdminKickModule.cs already get. Reviewed by reading.
Notes¶
Unverified engine assumptions this module depends on — named, not faked, same practice as
Documentation/ROADMAP.md's Verification debt section: S&box's stock movement/character-
controller component's exact type and its noclip-equivalent API; whether a NetworkSpawn()'d
GameObject's Transform replicates automatically with no custom [Sync] mirror needed. See
Documentation/Architecture/22_PLAYER_PRESENCE_AND_ADMIN_TOOLS.md §5
for the full list.
Resolved during Milestone 12 (EnterVehicle/ExitVehicle, ADR-0012): checked directly
against ~/Projects/sbox-public rather than left as an editor-only question.
GameObject.SetParent(GameObject, bool keepWorldPosition = true) exists, accepts null to
re-parent to the scene root, and replicates automatically to every observing client for a
NetworkSpawn()'d GameObject via an [Rpc.Broadcast] internal to the engine — no custom
[Sync] mirror needed, resolving the general "does reparenting replicate" question this
section's first bullet already asked in the Teleport/position-only case.
GameObject.LocalPosition/LocalRotation and Transform.PointToWorld are real too. What
remains unconfirmed is runtime behaviour with a second connected client actually watching
(interpolation smoothness, client-side prediction/camera awareness of the switch) — see
PawnComponent.cs's own header comment and ADR-0012's Notes for the full citation, and
Documentation/ROADMAP.md's Verification debt section for where this is tracked project-wide.
The same dig found that IsInVehicle alone is not confirmed sufficient to stop movement —
the real PlayerController.UseInputControls gates new input the way IsNoclipping already
assumes, but residual physics velocity is untouched by that flag; see PawnComponent.cs's
header comment for the exact citation. This does not change IsNoclipping's own status on this
list, which remains a separate, still-unconfirmed question about noclip specifically.
Resolved: "S&box's stock movement/character-controller component's exact type" — checked
directly against ~/Projects/sbox-public: this codebase never actually adopted
Sandbox.PlayerController — PawnComponent sits alongside a bare Sandbox.CharacterController,
confirmed (by its own integration test, Tests/.../CharacterControllerTests.cs) to be entirely
trace-driven, with no gravity or input of its own. PlayerMovementComponent (this module,
added to drive it — see its own header comment) suspends movement by zeroing
CharacterController.Velocity directly while IsNoclipping/IsInVehicle is set, which
sidesteps the residual-physics-velocity gap named above for PlayerController.UseInputControls
entirely, rather than inheriting it. IsNoclipping's own walk-through-geometry behaviour is
still not implemented — this component only suspends normal movement while the flag is set, it
does not itself change CharacterController collision — so noclip's collision-suppression half
remains the one open item on this list.