開始使用 Input SDK

本文說明如何在 支援 Google Play 遊戲電腦版的遊戲。工作包括新增 SDK 並產生輸入項目對應,其中包含 指派。

事前準備

在遊戲中新增 Input SDK 之前,您必須先使用遊戲引擎的輸入系統,支援鍵盤和滑鼠輸入功能。

Input SDK 會向 Google Play 遊戲電腦版提供資訊,說明遊戲所使用的控制項,以便系統向使用者顯示這些控制項。此 SDK 也可視需要允許使用者重新對應按鍵。

每個控制項皆為 InputAction (例如按下「J」鍵可「跳躍」),而且您可以將 InputActions 整理為 InputGroupsInputGroup 可能代表不同的遊戲模式,例如「開車」、「步行」或「主選單」。您也可以使用 InputContexts,指出在不同遊戲階段中使用的群組。

您可以啟用可自動處理按鍵重新對應事件的功能,但如果想自行提供控制項重新對應介面,則可停用 Input SDK 重新對應功能。

以下序列圖表說明 Input SDK API 的運作方式:

序列圖表顯示呼叫 Input SDK API 的遊戲實作項目,以及遊戲與 Android 裝置的互動。

如果遊戲實作 Input SDK,控制項會顯示在 Google Play 遊戲電腦版疊加畫面。

Google Play 遊戲電腦版疊加畫面

Google Play 遊戲電腦版疊加畫面 (以下簡稱「疊加畫面」) 會顯示遊戲定義的控制項。使用者只要按下 Shift + Tab 鍵,即可隨時存取疊加畫面。

Google Play 遊戲電腦版疊加畫面。

設計按鍵繫結的最佳做法

設計按鍵繫結時,請參考以下最佳做法:

  • InputActions 分入邏輯相關的 InputGroups,提升控制項在遊戲過程中的導覽情形和曝光度。
  • 將各個 InputGroup 指派至最多一個 InputContext。精細的 InputMap 可帶來良好體驗,協助使用者在疊加畫面中瀏覽控制項。
  • 為每個不同的遊戲場景類型建立 InputContext。一般而言,所有「選單類」場景可以共用一個 InputContext。針對遊戲內小遊戲或單一場景中的其他控制項,則可使用不同的 InputContexts
  • 如果兩個動作在相同 InputContext 中使用相同按鍵,請使用標籤字串,例如「互動/發射」。
  • 如果兩個按鍵繫結至相同 InputAction,請使用 2 個可執行相同遊戲動作的不同 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 遊戲電腦版可根據遊戲使用 Input SDK 所提供的按鍵繫結,支援鍵盤控制項重新對應功能。這屬於選用功能,可以完全停用。舉例來說,如果您希望自行提供按鍵重新對應介面,即可停用這項功能。如要為遊戲停用重新對應功能,只須指定停用 InputMap 的重新對應選項。詳情請參閱「建立輸入項目對應」。

使用者如要使用這項功能,需要開啟疊加畫面,然後點選要重新對應的動作。在每個重新對應事件後,Google Play 遊戲電腦版會將經過使用者重新對應的控制項對應至遊戲預期接收的預設控制項,這樣遊戲就不必瞭解玩家重新對應的控制項。您可以視需要更新顯示鍵盤控制項的遊戲資產,方法是新增重新對應事件的回呼。

嘗試重新對應按鍵

Google Play 遊戲電腦版會將使用者重新對應的控制項儲存在本機,以便在遊戲過程中保存這些設定。系統只會針對電腦平台將這項資訊儲存在磁碟,並不會影響行動裝置體驗。使用者解除安裝或重新安裝 Google Play 遊戲電腦版時,系統會刪除控制項資料。這類資料不會跨電腦儲存。

如要在遊戲中支援重新對應功能,請避免以下限制情況:

導致重新對應功能受限制的情況

