输入 SDK 使用入门

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

准备工作

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

输入 SDK 会向 Google Play 游戏电脑版提供与游戏所使用的控件相关的信息,以便向用户显示相关控件。此外,它还可以允许为用户进行键盘重新映射(可选)。

每个控件都是一个 InputAction(例如“J”代表“Jump”[跳跃]),并且您可以将 InputActions 整理为 InputGroupsInputGroup 可能代表游戏中的不同模式,例如“驾车”“步行”或“主菜单”。此外,您还可以使用 InputContexts 来指示在游戏的不同时刻哪些组要处于活跃状态。

您可以让系统自动为您处理键盘重新映射,但如果您更愿意提供自己的控件重新映射接口,则可以停用输入 SDK 重新映射。

下面的序列图展示了输入 SDK 的 API 的工作原理:

显示调用输入 SDK API 的游戏实现及其与 Android 设备的互动过程的序列图。

如果您的游戏实现了输入 SDK,您的控件便会显示在 Google Play 游戏电脑版叠加层中。

Google Play 游戏电脑版叠加层

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

Google Play 游戏电脑版叠加层。

有关按键绑定设计的最佳实践

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

  • InputActions 整理为逻辑上相关的 InputGroups,以便改进游戏过程中的控件导航和易发现程度。
  • 每个 InputGroup 最多分配到一个 InputContext。如果 InputMap 粒度精细,便可以在叠加层中提供更好的控件导航体验。
  • 为游戏的每个不同场景类型分别创建一个 InputContext。通常,您可以针对所有“菜单类”场景使用一个 InputContext。对于游戏中的任何迷你游戏或单个场景的替代控件,请分别使用不同的 InputContexts
  • 如果您将两项操作设计为使用同一 InputContext 下的同一按键,请利用“Interact / Fire”等标签字符串。
  • 如果将两个按键设计为绑定到同一 InputAction,则应使用两个不同的 InputActions 在游戏中执行相同的操作。您可以针对这两个 InputActions 使用相同的标签字符串,但其 ID 必须有所区别。
  • 如果将某个辅助键应用于一组按键,请考虑让一个 InputAction 使用该辅助键,而不是让多个 InputActions 组合使用该辅助键(例如:应使用 Shift 键W、A、S、D,而非 Shift 键 + W、Shift 键 + A、Shift 键 + S、Shift 键 + D)。
  • 当用户向文本字段写入内容时,系统应自动停用输入重新映射。请遵循与实现 Android 文本字段相关的最佳实践,以确保 Android 能够检测游戏中的文本字段,并防止经过重新映射的按键对这些字段产生干扰。如果您的应用必须使用非常规的文本字段,请使用 setInputContext() 和包含空 InputGroups 列表的 InputContext 来手动停用重新映射。
  • 如果您的应用支持重新映射,请考虑更新可能与用户保存的版本存在冲突的敏感操作的按键绑定。请尽可能避免更改现有控件的 ID。

重新映射功能

Google Play 游戏电脑版支持根据您的游戏使用输入 SDK 提供的按键绑定进行键盘控件重新映射。这是可选设置,可以完全停用。例如,您可能想要提供自己的键盘重新映射接口。如需为您的游戏停用重新映射,您只需指定为 InputMap 停用重新映射选项即可(如需了解详情,请参阅构建 InputMap)。

如需访问该功能,用户需要打开叠加层,然后点击要重新映射的操作。在每个重新映射事件发生后,Google Play 游戏电脑版都会将每个用户重新映射的控件映射到您的游戏预期会接收的默认控件,因此您的游戏并不需要了解玩家的重新映射。您可以选择添加重新映射事件的回调,以便更新用于在游戏中显示键盘控件的资源。

尝试重新映射按键

Google Play 游戏电脑版会将为每位用户重新映射的控件存储在本地,以便让这类控件在不同的游戏会话之间保持不变。只有在 PC 平台上这些信息才会存储在磁盘中,因此不会对移动体验产生任何影响。如果用户卸载或重新安装 Google Play 游戏电脑版,系统将删除控件数据。这些数据无法在多台 PC 设备之间保持不变。

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

重新映射的相关限制

如果按键绑定符合以下任何情况,重新映射功能可能会在您的游戏中停用:

  • 多按键 InputActions 并非由辅助键 + 非辅助键组成。例如:Shift 键 + A 就符合条件,但 A + BCtrl 键 + Alt 键Shift 键 + A + Tab 键就不符合条件。
  • InputMap 包含的 InputActionsInputGroupsInputContexts 具有重复的唯一 ID。

