Input SDK 入门

本文档介绍了如何在支持 Google Play 游戏电脑版的游戏中设置和显示 Input SDK。这些任务包括将 SDK 添加到您的游戏中以及生成包含游戏操作到用户输入分配的输入映射。

开始之前

在将 Input SDK 添加到您的游戏之前,您必须使用游戏引擎的输入系统支持键盘和鼠标输入。

Input SDK 向 Google Play 游戏电脑版提供有关您的游戏使用哪些控件的信息,以便将其显示给用户。它还可以选择性地允许用户重新映射键盘。

每个控件都是一个 InputAction(例如,“J”代表“Jump”),您将 InputAction 组织到 InputGroup 中。InputGroup 可能代表您游戏中的不同模式,例如“驾驶”或“步行”或“主菜单”。您还可以使用 InputContexts 来指示在游戏的哪些点激活哪些组。

您可以启用键盘重新映射以自动为您处理,但如果您更喜欢提供自己的控件重新映射界面,则可以禁用 Input SDK 重新映射。

以下序列图描述了 Input SDK 的 API 如何工作

Sequence diagram of a game implementation that calls Input SDK API
and its interaction with the Android
device.

当您的游戏实现 Input SDK 时,您的控件会显示在 Google Play 游戏电脑版叠加层中。

Google Play 游戏电脑版叠加层

Google Play 游戏电脑版叠加层(“叠加层”)显示您的游戏定义的控件。用户随时可以通过按 Shift + Tab 访问叠加层。

The Google Play Games on PC overlay.

设计按键绑定的最佳实践

设计按键绑定时,请考虑以下最佳实践

  • 将您的 InputAction 逻辑分组到 InputGroup 中,以提高游戏过程中控件的导航和可发现性。
  • 将每个 InputGroup 最多分配给一个 InputContext。细粒度的 InputMap 可在叠加层中导航您的控件时带来更好的体验。
  • 为游戏的每种不同场景类型创建 InputContext。通常,您可以为所有“菜单式”场景使用单个 InputContext。对于游戏中的任何小游戏或单个场景的替代控件,请使用不同的 InputContext
  • 如果两个操作旨在在同一个 InputContext 下使用相同的键,请使用“互动 / 射击”等标签字符串。
  • 如果两个键设计为绑定到同一个 InputAction,则使用 2 个不同的 InputAction 在您的游戏中执行相同的操作。您可以对两个 InputAction 使用相同的标签字符串,但其 ID 必须不同。
  • 如果将修饰键应用于一组键,请考虑使用带有修饰键的单个 InputAction,而不是多个组合修饰键的 InputAction(例如:使用 ShiftW、A、S、D,而不是 Shift + W、Shift + A、Shift + S、Shift + D)。
  • 当用户在文本字段中写入时,输入重新映射会自动禁用。遵循实现 Android 文本字段的最佳实践,以确保 Android 可以检测游戏中的文本字段并防止重新映射的键干扰它们。如果您的游戏必须使用非常规文本字段,您可以使用包含空 InputGroups 列表的 InputContext 调用 setInputContext() 来手动禁用重新映射。
  • 如果您的游戏支持重新映射,请考虑将更新按键绑定视为敏感操作,这可能会与用户保存的版本发生冲突。尽可能避免更改现有控件的 ID。

重新映射功能

Google Play 游戏电脑版支持根据您的游戏使用 Input SDK 提供的按键绑定进行键盘控件重新映射。这是可选的,可以完全禁用。例如,您可能希望提供自己的键盘重新映射界面。要为您的游戏禁用重新映射,您只需为您的 InputMap 指定禁用重新映射选项(有关更多信息,请参阅构建 InputMap)。

要访问此功能,用户需要打开叠加层,然后点击他们想要重新映射的操作。在每个重新映射事件之后,Google Play 游戏电脑版会将每个用户重新映射的控件映射到您的游戏期望接收的默认控件,因此您的游戏不需要了解玩家的重新映射。您可以通过为重新映射事件添加回调来选择性地更新用于在游戏中显示键盘控件的资源。

Attempt to remap the key

Google Play 游戏电脑版会在本地为每个用户存储重新映射的控件,从而使控件在游戏会话之间保持持久性。此信息仅针对 PC 平台存储在磁盘上,不影响移动体验。用户卸载或重新安装 Google Play 游戏电脑版后,控件数据将被删除。此数据在多个 PC 设备之间不持久。

为了在您的游戏中支持重新映射功能,请避免以下限制

重新映射的限制

如果按键绑定包含以下任何情况,则可以在您的游戏中禁用重新映射功能

  • 不由修饰键 + 非修饰键组成的多键 InputAction。例如,Shift + A 是有效的,但 A + BCtrl + AltShift + A + Tab 则无效。
  • InputMap 包含具有重复唯一 ID 的 InputActionsInputGroupsInputContexts

重新映射的局限性

为重新映射设计按键绑定时,请考虑以下限制

  • 不支持重新映射到组合键。例如,用户无法将 Shift + A 重新映射到 Ctrl + B 或将 A 重新映射到 Shift + A
  • 不支持带有鼠标按钮的 InputAction 的重新映射。例如,无法重新映射 Shift + 右键点击

在 Google Play 游戏电脑版模拟器上测试按键重新映射

您随时可以通过发出以下 adb 命令在 Google Play 游戏电脑版模拟器中启用重新映射功能

adb shell dumpsys input_mapping_service --set RemappingFlagValue true

叠加层更改如下图所示

The overlay with key remapping enabled.

添加 SDK

根据您的开发平台安装 Input SDK。

Java 和 Kotlin

通过在您的模块级 build.gradle 文件中添加依赖项来获取适用于 Java 或 Kotlin 的 Input SDK

dependencies {
  implementation 'com.google.android.libraries.play.games:inputmapping:1.1.1-beta'
  ...
}

Unity

Input SDK 是一个标准的 Unity 包,具有多个依赖项。

需要安装包含所有依赖项的包。有几种方法可以安装这些包。

安装 .unitypackage

下载 Input SDK unitypackage 文件及其所有依赖项。您可以通过选择 Assets > Import package > Custom Package 并找到您下载的文件来安装 .unitypackage

使用 UPM 安装