如果按鍵繫結出現下列情況,系統可能會停用遊戲中的重新對應功能:

  • 多鍵 InputActions 並非由輔助鍵 + 非輔助鍵組成。舉例來說,Shift + A 是有效的組合,但 A + BCtrl + AltShift + A + Tab 皆無效。
  • InputMapInputActionsInputGroupsInputContexts 的專屬 ID 有所重複。

重新對應功能的限制

設計按鍵繫結時,請考量以下限制:

  • 系統不支援重新對應至按鍵組合。舉例來說,使用者無法將 Shift + A 重新對應至 Ctrl + B,也無法將 A 重新對應至 Shift + A
  • 系統不支援重新對應至含滑鼠按鍵的 InputActions,例如無法重新對應至 Shift + 滑鼠右鍵

在 Google Play 遊戲電腦版模擬器上測試按鍵重新對應功能

您可以隨時發出以下 ADB 指令,停用 Google Play 遊戲電腦版模擬器的重新對應功能:

adb shell dumpsys input_mapping_service --set RemappingFlagValue true

疊加畫面的變更情形如下圖所示:

已啟用按鍵重新對應功能的疊加畫面。

新增 SDK

請根據您使用的開發平台安裝 Input SDK。

Java 和 Kotlin

在模組層級 build.gradle 檔案中新增依附元件,即可取得 Java 或 Kotlin 的 Input SDK:

dependencies {
  implementation 'com.google.android.libraries.play.games:inputmapping:1.1.0-beta'
  ...
}

Unity

Input SDK 是含有多個依附元件的標準 Unity 套件。

您必須安裝此套件和所有依附元件。安裝套件的方法有很多種。

安裝 .unitypackage

下載 Input SDK unitypackage 檔案和其所有依附元件。依序選取「Assets」>「Import package」>「Custom Package」,並找出下載的檔案,即可安裝 .unitypackage

使用 UPM 安裝

或者,您可以使用 Unity 的 Package Manager,透過下載 .tgz 和安裝依附元件來安裝套件:

使用 OpenUPM 安裝

您可以使用 OpenUPM 安裝套件。

$ openupm add com.google.android.libraries.play.games.inputmapping

範例遊戲

如需 Input SDK 整合方法的範例,請參閱適用於 Kotlin 或 Java 遊戲的 AGDK Tunnel,以及適用於 Unity 遊戲的 Trivial Kart

產生按鍵繫結

只要建構 InputMap 並使用 InputMappingProvider 傳回,就可以註冊按鍵繫結。以下範例說明 InputMappingProvider

Kotlin

class InputSDKProvider : InputMappingProvider {
  override fun onProvideInputMap(): InputMap {
    TODO("Not yet implemented")
  }
}

Java

public class InputSDKProvider implements InputMappingProvider {
    private static final String INPUTMAP_VERSION = "1.0.0";

    @Override
    @NonNull
    public InputMap onProvideInputMap() {
        // TODO: return an InputMap
    }
}

C#

#if PLAY_GAMES_PC
using Java.Lang;
using Java.Util;
using Google.Android.Libraries.Play.Games.Inputmapping;
using Google.Android.Libraries.Play.Games.Inputmapping.Datamodel;

public class InputSDKProvider : InputMappingProviderCallbackHelper
{
    public static readonly string INPUT_MAP_VERSION = "1.0.0";

    public override InputMap OnProvideInputMap()
    {
        // TODO: return an InputMap
    }
}
#endif

定義輸入動作

InputAction 類別是用於將按鍵或按鍵組合對應至遊戲動作。InputActions 必須在所有 InputActions 中擁有專屬 ID。

如果您支援重新對應功能,就能定義哪些 InputActions 可供重新對應。若遊戲不支援重新對應功能,則應將所有 InputActions 的重新對應選項設為停用。不過 Input SDK 夠聰明,只要 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
);

疊加畫面顯示單鍵 InputAction。

動作也可以代表滑鼠輸入。這個示例會將滑鼠左鍵設為「移動」動作:

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。

