Este documento descreve como configurar e mostrar o SDK de entrada em jogos compatíveis com o Google Play Games no PC. As tarefas incluem adicionar o SDK ao jogo e gerar um mapa de entrada, que contém os do tipo game-actions-to-user-input.
Antes de começar
Antes de adicionar o SDK de entrada ao jogo, é preciso oferecer suporte para entrada de teclado e mouse usando o sistema de entrada do mecanismo de jogo.
O SDK de entrada fornece informações ao Google Play Games no PC sobre os controles que o jogo usa, para que sejam mostrados ao usuário. Também é possível permitir o remapeamento do teclado para usuários.
Cada controle é uma InputAction
(por exemplo, "J" para "Jump") e você organiza suas
InputActions
em InputGroups
. Um InputGroup
pode representar um modo diferente
no jogo, como "Dirigir", "Caminhar" ou "Menu principal". Você também pode
usar InputContexts
para indicar os grupos que estão ativos em pontos diferentes do
jogo.
É possível ativar o remapeamento do teclado automaticamente, mas se você preferir fornecer sua própria interface de remapeamento de controle, pode desativar o remapeamento do SDK de entrada.
O diagrama de sequência abaixo descreve como funciona a API do SDK de entrada:
Quando o jogo implementa o SDK de entrada, seus controles são mostrados na sobreposição do Google Play Games no PC.
Sobreposição do Google Play Games no PC
A sobreposição do Google Play Games no PC ("a sobreposição") mostra os controles definidos pelo jogo. Os usuários podem acessar a sobreposição a qualquer momento pressionando Shift + Tab.
Práticas recomendadas para o design de vinculações de teclas
Ao desenvolver o design das vinculações de teclas, considere as seguintes práticas recomendadas:
- Agrupe as
InputActions
relacionadas de maneira lógica emInputGroups
para melhorar a navegação e a facilidade de descoberta dos controles na jogabilidade. - Atribua cada
InputGroup
a pelo menos umInputContext
. UmInputMap
bem granulado resulta em uma melhor experiência de navegação dos controles na sobreposição. - Crie um
InputContext
para cada tipo de caso diferente do jogo. Normalmente, você pode usar um únicoInputContext
para todos os casos de menus. Use diferentesInputContexts
para minijogos no seu app ou para controles alternativos para um único cenário. - Se duas ações usarem a mesma tecla no mesmo
InputContext
, use uma string de rótulo, como "Interagir / Atirar". - Se duas teclas estiverem vinculadas à mesma
InputAction
, use duasInputActions
diferentes que executam a mesma ação no jogo. Você pode usar a mesma string de rótulo para as duasInputActions
, mas o ID precisa ser diferente. - Se uma tecla modificadora for aplicada a um conjunto de teclas, considere ter uma única
InputAction
com a tecla modificadora, em vez de váriasInputActions
combinadas com a tecla modificadora. Por exemplo: use Shift e W, A, S, D em vez de Shift + W, Shift + A, Shift + S, Shift + D. - O remapeamento de entrada fica desativado automaticamente quando o usuário escreve nos campos de
texto. Siga as práticas recomendadas para implementar os campos de texto do Android e garantir
que ele possa detectá-los no seu jogo e evitar a interferência de
teclas remapeadas. Caso seu jogo tenha que usar campos de texto não convencionais,
você pode usar
setInputContext()
com umInputContext
contendo uma lista vazia deInputGroups
para desativar manualmente o remapeamento. - Se o jogo oferecer suporte ao remapeamento, considere que a atualização das vinculações de teclas é uma operação sensível que pode entrar em conflito com as versões salvas pelo usuário. Evite trocar IDs de controles existentes quando possível.
Recurso de remapeamento
O Google Play Games no PC oferece suporte ao remapeamento de controles de teclado com base nas
vinculações que seu jogo oferece usando o SDK de entrada. Isso é opcional e
pode ser totalmente desativado. Por exemplo, talvez você queira fornecer sua própria interface de
remapeamento de teclado. Para desativar esse recurso no seu jogo, basta especificar
a opção de remapeamento desativada para o InputMap
. Consulte
Criar um InputMap para mais informações.
Para acessar esse recurso, os usuários precisam abrir a sobreposição e clicar na ação que querem remapear. Depois de cada evento de remapeamento, o Google Play Games no PC remapeia cada controle mapeado pelo usuário para os controles padrão que seu jogo espera receber, de maneira que ele não precise estar ciente do remapeamento do jogador. Você tem a opção de atualizar os recursos usados para mostrar os controles de teclado no seu jogo adicionando um callback para eventos de remapeamento.
O Google Play Games no PC armazena controles remapeados localmente para cada usuário, para que eles persistam entre as sessões de jogo. Essas informações são armazenadas em disco apenas para a plataforma do PC e não afetam a experiência em dispositivos móveis. Os dados de controle são excluídos quando o usuário desinstala ou reinstala o Google Play Games no PC. Esses dados não são persistentes entre vários dispositivos de PC diferentes.
Para oferecer suporte ao recurso de remapeamento no jogo, evite as seguintes restrições:
Restrições de remapeamento
Os recursos de remapeamento podem ser desativados no jogo se as vinculações de teclas contiverem algum dos seguintes casos:
InputActions
de várias teclas que não são compostas por uma tecla modificadora + uma tecla não modificadora. Por exemplo, a combinação Shift + A é válida, mas A + B, Ctrl + Alt ou Shift + A + Tab não são.- O
InputMap
contémInputActions
,InputGroups
ouInputContexts
com IDs exclusivos repetidos.
Limitações de remapeamento
Ao desenvolver o design das vinculações de teclas para remapeamento, considere as seguintes limitações:
- Não há suporte para o remapeamento de combinações de teclas. Por exemplo, os usuários não podem remapear Shift + A para Ctrl + B ou A para Shift + A.
- O remapeamento não tem suporte para
InputActions
com botões do mouse. Por exemplo, Shift + clicar com o botão direito do mouse não pode ser remapeado.
Testar o remapeamento de teclas no emulador do Google Play Games no PC
Você pode ativar o recurso de remapeamento no emulador do Google Play Games no PC quando quiser com este comando adb:
adb shell dumpsys input_mapping_service --set RemappingFlagValue true
A sobreposição muda conforme a imagem abaixo:
Adicionar o SDK
Instale o SDK de entrada de acordo com a plataforma de desenvolvimento.
Java e Kotlin
Faça o download do SDK de entrada para Java ou Kotlin adicionando uma dependência ao
arquivo build.gradle
de nível de módulo:
dependencies {
implementation 'com.google.android.libraries.play.games:inputmapping:1.1.0-beta'
...
}
Unity
O SDK de entrada é um pacote padrão do Unity com várias dependências.
É necessário instalar o pacote com todas as dependências. Há várias maneiras de instalar os pacotes.
Instalar o .unitypackage
Faça o download do arquivo do SDK de entrada para Unity
com todas as dependências. Você pode instalar o .unitypackage
selecionando
Assets > Import package > Custom Package e localizando o arquivo que você salvou.
Instalar usando UPM
Como alternativa, você pode instalar o pacote usando o
Gerenciador de pacotes do Unity (link em inglês) fazendo o
download de .tgz
e instalando as dependências:
- com.google.external-dependency-manager-1.2.172
- com.google.librarywrapper.java-0.2.0 (link em inglês)
- com.google.librarywrapper.openjdk8-0.2.0 (link em inglês)
- com.google.android.libraries.play.games.inputmapping-1.1.0-beta (link em inglês)
Instalar usando OpenUPM
Você pode instalar o pacote usando OpenUPM (link em inglês).
$ openupm add com.google.android.libraries.play.games.inputmapping
Exemplos de jogos
Para aprender a fazer a integração com o SDK de entrada, consulte o exemplo AGDK Tunnel para jogos Kotlin ou Java e o Trivial Kart para jogos Unity (links em inglês).
Gerar suas vinculações de teclas
Registre as vinculações de teclas criando um InputMap
e retornando com um
InputMappingProvider
. O exemplo abaixo define um
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
Definir suas ações de entrada
A classe InputAction
é usada para mapear uma tecla ou uma combinação de teclas para uma ação
do jogo. InputActions
precisam ter IDs exclusivos em todas as InputActions
.
Se você oferece suporte a remapeamento, pode definir quais InputActions
podem ser
remapeadas. Se o jogo não oferecer suporte ao remapeamento, defina essa opção
como desativada para todas as InputActions
. Porém, o SDK de entrada é
inteligente o suficiente para desativar o remapeamento se você não oferecer suporte no seu
InputMap
.
Este exemplo mapeia a tecla de
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 );
As ações também podem representar entradas de mouse. Este exemplo define o botão esquerdo do mouse para a ação de avançar:
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 );
As combinações de teclas são especificadas transmitindo vários códigos de teclas para a
InputAction
. Neste exemplo,
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 );
O SDK de entrada permite combinar botões do mouse e do teclado para uma
única ação. Este exemplo indica que
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 );
A InputAction tem os campos abaixo:
ActionLabel
: a string mostrada na interface para representar essa ação. A localização não é automática, portanto, faça isso antecipadamente.InputControls
: define os controles de entrada que essa ação usa. Os controles mapeiam para glifos consistentes na sobreposição.InputActionId
: o objetoInputIdentifier
que armazena o ID de número e versão daInputAction
. Consulte Como rastrear IDs de tecla para mais informações.InputRemappingOption
: um deInputEnums.REMAP_OPTION_ENABLED
ouInputEnums.REMAP_OPTION_DISABLED
. Define se a ação para remapear está ativada. Se o jogo não oferecer suporte ao remapeamento, ignore esse campo ou simplesmente configure como desativado.RemappedInputControls
: objeto somente leituraInputControls
usado para a leitura da tecla remapeada definida pelo usuário nos eventos de remapeamento. É usada para receber notificação de eventos de remapeamento.
InputControls
representa as entradas associadas a uma ação e contém os
campos abaixo:
AndroidKeycodes
: é uma lista de números inteiros que representam entradas de teclado associadas a uma ação. Eles são definidos na classe KeyEvent ou AndroidKeycode do Unity.MouseActions
: é uma lista de valores deMouseAction
que representam entradas de mouse associadas a essa ação.
Definir seus grupos de entrada
InputActions
são agrupadas com ações relacionadas logicamente usando InputGroups
para
melhorar a navegação e a facilidade de descoberta de controles na sobreposição. Cada
ID de InputGroup
precisa ser exclusivo em todos os InputGroups
do jogo.
Se você dividir as ações de entrada em grupos, o jogador vai poder encontrar as teclas de atalho corretas para o contexto atual com mais facilidade.
Se você oferecer suporte a remapeamento, poderá definir quais InputGroups
podem ser
remapeados. Se o jogo não oferecer suporte ao remapeamento, defina essa opção
como desativada para todas as InputGroups
. Porém, o SDK de entrada é
inteligente o suficiente para desativar o remapeamento se você não oferecer suporte no seu
InputMap
.
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 );
O exemplo abaixo mostra os grupos de entrada dos controles de estrada e dos controles de menu na sobreposição:
InputGroup
tem os campos abaixo:
GroupLabel
: é uma string que será mostrada na interface e pode ser usada para agrupar um conjunto de ações de maneira lógica. Essa string não é localizada automaticamente.InputActions
: uma lista de objetosInputAction
que você define na etapa anterior. Todas essas ações são mostradas visualmente sob o título do grupo.InputGroupId
: o objetoInputIdentifier
que armazena o ID de número e versão doInputGroup
. Consulte Como rastrear IDs de tecla para mais informações.InputRemappingOption
: um deInputEnums.REMAP_OPTION_ENABLED
ouInputEnums.REMAP_OPTION_DISABLED
. Se desativada, todos os objetosInputAction
pertencentes a esse grupo terão o remapeamento desativado, mesmo que eles tenham a essa opção ativada. Se ativada, todas as ações pertencentes a esse grupo poderão ser remapeadas, a menos que sejam especificadas como desativadas pelas ações.
Definir seus contextos de entrada
InputContexts
permite que o jogo use um conjunto diferente de controles de teclado para
diferentes cenários do jogo. Por exemplo:
- Você pode especificar diferentes conjuntos de entradas para navegar em menus ou avançar no jogo.
- Você pode especificar diferentes conjuntos de entradas, dependendo do modo de locomoção no jogo, como dirigir ou caminhar.
- Você pode especificar diferentes conjuntos de entradas com base no estado atual do jogo, como navegar em um mapa do mundo ou percorrer um nível individual.
Ao usar InputContexts
, a sobreposição mostra primeiro os grupos do contexto
em uso. Para ativar esse comportamento, chame setInputContext()
para definir o
contexto sempre que o jogo entrar em um cenário diferente. A imagem abaixo
demonstra esse comportamento: no cenário de "dirigir". As ações dos controles de estrada
são mostradas na parte de cima da sobreposição. Ao abrir o menu "loja", as
ações dos "controles de Menu" são mostradas na parte de cima da sobreposição.
Essas atualizações de sobreposição são feitas configurando um InputContext
diferente em
pontos diferentes do jogo. Siga estas etapas:
- Agrupe as
InputActions
com ações relacionadas de maneira lógica usandoInputGroups
. - Atribua esses
InputGroups
a umInputContext
para as diferentes partes do jogo.
InputGroups
que pertencem ao mesmo InputContext
não podem entrar em conflito com
InputActions
em que a mesma tecla é usada. Recomendamos atribuir cada
InputGroup
a um único InputContext
.
O código do exemplo abaixo demonstra a lógica do 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
tem os campos abaixo:
LocalizedContextLabel
: uma string descrevendo os grupos que pertencem ao contexto.InputContextId
: o objetoInputIdentifier
que armazena o ID de número e versão daInputContext
. Consulte Como rastrear IDs de tecla para mais informações.ActiveGroups
: uma lista deInputGroups
a ser usada e mostrada na parte de cima da sobreposição quando esse contexto está ativo.
Criar um mapa de entrada
Um InputMap
é uma coleção de todos os objetos InputGroup
disponíveis em um
jogo e, portanto, todos os objetos InputAction
que um jogador pode esperar
realizar.
Ao informar as vinculações de teclas, você cria um InputMap
com todos os
InputGroups
usados no jogo.
Se o jogo não oferecer suporte ao remapeamento, defina essa opção como desativada e mantenha as teclas reservadas vazias.
O exemplo abaixo cria um InputMap
usado para informar uma coleção de
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
tem os campos abaixo:
InputGroups
: os InputGroups informados pelo jogo. Os grupos são mostrados em ordem na sobreposição, a menos que sejam especificados os grupos atuais em uso chamandosetInputContext()
.MouseSettings
: os objetosMouseSettings
indicam que a sensibilidade do mouse pode ser ajustada e que o mouse é invertido no eixo y.InputMapId
: o objetoInputIdentifier
que armazena o ID de número e versão daInputMap
. Consulte Como rastrear IDs de tecla para mais informações.InputRemappingOption
: um deInputEnums.REMAP_OPTION_ENABLED
ouInputEnums.REMAP_OPTION_DISABLED
. Define se o recurso de remapeamento está ativado.ReservedControls
: uma lista deInputControls
que os usuários não vão ter permissão para remapear.
Rastrear IDs de tecla
Os objetos InputAction
, InputGroup
, InputContext
e InputMap
contêm um objeto
InputIdentifier
que armazena um ID de número exclusivo e um ID de versão da string.
Rastrear a versão de string dos seus objetos é opcional, mas recomendamos rastrear
as versões dos objetos InputMap
. Se a versão da string não for fornecida, ela
vai estar vazia. É necessária uma versão da string para objetos InputMap
.
O exemplo abaixo atribui uma versão da string para InputActions
ou
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
Os IDs dos número de objetos InputAction
precisam ser exclusivos em todas as InputActions
no
seu InputMap
. Da mesma forma, IDs de objetos InputGroup
precisam ser exclusivos para todos os
InputGroups
em um InputMap
. O exemplo abaixo demonstra como usar uma
enum
para rastrear os IDs exclusivos do objeto:
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
tem os campos abaixo:
UniqueId
: um ID de número exclusivo definido para identificar claramente um determinado conjunto de dados de entrada de maneira única.VersionString
: uma string de versão legível por humanos definida para identificar uma versão dos dados de entrada entre duas versões de mudanças nos dados de entrada.
Receber notificações sobre eventos de remapeamento (opcional)
Receba notificações sobre eventos de remapeamento para saber sobre as teclas que estão sendo usadas no seu jogo. Isso permite que o jogo atualize os recursos mostrados na tela do jogo usada para exibir os controles de ação.
A imagem abaixo mostra um exemplo desse comportamento em que, após remapear as
teclas
Essa funcionalidade precisa do registro de um callback
InputRemappingListener
. Para implementar esse recurso, comece registrando uma instância
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
O InputRemappingListener
é notificado no momento da inicialização após o carregamento dos
controles remapeados salvos pelo usuário e sempre que o usuário remapeia as teclas.
Inicialização
Se você estiver usando InputContexts
, defina o contexto em cada
transição para um novo cenário, incluindo o primeiro contexto usado para o cenário
inicial. É preciso definir o InputContext
depois de registrar o
InputMap
.
Se você estiver usando InputRemappingListeners
para receber notificações sobre eventos de remapeamento,
registre o InputRemappingListener
antes de registrar o
InputMappingProvider
. Caso contrário o jogo pode perder eventos importantes durante
a inicialização.
O exemplo abaixo demonstra como inicializar a 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 } }
Limpeza
Cancele a inscrição da instância InputMappingProvider
e de qualquer instância InputRemappingListener
quando o jogo for fechado, embora o SDK de entrada seja inteligente
o suficiente para evitar vazamento de recursos:
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 } }
Testar
É possível testar a implementação do SDK de entrada abrindo manualmente a sobreposição para visualizar a experiência do jogador, ou pelo shell do adb para testes e verificação automatizada.
O emulador do Google Play Games no PC verifica a exatidão do mapa de entrada em relação a erros comuns. Para cenários como IDs exclusivos duplicados, uso de diferentes mapas de entrada ou falha nas regras de remapeamento (se estiver ativado), a sobreposição mostra uma mensagem de erro, conforme abaixo:
Verifique a implementação do SDK de entrada usando adb
na linha de comando.
Para conferir o mapa de entrada atual, use o comando adb shell
abaixo (substitua
MY.PACKAGE.NAME
pelo nome do jogo):
adb shell dumpsys input_mapping_service --get MY.PACKAGE.NAME
Você vai encontrar uma saída semelhante a esta se tiver registrado o
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
}
Localização
O SDK de entrada não usa o sistema de localização do Android. Como
resultado, é necessário fornecer strings localizadas ao enviar um InputMap
. Isso
também permite usar o sistema de localização do mecanismo do jogo.
ProGuard
Ao usar o ProGuard para reduzir o jogo, adicione as regras abaixo ao arquivo de configuração para garantir que o SDK não seja removido do pacote final:
-keep class com.google.android.libraries.play.hpe.** { *; }
-keep class com.google.android.libraries.play.games.inputmapping.** { *; }
A seguir
Depois de integrar o SDK de entrada ao jogo, é possível continuar com todos os requisitos restantes do Google Play Games no PC. Para mais informações, consulte Começar a usar o Google Play Games no PC.