或者,您可以使用 Unity 包管理器通过下载 .tgz 并安装其依赖项来安装包

使用 OpenUPM 安装

您可以使用 OpenUPM 安装包。

$ openupm add com.google.android.libraries.play.games.inputmapping

示例游戏

有关如何与 Input SDK 集成的示例,请参阅 Kotlin 或 Java 游戏的 AGDK Tunnel 以及 Unity 游戏的 Trivial Kart

生成您的按键绑定

通过构建 InputMap 并使用 InputMappingProvider 返回它来注册您的按键绑定。以下示例概述了 InputMappingProvider

Kotlin

class InputSDKProvider : InputMappingProvider {
  override fun onProvideInputMap(): InputMap {
    TODO("Not yet implemented")
  }
}

Java

public class InputSDKProvider implements InputMappingProvider {
    private static final String INPUTMAP_VERSION = "1.0.0";

    @Override
    @NonNull
    public InputMap onProvideInputMap() {
        // TODO: return an InputMap
    }
}

C#

#if PLAY_GAMES_PC
using Java.Lang;
using Java.Util;
using Google.Android.Libraries.Play.Games.Inputmapping;
using Google.Android.Libraries.Play.Games.Inputmapping.Datamodel;

public class InputSDKProvider : InputMappingProviderCallbackHelper
{
    public static readonly string INPUT_MAP_VERSION = "1.0.0";

    public override InputMap OnProvideInputMap()
    {
        // TODO: return an InputMap
    }
}
#endif

定义您的输入操作

InputAction 类用于将键或组合键映射到游戏操作。InputAction 在所有 InputAction 中必须具有唯一的 ID。

如果您支持重新映射,则可以定义哪些 InputAction 可以重新映射。如果您的游戏不支持重新映射,则应为所有 InputAction 禁用重新映射选项,但如果您在 InputMap 中不支持重新映射,Input SDK 足够智能,可以关闭重新映射。

此示例将空格键映射到“驾驶”操作。

Kotlin

companion object {
  private val driveInputAction = InputAction.create(
    "Drive",
    InputActionsIds.DRIVE.ordinal.toLong(),
    InputControls.create(listOf(KeyEvent.KEYCODE_SPACE), emptyList()),
    InputEnums.REMAP_OPTION_ENABLED)
}

Java

private static final InputAction driveInputAction = InputAction.create(
    "Drive",
    InputEventIds.DRIVE.ordinal(),
    InputControls.create(
            Collections.singletonList(KeyEvent.KEYCODE_SPACE),
            Collections.emptyList()),
    InputEnums.REMAP_OPTION_ENABLED
);

C#

private static readonly InputAction driveInputAction = InputAction.Create(
    "Drive",
    (long)InputEventIds.DRIVE,
    InputControls.Create(
        new[] { new Integer(AndroidKeyCode.KEYCODE_SPACE) }.ToJavaList(),
        new ArrayList<Integer>()),
    InputEnums.REMAP_OPTION_ENABLED
);

Single key InputAction displayed in the overlay.

操作也可以表示鼠标输入。此示例将左键点击设置为“移动”操作

Kotlin

companion object {
  private val mouseInputAction = InputAction.create(
    "Move",
    InputActionsIds.MOUSE_MOVEMENT.ordinal.toLong(),
    InputControls.create(emptyList(), listOf(InputControls.MOUSE_LEFT_CLICK)),
    InputEnums.REMAP_OPTION_DISABLED)
}

Java

private static final InputAction mouseInputAction = InputAction.create(
    "Move",
    InputActionsIds.MOUSE_MOVEMENT.ordinal(),
    InputControls.create(
            Collections.emptyList(),
            Collections.singletonList(InputControls.MOUSE_LEFT_CLICK)
    ),
    InputEnums.REMAP_OPTION_DISABLED
);

C#

private static readonly InputAction mouseInputAction = InputAction.Create(
    "Move",
    (long)InputEventIds.MOUSE_MOVEMENT,
    InputControls.Create(
        new ArrayList<Integer>(),
        new[] { new Integer((int)PlayMouseAction.MouseLeftClick) }.ToJavaList()
    ),
    InputEnums.REMAP_OPTION_DISABLED
);

Mouse InputAction displayed in the overlay.

通过将多个键码传递给您的 InputAction 来指定组合键。在此示例中,空格 + Shift映射到“Turbo”操作,即使空格映射到“驾驶”时也有效。

Kotlin

companion object {
  private val turboInputAction = InputAction.create(
    "Turbo",
    InputActionsIds.TURBO.ordinal.toLong(),
    InputControls.create(
      listOf(KeyEvent.KEYCODE_SHIFT_LEFT, KeyEvent.KEYCODE_SPACE),
      emptyList()),
    InputEnums.REMAP_OPTION_ENABLED)
}

Java

private static final InputAction turboInputAction = InputAction.create(
    "Turbo",
    InputActionsIds.TURBO.ordinal(),
    InputControls.create(
            Arrays.asList(KeyEvent.KEYCODE_SHIFT_LEFT, KeyEvent.KEYCODE_SPACE),
            Collections.emptyList()
    ),
    InputEnums.REMAP_OPTION_ENABLED
);

C#

private static readonly InputAction turboInputAction = InputAction.Create(
    "Turbo",
    (long)InputEventIds.TURBO,
    InputControls.Create(
        new[]
        {
            new Integer(AndroidKeyCode.KEYCODE_SHIFT_LEFT),
            new Integer(AndroidKeyCode.KEYCODE_SPACE)
        }.ToJavaList(),
        new ArrayList<Integer>()),
    InputEnums.REMAP_OPTION_ENABLED
);

Multi-key InputAction displayed in the overlay.

Input SDK 允许您将鼠标和按键组合在一起用于单个操作。此示例表明Shift右键点击同时按下会在此示例游戏中添加一个航路点

Kotlin

companion object {
  private val addWaypointInputAction = InputAction.create(
    "Add waypoint",
    InputActionsIds.ADD_WAYPOINT.ordinal.toLong(),
    InputControls.create(
      listOf(KeyEvent.KeyEvent.KEYCODE_TAB),
      listOf(InputControls.MOUSE_RIGHT_CLICK)),
    InputEnums.REMAP_OPTION_DISABLED)
}

