Virtual Joystick Movement — Design Spec¶
Date: 2026-07-21
Scope: Unity project (unity/raiders). Replace tap-to-move pointer input with a floating
virtual joystick. Sprint, dodge, tap-to-hunt, and map tap-to-move are parked (kept in code,
disabled, revivable) — not deleted.
Goal¶
The player steers directly with a floating on-screen stick (casual-mobile standard: Archero,
Survivor.io). No A* autopilot for the player while the stick is held. Combat stays fully
automatic (existing AutoFire / AutoMelee / stance arbiter — untouched).
Decisions (confirmed)¶
| Question | Decision |
|---|---|
| Joystick style | Floating — base appears where the thumb lands, fades on release |
Tap-an-enemy hunt (SetHunt homing, stealth-chase) |
Parked — walk at enemies yourself; auto-combat handles the rest |
| Swipe dodge roll | Parked with sprint — re-attach later (e.g. as a button) if wanted |
| Tactical-map tap-to-move | Parked — map tap just closes the map |
| Sprint | Parked (explicit user request) — machinery stays for rival raiders |
Architecture¶
1. New input layer (2 scripts)¶
VirtualJoystickView (Scripts/UI/InGame/) — UGUI view, built at runtime by
InGameViewController and parented inside the InGameView HUD as first sibling:
- Inherits HUD visibility: hidden until Descend (
RaidState.Active), gone on raid end. - First sibling ⇒ every existing HUD button draws above it and blocks its raycasts — no
IsPointerOverGameObjectpolling needed. - Fullscreen transparent
Imageraycast catcher implementingIPointerDownHandler/IDragHandler/IPointerUpHandler. On pointer-down: stick base (ring) + knob (disc) appear at the touch point; knob clamps tojoyRadius; on release both fade out. - Tracks the pointerId that grabbed the stick, so a second simultaneous finger can still press HUD buttons (multi-touch safe).
- Exposes
Vector2 Value(normalized, dead-zoned viajoyDeadZone) andbool Active. - Visuals: simple UGUI images (generated sprites or Unity built-ins), semi-transparent;
respects
UiScalereference resolution.
JoystickInput (Scripts/World/) — world component on the Player object, replacing
ClickToMove in Game.unity:
- Each frame: read
VirtualJoystickView.Value; in editor/dev also read WASD/arrow keys (bothENABLE_INPUT_SYSTEM/ENABLE_LEGACY_INPUT_MANAGERpaths, same ifdef styleClickToMoveused). - Rotate the screen-space stick vector by the camera's Y yaw so stick-up = camera-forward projected on XZ (camera is a fixed-yaw ortho iso rig).
- Gate on
PlayerController.Controllable(dead / raid over ⇒ zero input). - Feed the result to
PathAgent.SetMoveInput(worldDir)every frame (zero vector when idle). - Finds the joystick view via a static registry hook (
VirtualJoystickView.Current) since the HUD is DI-spawned after the player.
2. PathAgent direct-steer mode (extend, don't fork)¶
PathAgent stays the single mover — enemies/rivals keep using it exactly as today.
- New:
public void SetMoveInput(Vector3 worldDir)(stamped each frame; consumed inUpdate). - Non-zero input: drop any path (
DropPath()semantics), then stepspeed × externalSpeedMul × dtalong the direction with the same per-axis wall-slide the dodge/hunt code uses (Los.WallBlocked, body radius 0.32) — sliding along walls, no tunneling. - Reuses the existing speed pipeline untouched: gear speed, bush perk (
BushSpeed), spider web-slow (externalSpeedMul) all keep working because they already flow through_agent.speed/externalSpeedMul. - Updates
Facing(withturnRateeasing), rotation, andVelocityidentically to path moves (velocity inheritance for projectiles keeps working). - New:
public bool IsMoving => HasPath || <stick input this frame>. - Sprint fields (
Sprinting,sprintMultiplier) untouched — rival raiders still drive them.
3. "Is the player moving" consumers switch HasPath → IsMoving¶
| Site | Change |
|---|---|
PlayerController heal-channel cancel |
_agent.HasPath → _agent.IsMoving |
PlayerController melee/fire facing arbitration (!_agent.HasPath && _huntTarget == null) |
use IsMoving (movement facing wins while steering) |
Gate.cs idle check |
!_playerAgent.HasPath → !_playerAgent.IsMoving |
CameraRig moving-zoom / look-ahead |
agent.HasPath → agent.IsMoving (look-ahead along Facing works as-is) |
4. Parked (kept, disabled, marked PARKED)¶
ClickToMove,ClickRipple: components removed fromGame.unity; files kept with aPARKED — replaced by virtual joystick (see 2026-07-21 spec)header note.PlayerController: sprint/stamina tick block, stealth-chase (_chaseSprint), hunt homing (UpdateHunt,SetHunt,ClearHunt,FindTapTarget),Dodge,MoveTo— call sites removed/commented with PARKED notes. Public members stay so parked callers still compile;HuntTargetremains but is always null ⇒TargetingViewhunt mark andCameraRighunt framing die naturally (theirFireTargetbranches still work).MapController.OnBigMapClick: tap no longer callsMovePlayerTo— just closes.MovePlayerTo/PlayerController.MoveTokept, parked.PlayerVitalsView: stamina ring hidden (stamina never changes).StaminaModelstays constructed (cheap, null-safe) but itsTickis no longer called.PathVisualizer: stays; the player simply never has a path (draws nothing).RaidState.PlayerHunting: stays, permanently false —Chest/Gateproximity gating degrades gracefully.- Tutorial:
Sprintedcondition +SprintedLlatch stay in code, parked.
5. Config (GameConfig.Move)¶
- New knobs:
joyRadius(stick travel radius, px at reference resolution, default ~110) andjoyDeadZone(0..1 normalized, default 0.12). Written toGameConfig.assettoo (project rule: new fields get asset values, not just C# defaults). - Sprint/stamina/dodge/stealth-chase knobs (
sprintMult,sprintDrain,stamRegen,stamMax,stamRegenDelay,dodgeCost,dodgeTime,dodgeSpeed,dodgeEase,dodgeCd,stealthChaseSprint) grouped under a— PARKED (sprint/dodge — no input attached) —comment block. Fields stay serialized so.assetvalues survive for revival. (sprintMultis still read at spawn for_agent.sprintMultiplier— harmless, rivals use it.)
6. Content changes¶
Game.unity: Player object — removeClickToMove, addJoystickInput.TutStage_1_FirstSteps.asset:- Step 1 text: "Tap anywhere to move" → joystick wording ("Drag anywhere to move with the joystick…").
- Step 2 "Sprint ahead" removed — its
Sprinted(type 2) condition would soft-lock the FTUE with sprint parked.
Error handling / edge cases¶
- Stick held when the player dies / raid ends:
JoystickInputgates onControllable; the HUD (and stick) also deactivate.PathAgentgets a zero input stamp ⇒ stops. - Stick held while a heal channel starts: movement wins —
StartHealChannelis a no-op while_agent.IsMoving(release the stick to drink). A stick push during an already-running channel still cancels it, same as tap-move's move-cancels rule. - Pointer starts on a HUD button: button's raycast blocks the catcher ⇒ no stick. Pointer starts on the catcher, drags over a button: stick keeps the pointer (UGUI drag capture).
- No touch device (editor): mouse drag works through the same UGUI pointer events; WASD as a dev convenience.
Verification¶
- Editor play-mode: joystick appears/steers/fades; walls slide; bush perk + web-slow still scale speed; auto-melee/auto-fire engage while strafing; heal channel cancels on stick input; chest/portal/gate proximity interactions fire when idle; FTUE stage 1 completes without the sprint step; camera moving-zoom responds to stick movement.
- No compile errors via Unity MCP
unity_get_compilation_errors.