本文档介绍了如何在支持 Google Play 游戏电脑版的游戏中设置和显示 Input SDK。这些任务包括将 SDK 添加到您的游戏中以及生成包含游戏操作到用户输入分配的输入映射。
开始之前
在将 Input SDK 添加到您的游戏之前,您必须使用游戏引擎的输入系统支持键盘和鼠标输入。
Input SDK 向 Google Play 游戏电脑版提供有关您的游戏使用哪些控件的信息,以便将其显示给用户。它还可以选择性地允许用户重新映射键盘。
每个控件都是一个 InputAction
(例如,“J”代表“Jump”),您将 InputAction
组织到 InputGroup
中。InputGroup
可能代表您游戏中的不同模式,例如“驾驶”或“步行”或“主菜单”。您还可以使用 InputContexts
来指示在游戏的哪些点激活哪些组。
您可以启用键盘重新映射以自动为您处理,但如果您更喜欢提供自己的控件重新映射界面,则可以禁用 Input SDK 重新映射。
以下序列图描述了 Input SDK 的 API 如何工作
当您的游戏实现 Input SDK 时,您的控件会显示在 Google Play 游戏电脑版叠加层中。
Google Play 游戏电脑版叠加层
Google Play 游戏电脑版叠加层(“叠加层”)显示您的游戏定义的控件。用户随时可以通过按 Shift + Tab 访问叠加层。
设计按键绑定的最佳实践
设计按键绑定时,请考虑以下最佳实践
- 将您的
InputAction
逻辑分组到InputGroup
中,以提高游戏过程中控件的导航和可发现性。 - 将每个
InputGroup
最多分配给一个InputContext
。细粒度的InputMap
可在叠加层中导航您的控件时带来更好的体验。 - 为游戏的每种不同场景类型创建
InputContext
。通常,您可以为所有“菜单式”场景使用单个InputContext
。对于游戏中的任何小游戏或单个场景的替代控件,请使用不同的InputContext
。 - 如果两个操作旨在在同一个
InputContext
下使用相同的键,请使用“互动 / 射击”等标签字符串。 - 如果两个键设计为绑定到同一个
InputAction
,则使用 2 个不同的InputAction
在您的游戏中执行相同的操作。您可以对两个InputAction
使用相同的标签字符串,但其 ID 必须不同。 - 如果将修饰键应用于一组键,请考虑使用带有修饰键的单个
InputAction
,而不是多个组合修饰键的InputAction
(例如:使用 Shift 和 W、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 游戏电脑版会将每个用户重新映射的控件映射到您的游戏期望接收的默认控件,因此您的游戏不需要了解玩家的重新映射。您可以通过为重新映射事件添加回调来选择性地更新用于在游戏中显示键盘控件的资源。
Google Play 游戏电脑版会在本地为每个用户存储重新映射的控件,从而使控件在游戏会话之间保持持久性。此信息仅针对 PC 平台存储在磁盘上,不影响移动体验。用户卸载或重新安装 Google Play 游戏电脑版后,控件数据将被删除。此数据在多个 PC 设备之间不持久。
为了在您的游戏中支持重新映射功能,请避免以下限制
重新映射的限制
如果按键绑定包含以下任何情况,则可以在您的游戏中禁用重新映射功能
- 不由修饰键 + 非修饰键组成的多键
InputAction
。例如,Shift + A 是有效的,但 A + B、Ctrl + Alt 或 Shift + A + Tab 则无效。 InputMap
包含具有重复唯一 ID 的InputActions
、InputGroups
或InputContexts
。
重新映射的局限性
为重新映射设计按键绑定时,请考虑以下限制
- 不支持重新映射到组合键。例如,用户无法将 Shift + A 重新映射到 Ctrl + B 或将 A 重新映射到 Shift + A。
- 不支持带有鼠标按钮的
InputAction
的重新映射。例如,无法重新映射 Shift + 右键点击。
在 Google Play 游戏电脑版模拟器上测试按键重新映射
您随时可以通过发出以下 adb 命令在 Google Play 游戏电脑版模拟器中启用重新映射功能
adb shell dumpsys input_mapping_service --set RemappingFlagValue true
叠加层更改如下图所示
添加 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
并安装其依赖项来安装包
- com.google.external-dependency-manager-1.2.172
- com.google.librarywrapper.java-0.2.0
- com.google.librarywrapper.openjdk8-0.2.0
- com.google.android.libraries.play.games.inputmapping-1.1.1-beta(或从此存档中选择
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 );
操作也可以表示鼠标输入。此示例将左键点击设置为“移动”操作
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
来指定组合键。在此示例中,
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 );
Input SDK 允许您将鼠标和按键组合在一起用于单个操作。此示例表明
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 具有以下字段
ActionLabel
:在 UI 中显示的字符串,用于表示此操作。本地化不是自动完成的,因此请提前执行任何本地化。InputControls
:定义此操作使用的输入控件。控件在叠加层中映射到一致的字形。InputActionId
:InputIdentifier
对象,存储InputAction
的数字 ID 和版本(有关更多信息,请参阅跟踪键 ID)。InputRemappingOption
:InputEnums.REMAP_OPTION_ENABLED
或InputEnums.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 );
以下示例在叠加层中显示了道路控制和菜单控制输入组
InputGroup
具有以下字段
GroupLabel
:将在叠加层中显示的字符串,可用于逻辑分组一组操作。此字符串不会自动本地化。InputActions
:您在上一步中定义的InputAction
对象列表。所有这些操作都将显示在组标题下。InputGroupId
:InputIdentifier
对象,用于存储InputGroup
的数字 ID 和版本。有关更多信息,请参阅跟踪键 ID。InputRemappingOption
:InputEnums.REMAP_OPTION_ENABLED
或InputEnums.REMAP_OPTION_DISABLED
之一。如果禁用,则即使属于此组的所有InputAction
对象指定其重新映射选项已启用,它们也将禁用重新映射。如果启用,则除非各个操作指定禁用,否则属于此组的所有操作都可以重新映射。
定义您的输入上下文
InputContexts
允许您的游戏为游戏的不同场景使用不同的键盘控件集。例如
- 您可以为导航菜单和在游戏中移动指定不同的输入集。
- 您可以根据游戏中移动模式(例如驾驶与步行)指定不同的输入集。
- 您可以根据游戏的当前状态(例如导航大世界与玩单个级别)指定不同的输入集。
使用 InputContexts
时,叠加层会首先显示正在使用的上下文的组。要启用此行为,每当您的游戏进入不同的场景时,调用 setInputContext()
来设置上下文。下图演示了此行为:在“驾驶”场景中,道路控制操作显示在叠加层的顶部。打开“商店”菜单时,“菜单控制”操作显示在叠加层的顶部。
这些叠加层更新是通过在游戏的不同点设置不同的 InputContext
来实现的。为此
- 使用
InputGroups
将您的InputAction
与逻辑相关的操作分组 - 将这些
InputGroup
分配给游戏不同部分的InputContext
属于同一个 InputContext
的 InputGroup
不能有冲突的 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
:描述属于上下文的组的字符串。InputContextId
:InputIdentifier
对象,存储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()
指定了当前正在使用的组。MouseSettings
:MouseSettings
对象指示可以调整鼠标灵敏度以及鼠标在 Y 轴上是否反转。InputMapId
:InputIdentifier
对象,存储InputMap
的数字 ID 和版本(有关更多信息,请参阅跟踪键 ID)。InputRemappingOption
:InputEnums.REMAP_OPTION_ENABLED
或InputEnums.REMAP_OPTION_DISABLED
之一。定义重新映射功能是否启用。ReservedControls
:用户不允许重新映射到的InputControls
列表。
跟踪键 ID
InputAction
、InputGroup
、InputContext
和 InputMap
对象包含一个 InputIdentifier
对象,该对象存储一个唯一的数字 ID 和一个字符串版本 ID。跟踪对象的字符串版本是可选的,但建议跟踪 InputMap
的版本。如果未提供字符串版本,则字符串为空。InputMap
对象需要字符串版本。
以下示例为 InputAction
或 InputGroup
分配字符串版本
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
:一个人类可读的版本字符串,用于在两次输入数据更改之间识别输入数据的版本。
获取重新映射事件通知(可选)
接收重新映射事件通知,以了解您的游戏中正在使用的键。这允许您的游戏更新游戏屏幕上显示的用于显示操作控件的资源。
以下图片显示了此行为的示例,其中在重新映射键
此功能通过注册 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、使用不同输入映射或违反重新映射规则(如果启用重新映射)等场景,叠加层会显示如下错误消息:
使用命令行中的 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 游戏电脑版入门。