按鍵組合的指定方式是將多個按鍵程式碼傳送至您的 InputAction。在這個範例中,space + shift 會對應至以下值: 「Turbo」動作,即使聊天室是對應至雲端硬碟也能使用。

Kotlin

companion object {
  private val turboInputAction = InputAction.create(
    "Turbo",
    InputActionsIds.TURBO.ordinal.toLong(),
    InputControls.create(
      listOf(KeyEvent.KEYCODE_SHIFT_LEFT, KeyEvent.KEYCODE_SPACE),
      emptyList()),
    InputEnums.REMAP_OPTION_ENABLED)
}

Java

private static final InputAction turboInputAction = InputAction.create(
    "Turbo",
    InputActionsIds.TURBO.ordinal(),
    InputControls.create(
            Arrays.asList(KeyEvent.KEYCODE_SHIFT_LEFT, KeyEvent.KEYCODE_SPACE),
            Collections.emptyList()
    ),
    InputEnums.REMAP_OPTION_ENABLED
);

C#

private static readonly InputAction turboInputAction = InputAction.Create(
    "Turbo",
    (long)InputEventIds.TURBO,
    InputControls.Create(
        new[]
        {
            new Integer(AndroidKeyCode.KEYCODE_SHIFT_LEFT),
            new Integer(AndroidKeyCode.KEYCODE_SPACE)
        }.ToJavaList(),
        new ArrayList<Integer>()),
    InputEnums.REMAP_OPTION_ENABLED
);

疊加畫面顯示多鍵 InputAction。

Input SDK 可讓您結合滑鼠和按鍵按鈕,並用於單一動作。在以下範例中,只要同時按下 Shift 鍵和滑鼠右鍵,就能在這款範例遊戲中增加路線控點:

Kotlin

companion object {
  private val addWaypointInputAction = InputAction.create(
    "Add waypoint",
    InputActionsIds.ADD_WAYPOINT.ordinal.toLong(),
    InputControls.create(
      listOf(KeyEvent.KeyEvent.KEYCODE_TAB),
      listOf(InputControls.MOUSE_RIGHT_CLICK)),
    InputEnums.REMAP_OPTION_DISABLED)
}

Java

private static final InputAction addWaypointInputAction = InputAction.create(
    "Add waypoint",
    InputActionsIds.ADD_WAYPOINT.ordinal(),
    InputControls.create(
            Collections.singletonList(KeyEvent.KEYCODE_TAB),
            Collections.singletonList(InputControls.MOUSE_RIGHT_CLICK)
    ),
    InputEnums.REMAP_OPTION_DISABLED
);

C#

private static readonly InputAction addWaypointInputAction = InputAction.Create(
    "Add waypoint",
    (long)InputEventIds.ADD_WAYPOINT,
    InputControls.Create(
        new[] { new Integer(AndroidKeyCode.KEYCODE_SPACE) }.ToJavaList(),
        new[] { new Integer((int)PlayMouseAction.MouseRightClick) }.ToJavaList()
    ),
    InputEnums.REMAP_OPTION_DISABLED
);

疊加畫面顯示按鍵 + 滑鼠的 InputAction 組合。

InputAction 具有以下欄位:

  • ActionLabel:這是在 UI 顯示的字串,代表這項動作。系統不會自動完成本地化,因此請事先完成本地化。
  • InputControls:這會定義此動作使用的輸入控制項。控制項會對應至與疊加畫面一致的字符。
  • InputActionId:這個 InputIdentifier 物件可儲存 InputAction 的數字 ID 和版本。詳情請參閱「追蹤按鍵 ID」。
  • InputRemappingOptionInputEnums.REMAP_OPTION_ENABLEDInputEnums.REMAP_OPTION_DISABLED。這個項目會定義動作是否可供重新對應。如果遊戲不支援重新對應功能,您可以略過這個欄位,或是直接設為停用。
  • RemappedInputControls:這是唯讀 InputControls 物件,可在重新對應事件中讀取使用者重新對應的按鍵 (用於取得重新對應事件通知)。

