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

32 KiB
Raw Blame History

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/
    • 独立基础层
    • 提供 InputContextIdInputActionIdInputCommand 等基础定义
  • 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

接入示例:

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
  • 但它不是“高一层服务”,而是与 BuiltinDataComponentUIComponentSoundComponent 等在运行时层面平级协作

4. 在游戏启动 Procedure 中显式初始化

推荐在某个启动流程里调用:

GameEntry.InputModule.OnInit();

例如:

protected override void OnEnter(IFsm<IProcedureManager> procedureOwner)
{
    base.OnEnter(procedureOwner);
    GameEntry.InputModule.OnInit();
}

说明:

  • OnInit() 是模块正式开始工作的入口
  • 这一步会初始化 action asset、缓存 action map、订阅输入回调、按配置加载重绑数据

5. 在具体业务 Procedure / UI 中切换输入上下文

示例:

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

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()

protected override void OnEnter(ProcedureOwner procedureOwner)
{
    base.OnEnter(procedureOwner);
    // ... 其他初始化 ...
    GameEntry.InputModule.OnInit();
}

第三步Launcher 场景挂载

Game Framework > Customs 节点下新建 GameObject InputModule,挂载 InputModuleComponent。Inspector 中 _inputActionsAsset 留空即使用默认生成的 action map。

第四步:业务中切换上下文

// 进入游戏玩法
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
  • 挂载 InputModuleComponentSepCore.InputModule.Runtime.InputModuleComponent
  • _inputActionsAsset:如果不使用默认 action拖入自定义 InputActionAsset;否则留空(自动使用代码生成的默认 action
  • _startupContext:推荐设为 GameplayExploreNone
  • _loadBindingOverridesOnInit:一般保持 true
  • _saveBindingOverridesOnDestroy:按需设置
  • 检查 EventSystemInputSystemUIInputModule 保持启用,作为当前阶段 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: 游戏探索中打开菜单

// 打开菜单
GameEntry.InputModule.SetContext(InputContextId.UI);

// 关闭菜单,恢复探索
GameEntry.InputModule.SetGameplayExploreContext();

场景 2: 游戏探索中触发对话

// 对话打开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 中弹出确认对话框

// 弹出确认框(替换当前 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

protected override void OnEnter(ProcedureOwner procedureOwner)
{
    base.OnEnter(procedureOwner);
    // ... 其他初始化 ...
    GameEntry.InputModule?.OnInit();
}

ProcedureMenu进入菜单

protected override void OnEnter(ProcedureOwner procedureOwner)
{
    base.OnEnter(procedureOwner);
    GameEntry.InputModule?.SetContext(InputContextId.UI);
    // ...
}

ProcedureMain进入游戏

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

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 示例:

public class DialogForm : InputAwareGuiForm
{
    protected override InputContextId InputContext => InputContextId.Dialog;
    // ... 其余不变
}

说明:

  • InputContext 返回 None 时不做任何上下文操作(默认行为,等同于直接继承 UGuiForm
  • isShutdown 时跳过 Pop避免游戏关闭时栈状态不一致
  • 所有调用使用 ?. 运算符InputModule 未接入时行为不变

业务接入约束

1. 不要直接读取具体设备

业务层代码应该消费 InputCommand 提供的语义信息,而不是直接查询当前设备类型。

推荐:

GameEntry.InputModule.RegisterListener(InputActionId.Move, OnMove);

private void OnMove(InputCommand cmd)
{
    // 消费 Vector2 值,不关心来自键盘还是手柄
    Vector2 direction = cmd.Vector2Value;
}

避免:

// 不推荐:业务层直接判断设备类型
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、SprintInputModuleComponent
  • UI 打开 / 关闭 → 只负责切换 gameplay context如 Push/Pop Dialog不负责转发 UI 事件

原因:

  • EventSystem 已经成熟处理 UI 交互,不需要重复实现
  • PC 鼠标指针、Mobile 触摸、主机导航键各自有不同的 UI 处理路径
  • 强行统一会引入双重分发,增加维护复杂度

3. UI 打开 / 关闭只切换 context不接管事件分发

UIForm / Procedure 通过 SetContext / PushContext / PopContext 管理 gameplay 输入的启用与暂停,但不应该尝试通过 InputModule 分发 UI 事件。

正确示例:

// DialogForm 打开时暂停 gameplay 输入
GameEntry.InputModule.PushContext(InputContextId.Dialog);

// DialogForm 中的按钮点击仍然走 UnityEvent / EventSystem
_confirmButton.onClick.AddListener(OnConfirm);

错误示例:

// 不要这样做:试图用 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 的按键提示”。

核心类型:

  • InputPromptBase 层 readonly struct承载提示数据
    • TextLabel:文本标签(如 ”A”, ”Enter”, ”E”
    • SpriteName:可选的 sprite 资源键(项目自定义,模块不管理 sprite 资源);多个 sprite 可用 | 分隔,如 keyboard_w|keyboard_a|keyboard_s|keyboard_d
    • HasSprite / IsValid:便捷检查属性
  • IInputPromptMapBase 层接口):项目可自定义实现
    • 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

使用方式:

// 便捷方法:自动使用当前设备类型
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 文本的工具:

if (GameEntry.InputModule.TryGetPrompt(InputActionId.Move, out InputPrompt prompt))
{
    _movePromptText.text = InputPromptTextUtility.BuildTmpText(prompt);
}
  • BuildTmpText(InputPrompt):输出 "<sprite name=\"keyboard_w\"> <sprite name=\"keyboard_a\"> ... 移动" 这类可直接给 TMP_Text.text 的字符串
  • BuildSpriteTags(string):只把 SpriteName 转成 TMP sprite tag
  • SpriteName 使用 | 分隔时会输出多个 sprite tag单个 sprite 仍按原方式输出

自定义 PromptMap 示例:

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 需要展示的提示字符串,如 MovePromptConfirmPromptCancelPrompt
  • Form只负责展示不直接判断设备类型也不直接查询 InputModule

Hud 示例:

public class HudContext : UIContext
{
    public string MovePrompt { get; set; }
}
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);
}
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 图集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 配合:

if (GameEntry.InputModule.TryGetPrompt(InputActionId.Interact, out InputPrompt prompt))
{
    _label.text = InputPromptTextUtility.BuildTmpText(prompt);
}

如果只想显示图标,可以使用:

_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 映射的 ActionMove Move
_contextId 所属上下文 GameplayExplore
_injectDeviceKind 触摸时是否强制设备类型为 Touch true

使用方式:

  1. 在 Canvas 下创建空 GameObject
  2. 挂载 VariableJoystick(或 FloatingJoystick / FixedJoystick
  3. 在同 GameObject 上挂载 VirtualJoystickBridge
  4. 配置 _actionIdMove
// 业务层无需额外代码
// 摇杆输入会自动通过 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 映射的 ActionSprint / Interact
_contextId 所属上下文 GameplayExplore
_injectDeviceKind 触摸时是否强制设备类型为 Touch true

使用方式:

  1. 在 Canvas 下创建 UI 按钮 GameObject
  2. 挂载 VirtualButtonBridge
  3. 配置 _actionIdSprintInteract
GameEntry.InputModule.RegisterListener(InputActionId.Sprint, OnSprint);

设备切换

当玩家触摸虚拟摇杆或虚拟按钮时:

  • VirtualJoystickBridge / VirtualButtonBridge 自动调用 GameEntry.InputModule.ForceDeviceKind(InputDeviceKind.Touch)
  • 触发 DeviceKindChanged 事件
  • UI 层监听该事件,刷新按键提示(如将 "E" 切换为 "Tap"

虚拟控件显示 / 隐藏

由业务层根据 DeviceKindChanged 控制InputModule 不强制管理:

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