Java

private static final InputAction addWaypointInputAction = InputAction.create(
    "Add waypoint",
    InputActionsIds.ADD_WAYPOINT.ordinal(),
    InputControls.create(
            Collections.singletonList(KeyEvent.KEYCODE_TAB),
            Collections.singletonList(InputControls.MOUSE_RIGHT_CLICK)
    ),
    InputEnums.REMAP_OPTION_DISABLED
);

C#

private static readonly InputAction addWaypointInputAction = InputAction.Create(
    "Add waypoint",
    (long)InputEventIds.ADD_WAYPOINT,
    InputControls.Create(
        new[] { new Integer(AndroidKeyCode.KEYCODE_SPACE) }.ToJavaList(),
        new[] { new Integer((int)PlayMouseAction.MouseRightClick) }.ToJavaList()
    ),
    InputEnums.REMAP_OPTION_DISABLED
);

Combination of key + mouse InputAction displayed in the overlay.

InputAction 具有以下字段

  • ActionLabel:在 UI 中显示的字符串,用于表示此操作。本地化不是自动完成的,因此请提前执行任何本地化。
  • InputControls:定义此操作使用的输入控件。控件在叠加层中映射到一致的字形。
  • InputActionIdInputIdentifier 对象,存储 InputAction 的数字 ID 和版本(有关更多信息,请参阅跟踪键 ID)。
  • InputRemappingOptionInputEnums.REMAP_OPTION_ENABLEDInputEnums.REMAP_OPTION_DISABLED 之一。定义操作是否启用重新映射。如果您的游戏不支持重新映射,您可以跳过此字段或直接将其设置为禁用。
  • RemappedInputControls:只读 InputControls 对象,用于在重新映射事件中读取用户设置的重新映射键(用于在重新映射事件中获取通知)。

InputControls 表示与操作关联的输入,并包含以下字段:

  • AndroidKeycodes:与操作关联的键盘输入整数列表。这些在 KeyEvent 类或 Unity 的 AndroidKeycode 类中定义。
  • MouseActions:表示与此操作关联的鼠标输入的 MouseAction 值列表。

定义您的输入组

InputAction 使用 InputGroup 与逻辑相关的操作分组,以改善叠加层中的导航和控件可发现性。每个 InputGroup ID 在您游戏的所有 InputGroup 中都必须是唯一的。

通过将输入操作组织到组中,您可以让玩家更容易找到当前上下文的正确按键绑定。

如果您支持重新映射,则可以定义哪些 InputGroup 可以重新映射。如果您的游戏不支持重新映射,则应为所有 InputGroup 禁用重新映射选项,但如果您在 InputMap 中不支持重新映射,Input SDK 足够智能,可以关闭重新映射。

Kotlin

companion object {
  private val menuInputGroup = InputGroup.create(
    "Menu keys",
    listOf(
      navigateUpInputAction,
      navigateLeftInputAction,
      navigateDownInputAction,
      navigateRightInputAction,
      openMenuInputAction,
      returnMenuInputAction),
    InputGroupsIds.MENU_ACTION_KEYS.ordinal.toLong(),
    InputEnums.REMAP_OPTION_ENABLED
  )
}

Java

private static final InputGroup menuInputGroup = InputGroup.create(
    "Menu keys",
    Arrays.asList(
           navigateUpInputAction,
           navigateLeftInputAction,
           navigateDownInputAction,
           navigateRightInputAction,
           openMenuInputAction,
           returnMenuInputAction),
    InputGroupsIds.MENU_ACTION_KEYS.ordinal(),
    REMAP_OPTION_ENABLED
);

C#

private static readonly InputGroup menuInputGroup = InputGroup.Create(
    "Menu keys",
    new[]
    {
        navigateUpInputAction,
        navigateLeftInputAction,
        navigateDownInputAction,
        navigateRightInputAction,
        openMenuInputAction,
        returnMenuInputAction,
    }.ToJavaList(),
    (long)InputGroupsIds.MENU_ACTION_KEYS,
    InputEnums.REMAP_OPTION_ENABLED
);

以下示例在叠加层中显示了道路控制菜单控制输入组

The overlay displaying an InputMap that contains the Road controls and the
Menu controls input groups.

InputGroup 具有以下字段

  • GroupLabel:将在叠加层中显示的字符串,可用于逻辑分组一组操作。此字符串不会自动本地化。
  • InputActions:您在上一步中定义的 InputAction 对象列表。所有这些操作都将显示在组标题下。
  • InputGroupIdInputIdentifier 对象,用于存储 InputGroup 的数字 ID 和版本。有关更多信息,请参阅跟踪键 ID
  • InputRemappingOptionInputEnums.REMAP_OPTION_ENABLEDInputEnums.REMAP_OPTION_DISABLED 之一。如果禁用,则即使属于此组的所有 InputAction 对象指定其重新映射选项已启用,它们也将禁用重新映射。如果启用,则除非各个操作指定禁用,否则属于此组的所有操作都可以重新映射。

定义您的输入上下文

InputContexts 允许您的游戏为游戏的不同场景使用不同的键盘控件集。例如

  • 您可以为导航菜单和在游戏中移动指定不同的输入集。
  • 您可以根据游戏中移动模式(例如驾驶与步行)指定不同的输入集。
  • 您可以根据游戏的当前状态(例如导航大世界与玩单个级别)指定不同的输入集。

使用 InputContexts 时,叠加层会首先显示正在使用的上下文的组。要启用此行为,每当您的游戏进入不同的场景时,调用 setInputContext() 来设置上下文。下图演示了此行为:在“驾驶”场景中,道路控制操作显示在叠加层的顶部。打开“商店”菜单时,“菜单控制”操作显示在叠加层的顶部。

InputContexts sorting groups in the overlay.

这些叠加层更新是通过在游戏的不同点设置不同的 InputContext 来实现的。为此

  1. 使用 InputGroups 将您的 InputAction 与逻辑相关的操作分组
  2. 将这些 InputGroup 分配给游戏不同部分的 InputContext

属于同一个 InputContextInputGroup 不能有冲突的 InputAction,即使用相同的键。将每个 InputGroup 分配给单个 InputContext 是一个好习惯。