重新映射的局限性

设计用于重新映射的按键绑定时,请考虑以下局限性:

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

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

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

adb shell dumpsys input_mapping_service --set RemappingFlagValue true

叠加层将如下图所示发生变化:

启用了按键重新映射功能的叠加层。

添加 SDK

根据开发平台安装输入 SDK。

Java 和 Kotlin

通过将依赖项添加到模块级 build.gradle 文件,获取适用于 Java 或 Kotlin 的输入 SDK:

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

Unity

输入 SDK 是一种标准 Unity 软件包,且具有多个依赖项。

您需要安装该软件包以及所有依赖项。您可以通过多种方式安装该软件包。

安装 .unitypackage

下载输入 SDK unitypackage 文件及其所有依赖项。您可以按以下步骤安装 .unitypackage:依次选择 Assets > Import package > Custom Package,然后找到您下载的文件。

使用 UPM 进行安装

或者,您也可以使用 Unity 的 Package Manager 安装该软件包,只需下载 .tgz 并安装其依赖项即可:

使用 OpenUPM 进行安装

您可以使用 OpenUPM 安装该软件包。

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

示例游戏

有关如何与输入 SDK 集成的示例,请参阅适用于 Kotlin 或 Java 游戏的 AGDK 隧道以及适用于 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 类用于将按键或按键组合映射到游戏操作。InputActions 的唯一 ID 必须有别于所有其他 InputActions

如果您要支持重新映射,您可以定义哪些 InputActions 可以进行重新映射。如果您的游戏不支持重新映射,您应该为所有 InputActions 停用重新映射选项;但如果您的 InputMap 中不支持重新映射,则输入 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
);

叠加层中显示单按键 InputAction。

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

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

叠加层中显示鼠标 InputAction。

您可以通过向 InputAction 传递多个按键代码来指定按键组合。在本例中,空格键 + Shift 键映射到“加速”操作,即使空格键映射到“跳跃”操作也没关系。

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

叠加层中显示多按键 InputAction。

借助输入 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
);

叠加层中显示按键 + 鼠标组合 InputAction。

InputAction 包含以下字段:

  • ActionLabel:界面中显示的用于表示操作的字符串。本地化无法自动完成,因此请预先执行任何本地化。
  • InputControls:用于定义操作使用的输入控件。这些控件在叠加层中映射为一致的字形。
  • InputActionId:用于存储 InputAction 的数字 ID 和版本的 InputIdentifier 对象(如需了解详情,请参阅跟踪按键 ID)。
  • InputRemappingOptionInputEnums.REMAP_OPTION_ENABLEDInputEnums.REMAP_OPTION_DISABLED 其中之一。用于定义操作是否支持重新映射。如果您的游戏不支持重新映射,您可以跳过该字段或直接将其设置为停用。
  • RemappedInputControls:只读 InputControls 对象,用于从重新映射事件中读取用户设置的重新映射按键(用于获取有关重新映射事件的通知)。

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

  • AndroidKeycodes:一个整数列表,表示与操作相关联的键盘输入。这些是在 Unity 的 KeyEvent 类或 AndroidKeycode 类中定义的。
  • MouseActionsMouseAction 值的列表,表示与操作相关联的鼠标输入。

定义输入组

使用 InputGroups 按逻辑相关的操作对 InputActions 进行分组,以便改进导航效果,并使控件在叠加层中更容易被发现。每个 InputGroup ID 都必须有别于游戏中的所有其他 InputGroups

通过将输入操作划分为多个组,可以让玩家更轻松地找到适合当前上下文的正确按键绑定。

如果您要支持重新映射,您可以定义哪些 InputGroups 可以进行重新映射。如果您的游戏不支持重新映射,您应该为所有 InputGroups 停用重新映射选项;但如果您的 InputMap 中不支持重新映射,则输入 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
);

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

显示包含“道路控件”和“菜单控件”输入组的 InputMap 的叠加层。

InputGroup 具有以下字段:

  • GroupLabel:要在叠加层中显示的字符串,可用于对一组操作进行逻辑分组。系统不会自动本地化该字符串。
  • InputActions:您在上一步中定义的 InputAction 对象的列表。所有这些操作都直观地显示在组标题下。
  • InputGroupId:用于存储 InputGroup 的数字 ID 和版本的 InputIdentifier 对象。如需了解详情,请参阅跟踪按键 ID
  • InputRemappingOptionInputEnums.REMAP_OPTION_ENABLEDInputEnums.REMAP_OPTION_DISABLED 其中之一。如果停用,属于该组的所有 InputAction 对象都将停用重新映射,即使其指定重新映射选项已启用也是如此。如果启用,除非被单个操作指定停用,否则属于该组的所有操作都可以重新映射。

