feat: add reusable virtual hud input
This commit is contained in:
@@ -0,0 +1,334 @@
|
||||
# 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<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.
|
||||
|
||||
```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<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.
|
||||
|
||||
```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.
|
||||
Reference in New Issue
Block a user