InputControls 代表與動作相關聯的輸入項目,具有以下欄位:

  • AndroidKeycodes:這是整數清單,代表與動作相關聯的鍵盤輸入項目。其定義位於 KeyEvent 類別或 Unity 的AndroidKeycode 類別。
  • MouseActionsMouseAction 值清單,代表與這項動作相關聯的滑鼠輸入。

定義輸入群組

請使用 InputGroups,依邏輯相關性為 InputActions 分組,提升控制項在疊加畫面中的導覽情形和曝光度。遊戲中所有 InputGroups 都必須擁有專屬的 InputGroup ID。

透過將輸入動作分成群組,玩家可以更輕鬆找出目前情境適用的正確按鍵繫結。

如果您支援重新對應功能,就能定義哪些 InputGroups 可供重新對應。若遊戲不支援重新對應功能,則應將所有 InputGroups 的重新對應選項設為停用。不過 Input SDK 夠聰明,只要 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
);

以下範例顯示疊加畫面中的「Road controls」和「Menu controls」輸入群組:

疊加畫面顯示 InputMap 含有「Road controls」和「Menu controls」輸入群組。

InputGroup 具有以下欄位:

  • GroupLabel:這是在疊加畫面顯示的字串,可用來依特定邏輯為動作分組。這個字串不會自動本地化。
  • InputActions:這是您在先前步驟定義的 InputAction 物件清單。所有這些動作都會顯示在群組標題下方。
  • InputGroupId:這個 InputIdentifier 物件可儲存 InputGroup 的數字 ID 和版本。詳情請參閱「追蹤按鍵 ID」。
  • InputRemappingOptionInputEnums.REMAP_OPTION_ENABLEDInputEnums.REMAP_OPTION_DISABLED。如果停用這個項目,屬於此群組的所有 InputAction 物件都會停用重新對應功能,即使已指定啟用重新對應選項也一樣。如果啟用這個項目,屬於此群組的所有動作皆可供重新對應,除非個別動作已指定停用這項功能。

定義輸入情境

InputContexts 可讓遊戲在不同場景使用不同的按鍵控制項組合,例如:

  • 針對選單瀏覽動作和遊戲中的移動方式,指定不同的輸入項目組合。
  • 根據開車和步行等遊戲中的移動模式,指定不同的輸入項目組合。
  • 根據瀏覽整個世界和前往個別關卡等目前的遊戲狀態,指定不同的輸入項目組合。

使用 InputContexts 時,疊加畫面會先顯示目前情境的群組。如要啟用這項行為,請呼叫 setInputContext(),在每次遊戲進入不同場景時設定情境。這項行為如下圖所示:在「開車」場景中,「Road controls」動作會顯示在疊加畫面頂端。開啟「Store」選單時,則是「Menu controls」動作顯示在疊加畫面頂端。

InputContext 會在疊加畫面中為群組排序。

如要更新疊加畫面,只需在遊戲的不同階段設定不同的 InputContext 即可。步驟如下:

  1. 使用 InputGroups,依邏輯相關性為 InputActions 分組
  2. 針對遊戲的不同部分,將 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:遊戲回報的 InputGroup。這些群組會依序顯示在疊加畫面中,除非已呼叫 setInputContext() 指定目前使用的群組。
  • MouseSettingsMouseSettings 物件代表可調整滑鼠靈敏度,且滑鼠會在 Y 軸上反轉。
  • InputMapId:這個 InputIdentifier 物件可儲存 InputMap 的數字 ID 和版本。詳情請參閱「追蹤按鍵 ID」。
  • InputRemappingOptionInputEnums.REMAP_OPTION_ENABLEDInputEnums.REMAP_OPTION_DISABLED。這個項目會定義是否啟用重新對應功能。
  • ReservedControls:這個 InputControls 清單會定義使用者不得重新對應至哪些控制項。