以下示例代码演示了 InputContext 逻辑

Kotlin

companion object {
  val menuSceneInputContext = InputContext.create(
    "Menu",
    InputIdentifier.create(
      INPUTMAP_VERSION,
      InputContextIds.MENU_SCENE.ordinal.toLong()),
    listOf(basicMenuNavigationInputGroup, menuActionsInputGroup))

  val gameSceneInputContext = InputContext.create(
    "Game",
    InputIdentifier.create(
      INPUTMAP_VERSION,
      InputContextIds.GAME_SCENE.ordinal.toLong()),
    listOf(
      movementInputGroup,
      mouseActionsInputGroup,
      emojisInputGroup,
      gameActionsInputGroup))
}

Java

public static final InputContext menuSceneInputContext = InputContext.create(
        "Menu",
        InputIdentifier.create(
                INPUTMAP_VERSION,
                InputContextIds.MENU_SCENE.ordinal()),
        Arrays.asList(
                basicMenuNavigationInputGroup,
                menuActionsInputGroup
        )
);

public static final InputContext gameSceneInputContext = InputContext.create(
        "Game",
        InputIdentifier.create(
                INPUTMAP_VERSION,
                InputContextIds.GAME_SCENE.ordinal()),
        Arrays.asList(
                movementInputGroup,
                mouseActionsInputGroup,
                emojisInputGroup,
                gameActionsInputGroup
        )
);

C#

public static readonly InputContext menuSceneInputContext = InputContext.Create(
    "Menu",
    InputIdentifier.Create(
        INPUT_MAP_VERSION,
        (long)InputContextsIds.MENU_SCENE),
    new[]
    {
        basicMenuNavigationInputGroup,
        menuActionsInputGroup
    }.ToJavaList()
);

public static readonly InputContext gameSceneInputContext = InputContext.Create(
    "Game",
    InputIdentifier.Create(
        INPUT_MAP_VERSION,
        (long)InputContextsIds.GAME_SCENE),
    new[]
    {
        movementInputGroup,
        mouseActionsInputGroup,
        emojisInputGroup,
        gameActionsInputGroup
    }.ToJavaList()
);

InputContext 具有以下字段

  • LocalizedContextLabel:描述属于上下文的组的字符串。
  • InputContextIdInputIdentifier 对象,存储 InputContext 的数字 ID 和版本(有关更多信息,请参阅跟踪键 ID)。
  • ActiveGroups:当此上下文处于活动状态时,将用于并显示在叠加层顶部的 InputGroups 列表。

构建输入映射

一个 InputMap 是游戏中所有可用的 InputGroup 对象的集合,因此也是玩家可以执行的所有 InputAction 对象的集合。

报告您的按键绑定时,您将使用游戏中使用的所有 InputGroup 构建一个 InputMap

如果您的游戏不支持重新映射,请将重新映射选项设置为禁用并将保留键设置为空。

以下示例构建了一个 InputMap,用于报告 InputGroup 的集合。

Kotlin

companion object {
  val gameInputMap = InputMap.create(
    listOf(
      basicMenuNavigationInputGroup,
      menuActionKeysInputGroup,
      movementInputGroup,
      mouseMovementInputGroup,
      pauseMenuInputGroup),
    MouseSettings.create(true, false),
    InputIdentifier.create(INPUTMAP_VERSION, INPUT_MAP_ID.toLong()),
    InputEnums.REMAP_OPTION_ENABLED,
    // Use ESCAPE as reserved remapping key
    listof(InputControls.create(listOf(KeyEvent.KEYCODE_ESCAPE), emptyList()))
  )
}

Java

public static final InputMap gameInputMap = InputMap.create(
        Arrays.asList(
                basicMenuNavigationInputGroup,
                menuActionKeysInputGroup,
                movementInputGroup,
                mouseMovementInputGroup,
                pauseMenuInputGroup),
        MouseSettings.create(true, false),
        InputIdentifier.create(INPUTMAP_VERSION, INPUT_MAP_ID),
        REMAP_OPTION_ENABLED,
        // Use ESCAPE as reserved remapping key
        Arrays.asList(
                InputControls.create(
                        Collections.singletonList(KeyEvent.KEYCODE_ESCAPE),
                        Collections.emptyList()
                )
        )
);

C#

public static readonly InputMap gameInputMap = InputMap.Create(
    new[]
    {
        basicMenuNavigationInputGroup,
        menuActionKeysInputGroup,
        movementInputGroup,
        mouseMovementInputGroup,
        pauseMenuInputGroup,
    }.ToJavaList(),
    MouseSettings.Create(true, false),
    InputIdentifier.Create(INPUT_MAP_VERSION, INPUT_MAP_ID),
    InputEnums.REMAP_OPTION_ENABLED,
    // Use ESCAPE as reserved remapping key
    new[]
    {
        InputControls.Create(
            New[] {
            new Integer(AndroidKeyCode.KEYCODE_ESCAPE)
        }.ToJavaList(),
        new ArrayList<Integer>())
    }.ToJavaList()
);

InputMap 具有以下字段

  • InputGroups:您的游戏报告的 InputGroups。这些组在叠加层中按顺序显示,除非通过调用 setInputContext() 指定了当前正在使用的组。
  • MouseSettingsMouseSettings 对象指示可以调整鼠标灵敏度以及鼠标在 Y 轴上是否反转。
  • InputMapIdInputIdentifier 对象,存储 InputMap 的数字 ID 和版本(有关更多信息,请参阅跟踪键 ID)。
  • InputRemappingOptionInputEnums.REMAP_OPTION_ENABLEDInputEnums.REMAP_OPTION_DISABLED 之一。定义重新映射功能是否启用。
  • ReservedControls:用户不允许重新映射到的 InputControls 列表。

跟踪键 ID

InputActionInputGroupInputContextInputMap 对象包含一个 InputIdentifier 对象,该对象存储一个唯一的数字 ID 和一个字符串版本 ID。跟踪对象的字符串版本是可选的,但建议跟踪 InputMap 的版本。如果未提供字符串版本,则字符串为空。InputMap 对象需要字符串版本。

以下示例为 InputActionInputGroup 分配字符串版本

Kotlin

