输入

基于 Unity Input System 的动作式输入,带上下文栈与交互式改键。

建立在 Unity Input System 之上的动作表、上下文栈与交互式改键 · 可选地向事件总线发布事件 · 无操作替代:NullInputService

CGS 通过 IInputService 读取键盘、鼠标和手柄输入——它是 Unity Input System 包之上的一个薄层。你不再把原始键码检查散落在代码各处,而是使用动作表(action map):命名的控制集合,例如 "Gameplay" 或 "Menu",在一个 .inputactions 资源中定义一次。在此之上,该服务补上了三样 Unity 没有开箱提供的东西:切换当前激活动作表的上下文栈、带自动保存的交互式改键("按下一个键完成重新指定"),以及可选地把输入事件发布到事件总线

先决条件:你的项目必须使用新的输入后端。 安装 Input System (CGS 的依赖清单会替你完成)并不会切换名为 Active Input Handling 的项目设置。在全新项目上它保持为 Input Manager (Old)——于是演示场景、可运行示例和你自己的动作表都会无声地忽略每一次按键。Welcome 窗口(Tools > Common Game System > Welcome)会检测这种情况并显示一键式的 Enable the new Input System 按钮;编辑器重启一次后输入即可工作。手动路径:Edit > Project Settings > Player > Other Settings > Active Input Handling → "Input System Package (New)" 或 "Both"。详见 FAQ

功能概述

  • 基于动作的读取。 查找动作一次(GetAction("Gameplay", "Jump")),然后每帧轮询它或订阅它的事件。你的玩法代码从不提及具体的按键或按钮,因此键盘和手柄走同一条路径。
  • 上下文栈。 压入一个上下文(PushContext("Menu"))以仅启用该动作表并禁用其他所有表;弹出后,先前的上下文重新接管。最新的上下文总是获胜——这正是"玩法之上叠嵌套菜单"想要的行为。
  • 交互式改键。 启动一次改键,让玩家按下新键,然后收到完成回调。一次调用即可把覆盖持久化到 PlayerPrefs
  • 事件发布(可选启用)。 让服务把某个动作的按下发布到事件总线,使解耦的系统无需持有 InputAction 引用即可响应输入。

该服务位于可选的 CommonGameSystem.Input 程序集中。如果你从项目中移除 Input System 包,该程序集会自动把自己排除,CGS 的其余部分照常编译与启动。

获取服务

Awake 中解析一次并缓存引用:

using CommonGameSystem.Core;
using UnityEngine;

[DefaultExecutionOrder(100)]
public class MyGameMode : MonoBehaviour
{
    private IInputService _input;

    private void Awake()
    {
        _input = ServiceLocator.Resolve<IInputService>();
    }
}

[DefaultExecutionOrder(100)] 特性确保 CGS 引导程序在你的 Awake 运行之前已完成服务注册。绝不要在 Update() 里调用 Resolve——那是一次字典查找;缓存一次即可。

API 参考

动作查找

InputAction GetAction(string actionMapName, string actionName)

返回诸如 "Gameplay" / "Jump" 对应的 InputAction;若你的 .inputactions 资源中未定义该表或该动作则返回 null。请缓存结果——不要在热路径上查找动作。

轮询(在 UpdateFixedUpdate 中调用)

T ReadValue<T>(InputAction action)           // Current-frame value (Vector2, float, ...)
bool IsPressed(InputAction action)           // true while held
bool WasPressedThisFrame(InputAction action) // true only on the press frame
bool WasReleasedThisFrame(InputAction action)// true only on the release frame

发布到事件总线(可选启用)

IDisposable PublishOnStarted(InputAction action)   // publishes InputActionStartedEvent
IDisposable PublishOnPerformed(InputAction action) // publishes InputActionPerformedEvent
IDisposable PublishOnCanceled(InputAction action)  // publishes InputActionCanceledEvent

每次调用都返回一个令牌;释放它即停止发布。订阅方式见事件总线页面。

上下文栈

IDisposable PushContext(string actionMapName) // Enable only this map, disable the rest
string CurrentContext { get; }                // Top of the stack (null when empty)
IReadOnlyList<string> ContextStack { get; }   // Bottom-to-top snapshot (allocates; debug use)

PushContext 返回一个令牌;释放该令牌会弹出对应上下文,并重新激活它下面的那个。对同一个表名压入两次会创建两个独立的栈条目。

交互式改键

IInputRebindOperation StartInteractiveRebind(
    InputAction action,
    int bindingIndex = -1,            // -1 = the action's first binding
    string controlsExcluding = "Mouse") // comma-separated controls to ignore

返回一个操作令牌,带 IsCompletedIsCanceledResultBindingPath 和一个 Completed 事件。同一时间只能进行一次改键——启动第二次会抛出 InvalidOperationException。如果玩家在超时时间内(默认 5 秒;可通过 InputServiceOptions.RebindTimeoutSeconds 配置)没有按任何键,改键会自行取消。