追蹤按鍵 ID

InputActionInputGroupInputContextInputMap 物件包含 InputIdentifier 物件,可用來儲存專屬數字 ID 和字串版本 ID。追蹤物件的字串版本並非必要操作,但建議您追蹤 InputMap 的版本。如未提供字串版本,該字串會空白。InputMap 物件必須具有字串版本。

以下範例會將字串版本指派至 InputActionsInputGroups

Kotlin

class InputSDKProviderKotlin : InputMappingProvider {
  companion object {
    const val INPUTMAP_VERSION = "1.0.0"
    private val enterMenuInputAction = InputAction.create(
      "Enter menu",
      InputControls.create(listOf(KeyEvent.KEYCODE_ENTER), emptyList()),
      InputIdentifier.create(
        INPUTMAP_VERSION, InputActionsIds.ENTER_MENU.ordinal.toLong()),
      InputEnums.REMAP_OPTION_ENABLED
    )

    private val movementInputGroup  = InputGroup.create(
      "Basic movement",
      listOf(
        moveUpInputAction,
        moveLeftInputAction,
        moveDownInputAction,
        mouseGameInputAction),
      InputIdentifier.create(
        INPUTMAP_VERSION, InputGroupsIds.BASIC_MOVEMENT.ordinal.toLong()),
      InputEnums.REMAP_OPTION_ENABLED)
  }
}

Java

public class InputSDKProvider implements InputMappingProvider {
    public static final String INPUTMAP_VERSION = "1.0.0";

    private static final InputAction enterMenuInputAction = InputAction.create(
            "Enter menu",
            InputControls.create(
                    Collections.singletonList(KeyEvent.KEYCODE_ENTER),
                    Collections.emptyList()),
            InputIdentifier.create(
                    INPUTMAP_VERSION, InputActionsIds.ENTER_MENU.ordinal()),
            InputEnums.REMAP_OPTION_ENABLED
    );

    private static final InputGroup movementInputGroup = InputGroup.create(
            "Basic movement",
            Arrays.asList(
                    moveUpInputAction,
                    moveLeftInputAction,
                    moveDownInputAction,
                    moveRightInputAction,
                    mouseGameInputAction
            ),
            InputIdentifier.create(
                    INPUTMAP_VERSION, InputGroupsIds.BASIC_MOVEMENT.ordinal()),
            InputEnums.REMAP_OPTION_ENABLED
    );
}

C#


#if PLAY_GAMES_PC

using Java.Lang;
using Java.Util;
using Google.Android.Libraries.Play.Games.Inputmapping;
using Google.Android.Libraries.Play.Games.Inputmapping.Datamodel;

public class InputSDKMappingProvider : InputMappingProviderCallbackHelper
{
    public static readonly string INPUT_MAP_VERSION = "1.0.0";

    private static readonly InputAction enterMenuInputAction =
        InputAction.Create(
            "Enter menu",
            InputControls.Create(
                new[] { new Integer(AndroidKeyCode.KEYCODE_SPACE)}.ToJavaList(),
                new ArrayList<Integer>()),
            InputIdentifier.Create(
                INPUT_MAP_VERSION,
                (long)InputEventIds.ENTER_MENU),
            InputEnums.REMAP_OPTION_ENABLED
        );

    private static readonly InputGroup movementInputGroup = InputGroup.Create(
        "Basic movement",
        new[]
        {
            moveUpInputAction,
            moveLeftInputAction,
            moveDownInputAction,
            moveRightInputAction,
            mouseGameInputAction
        }.ToJavaList(),
        InputIdentifier.Create(
            INPUT_MAP_VERSION,
            (long)InputGroupsIds.BASIC_MOVEMENT),
        InputEnums.REMAP_OPTION_ENABLED
    );
}
#endif

