Steamworks 文獻庫
Steam 輸入遊戲手把模擬 - 最佳作法
Steam 輸入是一項服務,讓 Steam 使用者可以用自己選擇的裝置遊玩任何支援控制器的遊戲。 Steam 輸入會透過遊戲手把模擬、滑鼠和鍵盤模擬或 Steam 輸入 API,將使用者的輸入轉譯為遊戲能明白的訊號。 本文將重點說明使用 Steam 輸入遊戲手把模擬的最佳作法,以擴展您遊戲現有的控制器支援。

何謂遊戲手把模擬?
  • 在 Windows 中,Steam 內嵌介面會與傳統的遊戲手把輸入 API 掛鉤,例如 XInput、DirectInput、RawInput 和 Windows.Gaming.Input,並插入模擬的 Xbox 控制器裝置。 而 macOS 和 Linux 中的模擬控制器輸入則由驅動程式提供
  • 模擬控制器會在您的遊戲內顯示為 Xbox 控制器,這代表在擁有額外輸入方式的控制器上,某些輸入會有重複,例如 PlayStation 觸控板的點擊和選項按鈕都對應至 XInput 的 Start 鍵
  • 除了一般的遊戲手把輸入以外,還可以將 Switch、PlayStation 和 Steam 控制器陀螺儀輸入綁定為滑鼠模擬,並提供動態控制。 這僅適用於單人單機的遊戲,因為只會有一個滑鼠輸入,且遊戲必須同時接受滑鼠與遊戲手把輸入。 若對這些功能感興趣,但不想受到上述限制,請考慮添加 Steam 輸入 API
  • 您可以向 Steam 查詢目前使用的控制器類型,來顯示特定裝置的字符,但只限於您目前的 Steamworks SDK 支援的類型。 若有興趣使用具未來沿用性的字符,請整合 Steam 輸入 API
  • 即使您已經支援某些控制器,仍可能有使用者透過 Steam 輸入來使用這些控制器,因為 Steam 遠端暢玩會用來在串流時提供輸入,也有相當一部分的使用者啟用 Steam 輸入,為其整個 Steam 收藏庫重新配置控制器。 整體來說,Steam 在 2022 年所有的控制器遊戲階段中,約有四分之一使用的是 Steam 輸入,其中將近一半為 PlayStation 控制器

Steam 上有 2000 款遊戲使用至少一種控制器類型的遊戲手把模擬,包括《Monster Hunter: World》、《Ace Combat 7》、《勇者鬥惡龍XI》、《Into the Breach》和《Middle Earth: Shadow of War》,儘管並非所有遊戲都採用每一項最佳做法。 這裡我們將以《Into the Breach》作為範例,因為這款遊戲在遵照最佳作法方面做得非常好。

顯示裝置特定的圖像素材

我們支援數種使用 Steam 輸入遊戲手把模擬來獲得裝置特定的字符的管道——一種用於可在執行時載入具沿用性的圖片的遊戲(這表示在 Steam 更新 Steam 輸入時,無需更新遊戲也能使用),另外兩種是需要將圖片預先納入至其資產中的遊戲,或希望使用和遊戲主機連接埠相同的風格化圖像素材的遊戲。 《Into the Breach》採用了自己為 Xbox 和 Steam 控制器所製作的圖像素材:

intothebreach_xbox.PNG

PlayStation:

intothebreach_ps4.PNG

Nintendo Switch 控制器:

intothebreach_switchpro.PNG
備註:即使是使用自己的圖像素材,我們也建議您在無法辨識控制器時以 Steam 的圖像素材為遞補,或呼叫我們的幫手函式,以尋找在遊戲發行當時存在的最接近的選項。如此一來,未來新增裝置時便有合適的字符可用。

顯示 Steam 具未來沿用性的字符

您將需要使用以下函式:

程式碼範例:

// 使用各函式前須先初始化介面——呼叫一次即可! SteamInput()->Init() // ... // SteamAPI_RunCallbacks() 會為所有初始化的介面呼叫 RunFrame 函式 // 大多數遊戲應已開始定期呼叫此函式。 如果沒有,則必須手動更新 // Steam 輸入介面 SteamInput()->RunFrame(); // ... // 以要查詢的 XInput 欄位取代。 數值為 0 到 3 之間 // 若要在選擇欲使用的 API 前先以 RawInput 辨別裝置,請見 // 下方的「使用 RawInput 辨別裝置」段落 int nXinputSlot = 0; // 以要查詢的按鍵取代 EXboxOrigin eXboxButtonToGetGlyphFor = k_EXboxOrigin_A; EInputActionOrigin buttonOrigin = k_EInputActionOrigin_XBoxOne_A; // 若控制器是透過 Steam 輸入所配置,則轉譯該按鍵 InputHandle_t controller1Handle = SteamInput()->GetControllerForGamepadIndex( nXinputSlot ); if ( controller1Handle > 0 ) { // 有效的控制代碼不為 0,這是透過 Steam 輸入所配置的控制器 // 備註:使用 Steam 輸入 API 的控制器不會透過 GetControllerForGamepadIndex() 回傳控制代碼 buttonOrigin = SteamInput()->GetActionOriginFromXboxOrigin( controller1Handle, k_EXboxOrigin_A ); } else { // 有效的控制代碼不為 0,這是一般的 Xbox 控制器 // 繼續使用原本的按鍵 } // EInputActionOrigin 的值會隨著 Steam 新增支援而提高,但這並不會造成問題 // 因為在此範例中,我們將從 Steam 取得裝置圖像,而其會提供新的字符圖像 // 從 Steam 用戶端取得圖像 const char *localGlyphPath = SteamInput()->GetGlyphForActionOrigin( buttonOrigin ); printf( "path = %s\n", localGlyphPath ); // "path = C:\Program Files (x86)\Steam\tenfoot\resource\images\library\controller\api\ps4_button_x.png" // 以遊戲中會將檔案路徑變為可用遊戲材質的函式取代 glyphTextureID = loadButtonGlyphTextureFromLocalPath( localGlyphPath );

顯示自己的圖像素材——字符選擇區(Glyph Palette)

若根據控制器類型而從多個字符選擇區擇一使用,則可以使用以下函式以 XInput 欄位向 Steam 詢問所需資訊:

程式碼範例:
// 使用各函式前須先初始化介面——呼叫一次即可! SteamInput()->Init() // ... // SteamAPI_RunCallbacks() 會為所有初始化的介面呼叫 RunFrame 函式 // 大多數遊戲應已開始定期呼叫此函式。 如果沒有,則必須手動更新 // Steam 輸入介面 SteamInput()->RunFrame(); // ... // 以要查詢的 XInput 欄位取代。 數值為 0 到 3 之間 // 若要在選擇欲使用的 API 前先以 RawInput 辨別裝置,請見 // 下方的「使用 RawInput 辨別裝置」段落 int nXinputSlotIndex = 0; InputHandle_t inputHandle = SteamInput()->GetControllerForGamepadIndex( nXinputSlotIndex ); if ( inputHandle == 0 ) { // 有效的輸入控制代碼不為 0,這是一般的 Xbox 控制器 } else { // Steam 一定會回傳可用於您 SDK 版本的列舉值 ESteamInputType inputType = SteamInput()->GetInputTypeForHandle( inputHandle ); switch( inputType ) { case k_ESteamInputType_Unknown: printf( "unknown!\n" ); break; case k_ESteamInputType_SteamController: printf( "Steam controller!\n" ); break; case k_ESteamInputType_XBox360Controller: printf( "XBox 360 controller!\n" ); break; case k_ESteamInputType_XBoxOneController: printf( "XBox One controller!\n" ); break; case k_ESteamInputType_GenericXInput: printf( "Generic XInput!\n" ); break; case k_ESteamInputType_PS4Controller: printf( "PS4 controller!\n" ); break; } }

