# 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 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 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 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 时不会误发释放事件。 - 缺少可选视觉节点不会导致运行时异常。 - 现有小游戏框架合同不需要变更。