定义输入上下文

InputContexts 允许您的游戏针对游戏的不同场景使用一组不同的键盘控件。例如:

  • 您可以分别为导航菜单和游戏内移动指定一组不同的输入。
  • 您可以根据游戏中的移动模式(例如驾车与步行),分别为其指定一组不同的输入。
  • 您可以根据游戏的当前状态(例如在主地图中导航与玩单独的关卡),分别为其指定一组不同的输入。

使用 InputContexts 时,叠加层首先显示正在使用的上下文组。如需启用该行为,请在游戏每次进入不同场景时调用 setInputContext() 以设置上下文。下图展示了该行为:在“驾车”场景中,道路控件操作将显示在叠加层的顶部。打开“商店”菜单时,“菜单控件”操作将显示在叠加层的顶部。

InputContexts 在叠加层中对组进行排序。

这些叠加层更新是通过在游戏中的不同时刻设置不同的 InputContext 来实现的。具体方法如下:

  1. 使用 InputGroups 按逻辑相关的操作对 InputActions 进行分组
  2. 将这些 InputGroups 分配给游戏不同部分的 InputContext

对于属于同一 InputContextInputGroups,其 InputActions 不得存在冲突(使用相同的按键)。最好将每个 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:用于描述属于上下文的组的字符串。
  • InputContextId:用于存储 InputContext 的数字 ID 和版本的 InputIdentifier 对象(如需了解详情,请参阅跟踪按键 ID)。
  • ActiveGroups:当上下文处于活动状态时要使用并显示在叠加层顶部的 InputGroups 的列表。

构建输入映射

InputMap 是游戏中可用的所有 InputGroup 对象的集合,也就是玩家预期会执行的所有 InputAction 对象。

报告按键绑定时,您要构建一个包含游戏中使用的所有 InputGroupsInputMap

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

以下示例构建了一个用于报告一系列 InputGroupsInputMap

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 轴上反转。
  • InputMapId:用于存储 InputMap 的数字 ID 和版本的 InputIdentifier 对象(如需了解详情,请参阅跟踪按键 ID)。
  • InputRemappingOptionInputEnums.REMAP_OPTION_ENABLEDInputEnums.REMAP_OPTION_DISABLED 其中之一。用于定义重新映射功能是否已启用。
  • ReservedControls:不允许用户重新映射到的 InputControls 列表。

跟踪按键 ID

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

以下示例将字符串版本分配给了 InputActionsInputGroups

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 中的所有其他 InputActions。同样,InputGroup 对象的 ID 也必须有别于 InputMap 中的所有其他 InputGroups。以下示例演示了如何使用 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:人类可读的版本字符串,用于标识两个版本的输入数据更改之间的输入数据版本。

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

接收有关重新映射事件的通知,了解游戏中使用的按键。这样一来,您的游戏便可以更新游戏屏幕上显示的用于显示操作控件的资源。

下图显示了该行为的示例:将按键 GPS 分别重新映射到 JXT 以后,游戏的界面元素将会更新,以便显示用户设置的按键。

界面使用 InputRemappingListener 回调响应重新映射事件。

该功能可以通过注册 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 来获取有关重新映射事件的通知,请先注册 InputRemappingListener,再注册 InputMappingProvider,否则您的游戏可能会在启动时错过重要事件。

以下示例展示了如何初始化该 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 实例,不过输入 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
    }
}

测试

您可以测试输入 SDK 实现,方法是手动打开叠加层以查看玩家体验,或通过 adb shell 进行自动测试和验证。

Google Play 游戏电脑版模拟器会检查您的输入映射是否正确,以避免出现常见错误。对于唯一 ID 重复、使用不同的输入映射或重新映射规则失败(如已启用重新映射)等情况,叠加层会显示如下错误消息:输入 SDK 叠加层。

在命令行中使用 adb 验证您的输入 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
}

本地化

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

ProGuard

使用 ProGuard 缩减游戏大小时,请将以下规则添加到 ProGuard 配置文件中,以确保系统不会从最终软件包中删除该 SDK:

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

后续操作

将输入 SDK 集成到游戏中后,您可以继续满足剩下的 Google Play 游戏电脑版要求。如需了解详情,请参阅开始针对 Google Play 游戏电脑版开发游戏