顯示自己的圖像素材——各按鍵的自訂圖像

若是使用以動作來源做索引的單一查閱表格,便可使用以下函式來實作特定裝置的字符,並將無法辨識的控制器轉換為最為接近的形式。

程式碼範例:
// 使用各函式前須先初始化介面——呼叫一次即可! SteamInput()->Init() // ... // SteamAPI_RunCallbacks() 會為所有初始化的介面呼叫 RunFrame 函式 // 大多數遊戲應已開始定期呼叫此函式。 如果沒有,則必須手動更新 // Steam 輸入介面 SteamInput()->RunFrame(); // ... // 以要查詢的 XInput 欄位取代。 數值為 0 到 3 之間 // 若要在選擇欲使用的 API 前先以 RawInput 辨別裝置,請見 // 下方的「使用 RawInput 辨別裝置」段落 int nXinputSlot = 0; // 以要查詢的按鍵取代 EXboxOrigin eXboxButtonToGetGlyphFor = k_EXboxOrigin_A; EInputActionOrigin buttonOrigin = k_EInputActionOrigin_XBoxOne_A; // 若控制器是透過 Steam 輸入所配置,則轉譯該按鍵 InputHandle_t controller1Handle = SteamInput()->GetControllerForGamepadIndex( nXinputSlot ); if ( controller1Handle > 0 ) { // 有效的控制代碼不為 0,這是透過 Steam 輸入所配置的控制器 // 備註:使用 Steam 輸入 API 的控制器不會透過 GetControllerForGamepadIndex() 回傳控制代碼 buttonOrigin = SteamInput()->GetActionOriginFromXboxOrigin( controller1Handle, k_EXboxOrigin_A ); // 也就是 k_EInputActionOrigin_PS4_X } else { // 有效的控制代碼不為 0,這是一般的 Xbox 控制器 // 繼續使用原本的按鍵 } // Steam 將持續新增動作來源,未來的控制器將超過現在的範圍 // 若希望進行測試,可以假裝 Switch / PS5 控制器不存在,而改為: // if ( buttonOrigin >= k_EInputActionOrigin_XBox360_Reserved10 ) if ( buttonOrigin >= k_EInputActionOrigin_Count ) { // 我們的遊戲中沒有為此來源準備的圖像素材! 我想 Steam 添加了對 // 新控制器的支援。 從我們製作遊戲所使用的 SDK 取得有支援且最接近的值吧 buttonOrigin = SteamInput()->TranslateActionOrigin( k_ESteamInputType_Unknown, buttonOrigin ); } // 以遊戲中將回傳您的按鍵圖像素材的函式取代 int glyphTextureID = getHardCodedButtonGlyphTexture( buttonOrigin );

使用 RawInput 辨別裝置

部分遊戲將使用 RawInput 來支援 4 支以上的控制器,或用來判斷連上的是否為 PlayStation 控制器。 這項 API 也會回傳其它非遊戲手把的裝置,並且沒有可靠的索引碼能直接與 Steam 輸入函式對應。 您可以改為從 GetRawInputDeviceInfo() 查詢 RIDI_DEVICENAME 字串來獲得此資訊,而前者是以真實裝置的 USB VID/PID、遊戲手把索引碼、和 Steam 輸入控制代碼所編碼。字串格式如下:
\\.\pipe\HID#VID_045E&PID_028E&IG_00#{真實裝置 VID}&{真實裝置 PID}&{Steam 輸入 API 控制代碼}#{Steam 輸入遊戲手把控制代碼}#{處理序 ID}
舉例來說,\\.\pipe\HID#VID_045E&PID_028E&IG_00#045E&0B00&00045EB00704DAF3#0#20160 便是 VID/PID 為 0x45e/0x0b00 的 Xbox One 控制器。

