输入 SDK 入门

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

开始之前

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

输入 SDK 向 Google Play 游戏 (适用于 PC) 提供有关游戏使用哪些控件的信息,以便可以将其显示给用户。它还可以选择允许用户重新映射键盘。

每个控件都是一个InputAction(例如,“J”表示“跳跃”),您可以将InputActions组织到InputGroups中。InputGroup可能表示游戏中不同的模式,例如“驾驶”、“步行”或“主菜单”。您还可以使用InputContexts来指示游戏中不同点哪些组处于活动状态。

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

以下时序图描述了输入 SDK 的 API 如何工作

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

当您的游戏实现输入 SDK 时,您的控件将显示在 Google Play 游戏 (适用于 PC) 叠加层中。

Google Play 游戏 (适用于 PC) 叠加层

Google Play 游戏 (适用于 PC) 叠加层(“叠加层”)显示由您的游戏定义的控件。用户可以随时按Shift + Tab访问叠加层。

The Google Play Games on PC overlay.

设计按键绑定最佳实践

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

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

重新映射功能

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

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

Attempt to remap the key

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

要支持游戏中重新映射功能,请避免以下限制

重新映射限制

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

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

重新映射限制

在设计用于重新映射的键绑定时,请考虑以下限制

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

在 Google Play Games for PC 模拟器上测试键重新映射

您可以随时通过发出以下 adb 命令在 Google Play Games for PC 模拟器中启用重新映射功能

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 类用于将键或键组合映射到游戏操作。InputActions 必须在所有InputActions 中具有唯一的 ID。

如果您支持重新映射,则可以定义哪些InputActions 可以重新映射。如果您的游戏不支持重新映射,则应为所有InputActions 设置禁用重新映射选项,但如果您的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:定义此操作使用的输入控件。这些控件映射到叠加层中一致的字形。
  • InputActionId:存储InputAction 的编号 ID 和版本的InputIdentifier 对象(有关更多信息,请参阅跟踪键 ID)。
  • InputRemappingOptionInputEnums.REMAP_OPTION_ENABLEDInputEnums.REMAP_OPTION_DISABLED 之一。定义操作是否启用重新映射。如果您的游戏不支持重新映射,您可以跳过此字段或将其简单地设置为禁用。
  • RemappedInputControls:用于在重新映射事件中读取用户在重新映射事件中设置的重新映射键集的只读InputControls 对象(用于在重新映射事件中获取通知)。

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

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

定义您的输入组

使用InputGroups 将逻辑相关的操作分组到InputActions 中,以提高叠加层中的导航和控件可发现性。每个InputGroup ID 需要在游戏中的所有InputGroups 中都是唯一的。

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

如果您支持重新映射,则可以定义哪些InputGroups 可以重新映射。如果您的游戏不支持重新映射,则应为所有InputGroups 设置禁用重新映射选项,但如果您的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 对象列表。所有这些操作都将在组标题下以视觉方式显示。
  • InputGroupId:存储InputGroup 的编号 ID 和版本的InputIdentifier 对象。有关更多信息,请参阅跟踪键 ID
  • InputRemappingOptionInputEnums.REMAP_OPTION_ENABLEDInputEnums.REMAP_OPTION_DISABLED 之一。如果禁用,则属于此组的所有InputAction 对象都将禁用重新映射,即使它们指定了其重新映射选项已启用。如果启用,则属于此组的所有操作都可以重新映射,除非由各个操作指定为禁用。

定义您的输入上下文

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

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

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

InputContexts sorting groups in the overlay.

这些叠加层更新是通过在游戏的不同点设置不同的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:描述属于该上下文的组的字符串。
  • InputContextIdInputIdentifier对象,存储InputContext的数字ID和版本(有关更多信息,请参阅跟踪键ID)。
  • ActiveGroups:要在该上下文处于活动状态时使用并在叠加层顶部显示的InputGroups列表。

构建输入映射

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

在报告您的键绑定时,您将构建一个包含游戏中所有InputGroupsInputMap

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

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

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对象,需要字符串版本。

以下示例将字符串版本分配给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:一个人类可读的版本字符串,用于在两个版本的输入数据更改之间识别输入数据的版本。

接收重新映射事件的通知(可选)

接收重新映射事件的通知,以了解游戏中使用的键。这使您的游戏能够更新游戏屏幕上显示操作控件的资产。

下图显示了此行为的一个示例,其中重新映射键后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游戏在PC模拟器上会检查您的输入映射是否包含常见错误。对于重复唯一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游戏在PC上的要求。有关更多信息,请参阅开始使用Google Play游戏在PC上