Unity Input System 的控制方案(control scheme)应该怎么用?

从 Unity 2019.1 开始,Unity 提供了一个新的模块 Input System。它提供了方便的控制器切换、多控制方式可配置性、可扩展的控制器支持(例如映射虚拟控制器)等等。比起以前的 Input Manager 来说结构化了不少,不用每个游戏都自己再写一套映射系统了。关于它的基础使用我就不说了,网上在它发布不久早就有了许多教程。这篇博文要记录的是绝大多数地方都没提到的,它的“控制方案”(control schemes)的使用方法和适合场景。

如果你使用的是 UGUI,项目中仅使用了 Input System(Player - Configuration - Active Input Handling 选择了 Input System),需要将其默认的 Input Manager 后端换为 Input System 后端。这里有操作指南。简单来说,直接找到场景中的 EventSystem 对象(通常是这个名字),将上面的 Input Module 换为 Input System UI Input Module。默认情况下,直接点击那个迁移按钮就行。

大多数情况下,你是不需要关心控制方案的,直接针对平台绑定输入源(按键、轴向输入)就行了。我一开始就是这么做的。这里以我做的玩意儿简化后的情况为例:

初始配置

这张图值得注意的是,每个平台都分配了惯例的确认和取消按键(PlayStation 上用的是亚洲方案),即使它们的方位不一样。另外,所有控制都共享一个控制方案,没有独立的控制方案,这样的 input actions 资产应用后是这个样子:

Player Input 设置生成的资产

这个文件粗看上去没什么问题。本地测试中,我测试了键盘和 XBox 手柄,两种控制器可以同时响应,手柄也是即插即用,不需要专门做控制切换的设置页面。(这是一个单人游戏,所以不用考虑本地多人之类的事情。)

然而,彩喵拿到 Switch 上测试之后反馈说,ABXY 根本没有响应。嗯?那 Unity 整一个专门的 Switch Pro Controller 控制器子类干什么呢?我不是针对这个 controller 分配了键 AB 吗?你怎么就设置设置得上,运行运行不起来呢?

答:你不能直接使用 ABXY 绑定,而是需要用 GamePad.buttonEastGamePad.buttonSouthGamePad.buttonNorthGamePad.buttonWest 来访问。也就是说整个 controller 必须退化成一个 generic D-pad controller,不能使用 Switch Pro Controller 子类。但是,如果你直接这么把原先分配给 Switch 的 AB 替换成 buttonEastbuttonSouth,就会发现一个问题:虽然我们的目标是仅在 Switch 上使用它,但它在 PC 上也抢了 XBox 手柄的键位,导致 XBox 手柄的 AB 变成了未定义行为。

到此直接分设备设置按键的尝试就失败了。怎么解决呢?我想到了分多钟控制方案。所以,第二版的是这样的:

首先在左上角添加四种控制方案:KeyboardXBoxPlayStationSwitch。添加时记得把对应的设备需求选上。这里以 XBox 为例:

XBox Control Scheme

然后,在主编辑界面将每个按键分配给对应的设备。这里还是以 XBox 为例:

每个按键分配到专门设备

设置到 Player Input 上之后:

Player Input 设置生成的资产

可以注意到在 Actions 下多了两项:Default SchemeAuto-Switch。这两项的意义可以阅读工具提示来理解。前者控制的是默认的控制方案,如果未指定(<Any>)则会寻找第一个匹配到的;后者决定如果某个方案因为设备不满足而失效(例如,手柄断线),是否会尝试寻找并应用另一个可以匹配的。一般情况下,如果是全局只存在一个 Player Input,那选择 <Any> 和 auto-switch 是完全足够的。关于匹配过程可以参考 Input System 的文档,一句话概括就是匹配成功项最多者胜出。

看起来好了,那运行起来吧。等等,为什么手柄没反应,只支持键盘了?刚才不是好好的吗?我不是设置了 XBox 手柄的控制方案吗?为什么它没有同时启用呢?于是我开始找诸如“Unity input system control scheme”之类的网页,可惜基本没有解释。就算是 Unity 自己的页面,里面的演示视频也只是简单添加了两种 control scheme 然后就正常运行了,看起来能动态切换,和我实测的并不一样。

直到我找到 Unity 官方论坛上一个讨论多个控制方案的帖子。其中一个开发者的回帖解答了我的疑问。(我之前也找过 Input System 相关的帖子,也见过这位老哥,他应该就是 Input System 的主要开发者之一。)以下是帖子节选,我加粗了重要部分:

… Control schemes are only really relevant if you want to separate multiple different means of controlling your game/app from each other such that they are mutually exclusive and such that you can know which one is active at any one time (e.g. to display control hints).

So in effect, the intention for having two control schemes, one for keyboard and one for gamepad, would be that keyboard input is not meant to be used concurrently with gamepad input. While the keyboard bindings are active, the gamepad bindings will not be. If both are in the same control scheme, however, they will concurrently be active side by side. …

