# InputModule 接入说明 ## 设计原则 `InputModule` 是**可选功能模块**。 因此依赖方向必须始终保持为: - `InputModule` 可以依赖当前基座项目约定(UGF / `GameEntry` / Procedure 接入方式) - **基座项目默认代码不能反向依赖 `InputModule`** 也就是说: - 本仓库中的基座代码默认不直接接入输入模块 - 当某个项目决定启用该模块时,再按下面步骤**显式接入** --- ## 编译归属说明 当前模块采用的是: - `Base` 独立程序集 - `Runtime` 通过 **asmref** 并入 `SepCore.Runtime` 也就是说: - `Assets/Plugins/InputModule/Base/` 中的代码仍然是独立模块基础层 - `Assets/Plugins/InputModule/Runtime/` 中的代码虽然命名空间仍是 `SepCore.InputModule.Runtime`,但**编译归属属于 `SepCore.Runtime`** 这样做的目的,是让 `InputModuleComponent` 能与基座里的其他 Runtime Component 保持**平级协作**,避免为了跨程序集调用再额外抽一层接口。 因此这里的“可选”含义是: - **接入流程是可选的** - **导入模块后是否在项目里启用它是可选的** - 而不是要求 Runtime 代码必须物理编译成一个独立运行时程序集 --- ## 目录说明 - `Assets/Plugins/InputModule/Base/` - 独立基础层 - 提供 `InputContextId`、`InputActionId`、`InputCommand` 等基础定义 - `Assets/Plugins/InputModule/Runtime/` - 运行时代码 - 通过 `SepCore.Runtime.InputModule.asmref` 并入 `SepCore.Runtime` - `Assets/Tests/PlayMode/InputModule/` - 当前模块的 PlayMode 测试 --- ## 推荐接入流程 ### 1. 导入模块 将 `Assets/Plugins/InputModule` 导入到目标项目。 导入后请确认: - `Assets/Plugins/InputModule/Base/InputModule.Base.asmdef` - `Assets/Plugins/InputModule/Runtime/SepCore.Runtime.InputModule.asmref` 都已一并导入。 其中: - `InputModule.Base.asmdef` 提供基础定义程序集 - `SepCore.Runtime.InputModule.asmref` 负责把 Runtime 代码并入 `SepCore.Runtime` --- ### 2. 在 `GameEntry.Custom.cs` 中增加模块访问入口 文件: - `Assets/GameMain/Scripts/Runtime/Base/GameEntry.Custom.cs` 接入示例: ```csharp using SepCore.InputModule.Runtime; using SepCore.CustomComponent; using UnityEngine; public partial class GameEntry : MonoBehaviour { public static BuiltinDataComponent BuiltinData { get; private set; } public static InputModuleComponent InputModule { get; private set; } private static void InitCustomComponents() { BuiltinData = UnityGameFramework.Runtime.GameEntry.GetComponent(); InputModule = UnityGameFramework.Runtime.GameEntry.GetComponent(); } } ``` 说明: - `InputModule` 访问入口是**接入方项目自己加的** - 这一步是“启用模块”的显式行为 - 不启用模块的项目,不需要添加这段代码 - 这里虽然 `InputModuleComponent` 的命名空间是 `SepCore.InputModule.Runtime`,但它实际已经通过 asmref 编译进 `SepCore.Runtime` --- ### 3. 在 `Launcher` 场景中挂载 `InputModuleComponent` 推荐做法: - 在 `Launcher` 场景中新建一个 GameObject - 挂载 `SepCore.InputModule.Runtime.InputModuleComponent` 建议命名: - `InputModule` 你也可以把它挂在已有的系统节点上,只要生命周期符合项目约定即可。 推荐理解方式: - `InputModuleComponent` 是插件提供的 Runtime Component - 但它不是“高一层服务”,而是与 `BuiltinDataComponent`、`UIComponent`、`SoundComponent` 等在运行时层面平级协作 --- ### 4. 在游戏启动 Procedure 中显式初始化 推荐在某个启动流程里调用: ```csharp GameEntry.InputModule.OnInit(); ``` 例如: ```csharp protected override void OnEnter(IFsm procedureOwner) { base.OnEnter(procedureOwner); GameEntry.InputModule.OnInit(); } ``` 说明: - `OnInit()` 是模块正式开始工作的入口 - 这一步会初始化 action asset、缓存 action map、订阅输入回调、按配置加载重绑数据 --- ### 5. 在具体业务 Procedure / UI 中切换输入上下文 示例: ```csharp GameEntry.InputModule.SetContext(InputContextId.UI); GameEntry.InputModule.SetContext(InputContextId.GameplayExplore); GameEntry.InputModule.PushContext(InputContextId.Dialog); GameEntry.InputModule.PopContext(); GameEntry.InputModule.ClearContexts(); ``` --- ## 最小接入示例 以下是从零到跑通 InputModule 的完整步骤,可直接复制使用。 **第一步:GameEntry.Custom.cs** ```csharp using SepCore.InputModule.Runtime; using SepCore.CustomComponent; using UnityEngine; public partial class GameEntry : MonoBehaviour { public static BuiltinDataComponent BuiltinData { get; private set; } public static InputModuleComponent InputModule { get; private set; } private static void InitCustomComponents() { BuiltinData = UnityGameFramework.Runtime.GameEntry.GetComponent(); InputModule = UnityGameFramework.Runtime.GameEntry.GetComponent(); } } ``` **第二步:启动 Procedure 中调用 OnInit()** ```csharp protected override void OnEnter(ProcedureOwner procedureOwner) { base.OnEnter(procedureOwner); // ... 其他初始化 ... GameEntry.InputModule.OnInit(); } ``` **第三步:Launcher 场景挂载** 在 `Game Framework > Customs` 节点下新建 GameObject `InputModule`,挂载 `InputModuleComponent`。Inspector 中 `_inputActionsAsset` 留空即使用默认生成的 action map。 **第四步:业务中切换上下文** ```csharp // 进入游戏玩法 GameEntry.InputModule.SetGameplayExploreContext(); // 打开菜单 GameEntry.InputModule.SetUIContext(); // 关闭菜单,恢复玩法 GameEntry.InputModule.SetGameplayExploreContext(); // 打开对话(从 GameplayExplore 压栈) GameEntry.InputModule.PushContext(InputContextId.Dialog); // 关闭对话(弹栈恢复 GameplayExplore) GameEntry.InputModule.PopContext(); ``` --- ## Launcher 场景挂载检查清单 - [ ] 在 `Game Framework > Customs` 节点下新建 GameObject,命名 `InputModule` - [ ] 挂载 `InputModuleComponent`(`SepCore.InputModule.Runtime.InputModuleComponent`) - [ ] `_inputActionsAsset`:如果不使用默认 action,拖入自定义 `InputActionAsset`;否则留空(自动使用代码生成的默认 action) - [ ] `_startupContext`:推荐设为 `GameplayExplore` 或 `None` - [ ] `_loadBindingOverridesOnInit`:一般保持 `true` - [ ] `_saveBindingOverridesOnDestroy`:按需设置 - [ ] 检查 EventSystem:`InputSystemUIInputModule` 保持启用,作为当前阶段 UI 主路径 --- ## 推荐 GameObject 命名规范 - 推荐命名:`InputModule` - 推荐挂载位置:`Game Framework > Customs` 节点下(与 `Builtin Data` 平级) - 不要挂在 `Builtin` 预制体内部(那是框架自带的,不应修改) - `InputModuleComponent` 标记了 `[DisallowMultipleComponent]`,同一 GameObject 上只能挂一个 --- ## 与 InputSystemUIInputModule 的职责划分 当前推荐策略是:**UI 原始输入继续交给 `InputSystemUIInputModule` / EventSystem,InputModule 只负责 gameplay 语义输入与上下文切换。** **分工:** - `InputSystemUIInputModule`:驱动 Unity EventSystem,处理 UI 交互(按钮点击、拖拽、导航等) - `StandaloneInputModule`:旧版输入模块兜底(是否保留按项目兼容需求决定) - `InputModuleComponent`:处理 gameplay / 业务语义输入(通过 `CommandTriggered` 事件和 `RegisterListener`),不直接驱动 EventSystem **推荐策略:** - **移动端 UI:** 普通按钮点击、拖拽、滚动等继续走 `InputSystemUIInputModule + EventSystem` - **PC UI:** 鼠标指针点击继续走 `InputSystemUIInputModule + EventSystem` - **主机 UI:** 当前阶段也优先沿用 EventSystem 导航能力,而不是为了统一入口额外接管 UI 输入 - **Gameplay 输入:** `Pause / Move / Interact / Sprint` 等语义继续由 `InputModule` 管理 换句话说: - **UI 事件分发** 不作为当前阶段 InputModule 的统一目标 - **Gameplay 语义输入统一** 才是当前阶段 InputModule 的主线目标 - **UI 打开/关闭** 对 InputModule 的主要影响是“切换 gameplay context”,而不是“替代 EventSystem” --- ## 上下文切换示例 ### 场景 1: 游戏探索中打开菜单 ```csharp // 打开菜单 GameEntry.InputModule.SetContext(InputContextId.UI); // 关闭菜单,恢复探索 GameEntry.InputModule.SetGameplayExploreContext(); ``` ### 场景 2: 游戏探索中触发对话 ```csharp // 对话打开(GameplayExplore 被压栈,只有栈顶 Dialog 生效,GameplayExplore 暂停) GameEntry.InputModule.PushContext(InputContextId.Dialog); // 监听对话输入 GameEntry.InputModule.RegisterListener(InputActionId.Confirm, OnDialogConfirm); GameEntry.InputModule.RegisterListener(InputActionId.Cancel, OnDialogCancel); // 对话关闭(弹栈,恢复 GameplayExplore) GameEntry.InputModule.UnregisterListener(InputActionId.Confirm, OnDialogConfirm); GameEntry.InputModule.UnregisterListener(InputActionId.Cancel, OnDialogCancel); GameEntry.InputModule.PopContext(); ``` ### 场景 3: UI 中弹出确认对话框 ```csharp // 弹出确认框(替换当前 UI 上下文) GameEntry.InputModule.SetContext(InputContextId.Dialog); // 确认框关闭后恢复 UI GameEntry.InputModule.SetUIContext(); ``` --- ## 上下文栈语义 栈的作用是**记住恢复历史**,而非同时启用所有上下文。`ApplyContextState()` 只启用 Global + 栈顶上下文: - `SetContext(X)`:清空栈,压入 X → 只有 Global + X 生效 - `PushContext(X)`:压入 X → 只有 Global + X 生效,之前的上下文被暂停 - `PopContext()`:弹出栈顶 → 恢复到前一个上下文 - `ClearContexts()`:清空栈 → 只有 Global 生效 这意味着 `PushContext(Dialog)` 可以安全地用在任何上下文之上,不需要担心绑定重叠。 ## Dialog 上下文说明 - Dialog action map 提供 `Navigate` / `Confirm` / `Cancel` 三个 action,绑定与 UI map 一致 - 从任何上下文切换到 Dialog 都可用 `PushContext(Dialog)`,栈顶语义保证只有 Dialog 生效 - Dialog 关闭后,用 `PopContext()` 恢复之前的上下文,或用 `SetContext()` 指定目标上下文 --- ## 当前 Phase 1 提供的能力 - 默认 `InputActionAsset` 代码生成 - `Global / UI / GameplayExplore / Dialog` 四组 action map - 设备识别: - `KeyboardMouse` - `Gamepad` - `Touch` - 输入上下文栈 - 命令分发 - 绑定覆盖保存 / 加载骨架 --- ## Phase 2:UI 与 Gameplay 协作能力 ### 设计约定 **上下文切换采用混合方案:** | 层级 | 职责 | 方式 | |------|------|------| | Procedure | 大场景级切换(Menu ↔ Main) | `SetContext()` 清栈重置 | | UIForm | 覆盖层管理(Dialog 弹出/关闭) | `PushContext()` / `PopContext()` 压栈弹栈 | **UI 阻断 Gameplay 输入**:栈语义天然支持。`ApplyContextState()` 只启用 Global + 栈顶上下文,当 Dialog 压栈后 GameplayExplore 自动禁用。 --- ### Procedure 层接入模板 在启动 Procedure 中初始化 InputModule,在业务 Procedure 中设置上下文。 **ProcedurePreload(或你选择的启动 Procedure):** ```csharp protected override void OnEnter(ProcedureOwner procedureOwner) { base.OnEnter(procedureOwner); // ... 其他初始化 ... GameEntry.InputModule?.OnInit(); } ``` **ProcedureMenu(进入菜单):** ```csharp protected override void OnEnter(ProcedureOwner procedureOwner) { base.OnEnter(procedureOwner); GameEntry.InputModule?.SetContext(InputContextId.UI); // ... } ``` **ProcedureMain(进入游戏):** ```csharp protected override void OnEnter(ProcedureOwner procedureOwner) { base.OnEnter(procedureOwner); GameEntry.InputModule?.SetContext(InputContextId.GameplayExplore); // ... } ``` --- ### UIForm 层接入模板(InputAwareGuiForm) `InputAwareGuiForm` 是一个可选的 UIForm 基类,自动管理输入上下文的 Push/Pop。将以下代码复制到你的项目中(例如 `Assets/GameMain/Scripts/UI/InputAwareGuiForm.cs`): ```csharp using SepCore.InputModule; namespace SepCore.UI { public abstract class InputAwareGuiForm : UGuiForm { protected virtual InputContextId InputContext => InputContextId.None; #if UNITY_2017_3_OR_NEWER protected override void OnOpen(object userData) #else protected internal override void OnOpen(object userData) #endif { base.OnOpen(userData); if (InputContext != InputContextId.None) { GameEntry.InputModule?.PushContext(InputContext); } } #if UNITY_2017_3_OR_NEWER protected override void OnClose(bool isShutdown, object userData) #else protected internal override void OnClose(bool isShutdown, object userData) #endif { if (!isShutdown && InputContext != InputContextId.None) { GameEntry.InputModule?.PopContext(); } base.OnClose(isShutdown, userData); } } } ``` **使用方式:** 让需要输入上下文管理的 UIForm 继承 `InputAwareGuiForm` 替代 `UGuiForm`,覆写 `InputContext` 属性。 **DialogForm 示例:** ```csharp public class DialogForm : InputAwareGuiForm { protected override InputContextId InputContext => InputContextId.Dialog; // ... 其余不变 } ``` **说明:** - `InputContext` 返回 `None` 时不做任何上下文操作(默认行为,等同于直接继承 `UGuiForm`) - `isShutdown` 时跳过 Pop,避免游戏关闭时栈状态不一致 - 所有调用使用 `?.` 运算符,InputModule 未接入时行为不变 --- ## 业务接入约束 ### 1. 不要直接读取具体设备 业务层代码应该消费 `InputCommand` 提供的语义信息,而不是直接查询当前设备类型。 **推荐:** ```csharp GameEntry.InputModule.RegisterListener(InputActionId.Move, OnMove); private void OnMove(InputCommand cmd) { // 消费 Vector2 值,不关心来自键盘还是手柄 Vector2 direction = cmd.Vector2Value; } ``` **避免:** ```csharp // 不推荐:业务层直接判断设备类型 if (GameEntry.InputModule.CurrentDeviceKind == InputDeviceKind.Gamepad) { // 设备相关的差异化逻辑应该放在 presentation 层或专门的适配层 } ``` **原因:** - 设备类型是运行时动态切换的(键鼠 ↔ 手柄热插拔) - 业务逻辑应该与输入设备解耦,只关心“玩家发出了什么语义指令” - 如果确实需要设备提示(如 UI 按钮图标切换),应在 UI 层监听 `DeviceKindChanged` 事件,而不是在 gameplay 业务中判断 ### 2. UI 原始输入继续走 EventSystem InputModule 不接管 UI 原始输入(点击、拖拽、滚动、导航)。UI 交互继续由 Unity EventSystem 处理。 **分工:** - **UI 点击 / 拖拽 / 滚动** → `EventSystem` / `InputSystemUIInputModule` - **Gameplay 语义输入**(Move、Interact、Sprint) → `InputModuleComponent` - **UI 打开 / 关闭** → 只负责切换 gameplay context(如 Push/Pop Dialog),不负责转发 UI 事件 **原因:** - EventSystem 已经成熟处理 UI 交互,不需要重复实现 - PC 鼠标指针、Mobile 触摸、主机导航键各自有不同的 UI 处理路径 - 强行统一会引入双重分发,增加维护复杂度 ### 3. UI 打开 / 关闭只切换 context,不接管事件分发 UIForm / Procedure 通过 `SetContext` / `PushContext` / `PopContext` 管理 gameplay 输入的启用与暂停,但不应该尝试通过 InputModule 分发 UI 事件。 **正确示例:** ```csharp // DialogForm 打开时暂停 gameplay 输入 GameEntry.InputModule.PushContext(InputContextId.Dialog); // DialogForm 中的按钮点击仍然走 UnityEvent / EventSystem _confirmButton.onClick.AddListener(OnConfirm); ``` **错误示例:** ```csharp // 不要这样做:试图用 InputModule 替代按钮点击 GameEntry.InputModule.RegisterListener(InputActionId.Confirm, _ => { // 不要在 UI 内部用 Confirm action 模拟按钮点击 // 这会导致 EventSystem 和 InputModule 双重响应 }); ``` --- ## 平台输入职责划分 ### PC / 主机 - Gameplay 输入:优先走 `InputModule` - UI:优先走 `EventSystem / InputSystemUIInputModule` - 若未来确实需要主机平台专门的 UI 统一入口,再单独设计和验证 ### 移动端 - 普通 UI 点击 / 拖拽 / 滚动:直接走 Unity EventSystem - 虚拟摇杆 / 虚拟按钮:通过 `VirtualJoystickBridge` / `VirtualButtonBridge` 适配进 InputModule - 不要求把每一次触屏 UI 操作都抽象成 `Confirm / Cancel / Navigate` --- ## 当前 Phase 1 不负责的内容 - 基座默认自动接入 - 自动修改 `GameEntry` / Procedure - 交互式重绑界面 - 具体业务语义绑定(例如战斗输入、菜单输入、剧情输入) 这些都应该在后续阶段或接入方项目里完成。 --- ## 为什么不能用 `InputModuleEntry` 像 `InputModuleEntry` 这种插件内部静态入口,看起来方便,但不符合这里的目标: 1. 它绕过了项目自己的 `GameEntry` 接入约定 2. 它弱化了“这是一个可选模块,需要显式接入”的边界 3. 它会让模块拥有一套平行于基座的入口体系,后续维护容易混乱 所以这里统一采用: - **模块提供 `InputModuleComponent`** - **项目自己决定是否在 `GameEntry` 中暴露 `InputModule`** - **项目自己决定在哪个 Procedure 里调用 `OnInit()`** - **模块 Runtime 代码通过 asmref 并入 `SepCore.Runtime`,与基座 Component 平级协作** 这才符合”可选模块”的接入方式。 --- ## P4:设备适配体验 ### 按钮提示映射表(Prompt Map) `InputModuleComponent` 提供了一个可选的 prompt map 系统,用于获取”当前设备下某 action 的按键提示”。 **核心类型:** - `InputPrompt`(Base 层 readonly struct):承载提示数据 - `TextLabel`:文本标签(如 `”A”`, `”Enter”`, `”E”`) - `SpriteName`:可选的 sprite 资源键(项目自定义,模块不管理 sprite 资源);多个 sprite 可用 `|` 分隔,如 `keyboard_w|keyboard_a|keyboard_s|keyboard_d` - `HasSprite` / `IsValid`:便捷检查属性 - `IInputPromptMap`(Base 层接口):项目可自定义实现 - `TryGetPrompt(InputActionId, InputDeviceKind, out InputPrompt)`:查找提示 **默认映射表(Xbox 惯例):** | InputActionId | KeyboardMouse | Gamepad | Touch | |---|---|---|---| | Pause | `Esc` | `Menu` | `Pause` | | Navigate | `WASD` | `L Stick` | `Swipe` | | Confirm | `Enter` | `A` | `Tap` | | Cancel | `Esc` | `B` | `Back` | | Move | `WASD` | `L Stick` | `Joystick` | | Sprint | `L Shift` | `L3` | *(无)* | | Interact | `E` | `A` | `Tap` | **使用方式:** ```csharp // 便捷方法:自动使用当前设备类型 if (GameEntry.InputModule.TryGetPrompt(InputActionId.Interact, out InputPrompt prompt)) { _interactLabel.text = prompt.TextLabel; } // 直接查询映射表:指定设备类型 IInputPromptMap map = GameEntry.InputModule.PromptMap; map.TryGetPrompt(InputActionId.Confirm, InputDeviceKind.Gamepad, out InputPrompt p); // 替换为自定义映射(如 PS 命名) GameEntry.InputModule.PromptMap = new MyPlayStationPromptMap(); ``` **TMP 文本工具:** `Assets/Plugins/InputModule/Presentation/InputPromptTextUtility.cs` 提供了将 `InputPrompt` 转成 TMP 文本的工具: ```csharp if (GameEntry.InputModule.TryGetPrompt(InputActionId.Move, out InputPrompt prompt)) { _movePromptText.text = InputPromptTextUtility.BuildTmpText(prompt); } ``` - `BuildTmpText(InputPrompt)`:输出 `" ... 移动"` 这类可直接给 `TMP_Text.text` 的字符串 - `BuildSpriteTags(string)`:只把 `SpriteName` 转成 TMP sprite tag - `SpriteName` 使用 `|` 分隔时会输出多个 sprite tag;单个 sprite 仍按原方式输出 **自定义 PromptMap 示例:** ```csharp public sealed class PlayStationPromptMap : IInputPromptMap { public bool TryGetPrompt(InputActionId actionId, InputDeviceKind deviceKind, out InputPrompt prompt) { if (deviceKind == InputDeviceKind.Gamepad) { switch (actionId) { case InputActionId.Confirm: prompt = new InputPrompt(“Cross”); return true; case InputActionId.Cancel: prompt = new InputPrompt(“Circle”); return true; // ... } } // 回退到默认 prompt = InputPrompt.None; return false; } } ``` ### 提示刷新协议 **触发器:** `DeviceKindChanged` 事件。无需单独的 `PromptsChanged` 事件。 **推荐接入现有 UI 架构:** 当前项目 UI 采用 `Controller -> Context -> Form`,输入提示也应沿用这条数据流: - Controller:监听 `GameEntry.InputModule.DeviceKindChanged`,查询 `TryGetPrompt()`,用 `InputPromptTextUtility.BuildTmpText()` 生成 TMP 文本 - Context:保存当前 UI 需要展示的提示字符串,如 `MovePrompt`、`ConfirmPrompt`、`CancelPrompt` - Form:只负责展示,不直接判断设备类型,也不直接查询 InputModule **Hud 示例:** ```csharp public class HudContext : UIContext { public string MovePrompt { get; set; } } ``` ```csharp private static HudContext BuildHudContext() { return new HudContext { MovePrompt = BuildPromptText(InputActionId.Move) }; } private static string BuildPromptText(InputActionId actionId) { if (GameEntry.InputModule == null || !GameEntry.InputModule.TryGetPrompt(actionId, out InputPrompt prompt)) { return null; } return InputPromptTextUtility.BuildTmpText(prompt); } protected override void SubscribeCustomEvents() { GameEntry.InputModule.DeviceKindChanged += OnDeviceKindChanged; } protected override void UnsubscribeCustomEvents() { GameEntry.InputModule.DeviceKindChanged -= OnDeviceKindChanged; } private void OnDeviceKindChanged(InputDeviceKind deviceKind) { if (Context == null || Form == null) { return; } Context.MovePrompt = BuildPromptText(InputActionId.Move); Form.RefreshMovePrompt(Context.MovePrompt); } ``` ```csharp public void RefreshMovePrompt(string prompt) { bool visible = !string.IsNullOrEmpty(prompt); _movePromptRoot.SetActive(visible); _movePromptText.text = prompt; } ``` **为什么不需要单独的 PromptsChanged 事件?** - `DeviceKindChanged` 已经捕获了唯一会改变提示的场景 - 如果未来需要”重绑后刷新提示”(不切换设备),可以届时再加新事件 ### UI 展示约定 **KeyboardMouse:** - 显示文本标签(”E”, “Enter”, “Esc”) - 推荐等宽字体或按键帽风格 - 可选:显示 key-cap sprite(如果 `InputPrompt.HasSprite` 为 true) **Gamepad:** - 显示按钮名(”A”, “B”, “L Stick”) - 默认使用 Xbox 惯例,PS 项目替换自定义映射 - 可选:显示按钮 sprite **Touch:** - 显示手势描述(”Tap”, “Swipe”) - 主要用于教程/引导 - Sprint 无默认 touch 提示(虚拟按钮为 P5 范围) **Unknown:** - 隐藏提示元素,或显示通用兜底”Press any button” - Unknown 状态仅在启动时、首次输入前出现 **通用规则:** - 不要硬编码提示字符串,始终查询 PromptMap - `TryGetPrompt` 返回 false 时隐藏或显示兜底 ### 灵敏度/死区配置 **评估结论:暂不实现。** 理由: 1. InputSystem 内置 `StickDeadzone`(默认 0.125)和 `AxisDeadzone` 处理器已自动应用 2. 项目中无任何灵敏度/死区相关代码,属于推测性需求 3. 当前范围为 PC + 主机,键盘/鼠标无灵敏度概念,手柄使用 InputSystem 默认值已足够 4. 移动端(虚拟摇杆死区)为 P5 范围 未来如需实现:Base 层加 `InputSensitivityConfig` 数据结构,Runtime 层在 `EnsureInitialized()` 中注入自定义 Processor。 --- ### 可选:TMP Sprite Asset(输入提示图标) `Assets/Plugins/InputModule/Input_Prompts/` 包含一个预制的 TMP Sprite Asset,可用于在 TextMeshPro 中显示按键图标。 **文件说明:** - `InputPrompt.asset` — TMP Sprite Asset,包含键盘、手柄、鼠标等按键图标 - `Input_Prompt.png` — 对应的 Sprite 图集(512x512,16x16 单元格) **完整 Sprite 名称对照表:** | Sprite Name | 说明 | Sprite Name | 说明 | |-------------|------|-------------|------| | `gamepad_button` | 通用手柄按钮(占位) | `gamepad_a` | A 键 | | `gamepad_b` | B 键 | `gamepad_x` | X 键 | | `gamepad_y` | Y 键 | `gamepad_map` | Map / View 键 | | `gamepad_menu` | Menu / Start 键 | `gamepad_control` | 方向键(十字键) | | `gamepad_up_control` | 方向键上 | `gamepad_right_control` | 方向键右 | | `gamepad_down_control` | 方向键下 | `gamepad_left_control` | 方向键左 | | `gamepad_horizon_control` | 方向键水平(占位) | `gamepad_vertical_control` | 方向键垂直(占位) | | `gamepad_full_control` | 方向键全方向(占位) | `gamepad_ls` | 左摇杆 | | `gamepad_up_ls` | 左摇杆上推 | `gamepad_right_ls` | 左摇杆右推 | | `gamepad_down_ls` | 左摇杆下推 | `gamepad_left_ls` | 左摇杆左推 | | `gamepad_horizon_ls` | 左摇杆水平(占位) | `gamepad_vertical_ls` | 左摇杆垂直(占位) | | `gamepad_full_ls` | 左摇杆全方向(占位) | `gamepad_press_ls` | 左摇杆按下(L3) | | `gamepad_rs` | 右摇杆 | `gamepad_up_rs` | 右摇杆上推 | | `gamepad_right_rs` | 右摇杆右推 | `gamepad_down_rs` | 右摇杆下推 | | `gamepad_left_rs` | 右摇杆左推 | `gamepad_horizon_rs` | 右摇杆水平(占位) | | `gamepad_vertical_rs` | 右摇杆垂直(占位) | `gamepad_full_rs` | 右摇杆全方向(占位) | | `gamepad_press_rs` | 右摇杆按下(R3) | `gamepad_left_trigger` | 左扳机(LT) | | `gamepad_right_trigger` | 右扳机(RT) | `gamepad_left_bumper` | 左肩键(LB) | | `gamepad_right_bumper` | 右肩键(RB) | | | | `keyboard_esc` | Esc 键 | `keyboard_f1` ~ `keyboard_f12` | F1 ~ F12 | | `keyboard_1` ~ `keyboard_0` | 1 ~ 0 | `keyboard_minus` | 减号 / 连字符 | | `keyboard_equal` | 等号 | `keyboard_backspace` | 退格键 | | `keyboard_q` ~ `keyboard_p` | Q ~ P | `keyboard_left_bracket` | 左方括号 `[` | | `keyboard_right_bracket` | 右方括号 `]` | `keyboard_backslash` | 反斜杠 `\` | | `keyboard_a` ~ `keyboard_l` | A ~ L | `keyboard_single_quote` | 单引号 `'` | | `keyboard_semicolon` | 分号 `;` | `keyboard_enter` | 回车键 | | `keyboard_z` ~ `keyboard_m` | Z ~ M | `keyboard_slash` | 斜杠 `/` | | `keyboard_up_arrow` | 上箭头 | `keyboard_right_arrow` | 右箭头 | | `keyboard_down_arrow` | 下箭头 | `keyboard_left_arrow` | 左箭头 | | `keyboard_alt` | Alt 键 | `keyboard_tab` | Tab 键 | | `keyboard_delete` | Delete 键 | `keyboard_period` | 句号 `.` | | `keyboard_control` | Ctrl 键 | `keyboard_capslock` | Caps Lock | | `keyboard_comma` | 逗号 `,` | `keyboard_space` | 空格键 | | `keyboard_shift` | Shift 键 | | | | `mouse` | 鼠标指针 | `mouse_left` | 鼠标左键 | | `mouse_right` | 鼠标右键 | `mouse_middle` | 鼠标中键 | | `mouse_scroll_up` | 滚轮上滚 | `mouse_scroll_down` | 滚轮下滚 | | `mouse_scroll` | 滚轮(占位) | | | **接入方式(可选):** 若项目需要在 UI 中显示按键图标(而非纯文本),将 `InputPrompt.asset` 设为 TMP 的 Default Sprite Asset: 1. 打开 **Edit > Project Settings > TextMeshPro** 2. 在 **Default Sprite Asset** 中拖入 `Assets/Plugins/InputModule/Input_Prompts/InputPrompt.asset` **在文本中使用:** ``` 按 交互 按 确认 ``` **与 PromptMap 配合:** ```csharp if (GameEntry.InputModule.TryGetPrompt(InputActionId.Interact, out InputPrompt prompt)) { _label.text = InputPromptTextUtility.BuildTmpText(prompt); } ``` 如果只想显示图标,可以使用: ```csharp _iconText.text = InputPromptTextUtility.BuildSpriteTags(prompt.SpriteName); ``` **说明:** - 此步骤完全可选;不设置 Default Sprite Asset 不影响任何核心功能 - 若项目已有自己的按键图标系统,可直接忽略此文件夹 - `SpriteName` 的值由 `IInputPromptMap` 实现决定,需与 `InputPrompt.asset` 中的 sprite name 保持一致 --- ## P5:移动端扩展 ### 架构 移动端输入沿用 P2 确定的边界: - **普通 UI 点击 / 拖拽 / 滚动** → 继续走 `EventSystem / InputSystemUIInputModule` - **Gameplay 语义输入**(Move / Sprint / Interact)→ 通过虚拟摇杆 / 虚拟按钮桥接进 `InputModuleComponent` 虚拟控件代码位于 `Assets/Plugins/InputModule/Presentation/`,通过 `SepCore.Presentation.InputModule.asmref` 编译进 `SepCore.Presentation`,与基座零耦合。 ### 虚拟摇杆(VirtualJoystickBridge) **组件位置:** `SepCore.InputModule.Runtime.VirtualInput.VirtualJoystickBridge` **挂载要求:** 挂在带有 `Joystick`(或 `VariableJoystick` / `FloatingJoystick` / `DynamicJoystick`)的 GameObject 上。 **字段:** | 字段 | 说明 | 默认值 | |------|------|--------| | `_actionId` | 映射的 Action(如 `Move`) | `Move` | | `_contextId` | 所属上下文 | `GameplayExplore` | | `_injectDeviceKind` | 触摸时是否强制设备类型为 `Touch` | `true` | **使用方式:** 1. 在 Canvas 下创建空 GameObject 2. 挂载 `VariableJoystick`(或 `FloatingJoystick` / `FixedJoystick`) 3. 在同 GameObject 上挂载 `VirtualJoystickBridge` 4. 配置 `_actionId` 为 `Move` ```csharp // 业务层无需额外代码 // 摇杆输入会自动通过 CommandTriggered / RegisterListener 分发 GameEntry.InputModule.RegisterListener(InputActionId.Move, OnMove); private void OnMove(InputCommand cmd) { Vector2 direction = cmd.Vector2Value; // 与键盘/手柄输入完全一致 } ``` ### 虚拟按钮(VirtualButtonBridge) **组件位置:** `SepCore.InputModule.Runtime.VirtualInput.VirtualButtonBridge` **挂载要求:** 挂在 UI 按钮(或任意带 `Collider2D` / `RectTransform` 的交互元素)上。需要场景中已有 `EventSystem`。 **字段:** | 字段 | 说明 | 默认值 | |------|------|--------| | `_actionId` | 映射的 Action(如 `Sprint` / `Interact`) | — | | `_contextId` | 所属上下文 | `GameplayExplore` | | `_injectDeviceKind` | 触摸时是否强制设备类型为 `Touch` | `true` | **使用方式:** 1. 在 Canvas 下创建 UI 按钮 GameObject 2. 挂载 `VirtualButtonBridge` 3. 配置 `_actionId` 为 `Sprint` 或 `Interact` ```csharp GameEntry.InputModule.RegisterListener(InputActionId.Sprint, OnSprint); ``` ### 设备切换 当玩家触摸虚拟摇杆或虚拟按钮时: - `VirtualJoystickBridge` / `VirtualButtonBridge` 自动调用 `GameEntry.InputModule.ForceDeviceKind(InputDeviceKind.Touch)` - 触发 `DeviceKindChanged` 事件 - UI 层监听该事件,刷新按键提示(如将 "E" 切换为 "Tap") ### 虚拟控件显示 / 隐藏 由业务层根据 `DeviceKindChanged` 控制,InputModule 不强制管理: ```csharp private void OnDeviceKindChanged(InputDeviceKind kind) { _joystickCanvas.SetActive(kind == InputDeviceKind.Touch); } ``` 推荐策略: - PC / 主机:隐藏虚拟控件 - 移动端(检测到 `Touch`):显示虚拟控件 - 键鼠 ↔ 手柄热插拔时不影响虚拟控件显隐(只有 `Touch` 才显示) ### 预制体 `Assets/Plugins/InputModule/Assets/Joystick/Prefabs/` 提供四种摇杆预制体: - `Fixed Joystick`:固定位置 - `Floating Joystick`:按下时于触点浮现 - `Dynamic Joystick`:按下时浮现,拖动超过阈值后背景跟随移动 - `Variable Joystick`:运行时可通过 `SetMode(JoystickType)` 切换以上三种模式 可直接拖入 Canvas 使用。 ### 与 Joystick Pack 的关系 `Assets/Plugins/InputModule/Presentation/JoystickPack/` 下的代码是 Joystick Pack 的副本(已修复 `AxisOptions` 递归属性 bug)。这些代码通过 `SepCore.Presentation.InputModule.asmref` 编译进 `SepCore.Presentation`,不修改基座程序集引用。 如果未来需要更新 Joystick Pack: 1. 替换 `JoystickPack/` 下的脚本文件 2. 重新修复第24行的 `AxisOptions` 属性 bug