docs: design virtual hud input

This commit is contained in:
ud18010
2026-07-30 10:17:07 +08:00
parent 42c3874d82
commit 3546d16632
@@ -0,0 +1,376 @@
# Virtual HUD Input Design
## 目标
设计一套可在大厅和小游戏中复用的 HUD 输入组件:
- 通用摇杆组件:复用当前大厅摇杆视觉和交互体验,输出纯方向输入事件。
- 通用按键组件:按 MOBA 手游右下角操作区设计,提供 1 个主按键 + 4 个次按键,支持完整技能键手势和状态反馈。
组件层只输出输入事件和输入状态,不直接发送网络消息,不绑定大厅移动、小游戏技能、角色职业或服务端协议。大厅和小游戏各自订阅事件,并把事件映射到自己的业务逻辑。
## 范围
本设计覆盖:
- 通用 `VirtualJoystick` 运行时组件。
- 通用 `VirtualActionButton` 单按钮组件。
- 通用 `VirtualActionPad` 组合组件。
- `UI_VirtualJoystick``UI_VirtualActionPad` 预设描述,沿用 `Doc/UIPrefabCreater/*.json` 到 prefab 的现有管线。
- 大厅摇杆从 `LobbyWorldController` 私有采样逻辑迁移到通用组件。
- 小游戏按需加载通用摇杆或右下角按键组,并消费纯输入事件。
不覆盖:
- 小游戏协议设计。
- 具体技能系统、伤害、冷却规则、服务器校验。
- 专用 MOBA 圆形技能切图生产。第一版复用现有 Common UI 资源,后续可替换美术资源而不改变输入 API。
- Unity 新 Input System 迁移。第一版继续使用当前项目已有的 UGUI/EventSystem/StandaloneInputModule 路线。
## 方案选择
采用“通用 VirtualInput 组件层”方案。
备选方案一是只重构现有大厅摇杆并单独新增技能键。这种方式改动少,但大厅和小游戏输入会快速分叉。
备选方案二是新增完整输入服务和动作映射系统。这更接近正式 Input System,但当前项目还没有动作注册、上下文切换、优先级和协议映射基础,第一版过重。
推荐方案在中间:新增独立组件层,组件只负责输入采样和视觉反馈,业务层自己映射响应。它能复用现有 UI 管线,也不会把小游戏协议提前写死到客户端框架里。
## 架构
新增目录:
```text
Client/Assets/Script/xmain/Input/
```
核心组件:
```csharp
namespace XGame.Input
{
public sealed class VirtualJoystick : MonoBehaviour
{
public Vector2 Value { get; }
public bool IsActive { get; }
public event Action Started;
public event Action<Vector2, bool> Changed;
public event Action Ended;
public bool IsScreenPositionInAcquireArea(Vector2 screenPosition);
}
}
```
`VirtualJoystick` 负责鼠标/触摸捕获、死区、归一化方向、摇杆头视觉偏移。它不负责移动、相机、网络同步。
```csharp
namespace XGame.Input
{
public enum ActionButtonId
{
Primary,
Skill1,
Skill2,
Skill3,
Skill4
}
public enum ActionButtonPhase
{
Down,
Drag,
HoldStart,
HoldTick,
HoldEnd,
Tap,
Release,
Cancel,
Blocked
}
public readonly struct ActionButtonEvent
{
public ActionButtonId ButtonId { get; }
public ActionButtonPhase Phase { get; }
public Vector2 ScreenPosition { get; }
public Vector2 LocalVector { get; }
public Vector2 Direction { get; }
public float Distance01 { get; }
public float HeldSeconds { get; }
public bool IsCanceled { get; }
public int PointerId { get; }
}
}
```
```csharp
namespace XGame.Input
{
public sealed class VirtualActionButton : MonoBehaviour
{
public ActionButtonId ButtonId;
public bool IsInteractable { get; }
public event Action<ActionButtonEvent> ButtonEvent;
public void SetCooldown(float remainingSeconds, float durationSeconds);
public void SetCharges(int count);
public void SetButtonEnabled(bool enabled);
public void SetCancelArea(RectTransform cancelArea);
public void SetIcon(Sprite icon);
}
}
```
`VirtualActionButton` 是单个技能键状态机。它负责点击、长按、拖拽瞄准、取消、冷却遮罩、次数、禁用视觉。它不关心按钮对应普攻、技能还是道具。
```csharp
namespace XGame.Input
{
public sealed class VirtualActionPad : MonoBehaviour
{
public event Action<ActionButtonEvent> ButtonEvent;
public VirtualActionButton GetButton(ActionButtonId id);
public void SetCooldown(ActionButtonId id, float remainingSeconds, float durationSeconds);
public void SetCharges(ActionButtonId id, int count);
public void SetButtonEnabled(ActionButtonId id, bool enabled);
public void SetCancelAreaVisible(bool visible);
}
}
```
`VirtualActionPad` 管理右下角 5 个按钮,按稳定 ID 转发事件,并提供统一状态刷新 API。
## 预设
### UI_VirtualJoystick
新增:
```text
Doc/UIPrefabCreater/UI_VirtualJoystick.json
Client/Assets/Game/Art/UI/Prefab/UI_VirtualJoystick.prefab
```
它从现有 `UI_LobbyJoystick` 演进而来,保留:
- `Base`
- `Ring`
- `Knob`
默认布局:
- 根节点锚到左下角。
- 参考分辨率 `2048x1024`
- 默认中心位置约 `(260, 220)`
- 默认尺寸 `148x148`
- 根节点挂 `VirtualJoystick`
- 继续复用 `lobby_joystick_base``lobby_joystick_ring``lobby_joystick_knob`
`UI_LobbyJoystick` 可以先保留,避免已有资源引用断裂。大厅运行时改用 `UI_VirtualJoystick` 后,旧预设可作为兼容资源,后续再清理。
### UI_VirtualActionPad
新增:
```text
Doc/UIPrefabCreater/UI_VirtualActionPad.json
Client/Assets/Game/Art/UI/Prefab/UI_VirtualActionPad.prefab
```
默认布局:
- 根节点锚到右下角。
- `Btn_Primary`:主按键,最大,位于右下核心位置。
- `Btn_Skill1``Btn_Skill2``Btn_Skill3`:围绕主按键左侧和上方弧形排布。
- `Btn_Skill4`:更靠上或靠左的小技能键,用于特殊动作、召唤师技能或道具。
- `CancelArea`:默认在按钮组上方偏中区域,仅拖拽技能时显示。
每个按钮标准子节点:
- `Icon`
- `CooldownMask`
- `CooldownText`
- `ChargeText`
- `AimIndicator``DragArrow`
第一版视觉复用现有 Common 资源:
- 主按键底图优先使用 `btn_primary_*`
- 次按键底图优先使用 `btn_small_*``btn_warning_*`
- 图标先使用 `ico_sword``ico_shield``ico_star``ico_heart``ico_item` 等临时示意图标。
如果后续补充圆形技能键切图,只需替换 JSON 资源和 prefab,不改变 `VirtualActionButton`/`VirtualActionPad` API。
## 摇杆交互
`VirtualJoystick` 行为:
1. 捕获鼠标左键或触摸。
2. 按 pointer ID 锁定一次操作,直到松开或取消。
3. 支持开始捕获区域放大倍率,默认视觉半径 `1.6x` 内都可开始控制。
4. 计算屏幕指针相对摇杆中心的向量,并归一化到 `[-1, 1]`
5. 小于死区时输出 `Value=Vector2.zero``IsActive=false`
6. 大于死区时输出归一化方向且 `IsActive=true`
7. `Knob` 最大视觉移动距离由容器半径和 knob 半径计算,避免不同尺寸下越界。
8. 松开时重置方向、复位 knob、触发 `Ended`
事件:
- `Started`:从未激活进入捕获。
- `Changed(value, active)`:方向或激活状态变化时触发。
- `Ended`:释放当前 pointer 后触发。
`IsScreenPositionInAcquireArea` 提供给相机、点击移动等外部输入排除逻辑复用,避免各业务重复计算摇杆区域。
## 按键交互
`VirtualActionButton` 行为:
- `PointerDown`:记录按下时间、pointer ID,发 `Down`
- `PointerDrag`:计算从按钮中心到当前指针的 `LocalVector``Direction``Distance01`,发 `Drag`
- 进入取消区时切换取消悬停视觉,并在事件中设置 `IsCanceled=true`
- 按住超过阈值后发一次 `HoldStart`
- 按住期间按固定间隔发 `HoldTick`
- 松开时:
- 如果在取消区,发 `Cancel`
- 如果拖拽距离超过释放阈值,发 `Release`,并携带方向。
- 如果未拖拽且未达到长按阈值,发 `Tap`
- 如果已经长按,先补 `HoldEnd`,再按最终状态发 `Release``Cancel`
- 冷却中、禁用、次数为 0 时不进入按下流程,只发 `Blocked`
默认阈值:
- 长按阈值:`0.35s`
- HoldTick 间隔:`0.1s`
- 拖拽释放阈值:按钮半径的 `0.25`
- 最大拖拽归一化距离:按钮半径的 `1.5`
这些阈值作为序列化字段暴露,允许 prefab 或业务按需调节。
## 状态与视觉
按钮状态:
- Normal:可点击。
- Pressed:按住。
- Dragging:拖拽瞄准。
- CancelHover:拖入取消区。
- Cooldown:冷却中。
- Disabled:禁用。
- EmptyCharges:次数为 0。
视觉刷新:
- `CooldownMask` 使用 Filled Image 或代码调整 `fillAmount`
- `CooldownText` 显示向上取整的剩余秒数。
- `ChargeText` 在次数大于 0 时显示次数,次数为 0 时按钮进入不可用视觉。
- `AimIndicator`/`DragArrow` 在拖拽时显示方向,松手后隐藏。
- `CancelArea` 默认隐藏,进入拖拽瞄准时显示;取消或释放后隐藏。
缺少可选节点时跳过对应视觉,不影响事件输出。缺少关键节点时记录日志并禁用该组件。
## 大厅接入
`LobbyWorldController` 保留大厅业务职责:
- 本地角色移动。
- 远端角色同步。
- 相机旋转和缩放。
- 大厅网络移动同步。
改动方向:
- 移除 `LobbyWorldController` 内部摇杆采样字段和方法。
- 加载 `UI_VirtualJoystick.prefab` 后获取 `VirtualJoystick`
- 订阅 `Changed/Ended`,把 `Vector2` 缓存为大厅移动输入。
- `GetMoveDirection` 继续把键盘 WASD 和摇杆方向合成世界方向。
- 相机输入排除摇杆区域时调用 `VirtualJoystick.IsScreenPositionInAcquireArea`
- 大厅进入小游戏隔离时,将通用摇杆 root 或 `VirtualJoystick` 列入 `MiniGameStageIsolation` 管理,避免大厅输入穿透。
## 小游戏接入
小游戏不强制显示这套 HUD。需要时在 `IGameClient.OnEnter` 中加载:
- `UI_VirtualJoystick`
- `UI_VirtualActionPad`
- 或两者都加载
小游戏客户端订阅事件并自行映射:
```text
Primary Tap -> 普攻 opcode
Skill1 Release -> 带方向释放技能 opcode
Skill2 Cancel -> 不发送技能,播放本地取消反馈
Joystick Changed -> 本地移动或发送移动输入
```
`GameClientCtx` 第一版不扩展输入服务,继续保持资源加载和事件订阅模式。这样不会改变小游戏框架合同,也不会影响已有 RPS 小游戏。
## 错误处理
- 摇杆 prefab 缺少 `Knob`:记录错误日志,禁用 `VirtualJoystick`
- 按钮缺少 `Icon`:仍可输出事件,但不显示图标。
- 按钮缺少 `CooldownMask``CooldownText``ChargeText`:跳过对应视觉刷新。
- 缺少 `CancelArea`:拖拽取消功能关闭,按钮仍支持点击和拖拽释放。
- 多点触控按 pointer ID 锁定,同一个按钮未释放前忽略其他 pointer。
- 不同按钮可以各自捕获不同 pointer。
- 冷却、禁用、次数为 0 时只发 `Blocked`,不发 `Tap``Release``Cancel`
- 组件销毁或禁用时,如果正在按住,需要复位内部状态和视觉,避免残留按下态。
## 测试策略
EditMode 测试:
1. `VirtualJoystick` 死区:小于死区输出 inactive,松开后归零。
2. `VirtualJoystick` 归一化:超过半径时 clamp 到单位圆。
3. `VirtualJoystick` knob 行程:按容器和 knob 尺寸计算,不越界。
4. `VirtualActionButton` 点击:Down -> Tap。
5. `VirtualActionButton` 拖拽释放:Down -> Drag -> Release,并携带方向。
6. `VirtualActionButton` 拖拽取消:Down -> Drag -> Cancel。
7. `VirtualActionButton` 长按:Down -> HoldStart -> HoldTick -> HoldEnd。
8. `VirtualActionButton` Blocked:冷却中、禁用、次数为 0 时不发释放类事件。
9. `VirtualActionPad` 能按 `ActionButtonId` 找到 5 个按钮并转发事件。
10. 标准节点绑定成功,缺少可选节点不抛异常。
11. 状态 API 能刷新冷却遮罩、冷却文字、次数文字和可用状态。
大厅接入测试:
- `LobbyWorldController` 消费 `VirtualJoystick` 输出值合成移动方向。
- 相机输入排除摇杆区域时使用通用组件判断。
- 小游戏隔离时大厅摇杆不可见或不可交互。
手动 Unity 烟测:
- PC 鼠标拖动摇杆。
- 触摸或模拟触摸拖动摇杆。
- 主按键点击释放。
- 技能键拖拽瞄准释放。
- 技能键拖拽到取消区取消。
- 冷却遮罩和倒计时显示。
- 次数耗尽后按钮进入不可用态。
- 退出小游戏后大厅输入恢复。
## 实施顺序建议
1.`VirtualJoystick``VirtualActionButton` 写核心状态测试。
2. 实现 `VirtualJoystick`
3. 实现 `VirtualActionButton``VirtualActionPad`
4. 新增 `UI_VirtualJoystick.json``UI_VirtualActionPad.json`
5. 生成并检查 prefab。
6. 迁移 `LobbyWorldController` 使用 `VirtualJoystick`
7. 加测试和手动烟测。
## 验收标准
- 大厅能继续使用左下角摇杆移动角色。
- 摇杆输入来自 `VirtualJoystick` 事件,而不是 `LobbyWorldController` 私有采样逻辑。
- 小游戏能加载右下角 `VirtualActionPad` 并收到 5 个按钮的纯输入事件。
- 技能键支持点击、长按、拖拽释放、拖拽取消、冷却、次数、禁用。
- 冷却中或次数为 0 时不会误发释放事件。
- 缺少可选视觉节点不会导致运行时异常。
- 现有小游戏框架合同不需要变更。