保存与恢复绑定

void SaveBindingOverrides()                           // Persist all overrides to PlayerPrefs
void LoadBindingOverrides()                           // Re-apply saved overrides
void ResetBindingOverrides(InputAction action = null) // Clear overrides (null = all actions)

设备查询

bool IsDeviceConnected<TDevice>() where TDevice : InputDevice // Any such device present?
TDevice GetDevice<TDevice>() where TDevice : InputDevice      // First matching device, or null

例如,IsDeviceConnected<Gamepad>() 告诉你是否应该显示手柄按钮提示。

完整示例

一个玩家控制器:轮询移动、把跳跃按下发布到事件总线,并在游戏暂停时切换上下文:

using System;
using CommonGameSystem.Core;
using UnityEngine;
using UnityEngine.InputSystem;

[DefaultExecutionOrder(100)]
public class PlayerController : MonoBehaviour
{
    [SerializeField] private float _moveSpeed = 5f;

    private IInputService _input;
    private InputAction _moveAction;
    private InputAction _jumpAction;
    private IDisposable _jumpEvents;
    private IDisposable _gameplayToken;
    private IDisposable _menuToken;

    private void Awake()
    {
        _input = ServiceLocator.Resolve<IInputService>();
        _moveAction = _input.GetAction("Gameplay", "Move");
        _jumpAction = _input.GetAction("Gameplay", "Jump");
    }

    private void Start()
    {
        // Publish Jump presses to the event bus.
        _jumpEvents = _input.PublishOnPerformed(_jumpAction);

        // Enable the Gameplay action map. Keep the token so we can
        // pop the context when the game pauses.
        _gameplayToken = _input.PushContext("Gameplay");
    }

    private void Update()
    {
        var move = _input.ReadValue<Vector2>(_moveAction);
        transform.Translate(move * _moveSpeed * Time.deltaTime);
    }

    public void OnPause()
    {
        _gameplayToken?.Dispose();               // Pop Gameplay
        _menuToken = _input.PushContext("Menu"); // Push Menu
    }

    public void OnResumeGame()
    {
        _menuToken?.Dispose();                   // Pop Menu
        _gameplayToken = _input.PushContext("Gameplay");
    }

    private void OnDestroy()
    {
        _jumpEvents?.Dispose();
        _menuToken?.Dispose();
        _gameplayToken?.Dispose();
    }
}

"Gameplay""Menu" 表以及 "Move""Jump" 动作来自你自己的 .inputactions 资源——服务读取你的资源,绝不会替你创建动作表。

关闭此服务

ServiceLocator.Replace<IInputService>(new NullInputService());

换成 null 实现后:

调用结果
ReadValue<T>()default(T)Vector2.zero0f……)
IsPressed() / WasPressedThisFrame() / WasReleasedThisFrame()false
GetAction()null
PushContext() / PublishOn*()一个空令牌(什么也不做)
StartInteractiveRebind()一个已处于取消状态的桩操作
Save/Load/ResetBindingOverrides()无操作
IsDeviceConnected() / GetDevice()false / null

用于只播过场动画的模式、无头服务器或测试挂具。其余一切(UI、场景流程)在没有输入的情况下照常编译和运行。

常见陷阱

  • 全新项目上输入毫无反应 = 后端不对。 如果键盘和鼠标都没有响应,先检查 Active Input Handling——见本页顶部的提示框和 FAQ

  • 上下文令牌按最新优先弹出。 释放 PushContext() 返回的令牌(通常在 OnDestroy 中)以弹出该上下文。如果对象以乱序销毁,服务会妥善应对,但弹出一个不在栈顶的上下文不会被记录日志。

  • 动作表来自你自己的资源。 服务不会创建表或动作。请在你的 .inputactions 资源中定义 "Gameplay"、"Menu"、"Dialogue" 等。

  • 同一时间只能有一次交互式改键。 改键进行中再调用 StartInteractiveRebind() 会抛出 InvalidOperationException。改键激活期间,请在设置 UI 中禁用其他改键按钮。

  • 交互处理由你的资源控制。 死区、长按交互、多次点按——全部在你的 .inputactions 资源的 Inspector 中配置。服务提供指导性默认值(例如 InputServiceOptions.StickDeadzoneMin = 0.125f),但绝不会覆盖你资源中的配置。

相关页面

  • 事件总线 —— 本服务可发布的输入事件:InputActionStartedEventInputActionPerformedEventInputActionCanceledEventInputContextPushedEventInputContextPoppedEventInputDeviceConnectedEventInputDeviceDisconnectedEventInputRebindStartedEventInputRebindCompletedEvent
  • UI 框架 —— 随面板开关自动压入和弹出输入上下文
  • 日志 —— 上下文压入/弹出、设备连接与改键超时记录在 Input 分类下
  • FAQ —— Active Input Handling 的逐步修复方法