程式碼範例:
#define VALVE_DIRECTINPUT_GAMEPAD_VID 0x28DE #define VALVE_DIRECTINPUT_GAMEPAD_PID 0x11FF #define STEAM_INPUT_VID_INDEX 37 #define STEAM_INPUT_PID_INDEX 42 #define STEAM_INPUT_SIAPI_HANDLE_INDEX 47 #define STEAM_INPUT_GAMEPAD_INDEX 64 #define MAX_PATH 260 //... void YourFunction() { PRAWINPUTDEVICELIST devices = NULL; UINT i, j, device_count = 0; if ((GetRawInputDeviceList(NULL, &device_count, sizeof(RAWINPUTDEVICELIST)) == -1) || (!device_count)) { return; /* 好吧 */ } devices = (PRAWINPUTDEVICELIST)malloc(sizeof(RAWINPUTDEVICELIST) * device_count); if (devices == NULL) { return; } if (GetRawInputDeviceList(devices, &device_count, sizeof(RAWINPUTDEVICELIST)) == -1) { free(devices); return; /* 好吧 */ } for (i = 0; i < device_count; i++) { RID_DEVICE_INFO rdi; char devName[MAX_PATH]; UINT rdiSize = sizeof(rdi); UINT nameSize = MAX_PATH; rdi.cbSize = sizeof(rdi); if ( devices[i].dwType == RIM_TYPEHID && GetRawInputDeviceInfoA( devices[i].hDevice, RIDI_DEVICEINFO, &rdi, &rdiSize ) != (UINT)-1 && GetRawInputDeviceInfoA( devices[i].hDevice, RIDI_DEVICENAME, devName, &nameSize ) != (UINT)-1 ) { if ( rdi.hid.dwVendorId == VALVE_DIRECTINPUT_GAMEPAD_VID && rdi.hid.dwProductId == VALVE_DIRECTINPUT_GAMEPAD_PID ) { uint32 ulVID = strtoul( &devName[STEAM_INPUT_VID_INDEX], NULL, 16 ); uint32 ulPID = strtoul( &devName[STEAM_INPUT_PID_INDEX], NULL, 16 ); uint64 ulDeviceHandle = strtoull( &devName[STEAM_INPUT_SIAPI_HANDLE_INDEX], NULL, 16 ); uint32 unGamepadIndex = strtoul( &devName[STEAM_INPUT_GAMEPAD_INDEX], NULL, 16 ); [/i][/i][/i] Log( "Raw input device: VID = 0x%x, PID = 0x%x, handle = 0x%llx, index = 0x%x, %s\n", ulVID, ulPID, ulDeviceHandle, unGamepadIndex, devName ); // 現在可以直接使用 VID/PID,或透過 GetInputTypeForHandle 使用 ulDeviceHandle 來辨別裝置 ESteamInputType inputType = SteamInput()->GetInputTypeForHandle( ulDeviceHandle ); switch( inputType ) { case k_ESteamInputType_Unknown: printf( "unknown!\n" ); break; case k_ESteamInputType_SteamController: printf( "Steam controller!\n" ); break; case k_ESteamInputType_XBox360Controller: printf( "XBox 360 controller!\n" ); break; case k_ESteamInputType_XBoxOneController: printf( "XBox One controller!\n" ); break; case k_ESteamInputType_GenericGamepad: printf( "Generic Gamepad(DirectInput)!\n" ); break; case k_ESteamInputType_PS3Controller: case k_ESteamInputType_PS4Controller: case k_ESteamInputType_PS5Controller: printf( "PlayStation controller!\n" ); break; } } else { // 裝置為無法透過 Steam 輸入使用的控制器,使用一般的控制器 ID 邏輯 continue; } } } free(devices); }

進行 Steamworks 設定

第一步是在 Steamworks 設定中的應用程式 -> Steam 輸入之下的「讓控制器支援 Steam 輸入」勾選欲使用 Steam 輸入的控制器類型。 若為支援 Xbox 控制器的標準遊戲,我們建議勾選 Xbox 以外所有核取方塊:

steamworks_steam_input_optin_settings.png

若遊戲支援飛行搖桿或賽車方向盤,則最好取消勾選通用 / DirectInput 控制器的方塊,因為 Steam 不支援此類裝置的對應。