class InputSDKProviderKotlin : InputMappingProvider {
  companion object {
    const val INPUTMAP_VERSION = "1.0.0"
    private val enterMenuInputAction = InputAction.create(
      "Enter menu",
      InputControls.create(listOf(KeyEvent.KEYCODE_ENTER), emptyList()),
      InputIdentifier.create(
        INPUTMAP_VERSION, InputActionsIds.ENTER_MENU.ordinal.toLong()),
      InputEnums.REMAP_OPTION_ENABLED
    )

    private val movementInputGroup  = InputGroup.create(
      "Basic movement",
      listOf(
        moveUpInputAction,
        moveLeftInputAction,
        moveDownInputAction,
        mouseGameInputAction),
      InputIdentifier.create(
        INPUTMAP_VERSION, InputGroupsIds.BASIC_MOVEMENT.ordinal.toLong()),
      InputEnums.REMAP_OPTION_ENABLED)
  }
}

Java

public class InputSDKProvider implements InputMappingProvider {
    public static final String INPUTMAP_VERSION = "1.0.0";

    private static final InputAction enterMenuInputAction = InputAction.create(
            "Enter menu",
            InputControls.create(
                    Collections.singletonList(KeyEvent.KEYCODE_ENTER),
                    Collections.emptyList()),
            InputIdentifier.create(
                    INPUTMAP_VERSION, InputActionsIds.ENTER_MENU.ordinal()),
            InputEnums.REMAP_OPTION_ENABLED
    );

    private static final InputGroup movementInputGroup = InputGroup.create(
            "Basic movement",
            Arrays.asList(
                    moveUpInputAction,
                    moveLeftInputAction,
                    moveDownInputAction,
                    moveRightInputAction,
                    mouseGameInputAction
            ),
            InputIdentifier.create(
                    INPUTMAP_VERSION, InputGroupsIds.BASIC_MOVEMENT.ordinal()),
            InputEnums.REMAP_OPTION_ENABLED
    );
}

C#

#if PLAY_GAMES_PC

using Java.Lang;
using Java.Util;
using Google.Android.Libraries.Play.Games.Inputmapping;
using Google.Android.Libraries.Play.Games.Inputmapping.Datamodel;

public class InputSDKMappingProvider : InputMappingProviderCallbackHelper
{
    public static readonly string INPUT_MAP_VERSION = "1.0.0";

    private static readonly InputAction enterMenuInputAction =
        InputAction.Create(
            "Enter menu",
            InputControls.Create(
                new[] { new Integer(AndroidKeyCode.KEYCODE_SPACE)}.ToJavaList(),
                new ArrayList<Integer>()),
            InputIdentifier.Create(
                INPUT_MAP_VERSION,
                (long)InputEventIds.ENTER_MENU),
            InputEnums.REMAP_OPTION_ENABLED
        );

    private static readonly InputGroup movementInputGroup = InputGroup.Create(
        "Basic movement",
        new[]
        {
            moveUpInputAction,
            moveLeftInputAction,
            moveDownInputAction,
            moveRightInputAction,
            mouseGameInputAction
        }.ToJavaList(),
        InputIdentifier.Create(
            INPUT_MAP_VERSION,
            (long)InputGroupsIds.BASIC_MOVEMENT),
        InputEnums.REMAP_OPTION_ENABLED
    );
}
#endif

InputAction 对象的数字 ID 在您的 InputMap 中的所有 InputAction 中必须是唯一的。同样,InputGroup 对象 ID 在 InputMap 中的所有 InputGroup 中必须是唯一的。以下示例演示了如何使用 enum 来跟踪对象的唯一 ID

Kotlin

enum class InputActionsIds {
    NAVIGATE_UP,
    NAVIGATE_DOWN,
    ENTER_MENU,
    EXIT_MENU,
    // ...
    JUMP,
    RUN,
    EMOJI_1,
    EMOJI_2,
    // ...
}

enum class InputGroupsIds {
    // Main menu scene
    BASIC_NAVIGATION, // WASD, Enter, Backspace
    MENU_ACTIONS, // C: chat, Space: quick game, S: store
    // Gameplay scene
    BASIC_MOVEMENT, // WASD, space: jump, Shift: run
    MOUSE_ACTIONS, // Left click: shoot, Right click: aim
    EMOJIS, // Emojis with keys 1,2,3,4 and 5
    GAME_ACTIONS, // M: map, P: pause, R: reload
}

enum class InputContextIds {
    MENU_SCENE, // Basic menu navigation, menu actions
    GAME_SCENE, // Basic movement, mouse actions, emojis, game actions
}

const val INPUT_MAP_ID = 0

Java

public enum InputActionsIds {
    NAVIGATE_UP,
    NAVIGATE_DOWN,
    ENTER_MENU,
    EXIT_MENU,
    // ...
    JUMP,
    RUN,
    EMOJI_1,
    EMOJI_2,
    // ...
}

public enum InputGroupsIds {
    // Main menu scene
    BASIC_NAVIGATION, // WASD, Enter, Backspace
    MENU_ACTIONS, // C: chat, Space: quick game, S: store
    // Gameplay scene
    BASIC_MOVEMENT, // WASD, space: jump, Shift: run
    MOUSE_ACTIONS, // Left click: shoot, Right click: aim
    EMOJIS, // Emojis with keys 1,2,3,4 and 5
    GAME_ACTIONS, // M: map, P: pause, R: reload
}

public enum InputContextIds {
    MENU_SCENE, // Basic navigation, menu actions
    GAME_SCENE, // Basic movement, mouse actions, emojis, game actions
}

public static final long INPUT_MAP_ID = 0;

C#

public enum InputActionsIds
{
    NAVIGATE_UP,
    NAVIGATE_DOWN,
    ENTER_MENU,
    EXIT_MENU,
    // ...
    JUMP,
    RUN,
    EMOJI_1,
    EMOJI_2,
    // ...
}

public enum InputGroupsIds
{
    // Main menu scene
    BASIC_NAVIGATION, // WASD, Enter, Backspace
    MENU_ACTIONS, // C: chat, Space: quick game, S: store
    // Gameplay scene
    BASIC_MOVEMENT, // WASD, space: jump, Shift: run
    MOUSE_ACTIONS, // Left click: shoot, Right click: aim
    EMOJIS, // Emojis with keys 1,2,3,4 and 5
    GAME_ACTIONS, // M: map, P: pause, R: reload
}