本质上,不同控制方案之间是互斥的,同时只能采用一种。如果你玩过一些老的主机移植游戏,就很好理解:那些游戏不支持同时使用键鼠和手柄,而且也未必支持动态插拔,如果你想换控制器就需要进入游戏里专门的设置页面去切换。

到此问题也就有了个答案。如果希望同时支持键盘和手柄,而且不想做专门的手动切换,那么就应该是按照游玩设备分类,而非控制器分类。将 Keyboard 方案重命名为 PC 方案,然后添加可选设备:

PC 控制方案的控制器设备设置

接着,将 PC 上可用的手柄输入也选上。这里以 XBox 为例:

手柄输入也适用于 PC

图中 XBox 手柄的 A 不仅在 XBox 上有效,也在 PC 上有效。而在 PC 上,键盘也是有效的。(PlayStation 控制器同理。)所以,在 PC 上,你就可以同时接收到键盘和手柄输入了。

然而设置完毕后还有一个问题。可以见到对于“确认”行为,我们为手柄指定了三种按键:

  • 需要 XBox 控制器(PC 上或 XBox 上)
    • A<XInputController>/buttonSouth
  • 需要 PlayStation 控制器(PC 上或 PlayStation 上)
    • <DualShockGamePad>/buttonEast
  • 需要 Switch Pro 控制器(Switch 上)
    • buttonEast<GamePad>/buttonEast

如果我没设置控制器需求的话,由于 XInputControllerGamePad 的子控制器类型,所以通用 GamePad 的设置也会被响应,就出现了前文的未定义行为问题。但是理论上,既然我设置了控制器需求,而且我没有接入 Switch Pro 控制器,那最后一条应该不能响应才对。但是在实测中,虽然 input system 自动选择了 PC 方案,但 Switch 方案仍然会被响应。我认为这是 input system 的一个 bug。

这个bug要完美解决的话比较麻烦。不过在我的目标工程里不需要搞手动选择也不需要考虑多人,所以可以通过以下方法绕过去:保持 Default Scheme<Any>,但是把 Auto-Switch 关了。该方法在 PC 和 Switch 上测试通过。(所以为什么 Switch 上又能识别 Switch Pro 控制器需求——HID,但直接配置特化不行呢?神秘。)


Input System 是带有调试工具的,可以方便地看到当前连接的设备、采用的方案、按键映射等等,还有一些其他功能(给虚拟设备和远程设备用的),直接单击任意 Player Input 右下角的按钮即可。比如,正确设置后,运行时可以看到如下的界面:

实时观察 Input System 状态

从图中可以看到:

  • 连接了3个设备(展开后可以看到是键盘、鼠标和 XBox 手柄)。
  • 识别了一个玩家(默认的使用例)。
    • 玩家所占用的设备是键盘和 XBox 手柄。
      • 因此选用了 PC 控制方案。
    • .inputactions 资产中定义的行为,实际被映射到了哪些控制器的哪些键/轴。
      • 如果出现了上文的“未定义行为”,可以看到 ConfirmCancel 两个行为都同时绑定了 buttonSouthbuttonEast,因此按下后实际触发哪个是不确定的。

04-10: 配置的 control scheme 还是不能生效,跟 Auto-Switch 没关系。最后还是改加了一个手工过滤器,如下(健壮性不强,不过暂时够用):

using System;
using UnityEngine;
using UnityEngine.InputSystem;

namespace Example
{
    public static class InputActionExtensions
    {

        public static bool MatchesExpected(this InputAction action)
        {
            // if (!PlayerInput.isSinglePlayer)
            // {
            //     Debug.LogWarning("Multiple players detected. Please fix PlayerInput setup.");
            //     return false;
            // }
            var player0 = PlayerInput.GetPlayerByIndex(0);
            if (player0 == null)
            {
                return false;
            }

            var activeControl = action.activeControl;
            var bindings = action.bindings;
            var schemeName = player0.currentControlScheme;

            foreach (var binding in bindings)
            {
                if (binding.groups.IndexOf(';') >= 0)
                {
                    var groups = binding.groups.Split(GroupSeparator, StringSplitOptions.RemoveEmptyEntries);
                    var found = false;

                    foreach (var group in groups)
                    {
                        if (group == schemeName)
                        {
                            found = true;
                            break;
                        }
                    }

                    if (!found)
                    {
                        continue;
                    }
                }
                else
                {
                    if (binding.groups != schemeName)
                    {
                        continue;
                    }
                }

                var bindingPath = binding.effectivePath;

                if (InputControlPath.Matches(bindingPath, activeControl))
                {
                    return true;
                }
            }

            return false;
        }

        private static readonly char[] GroupSeparator = { ';' };

    }
}

使用示例:

public void OnAction1(InputAction.CallbackContext context)
{
    if (context.performed && context.MatchesExpected())
    {
        // Logic here
    }
}
分享到 评论