選擇配置

您可以選擇建立自己的配置,或從我們預先做好的模板中挑選一個。 除非您計畫修改我們預設的配置,或根據遊戲內的作用各別標出按鍵配置,否則不需要製作自己的配置。 Steam 輸入的內建模板皆已在地化為所有 Steam 支援的語言,因此請務必將您的配置在地化,以避免向非英文使用者顯示英文。

選擇模板

對主機的控制器而言,不同版本的遊戲手把模板沒有區別,但這些設定對 Steam 控制器則很重要。 以下為不同模板的說明,及適用的遊戲類型:
  • 遊戲手把 - 將右觸控板轉變為原始模擬搖桿使用,以模擬 Xbox 控制器。 非常適合雙搖桿、平台遊戲,或體育類遊戲
  • 高準度視角 / 瞄準的手把 - FPS 或任何使用視角控制的遊戲的理想配置。 選用此選項前,請細心測試遊戲是否支援同時使用遊戲手把及滑鼠,包括在物品庫或地圖等非主要畫面當中
  • 可控制視角的手把 - 此配置將從觸控板取得滑鼠式的輸入,並將其轉譯為模擬搖桿的撥動。 如果您的遊戲可以配置為高搖桿靈敏度、無死區,以及線性加速,那麼此配置將順暢運作。否則,請考慮使用滑鼠 / 鍵盤配置

steam_input_gamepad_templates.png

請見此文了解在 Steamworks 中為您的遊戲r將模板開放使用的步驟。

建立自訂配置

大多數的控制器配置都可根據不同控制器類型自動轉換,但您至少需要製作一個 Steam 控制器和一個 Xbox 控制器的配置。
備註:如果您希望利用特定裝置的功能,像是動態控制,您可能也需要為 PlayStation 4 或 Nintendo Switch 控制器設定配置。

文字輸入


螢幕上的文字輸入嚴格說來並不是 ISteamInput(Steam 輸入)的一部份,而是屬於 ISteamUtils。 此功能目前只有實作用於玩家透過 Big Picture 模式啟動遊戲的情況。

快速參考資料:

在地化配置
在不使用 Steam 輸入 API 的情況下將配置在地化會較為複雜,但仍有方法做到。
  • 開始前,請先確定您的配置自上次匯出後未有任何修改 開啟配置器並進行修改(修改後再回復變更是無妨的)。
  • 前往 Steam\Logs\controller_ui.txt 記錄檔,尋找類似以下文字的字串:
    Steam Controller Saving Controller Configuration Autosave for [控制器序列號]- AppID: [您的 APPID]. Loaded Config for Local Selection Path for App ID [您的 APPID], Controller 0: F:\ProgramFiles\Steam\client\userdata\[STEAMID]\config\controller_configs\apps\[您的 APPID]\[控制器序列]\guest\controller_configuration.vdf
  • 為欲在地化的按鍵綁定加入索引對照碼
    • 標題 / 說明:
      "controller_mappings" { "version" "3" "revision" "5" "title" "#title" "description" "#description" ...
    • 按鍵綁定:
      "button_a" { "activators" { "Full_Press" { "bindings" { "binding" "xinput_button A, #abutton" } } } } ...
  • 然後於在地化區塊為每種語言加入相應的值:
    "localization" { "tchinese" { "title" "此為經過在地化的標題" "description" "此為經過在地化的說明。" 按鈕的菱形配置也有在地化名稱。{ "abutton" "您的名稱 1" "bbutton" "您的名稱 2" "xbutton" "您的名稱 3" "ybutton" "您的名稱 4" } ...
  • 現在即可匯出配置並根據這些指示在合作夥伴網站進行設置。 最好將其中一份配置設為勾選「使用動作區塊」。

於瀏覽器中輸入此網址即可在 Steam 用戶端中預覽範例配置:steam://controllerconfig/681280/1744110334 ,或由此取得 VFD 檔案。