public enum InputContextIds
{
    MENU_SCENE, // Basic navigation, menu actions
    GAME_SCENE, // Basic movement, mouse actions, emojis, game actions
}

public static readonly long INPUT_MAP_ID = 0;

InputIdentifier 具有以下字段

  • UniqueId:一个唯一的数字 ID,用于明确识别给定的一组输入数据。
  • VersionString:一个人类可读的版本字符串,用于在两次输入数据更改之间识别输入数据的版本。

获取重新映射事件通知(可选)

接收重新映射事件通知,以了解您的游戏中正在使用的键。这允许您的游戏更新游戏屏幕上显示的用于显示操作控件的资源。

以下图片显示了此行为的示例,其中在重新映射键G, PS分别到J, XT之后,游戏的 UI 元素会更新以显示用户设置的键。

UI reacting to remapping events using the InputRemappingListener callback.

此功能通过注册 InputRemappingListener 回调来实现。要实现此功能,请首先注册一个 InputRemappingListener 实例

Kotlin

class InputSDKRemappingListener : InputRemappingListener {
  override fun onInputMapChanged(inputMap: InputMap) {
    Log.i(TAG, "Received update on input map changed.")
    if (inputMap.inputRemappingOption() == InputEnums.REMAP_OPTION_DISABLED) {
      return
    }
    for (inputGroup in inputMap.inputGroups()) {
      if (inputGroup.inputRemappingOption() == InputEnums.REMAP_OPTION_DISABLED) {
        continue
      }
      for (inputAction in inputGroup.inputActions()) {
        if (inputAction.inputRemappingOption() != InputEnums.REMAP_OPTION_DISABLED) {
          // Found InputAction remapped by user
          processRemappedAction(inputAction)
        }
      }
    }
  }

  private fun processRemappedAction(remappedInputAction: InputAction) {
    // Get remapped action info
    val remappedControls = remappedInputAction.remappedInputControls()
    val remappedKeyCodes = remappedControls.keycodes()
    val mouseActions = remappedControls.mouseActions()
    val version = remappedInputAction.inputActionId().versionString()
    val remappedActionId = remappedInputAction.inputActionId().uniqueId()
    val currentInputAction: Optional<InputAction>
    currentInputAction = if (version == null || version.isEmpty()
      || version == InputSDKProvider.INPUTMAP_VERSION
    ) {
      getCurrentVersionInputAction(remappedActionId)
    } else {
      Log.i(TAG,
            "Detected version of user-saved input action defers from current version")
      getCurrentVersionInputActionFromPreviousVersion(
        remappedActionId, version)
    }
    if (!currentInputAction.isPresent) {
      Log.e(TAG, String.format(
        "can't find remapped input action with id %d and version %s",
        remappedActionId, if (version == null || version.isEmpty()) "UNKNOWN" else version))
      return
    }
    val originalControls = currentInputAction.get().inputControls()
    val originalKeyCodes = originalControls.keycodes()
    Log.i(TAG, String.format(
      "Found input action with id %d remapped from key %s to key %s",
      remappedActionId,
      keyCodesToString(originalKeyCodes),
      keyCodesToString(remappedKeyCodes)))

    // TODO: make display changes to match controls used by the user
  }

  private fun getCurrentVersionInputAction(inputActionId: Long): Optional<InputAction> {
    for (inputGroup in InputSDKProvider.gameInputMap.inputGroups()) {
      for (inputAction in inputGroup.inputActions()) {
        if (inputAction.inputActionId().uniqueId() == inputActionId) {
          return Optional.of(inputAction)
        }
      }
    }
    return Optional.empty()
  }

  private fun getCurrentVersionInputActionFromPreviousVersion(
    inputActionId: Long, previousVersion: String
  ): Optional<InputAction7gt; {
    // TODO: add logic to this method considering the diff between the current and previous
    //  InputMap.
    return Optional.empty()
  }

  private fun keyCodesToString(keyCodes: List<Int>): String {
    val builder = StringBuilder()
    for (keyCode in keyCodes) {
      if (!builder.toString().isEmpty()) {
        builder.append(" + ")
      }
      builder.append(keyCode)
    }
    return String.format("(%s)", builder)
  }

  companion object {
    private const val TAG = "InputSDKRemappingListener"
  }
}

Java

public class InputSDKRemappingListener implements InputRemappingListener {

    private static final String TAG = "InputSDKRemappingListener";

    @Override
    public void onInputMapChanged(InputMap inputMap) {
        Log.i(TAG, "Received update on input map changed.");
        if (inputMap.inputRemappingOption() ==
                InputEnums.REMAP_OPTION_DISABLED) {
            return;
        }
        for (InputGroup inputGroup : inputMap.inputGroups()) {
            if (inputGroup.inputRemappingOption() ==
                    InputEnums.REMAP_OPTION_DISABLED) {
                continue;
            }
            for (InputAction inputAction : inputGroup.inputActions()) {
                if (inputAction.inputRemappingOption() !=
                        InputEnums.REMAP_OPTION_DISABLED) {
                    // Found InputAction remapped by user
                    processRemappedAction(inputAction);
                }
            }
        }
    }

    private void processRemappedAction(InputAction remappedInputAction) {
        // Get remapped action info
        InputControls remappedControls =
            remappedInputAction.remappedInputControls();
        List<Integer> remappedKeyCodes = remappedControls.keycodes();
        List<Integer> mouseActions = remappedControls.mouseActions();
        String version = remappedInputAction.inputActionId().versionString();
        long remappedActionId = remappedInputAction.inputActionId().uniqueId();
        Optional<InputAction> currentInputAction;
        if (version == null || version.isEmpty()
                    || version.equals(InputSDKProvider.INPUTMAP_VERSION)) {
            currentInputAction = getCurrentVersionInputAction(remappedActionId);
        } else {
            Log.i(TAG, "Detected version of user-saved input action defers " +
                    "from current version");
            currentInputAction =
                    getCurrentVersionInputActionFromPreviousVersion(
                            remappedActionId, version);
        }
        if (!currentInputAction.isPresent()) {
            Log.e(TAG, String.format(
                    "input action with id %d and version %s not found",
                    remappedActionId, version == null || version.isEmpty() ?
                            "UNKNOWN" : version));
            return;
        }
        InputControls originalControls =
                currentInputAction.get().inputControls();
        List<Integer> originalKeyCodes = originalControls.keycodes();

        Log.i(TAG, String.format(
                "Found input action with id %d remapped from key %s to key %s",
                remappedActionId,
                keyCodesToString(originalKeyCodes),
                keyCodesToString(remappedKeyCodes)));

        // TODO: make display changes to match controls used by the user
    }

