Files
AIC-Project/docs/superpowers/plans/2026-07-30-virtual-hud-input.md

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: 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.

[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, 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.

[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-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.

[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 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.

[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.