vampire-like/Assets/Plugins/InputModule/接入说明.md

926 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<BuiltinDataComponent>();
InputModule = UnityGameFramework.Runtime.GameEntry.GetComponent<InputModuleComponent>();
}
}
```
说明:
- `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<IProcedureManager> 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<BuiltinDataComponent>();
InputModule = UnityGameFramework.Runtime.GameEntry.GetComponent<InputModuleComponent>();
}
}
```
**第二步:启动 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` / EventSystemInputModule 只负责 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 2UI 与 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 资源)
- `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();
```
**自定义 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` 事件。
**消费模式:**
```csharp
private Action<InputDeviceKind> _onDeviceChanged;
private void OnEnable()
{
_onDeviceChanged = _ => RefreshPrompts();
GameEntry.InputModule.DeviceKindChanged += _onDeviceChanged;
RefreshPrompts(); // 初始状态
}
private void OnDisable()
{
GameEntry.InputModule.DeviceKindChanged -= _onDeviceChanged;
_onDeviceChanged = null;
}
private void RefreshPrompts()
{
if (GameEntry.InputModule.TryGetPrompt(InputActionId.Interact, out InputPrompt p))
_interactLabel.text = p.TextLabel;
if (GameEntry.InputModule.TryGetPrompt(InputActionId.Cancel, out InputPrompt c))
_cancelLabel.text = c.TextLabel;
}
```
**为什么不需要单独的 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 图集512x51216x16 单元格)
**完整 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`
**在文本中使用:**
```
按 <sprite="InputPrompt" name=keyboard_e> 交互
按 <sprite="InputPrompt" name=gamepad_a> 确认
```
**与 PromptMap 配合:**
```csharp
if (GameEntry.InputModule.TryGetPrompt(InputActionId.Interact, out InputPrompt prompt))
{
// 文本标签
_label.text = prompt.TextLabel;
// 图标(通过 TMP Sprite Tag
if (prompt.HasSprite)
{
_iconText.text = $"<sprite=\"InputPrompt\" name={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