13 KiB
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 管线,也不会把小游戏协议提前写死到客户端框架里。
架构
新增目录:
Client/Assets/Script/xmain/Input/
核心组件:
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 负责鼠标/触摸捕获、死区、归一化方向、摇杆头视觉偏移。它不负责移动、相机、网络同步。
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; }
}
}
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 是单个技能键状态机。它负责点击、长按、拖拽瞄准、取消、冷却遮罩、次数、禁用视觉。它不关心按钮对应普攻、技能还是道具。
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
新增:
Doc/UIPrefabCreater/UI_VirtualJoystick.json
Client/Assets/Game/Art/UI/Prefab/UI_VirtualJoystick.prefab
它从现有 UI_LobbyJoystick 演进而来,保留:
BaseRingKnob
默认布局:
- 根节点锚到左下角。
- 参考分辨率
2048x1024。 - 默认中心位置约
(260, 220)。 - 默认尺寸
148x148。 - 根节点挂
VirtualJoystick。 - 继续复用
lobby_joystick_base、lobby_joystick_ring、lobby_joystick_knob。
旧 UI_LobbyJoystick 可以先保留,避免已有资源引用断裂。大厅运行时改用 UI_VirtualJoystick 后,旧预设可作为兼容资源,后续再清理。
UI_VirtualActionPad
新增:
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:默认在按钮组上方偏中区域,仅拖拽技能时显示。
每个按钮标准子节点:
IconCooldownMaskCooldownTextChargeTextAimIndicator或DragArrow
第一版视觉复用现有 Common 资源:
- 主按键底图优先使用
btn_primary_*。 - 次按键底图优先使用
btn_small_*或btn_warning_*。 - 图标先使用
ico_sword、ico_shield、ico_star、ico_heart、ico_item等临时示意图标。
如果后续补充圆形技能键切图,只需替换 JSON 资源和 prefab,不改变 VirtualActionButton/VirtualActionPad API。
摇杆交互
VirtualJoystick 行为:
- 捕获鼠标左键或触摸。
- 按 pointer ID 锁定一次操作,直到松开或取消。
- 支持开始捕获区域放大倍率,默认视觉半径
1.6x内都可开始控制。 - 计算屏幕指针相对摇杆中心的向量,并归一化到
[-1, 1]。 - 小于死区时输出
Value=Vector2.zero且IsActive=false。 - 大于死区时输出归一化方向且
IsActive=true。 Knob最大视觉移动距离由容器半径和 knob 半径计算,避免不同尺寸下越界。- 松开时重置方向、复位 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_VirtualJoystickUI_VirtualActionPad- 或两者都加载
小游戏客户端订阅事件并自行映射:
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 测试:
VirtualJoystick死区:小于死区输出 inactive,松开后归零。VirtualJoystick归一化:超过半径时 clamp 到单位圆。VirtualJoystickknob 行程:按容器和 knob 尺寸计算,不越界。VirtualActionButton点击:Down -> Tap。VirtualActionButton拖拽释放:Down -> Drag -> Release,并携带方向。VirtualActionButton拖拽取消:Down -> Drag -> Cancel。VirtualActionButton长按:Down -> HoldStart -> HoldTick -> HoldEnd。VirtualActionButtonBlocked:冷却中、禁用、次数为 0 时不发释放类事件。VirtualActionPad能按ActionButtonId找到 5 个按钮并转发事件。- 标准节点绑定成功,缺少可选节点不抛异常。
- 状态 API 能刷新冷却遮罩、冷却文字、次数文字和可用状态。
大厅接入测试:
LobbyWorldController消费VirtualJoystick输出值合成移动方向。- 相机输入排除摇杆区域时使用通用组件判断。
- 小游戏隔离时大厅摇杆不可见或不可交互。
手动 Unity 烟测:
- PC 鼠标拖动摇杆。
- 触摸或模拟触摸拖动摇杆。
- 主按键点击释放。
- 技能键拖拽瞄准释放。
- 技能键拖拽到取消区取消。
- 冷却遮罩和倒计时显示。
- 次数耗尽后按钮进入不可用态。
- 退出小游戏后大厅输入恢复。
实施顺序建议
- 为
VirtualJoystick和VirtualActionButton写核心状态测试。 - 实现
VirtualJoystick。 - 实现
VirtualActionButton和VirtualActionPad。 - 新增
UI_VirtualJoystick.json和UI_VirtualActionPad.json。 - 生成并检查 prefab。
- 迁移
LobbyWorldController使用VirtualJoystick。 - 加测试和手动烟测。
验收标准
- 大厅能继续使用左下角摇杆移动角色。
- 摇杆输入来自
VirtualJoystick事件,而不是LobbyWorldController私有采样逻辑。 - 小游戏能加载右下角
VirtualActionPad并收到 5 个按钮的纯输入事件。 - 技能键支持点击、长按、拖拽释放、拖拽取消、冷却、次数、禁用。
- 冷却中或次数为 0 时不会误发释放事件。
- 缺少可选视觉节点不会导致运行时异常。
- 现有小游戏框架合同不需要变更。