    private Optional<InputAction> getCurrentVersionInputAction(
            long inputActionId) {
        for (InputGroup inputGroup :
                    InputSDKProvider.gameInputMap.inputGroups()) {
            for (InputAction inputAction : inputGroup.inputActions()) {
                if (inputAction.inputActionId().uniqueId() == inputActionId) {
                    return Optional.of(inputAction);
                }
            }
        }
        return Optional.empty();
    }

    private Optional<InputAction>
            getCurrentVersionInputActionFromPreviousVersion(
                    long inputActionId, String previousVersion) {
        // TODO: add logic to this method considering the diff between your
        // current and previous InputMap.
        return Optional.empty();
    }

    private String keyCodesToString(List<Integer> keyCodes) {
        StringBuilder builder = new StringBuilder();
        for (Integer keyCode : keyCodes) {
            if (!builder.toString().isEmpty()) {
                builder.append(" + ");
            }
            builder.append(keyCode);
        }
        return String.format("(%s)", builder);
    }
}

C#

#if PLAY_GAMES_PC

using System.Text;
using Java.Lang;
using Java.Util;
using Google.Android.Libraries.Play.Games.Inputmapping;
using Google.Android.Libraries.Play.Games.Inputmapping.Datamodel;
using UnityEngine;

public class InputSDKRemappingListener : InputRemappingListenerCallbackHelper
{
    public override void OnInputMapChanged(InputMap inputMap)
    {
        Debug.Log("Received update on remapped controls.");
        if (inputMap.InputRemappingOption() == InputEnums.REMAP_OPTION_DISABLED)
        {
            return;
        }
        List<InputGroup> inputGroups = inputMap.InputGroups();
        for (int i = 0; i < inputGroups.Size(); i ++)
        {
            InputGroup inputGroup = inputGroups.Get(i);
            if (inputGroup.InputRemappingOption()
                    == InputEnums.REMAP_OPTION_DISABLED)
            {
                continue;
            }
            List<InputAction> inputActions = inputGroup.InputActions();
            for (int j = 0; j < inputActions.Size(); j ++)
            {
                InputAction inputAction = inputActions.Get(j);
                if (inputAction.InputRemappingOption()
                        != InputEnums.REMAP_OPTION_DISABLED)
                {
                    // Found action remapped by user
                    ProcessRemappedAction(inputAction);
                }
            }
        }
    }

    private void ProcessRemappedAction(InputAction remappedInputAction)
    {
        InputControls remappedInputControls =
                remappedInputAction.RemappedInputControls();
        List<Integer> remappedKeycodes = remappedInputControls.Keycodes();
        List<Integer> mouseActions = remappedInputControls.MouseActions();
        string version = remappedInputAction.InputActionId().VersionString();
        long remappedActionId = remappedInputAction.InputActionId().UniqueId();
        InputAction currentInputAction;
        if (string.IsNullOrEmpty(version)
                || string.Equals(
                version, InputSDKMappingProvider.INPUT_MAP_VERSION))
        {
            currentInputAction = GetCurrentVersionInputAction(remappedActionId);
        }
        else
        {
            Debug.Log("Detected version of used-saved input action defers" +
                " from current version");
            currentInputAction =
                GetCurrentVersionInputActionFromPreviousVersion(
                    remappedActionId, version);
        }
        if (currentInputAction == null)
        {
            Debug.LogError(string.Format(
                "Input Action with id {0} and version {1} not found",
                remappedActionId,
                string.IsNullOrEmpty(version) ? "UNKNOWN" : version));
            return;
        }
        InputControls originalControls = currentInputAction.InputControls();
        List<Integer> originalKeycodes = originalControls.Keycodes();

        Debug.Log(string.Format(
            "Found Input Action with id {0} remapped from key {1} to key {2}",
            remappedActionId,
            KeyCodesToString(originalKeycodes),
            KeyCodesToString(remappedKeycodes)));
        // TODO: update HUD according to the controls of the user
    }

    private InputAction GetCurrentVersionInputAction(
            long inputActionId)
    {
        List<InputGroup> inputGroups =
            InputSDKMappingProvider.gameInputMap.InputGroups();
        for (int i = 0; i < inputGroups.Size(); i++)
        {
            InputGroup inputGroup = inputGroups.Get(i);
            List<InputAction> inputActions = inputGroup.InputActions();
            for (int j = 0; j < inputActions.Size(); j++)
            {
                InputAction inputAction = inputActions.Get(j);
                if (inputAction.InputActionId().UniqueId() == inputActionId)
                {
                    return inputAction;
                }
            }
        }
        return null;
    }

    private InputAction GetCurrentVersionInputActionFromPreviousVersion(
            long inputActionId, string version)
    {
        // TODO: add logic to this method considering the diff between your
        // current and previous InputMap.
        return null;
    }

    private string KeyCodesToString(List<Integer> keycodes)
    {
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < keycodes.Size(); i ++)
        {
            Integer keycode = keycodes.Get(i);
            if (builder.Length > 0)
            {
                builder.Append(" + ");
            }
            builder.Append(keycode.IntValue());
        }
        return string.Format("({0})", builder.ToString());
    }
}
#endif

InputRemappingListener 在加载用户保存的重新映射控件后启动时,以及在用户每次重新映射其键后都会收到通知。

初始化

如果您使用 InputContexts,请在每次切换到新场景时设置上下文,包括用于初始场景的第一个上下文。您需要在注册 InputMap 后设置 InputContext

如果您使用 InputRemappingListeners 来接收重新映射事件通知,请在注册 InputMappingProvider 之前注册您的 InputRemappingListener,否则您的游戏可能会错过启动时的重要事件。