InputMap 的所有 InputActions 中,InputAction 物件必須具有專屬數字 ID。同理,在 InputMap 的所有 InputGroups 中,InputGroup 物件必須具有專屬 ID。以下範例說明如何使用 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:這是人類可讀的版本字串,用來在 2 個輸入資料變更版本之間識別輸入資料版本。

接收重新對應事件的通知 (選用)

您可以接收重新對應事件通知,瞭解遊戲中使用的按鍵。這樣一來,遊戲就能在顯示動作控制項的遊戲畫面上,更新所顯示的資產。

下圖顯示這項行為的範例。在此範例中,GPS 鍵分別重新對應至 JXT 鍵,之後遊戲便更新 UI 元素,顯示由使用者設定的按鍵。

UI 使用 InputRemappingListener 回呼,因應重新對應事件。

只要註冊 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 例項,雖然即使您不取消註冊,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
    }
}

測試

您可以測試 Input SDK 實作項目,方法是手動開啟疊加畫面來查看玩家體驗,或是透過 ADB 殼層執行自動測試和驗證程序。

Google Play 遊戲電腦版模擬器可檢查常見錯誤,確認您的輸入項目對應是否正確無誤。如果出現重複的專屬 ID、同時使用不同的輸入項目對應,或是重新對應規則失敗 (如已啟用重新對應功能),疊加畫面會顯示以下錯誤訊息:Input SDK 疊加畫面。

請在指令列使用 adb,驗證 Input SDK 實作項目。如要取得目前的輸入項目對應,請使用以下 adb shell 指令 (請將 MY.PACKAGE.NAME 替換成您的遊戲名稱):

adb shell dumpsys input_mapping_service --get MY.PACKAGE.NAME

成功註冊 InputMap 後,就會看到類似如下的輸出內容:

Getting input map for com.example.inputsample...
Successfully received the following inputmap:
# com.google.android.libraries.play.games.InputMap@d73526e1
input_groups {
  group_label: "Basic Movement"
  input_actions {
    action_label: "Jump"
    input_controls {
      keycodes: 51
      keycodes: 19
    }
    unique_id: 0
  }
  input_actions {
    action_label: "Left"
    input_controls {
      keycodes: 29
      keycodes: 21
    }
    unique_id: 1
  }
  input_actions {
    action_label: "Right"
    input_controls {
      keycodes: 32
      keycodes: 22
    }
    unique_id: 2
  }
  input_actions {
    action_label: "Use"
    input_controls {
      keycodes: 33
      keycodes: 66
      mouse_actions: MOUSE_LEFT_CLICK
      mouse_actions_value: 0
    }
    unique_id: 3
  }
}
input_groups {
  group_label: "Special Input"
  input_actions {
    action_label: "Jump"
    input_controls {
      keycodes: 51
      keycodes: 19
      keycodes: 62
      mouse_actions: MOUSE_LEFT_CLICK
      mouse_actions_value: 0
    }
    unique_id: 4
  }
  input_actions {
    action_label: "Duck"
    input_controls {
      keycodes: 47
      keycodes: 20
      keycodes: 113
      mouse_actions: MOUSE_RIGHT_CLICK
      mouse_actions_value: 1
    }
    unique_id: 5
  }
}
mouse_settings {
  allow_mouse_sensitivity_adjustment: true
  invert_mouse_movement: true
}

本地化

Input SDK 不會使用 Android 的本地化系統。因此,您必須在提交 InputMap 時提供本地化字串。您也可以使用遊戲引擎的本地化系統。

Proguard

使用 Proguard 壓縮遊戲時,請將以下規則新增至 Proguard 設定檔,確保系統不會從最終套件中移除 SDK:

-keep class com.google.android.libraries.play.hpe.** { *; }
-keep class com.google.android.libraries.play.games.inputmapping.** { *; }

後續步驟

將 Input SDK 整合到遊戲後,您就可以繼續完成其餘的 Google Play 遊戲電腦版規定。詳情請參閱「開始使用 Google Play 遊戲電腦版」。