Skip to content

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. - IPawnLocatorRegister, Unregister, Find(Guid), PositionOf(Guid), All. - PawnComponentCharacterId, IsNoclipping, Teleport(Vector3), SetNoclip(bool), IsInVehicle, EnterVehicle(GameObject, Vector3), ExitVehicle(Vector3). - PlayerMovementComponentWalkSpeed, 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. - PlayerSpawnerComponentPlayerPawnPrefab (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.PlayerControllerPawnComponent 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.