以下示例演示了如何初始化 API

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    if (isGooglePlayGamesOnPC()) {
        val inputMappingClient = Input.getInputMappingClient(this)
        // Register listener before registering the provider
        inputMappingClient.registerRemappingListener(InputSDKRemappingListener())
        inputMappingClient.setInputMappingProvider(
                InputSDKProvider())
        // Set the context after you have registered the provider.
        inputMappingClient.setInputContext(InputSDKProvider.menuSceneInputContext)
    }
}

Java

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    if (isGooglePlayGamesOnPC()) {
        InputMappingClient inputMappingClient =
                Input.getInputMappingClient(this);
        // Register listener before registering the provider
        inputMappingClient.registerRemappingListener(
                new InputSDKRemappingListener());
        inputMappingClient.setInputMappingProvider(
                new InputSDKProvider());
        // Set the context after you have registered the provider
        inputMappingClient.setInputContext(InputSDKProvider.menuSceneInputContext);
    }
}

C#

#if PLAY_GAMES_PC
using Google.Android.Libraries.Play.Games.Inputmapping;
using Google.Android.Libraries.Play.Games.InputMapping.ExternalType.Android.Content;
using Google.LibraryWrapper.Java;
#endif

public class GameManager : MonoBehaviour
{
#if PLAY_GAMES_PC
    private InputSDKMappingProvider _inputMapProvider =
        new InputSDKMappingProvider();
    private InputMappingClient _inputMappingClient;
#endif

    public void Awake()
    {
#if PLAY_GAMES_PC
        Context context = (Context)Utils.GetUnityActivity().GetRawObject();
        _inputMappingClient = Google.Android.Libraries.Play.Games.Inputmapping
            .Input.GetInputMappingClient(context);
        // Register listener before registering the provider.
        _inputMappingClient.RegisterRemappingListener(
            new InputSDKRemappingListener());
        _inputMappingClient.SetInputMappingProvider(_inputMapProvider);
        // Register context after you have registered the provider.
       _inputMappingClient.SetInputContext(
           InputSDKMappingProvider.menuSceneInputContext);
#endif
    }
}

清理

关闭游戏时,请注销您的 InputMappingProvider 实例和任何 InputRemappingListener 实例,尽管如果您不这样做,Input SDK 足够智能,可以避免资源泄漏

Kotlin

override fun onDestroy() {
    if (isGooglePlayGamesOnPC()) {
        val inputMappingClient = Input.getInputMappingClient(this)
        inputMappingClient.clearInputMappingProvider()
        inputMappingClient.clearRemappingListener()
    }

    super.onDestroy()
}

Java

@Override
protected void onDestroy() {
    if (isGooglePlayGamesOnPC()) {
        InputMappingClient inputMappingClient =
                Input.getInputMappingClient(this);
        inputMappingClient.clearInputMappingProvider();
        inputMappingClient.clearRemappingListener();
    }

    super.onDestroy();
}

C#

public class GameManager : MonoBehaviour
{
    private void OnDestroy()
    {
#if PLAY_GAMES_PC
        _inputMappingClient.ClearInputMappingProvider();
        _inputMappingClient.ClearRemappingListener();
#endif
    }
}

测试

您可以通过手动打开叠加层查看玩家体验,或通过 adb shell 进行自动化测试和验证来测试您的 Input SDK 实现。

Google Play 游戏电脑版模拟器会检查您的输入映射的正确性,以防范常见错误。对于重复唯一 ID、使用不同输入映射或违反重新映射规则(如果启用重新映射)等场景,叠加层会显示如下错误消息:Input SDK 叠加层。

使用命令行中的 adb 验证您的 Input SDK 实现。要获取当前输入映射,请使用以下 adb shell 命令(将 MY.PACKAGE.NAME 替换为您的游戏名称)

adb shell dumpsys input_mapping_service --get MY.PACKAGE.NAME

如果您成功注册了 InputMap,您将看到类似以下内容的输出

Getting input map for com.example.inputsample...
Successfully received the following inputmap:
# com.google.android.libraries.play.games.InputMap@d73526e1
input_groups {
  group_label: "Basic Movement"
  input_actions {
    action_label: "Jump"
    input_controls {
      keycodes: 51
      keycodes: 19
    }
    unique_id: 0
  }
  input_actions {
    action_label: "Left"
    input_controls {
      keycodes: 29
      keycodes: 21
    }
    unique_id: 1
  }
  input_actions {
    action_label: "Right"
    input_controls {
      keycodes: 32
      keycodes: 22
    }
    unique_id: 2
  }
  input_actions {
    action_label: "Use"
    input_controls {
      keycodes: 33
      keycodes: 66
      mouse_actions: MOUSE_LEFT_CLICK
      mouse_actions_value: 0
    }
    unique_id: 3
  }
}
input_groups {
  group_label: "Special Input"
  input_actions {
    action_label: "Jump"
    input_controls {
      keycodes: 51
      keycodes: 19
      keycodes: 62
      mouse_actions: MOUSE_LEFT_CLICK
      mouse_actions_value: 0
    }
    unique_id: 4
  }
  input_actions {
    action_label: "Duck"
    input_controls {
      keycodes: 47
      keycodes: 20
      keycodes: 113
      mouse_actions: MOUSE_RIGHT_CLICK
      mouse_actions_value: 1
    }
    unique_id: 5
  }
}
mouse_settings {
  allow_mouse_sensitivity_adjustment: true
  invert_mouse_movement: true
}

本地化

Input SDK 不使用 Android 的本地化系统。因此,提交 InputMap 时必须提供本地化字符串。您也可以使用您的游戏引擎的本地化系统。

Proguard

当使用 Proguard 缩小您的游戏时,请将以下规则添加到您的 Proguard 配置文件中,以确保 SDK 不会被从最终包中移除

-keep class com.google.android.libraries.play.hpe.** { *; }
-keep class com.google.android.libraries.play.games.inputmapping.** { *; }

后续步骤

将 Input SDK 集成到您的游戏后,您可以继续完成 Google Play 游戏电脑版的所有剩余要求。有关更多信息,请参阅Google Play 游戏电脑版入门