13 KiB
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: loadUI_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 childKnob, 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.
[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:
public Vector2 Value { get; private set; }
public bool IsActive { get; private set; }
public event Action Started;
public event Action<Vector2, bool> 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, optionalIcon,CooldownMask,CooldownText,ChargeText,AimIndicatororDragArrow. -
Step 1: Write the failing tests
Add tests for tap, drag release, cancel, blocked cooldown, and charges.
[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:
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-levelButtonEvent,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.
[Test]
public void Pad_FindsFiveButtonsAndForwardsEvents()
{
var pad = CreatePadWithButtons();
var events = new List<ActionButtonEvent>();
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
JsonUIPrefabBuildercomponent support forImage,TextMeshProUGUI,CanvasGroup,Button, andMonoBehaviour. -
Produces: prefab definitions that attach
XGame.Input.VirtualJoystickandXGame.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.
[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.
[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:
VirtualJoystickTests
VirtualActionButtonTests
VirtualActionPadTests
VirtualInputPrefabLayoutTests
LobbyWorldControllerVirtualJoystickTests
Expected: all pass.
- Step 2: Run broader existing affected tests
Run existing tests that touch UI and stage isolation:
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:
git commit -m "feat: add virtual hud input components"
Expected: commit includes no unrelated actor/UI asset modifications.