# Virtual HUD Input Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Build reusable Unity HUD input components for a virtual joystick and a MOBA-style action button pad that emit pure input events for lobby and minigame consumers. **Architecture:** Add focused runtime components under `Client/Assets/Script/xmain/Input/`: `VirtualJoystick`, `VirtualActionButton`, and `VirtualActionPad`. Add JSON prefab descriptions through the existing `Doc/UIPrefabCreater` pipeline, then migrate `LobbyWorldController` to consume `VirtualJoystick` instead of owning joystick sampling. **Tech Stack:** Unity UGUI, EventSystem pointer interfaces, TextMeshPro, NUnit EditMode tests, existing JSON-to-prefab builder. ## Global Constraints - Components emit pure input events and must not send network messages. - First version uses current UGUI/EventSystem/StandaloneInputModule, not Unity's new Input System. - First version reuses existing Common UI resources and does not require new MOBA circle button art. - Existing minigame framework contracts must not change. - Do not revert or overwrite unrelated working tree changes. --- ## File Structure - Create `Client/Assets/Script/xmain/Input/VirtualJoystick.cs`: joystick sampling, pointer capture, dead zone, knob motion, and public direction events. - Create `Client/Assets/Script/xmain/Input/VirtualActionButton.cs`: action button IDs, event payload, pointer state machine, cooldown/charge/enabled visual state. - Create `Client/Assets/Script/xmain/Input/VirtualActionPad.cs`: five-button registry, event forwarding, and pad-level state API. - Create `Client/Assets/Script/Tests/EditMode/VirtualJoystickTests.cs`: component math and state tests for joystick behavior. - Create `Client/Assets/Script/Tests/EditMode/VirtualActionButtonTests.cs`: button state machine tests and visual state tests. - Create `Client/Assets/Script/Tests/EditMode/VirtualActionPadTests.cs`: button discovery and event forwarding tests. - Create `Doc/UIPrefabCreater/UI_VirtualJoystick.json`: reusable joystick prefab definition. - Create `Doc/UIPrefabCreater/UI_VirtualActionPad.json`: reusable MOBA-style action pad prefab definition. - Modify `Client/Assets/Script/xmain/Client/LobbyWorldController.cs`: load `UI_VirtualJoystick`, subscribe to events, remove duplicated joystick sampling methods, and use the component for camera exclusion. --- ### Task 1: VirtualJoystick Runtime Component **Files:** - Create: `Client/Assets/Script/xmain/Input/VirtualJoystick.cs` - Test: `Client/Assets/Script/Tests/EditMode/VirtualJoystickTests.cs` **Interfaces:** - Produces: `XGame.Input.VirtualJoystick.Value`, `IsActive`, `Started`, `Changed`, `Ended`, `IsScreenPositionInAcquireArea(Vector2)`, `SetPointerForTest(Vector2?)` - Consumes: Unity `RectTransform`, optional child `Knob`, UGUI canvas camera conversion. - [ ] **Step 1: Write the failing tests** Add tests that create a `VirtualJoystick`, assign a root and knob `RectTransform`, call `SetPointerForTest`, and assert dead-zone, clamping, release, and acquire-area behavior. ```csharp [Test] public void Joystick_DeadZoneReportsInactiveAndZero() { var fixture = CreateJoystick(); fixture.Joystick.SetPointerForTest(new Vector2(106f, 100f)); Assert.That(fixture.Joystick.IsActive, Is.False); Assert.That(fixture.Joystick.Value, Is.EqualTo(Vector2.zero)); } [Test] public void Joystick_ClampsDirectionAndMovesKnobWithinTravel() { var fixture = CreateJoystick(); fixture.Joystick.SetPointerForTest(new Vector2(300f, 100f)); Assert.That(fixture.Joystick.IsActive, Is.True); Assert.That(fixture.Joystick.Value.x, Is.EqualTo(1f).Within(0.001f)); Assert.That(fixture.Knob.anchoredPosition.x, Is.EqualTo(24f).Within(0.001f)); } ``` - [ ] **Step 2: Run test to verify it fails** Run: Unity EditMode tests for `VirtualJoystickTests`. Expected: compile fails because `XGame.Input.VirtualJoystick` does not exist. - [ ] **Step 3: Implement minimal component** Implement `VirtualJoystick` with: ```csharp public Vector2 Value { get; private set; } public bool IsActive { get; private set; } public event Action Started; public event Action Changed; public event Action Ended; public bool IsScreenPositionInAcquireArea(Vector2 screenPosition); public void SetPointerForTest(Vector2? screenPosition); ``` It must bind `Knob` in `Awake`, compute screen center/radius from root or `Joystick` child, clamp values, apply dead zone, and reset on release. - [ ] **Step 4: Run test to verify it passes** Run: Unity EditMode tests for `VirtualJoystickTests`. Expected: all `VirtualJoystickTests` pass. --- ### Task 2: VirtualActionButton Runtime Component **Files:** - Create: `Client/Assets/Script/xmain/Input/VirtualActionButton.cs` - Test: `Client/Assets/Script/Tests/EditMode/VirtualActionButtonTests.cs` **Interfaces:** - Produces: `ActionButtonId`, `ActionButtonPhase`, `ActionButtonEvent`, `VirtualActionButton.ButtonEvent`, `SetCooldown`, `SetCharges`, `SetButtonEnabled`, `SetCancelArea`, `SetIcon` - Consumes: Unity `IPointerDownHandler`, `IDragHandler`, `IPointerUpHandler`, optional `Icon`, `CooldownMask`, `CooldownText`, `ChargeText`, `AimIndicator` or `DragArrow`. - [ ] **Step 1: Write the failing tests** Add tests for tap, drag release, cancel, blocked cooldown, and charges. ```csharp [Test] public void Button_TapEmitsDownThenTap() { var button = CreateButton(); var events = Capture(button); button.HandlePointerDownForTest(new Vector2(100f, 100f), 1, 0f); button.HandlePointerUpForTest(new Vector2(102f, 100f), 1, 0.1f); Assert.That(events.Select(e => e.Phase), Is.EqualTo(new[] { ActionButtonPhase.Down, ActionButtonPhase.Tap })); } [Test] public void Button_CooldownEmitsBlockedOnly() { var button = CreateButton(); var events = Capture(button); button.SetCooldown(3f, 5f); button.HandlePointerDownForTest(new Vector2(100f, 100f), 1, 0f); Assert.That(events.Single().Phase, Is.EqualTo(ActionButtonPhase.Blocked)); } ``` - [ ] **Step 2: Run test to verify it fails** Run: Unity EditMode tests for `VirtualActionButtonTests`. Expected: compile fails because action button types do not exist. - [ ] **Step 3: Implement minimal component** Implement pointer interfaces and test helpers: ```csharp public void HandlePointerDownForTest(Vector2 screenPosition, int pointerId, float now); public void HandleDragForTest(Vector2 screenPosition, int pointerId, float now); public void HandlePointerUpForTest(Vector2 screenPosition, int pointerId, float now); ``` Use a single active pointer, emit `Down`, `Drag`, `Tap`, `Release`, `Cancel`, `HoldStart`, `HoldTick`, `HoldEnd`, and `Blocked` according to the spec. Refresh cooldown mask fill, cooldown text, charge text, icon, and enabled/blocked visuals when matching nodes exist. - [ ] **Step 4: Run test to verify it passes** Run: Unity EditMode tests for `VirtualActionButtonTests`. Expected: all `VirtualActionButtonTests` pass. --- ### Task 3: VirtualActionPad Runtime Component **Files:** - Create: `Client/Assets/Script/xmain/Input/VirtualActionPad.cs` - Test: `Client/Assets/Script/Tests/EditMode/VirtualActionPadTests.cs` **Interfaces:** - Consumes: `VirtualActionButton` - Produces: `GetButton(ActionButtonId)`, pad-level `ButtonEvent`, `SetCooldown`, `SetCharges`, `SetButtonEnabled`, `SetCancelAreaVisible` - [ ] **Step 1: Write the failing tests** Add tests that build five children named `Btn_Primary`, `Btn_Skill1`, `Btn_Skill2`, `Btn_Skill3`, and `Btn_Skill4`, attach buttons, call `Awake` through Unity lifecycle, and assert lookup/event forwarding. ```csharp [Test] public void Pad_FindsFiveButtonsAndForwardsEvents() { var pad = CreatePadWithButtons(); var events = new List(); pad.ButtonEvent += events.Add; pad.GetButton(ActionButtonId.Primary).HandlePointerDownForTest(new Vector2(100f, 100f), 1, 0f); Assert.That(events.Single().ButtonId, Is.EqualTo(ActionButtonId.Primary)); } ``` - [ ] **Step 2: Run test to verify it fails** Run: Unity EditMode tests for `VirtualActionPadTests`. Expected: compile fails because `VirtualActionPad` does not exist. - [ ] **Step 3: Implement minimal component** Bind child buttons by existing component or child name, assign stable IDs, set each button's cancel area, subscribe to child events, and forward state API calls. - [ ] **Step 4: Run test to verify it passes** Run: Unity EditMode tests for `VirtualActionPadTests`. Expected: all `VirtualActionPadTests` pass. --- ### Task 4: JSON Prefab Descriptions **Files:** - Create: `Doc/UIPrefabCreater/UI_VirtualJoystick.json` - Create: `Doc/UIPrefabCreater/UI_VirtualActionPad.json` - Test: `Client/Assets/Script/Tests/EditMode/VirtualInputPrefabLayoutTests.cs` **Interfaces:** - Consumes: existing `JsonUIPrefabBuilder` component support for `Image`, `TextMeshProUGUI`, `CanvasGroup`, `Button`, and `MonoBehaviour`. - Produces: prefab definitions that attach `XGame.Input.VirtualJoystick` and `XGame.Input.VirtualActionPad`. - [ ] **Step 1: Write the failing prefab layout tests** Test the JSON files exist, define expected root names, include required child node names, and use expected prefab paths. ```csharp [Test] public void VirtualActionPadJson_DefinesFiveButtonsAndCancelArea() { string json = File.ReadAllText("Doc/UIPrefabCreater/UI_VirtualActionPad.json"); Assert.That(json, Does.Contain("\"prefabName\": \"UI_VirtualActionPad\"")); Assert.That(json, Does.Contain("\"Btn_Primary\"")); Assert.That(json, Does.Contain("\"Btn_Skill4\"")); Assert.That(json, Does.Contain("\"CancelArea\"")); } ``` - [ ] **Step 2: Run test to verify it fails** Run: Unity EditMode tests for `VirtualInputPrefabLayoutTests`. Expected: fail because JSON files do not exist. - [ ] **Step 3: Add JSON files** Add `UI_VirtualJoystick.json` from current `UI_LobbyJoystick.json` with generic names and `VirtualJoystick` component. Add `UI_VirtualActionPad.json` with the root, five buttons, cancel area, icon/cooldown/charge/aim child nodes, and `VirtualActionPad` component. - [ ] **Step 4: Run test to verify it passes** Run: Unity EditMode tests for `VirtualInputPrefabLayoutTests`. Expected: all layout tests pass. --- ### Task 5: LobbyWorldController Migration **Files:** - Modify: `Client/Assets/Script/xmain/Client/LobbyWorldController.cs` - Test: `Client/Assets/Script/Tests/EditMode/LobbyWorldControllerVirtualJoystickTests.cs` **Interfaces:** - Consumes: `XGame.Input.VirtualJoystick` - Produces: lobby movement that uses joystick events instead of private joystick sampling. - [ ] **Step 1: Write the failing integration test** Use reflection or exposed test helpers to verify `LobbyWorldController` can accept a `VirtualJoystick` value and `GetMoveDirection` includes it. If direct runtime reflection is too brittle, add a focused test for a new internal helper that maps joystick vector plus camera vectors to world direction. ```csharp [Test] public void LobbyMovement_UsesVirtualJoystickVector() { Vector3 dir = LobbyWorldController.MapMoveInputForTest( new Vector2(1f, 0f), Vector3.forward, Vector3.right, false, false, false, false); Assert.That(dir, Is.EqualTo(Vector3.right)); } ``` - [ ] **Step 2: Run test to verify it fails** Run: Unity EditMode tests for `LobbyWorldControllerVirtualJoystickTests`. Expected: fail because helper or migration is not present. - [ ] **Step 3: Migrate controller** Change `JoystickPrefabPath` to `UI_VirtualJoystick.prefab`, replace joystick root/knob sampling fields with one `VirtualJoystick` field, subscribe to `Changed` and `Ended`, keep the current `joystick` and `joystickActive` cached values as business input, and remove duplicated pointer sampling helpers. Camera input uses `virtualJoystick.IsScreenPositionInAcquireArea(pos)` when available. - [ ] **Step 4: Run test to verify it passes** Run: Unity EditMode tests for `LobbyWorldControllerVirtualJoystickTests`. Expected: integration test passes. --- ### Task 6: Verification **Files:** - All files changed in tasks 1-5. **Interfaces:** - Consumes: all implementation tasks. - Produces: verified working tree ready for review. - [ ] **Step 1: Run targeted EditMode tests** Run all new test classes: ```text VirtualJoystickTests VirtualActionButtonTests VirtualActionPadTests VirtualInputPrefabLayoutTests LobbyWorldControllerVirtualJoystickTests ``` Expected: all pass. - [ ] **Step 2: Run broader existing affected tests** Run existing tests that touch UI and stage isolation: ```text MiniGameStageIsolationTests UIManagerAttachTests MatchWaitingPrefabLayoutTests ``` Expected: all pass. - [ ] **Step 3: Inspect working tree** Run: `git status --short` Expected: new/modified files are limited to input components, tests, prefab JSON, plan/spec if updated, and `LobbyWorldController.cs`; unrelated existing Unity asset changes remain unstaged. - [ ] **Step 4: Commit only related files if tests pass** Stage only the files created or modified by this implementation and commit with: ```bash git commit -m "feat: add virtual hud input components" ``` Expected: commit includes no unrelated actor/UI asset modifications.