本文档介绍了如何在 支持 Google Play 游戏电脑版的游戏。任务包括添加 SDK 并生成输入映射,其中包含 游戏操作与用户输入之间的分配关系。
准备工作
在将输入 SDK 添加到游戏中之前,您必须使用游戏引擎的输入系统实现对键盘和鼠标输入的支持。
输入 SDK 会向 Google Play 游戏电脑版提供与游戏所使用的控件相关的信息,以便向用户显示相关控件。此外,它还可以允许为用户进行键盘重新映射(可选)。
每个控件都是一个 InputAction
(例如“J”代表“Jump”[跳跃]),并且您可以将 InputActions
整理为 InputGroups
。InputGroup
可能代表游戏中的不同模式,例如“驾车”“步行”或“主菜单”。此外,您还可以使用 InputContexts
来指示在游戏的不同时刻哪些组要处于活跃状态。
您可以让系统自动为您处理键盘重新映射,但如果您更愿意提供自己的控件重新映射接口,则可以停用输入 SDK 重新映射。
下面的序列图展示了输入 SDK 的 API 的工作原理:
如果您的游戏实现了输入 SDK,您的控件便会显示在 Google Play 游戏电脑版叠加层中。
Google Play 游戏电脑版叠加层
Google Play 游戏电脑版叠加层(简称“叠加层”)用于显示您的游戏所定义的控件。用户可随时通过按 Shift 键 + Tab 键来访问该叠加层。
有关按键绑定设计的最佳实践
设计按键绑定时,请考虑遵循以下最佳实践:
- 将
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 + B、Ctrl 键 + Alt 键或 Shift 键 + A + Tab 键就不符合条件。 InputMap
包含的InputActions
、InputGroups
或InputContexts
具有重复的唯一 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
并安装其依赖项即可:
- 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.0-beta
使用 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 );
操作也可以表示鼠标输入。此示例将左键点击设置为移动操作:
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 );
借助输入 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
:界面中显示的用于表示操作的字符串。本地化无法自动完成,因此请预先执行任何本地化。InputControls
:用于定义操作使用的输入控件。这些控件在叠加层中映射为一致的字形。InputActionId
:用于存储InputAction
的数字 ID 和版本的InputIdentifier
对象(如需了解详情,请参阅跟踪按键 ID)。InputRemappingOption
:InputEnums.REMAP_OPTION_ENABLED
或InputEnums.REMAP_OPTION_DISABLED
其中之一。用于定义操作是否支持重新映射。如果您的游戏不支持重新映射,您可以跳过该字段或直接将其设置为停用。RemappedInputControls
:只读InputControls
对象,用于从重新映射事件中读取用户设置的重新映射按键(用于获取有关重新映射事件的通知)。
InputControls
表示与操作相关联的输入,并且包含以下字段:
AndroidKeycodes
:一个整数列表,表示与操作相关联的键盘输入。这些是在 Unity 的 KeyEvent 类或 AndroidKeycode 类中定义的。MouseActions
:MouseAction
值的列表,表示与操作相关联的鼠标输入。
定义输入组
使用 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 );
以下示例在叠加层中显示了道路控件和菜单控件输入组:
InputGroup
具有以下字段:
GroupLabel
:要在叠加层中显示的字符串,可用于对一组操作进行逻辑分组。系统不会自动本地化该字符串。InputActions
:您在上一步中定义的InputAction
对象的列表。所有这些操作都直观地显示在组标题下。InputGroupId
:用于存储InputGroup
的数字 ID 和版本的InputIdentifier
对象。如需了解详情,请参阅跟踪按键 ID。InputRemappingOption
:InputEnums.REMAP_OPTION_ENABLED
或InputEnums.REMAP_OPTION_DISABLED
其中之一。如果停用,属于该组的所有InputAction
对象都将停用重新映射,即使其指定重新映射选项已启用也是如此。如果启用,除非被单个操作指定停用,否则属于该组的所有操作都可以重新映射。
定义输入上下文
InputContexts
允许您的游戏针对游戏的不同场景使用一组不同的键盘控件。例如:
- 您可以分别为导航菜单和游戏内移动指定一组不同的输入。
- 您可以根据游戏中的移动模式(例如驾车与步行),分别为其指定一组不同的输入。
- 您可以根据游戏的当前状态(例如在主地图中导航与玩单独的关卡),分别为其指定一组不同的输入。
使用 InputContexts
时,叠加层首先显示正在使用的上下文组。如需启用该行为,请在游戏每次进入不同场景时调用 setInputContext()
以设置上下文。下图展示了该行为:在“驾车”场景中,道路控件操作将显示在叠加层的顶部。打开“商店”菜单时,“菜单控件”操作将显示在叠加层的顶部。
这些叠加层更新是通过在游戏中的不同时刻设置不同的 InputContext
来实现的。具体方法如下:
- 使用
InputGroups
按逻辑相关的操作对InputActions
进行分组 - 将这些
InputGroups
分配给游戏不同部分的InputContext
对于属于同一 InputContext
的 InputGroups
,其 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
对象。
报告按键绑定时,您要构建一个包含游戏中使用的所有 InputGroups
的 InputMap
。
如果您的游戏不支持重新映射,请将重新映射选项设置为停用,并将预留按键设置为空。
以下示例构建了一个用于报告一系列 InputGroups
的 InputMap
。
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
:用于存储InputMap
的数字 ID 和版本的InputIdentifier
对象(如需了解详情,请参阅跟踪按键 ID)。InputRemappingOption
:InputEnums.REMAP_OPTION_ENABLED
或InputEnums.REMAP_OPTION_DISABLED
其中之一。用于定义重新映射功能是否已启用。ReservedControls
:不允许用户重新映射到的InputControls
列表。
跟踪按键 ID
InputAction
、InputGroup
、InputContext
和 InputMap
对象包含一个InputIdentifier
对象,该对象用于存储唯一的数字 ID 和字符串版本 ID。是否跟踪对象的字符串版本可由您自酌,但建议跟踪 InputMap
的版本。如果未提供字符串版本,则字符串为空。InputMap
对象需要字符串版本。
以下示例将字符串版本分配给了 InputActions
或 InputGroups
:
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
:人类可读的版本字符串,用于标识两个版本的输入数据更改之间的输入数据版本。
获取有关重新映射事件的通知(可选)
接收有关重新映射事件的通知,了解游戏中使用的按键。这样一来,您的游戏便可以更新游戏屏幕上显示的用于显示操作控件的资源。
下图显示了该行为的示例:将按键
该功能可以通过注册 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 重复、使用不同的输入映射或重新映射规则失败(如已启用重新映射)等情况,叠加层会显示如下错误消息:
在命令行中使用 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 游戏电脑版开发游戏。