本文档介绍了如何在支持 Google Play 游戏 (适用于 PC) 的游戏中设置和显示输入 SDK。这些任务包括将 SDK 添加到您的游戏中并生成输入映射,其中包含游戏操作到用户输入的分配。
开始之前
在将输入 SDK 添加到您的游戏中之前,您必须使用游戏引擎的输入系统支持键盘和鼠标输入。
输入 SDK 向 Google Play 游戏 (适用于 PC) 提供有关游戏使用哪些控件的信息,以便可以将其显示给用户。它还可以选择允许用户重新映射键盘。
每个控件都是一个InputAction
(例如,“J”表示“跳跃”),您可以将InputActions
组织到InputGroups
中。InputGroup
可能表示游戏中不同的模式,例如“驾驶”、“步行”或“主菜单”。您还可以使用InputContexts
来指示游戏中不同点哪些组处于活动状态。
您可以启用键盘重新映射以自动为您处理,但如果您希望提供自己的控件重新映射界面,则可以禁用输入 SDK 重新映射。
以下时序图描述了输入 SDK 的 API 如何工作
当您的游戏实现输入 SDK 时,您的控件将显示在 Google Play 游戏 (适用于 PC) 叠加层中。
Google Play 游戏 (适用于 PC) 叠加层
Google Play 游戏 (适用于 PC) 叠加层(“叠加层”)显示由您的游戏定义的控件。用户可以随时按Shift + Tab访问叠加层。
设计按键绑定最佳实践
在设计按键绑定时,请考虑以下最佳实践
- 将您的
InputActions
分组到逻辑相关的InputGroups
中,以在游戏过程中改善控件的导航和可发现性。 - 将每个
InputGroup
最多分配给一个InputContext
。细粒度的InputMap
可以带来更好的体验,以便在叠加层中导航控件。 - 为游戏的每种不同的场景类型创建一个
InputContext
。通常,您可以对所有“菜单式”场景使用单个InputContext
。对游戏中的任何迷你游戏或单个场景的替代控件使用不同的InputContexts
。 - 如果两个操作被设计为在同一
InputContext
下使用相同的键,请使用标签字符串,例如“交互/射击”。 - 如果两个键被设计为绑定到相同的
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 游戏 (适用于 PC) 支持基于游戏使用输入 SDK 提供的按键绑定进行键盘控件重新映射。这是可选的,可以完全禁用。例如,您可能希望提供自己的键盘重新映射界面。要禁用游戏的重新映射,您只需为InputMap
指定禁用重新映射选项即可(有关更多信息,请参阅构建 InputMap)。
要访问此功能,用户需要打开叠加层,然后点击他们想要重新映射的操作。在每次重新映射事件后,Google Play Games for PC 会将每个用户重新映射的控件映射到游戏期望接收的默认控件,因此您的游戏不需要了解玩家的重新映射。您可以选择通过为重新映射事件添加回调来更新游戏中用于显示键盘控件的资源。
Google Play Games for PC 会为每个用户本地存储重新映射的控件,从而实现跨游戏会话的控件持久性。此信息仅存储在 PC 平台的磁盘上,不会影响移动体验。卸载或重新安装 Google Play Games for PC 后,控件数据将被删除。此数据不会跨多个 PC 设备持久化。
要支持游戏中重新映射功能,请避免以下限制
重新映射限制
如果键绑定包含以下任何情况,则可以在游戏中禁用重新映射功能
- 不是由修饰键 + 非修饰键组成的多键
InputActions
。例如,Shift + A 有效,但 A + B、Ctrl + Alt 或 Shift + A + Tab 无效。 InputMap
包含具有重复唯一 ID 的InputActions
、InputGroups
或InputContexts
。
重新映射限制
在设计用于重新映射的键绑定时,请考虑以下限制
- 不支持重新映射到键组合。例如,用户无法将 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
叠加层更改如下面的图像所示
添加 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
使用 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 );
操作也可以表示鼠标输入。此示例将左键单击设置为“移动”操作
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
:存储InputAction
的编号 ID 和版本的InputIdentifier
对象(有关更多信息,请参阅跟踪键 ID)。InputRemappingOption
:InputEnums.REMAP_OPTION_ENABLED
或InputEnums.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 );
以下示例在叠加层中显示“道路控件”和“菜单控件”输入组
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
:InputIdentifier
对象,存储InputContext
的数字ID和版本(有关更多信息,请参阅跟踪键ID)。ActiveGroups
:要在该上下文处于活动状态时使用并在叠加层顶部显示的InputGroups
列表。
构建输入映射
一个InputMap
是游戏中所有可用InputGroup
对象的集合,因此也是玩家可以执行的所有InputAction
对象的集合。
在报告您的键绑定时,您将构建一个包含游戏中所有InputGroups
的InputMap
。
如果您的游戏不支持重新映射,请将重新映射选项禁用并将保留键设置为空。
以下示例构建了一个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()
指定了当前正在使用的组。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
对象,需要字符串版本。
以下示例将字符串版本分配给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
来接收重新映射事件的通知,请在注册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、使用不同的输入映射或重新映射规则失败(如果启用了重新映射)等情况,叠加层会显示如下错误消息:
使用命令行中的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上。