遊戲板和震動
本頁說明使用 [Windows.Gaming.Input.Gamepad][Game Pad] 和相關 API 為通用 Windows 平台 (UWP) 編寫遊戲平台的基本知識。
透過閱讀此頁,您將瞭解:
- 如何收集連線遊戲面板及其使用者的清單
- 如何偵測已新增或移除遊戲板
- 如何讀取一個或多個遊戲鍵盤的輸入
- 如何傳送振動和脈衝命令
- 遊戲墊如何作為 UI 導覽裝置
遊戲板概述
Xbox Wireless Controller 和 Xbox Wireless Controller S 等遊戲板是通用的遊戲輸入裝置。 它們是 Xbox One 上的標準輸入裝置,也是 Windows 玩家不喜愛鍵盤和滑鼠時的常用選擇。 Windows 10 或 Windows 11 和 Xbox UWP 應用程式透過 Windows.Gaming.Input namespace 支援遊戲板。
Xbox One 遊戲平台配備方向鍵 (或 D 鍵);A, B, X, Y, View 和四個振動馬達。 兩個拇指鍵在 X 軸和 Y 軸上提供兩個模擬讀數,當按向內時也充當按鈕。 每個觸發程式都提供一個模擬讀數,表示它被拉回的距離。
注意
Windows.Gaming.Input.Gamepad
同時支援 Xbox 360 遊戲板,其控制配置與標準 Xbox One 遊戲板相同。
振動和脈衝觸發器
Xbox One 遊戲面板提供兩個獨立的馬達用於強和微弱的遊戲面板振動,以及兩個專用馬達用於為每個觸發器提供尖銳振動 (這一獨特功能是因為 Xbox One 遊戲面板觸發器被稱作脈衝觸發器)。
注意
Xbox 360 遊戲機未配備脈衝觸發器。
如需詳細資訊,請參閱振動與脈衝觸發程式概述。
拇指搖桿死區
理想情況下,將拇指放在中心位置時,每次都能在 X 軸和 Y 軸上產生相同的中性讀數。 然而,由於機械力和指杆靈敏度的影響,中心位置的實際讀數僅接近理想的中性值,且可在後續讀數之間變化。 因此,您必須一律使用小的死區 (位於理想中心位置附近的一組被忽略的值) 來補償製造差異、機械磨損或其他遊戲板問題。
較大的死區提供將有意輸入和無意輸入分開的簡單策略。
有關更多資訊,請參見 閱讀拇指搖桿。
UI 導覽
為減少支援不同輸入裝置使用者介面瀏覽的作業負擔,並提升遊戲和裝置之間的一致性,大部分實體輸入裝置運作時,都能分別模擬不同的邏輯輸入裝置 (稱為「UI 瀏覽控制器」)。 該 UI 導航控制器提供跨輸入裝置的 UI 導航命令的公共辭彙。
作為 UI 導覽控制器,遊戲面板將所需的導覽指令集對應到左方的拇指搖桿、D-pad、View、Menu、A、B 按鈕。
導覽命令 | 遊戲板輸入 |
---|---|
Up | 左拇指搖桿向上/D-pad 向上 |
向下 | 左拇指搖桿向下/D-pad 向下 |
Left | 左拇指搖桿向左/D-pad 向左 |
Right | 左拇指搖桿向右/D-pad 向右 |
檢視 | 檢視按鍵 |
功能表 | Menu 按鈕 |
Accept | A 按鍵 |
取消 | B 按鈕 |
此外,遊戲板會將所有選擇的導航指令集對應到其餘輸入。
導覽命令 | 遊戲板輸入 |
---|---|
Page Up | 左觸發程式 |
Page Down | 右觸發程式 |
左頁 | 左保險槓 |
向右翻頁 | 右保險槓 |
向上捲動 | 右拇指搖桿向上 |
向下捲動 | 右拇指搖桿向下 |
向左捲動 | 右拇指搖桿向左 |
向右捲動 | 右拇指搖桿向右 |
內容 1 | X 按鈕 |
內容 2 | Y 按鈕 |
內容 3 | 左拇指搖桿壓下 |
內容 4 | 右拇指搖桿壓下 |
偵測並追蹤遊戲板
遊戲板由系統管理,因此您不必建立或初始化它們。 系統提供已連線遊戲板和事件的清單,以便在新增或移除遊戲板時通知您。
遊戲面板清單
Gamepad 類別提供靜態屬性 Gamepad,這是目前連線的遊戲盤的唯讀清單。 由於您可能只對部分連線的遊戲板感興趣,因此建議Gamepads
您保留自己的收藏而不透過屬性存取它們。
下列範例會將所有連線的遊戲板複製到新的集合中。 請備註,由於背景中的其他執行緒將存取此集合 (在 GamepadAdded、GamepadRemoved 事件中),因此您需要鎖定讀取或更新集合的所有程式碼。
auto myGamepads = ref new Vector<Gamepad^>();
critical_section myLock{};
for (auto gamepad : Gamepad::Gamepads)
{
// Check if the gamepad is already in myGamepads; if it isn't, add it.
critical_section::scoped_lock lock{ myLock };
auto it = std::find(begin(myGamepads), end(myGamepads), gamepad);
if (it == end(myGamepads))
{
// This code assumes that you're interested in all gamepads.
myGamepads->Append(gamepad);
}
}
private readonly object myLock = new object();
private List<Gamepad> myGamepads = new List<Gamepad>();
private Gamepad mainGamepad;
private void GetGamepads()
{
lock (myLock)
{
foreach (var gamepad in Gamepad.Gamepads)
{
// Check if the gamepad is already in myGamepads; if it isn't, add it.
bool gamepadInList = myGamepads.Contains(gamepad);
if (!gamepadInList)
{
// This code assumes that you're interested in all gamepads.
myGamepads.Add(gamepad);
}
}
}
}
新增及移除遊戲板
當新增或移除遊戲面板時,會引發 GamepadAdded和 GamepadRemoved 事件。 您可以註冊這些事件的處理常式,以追蹤目前連線的遊戲板。
下列範例會開始追蹤已新增的遊戲板。
Gamepad::GamepadAdded += ref new EventHandler<Gamepad^>(Platform::Object^, Gamepad^ args)
{
// Check if the just-added gamepad is already in myGamepads; if it isn't, add
// it.
critical_section::scoped_lock lock{ myLock };
auto it = std::find(begin(myGamepads), end(myGamepads), args);
if (it == end(myGamepads))
{
// This code assumes that you're interested in all new gamepads.
myGamepads->Append(args);
}
}
Gamepad.GamepadAdded += (object sender, Gamepad e) =>
{
// Check if the just-added gamepad is already in myGamepads; if it isn't, add
// it.
lock (myLock)
{
bool gamepadInList = myGamepads.Contains(e);
if (!gamepadInList)
{
myGamepads.Add(e);
}
}
};
下列範例會停止追蹤已移除的遊戲板。 您還需要處理移除後nullptr
您所追蹤的遊戲板所發生的狀況;例如,此程式碼只會追蹤一個遊戲板的輸入,並只會在移除時設定為它。 如果您的遊戲板是活動的,您需要檢查每個畫面,並更新當控制器連線或中斷連線時,要從哪個遊戲板收集輸入。
Gamepad::GamepadRemoved += ref new EventHandler<Gamepad^>(Platform::Object^, Gamepad^ args)
{
unsigned int indexRemoved;
critical_section::scoped_lock lock{ myLock };
if(myGamepads->IndexOf(args, &indexRemoved))
{
if (m_gamepad == myGamepads->GetAt(indexRemoved))
{
m_gamepad = nullptr;
}
myGamepads->RemoveAt(indexRemoved);
}
}
Gamepad.GamepadRemoved += (object sender, Gamepad e) =>
{
lock (myLock)
{
int indexRemoved = myGamepads.IndexOf(e);
if (indexRemoved > -1)
{
if (mainGamepad == myGamepads[indexRemoved])
{
mainGamepad = null;
}
myGamepads.RemoveAt(indexRemoved);
}
}
};
如需詳細資訊,請參閱 Input practices for games。
使用者和耳機
每個遊戲板都可以與一個使用者帳戶相關聯以將他們的身份連結到他們的遊戲玩法,並且可以附加一個頭戴式耳機以促進語音聊天或遊戲內功能。 要瞭解有關使用使用者和頭戴式耳機的更多資訊,請參閱追蹤使用者及其裝置和頭戴式耳機。
正在讀取遊戲板
識別出感興趣的遊戲平台之後,就可以從它收集輸入資訊。 但是,不同於其他輸入型別,遊戲板不會透過引發事件來傳達狀態改變。 相反,你透過輪詢 來定期檢視他們目前的狀態。
輪詢遊戲板
輪詢在精確的時間點擷取導航裝置的快照。 這種輸入收集方法非常適合大多數遊戲,因為它們的邏輯通常運行在一個確定性的循環中,而不是事件驅動的;從一次收集的輸入解釋遊戲命令通常也比從隨時間而收集的許多單個輸入解釋更加簡單。
您可以呼叫 GetCurrentReading,來輪詢遊戲板;此函式會傳回包含遊戲板狀態的 GamepadReading。
下列範例會輪詢遊戲板的目前狀態。
auto gamepad = myGamepads[0];
GamepadReading reading = gamepad->GetCurrentReading();
Gamepad gamepad = myGamepads[0];
GamepadReading reading = gamepad.GetCurrentReading();
除遊戲板狀態之外,每個讀取都包括一個時間戳記,精確地指示狀態被檢索的時間。 時間戳記對於與先前的讀數的時序或遊戲模擬的時序相關是有用的。
閱讀大棒
每個圖釘在 X 和 Y 軸上提供介於 –1.0 和 +1.0 之間的模擬讀數。 在X軸中,-1.0 的值對應至最左邊的指杆位置;+1.0 的值對應至最右邊的位置。 在Y軸中,值 –1.0 對應在最下方的指杆位置;值 +1.0 對應最上方的位置。 在兩個軸中,當棒子位於中心位置時,該值約為 0.0,但精確值變化是正常的,即使在後續讀數之間也是如此;減少這種變化的策略在本節稍後討論。
左側LeftThumbstickX
指杆的 X 軸值是從 GamepadReading 結構 Property 中讀取的;Y 軸的值是從LeftThumbstickY
屬性中讀取的。 從屬性中RightThumbstickX
讀取右指杆的 X 軸值;RightThumbstickY
從屬性中讀取 Y 軸值。
float leftStickX = reading.LeftThumbstickX; // returns a value between -1.0 and +1.0
float leftStickY = reading.LeftThumbstickY; // returns a value between -1.0 and +1.0
float rightStickX = reading.RightThumbstickX; // returns a value between -1.0 and +1.0
float rightStickY = reading.RightThumbstickY; // returns a value between -1.0 and +1.0
double leftStickX = reading.LeftThumbstickX; // returns a value between -1.0 and +1.0
double leftStickY = reading.LeftThumbstickY; // returns a value between -1.0 and +1.0
double rightStickX = reading.RightThumbstickX; // returns a value between -1.0 and +1.0
double rightStickY = reading.RightThumbstickY; // returns a value between -1.0 and +1.0
讀取指杆值時,您會備註到,當指杆在中心位置靜止時,它們不能可靠地產生 0.0 的中性讀數;相反,每次移動指杆並返回到中心位置時,它們會產生接近 0.0 的不同值。 若要緩解這些變化,您可以實作小的 死區,它是位於理想中心位置附近被忽略的值範圍。 設定死區的一種方法是,確定拇指杆移動的位置離中心有多遠,忽略掉讀數比你選擇的一些距離更近。 你可以粗略地計算距離,這不是精確的,因為拇指杆讀數基本上是極值,而不是平面值,只要使用勾股定理。 這產生徑向死區。
下列範例使用勾股定理,示範一個基本的徑向死區。
float leftStickX = reading.LeftThumbstickX; // returns a value between -1.0 and +1.0
float leftStickY = reading.LeftThumbstickY; // returns a value between -1.0 and +1.0
// choose a deadzone -- readings inside this radius are ignored.
const float deadzoneRadius = 0.1;
const float deadzoneSquared = deadzoneRadius * deadzoneRadius;
// Pythagorean theorem -- for a right triangle, hypotenuse^2 = (opposite side)^2 + (adjacent side)^2
auto oppositeSquared = leftStickY * leftStickY;
auto adjacentSquared = leftStickX * leftStickX;
// accept and process input if true; otherwise, reject and ignore it.
if ((oppositeSquared + adjacentSquared) > deadzoneSquared)
{
// input accepted, process it
}
double leftStickX = reading.LeftThumbstickX; // returns a value between -1.0 and +1.0
double leftStickY = reading.LeftThumbstickY; // returns a value between -1.0 and +1.0
// choose a deadzone -- readings inside this radius are ignored.
const double deadzoneRadius = 0.1;
const double deadzoneSquared = deadzoneRadius * deadzoneRadius;
// Pythagorean theorem -- for a right triangle, hypotenuse^2 = (opposite side)^2 + (adjacent side)^2
double oppositeSquared = leftStickY * leftStickY;
double adjacentSquared = leftStickX * leftStickX;
// accept and process input if true; otherwise, reject and ignore it.
if ((oppositeSquared + adjacentSquared) > deadzoneSquared)
{
// input accepted, process it
}
每個拇指杆在按向內時也作為一個按鈕;有關閱讀此輸入的更多資訊,請參閱閱讀按鈕。
讀取觸發程式
觸發程式會以 0.0 (完全釋放) 至 1.0 (完全壓下) 之間的浮點值來表示。 左觸發器的值是從 LeftTrigger
GamepadReading 結構的屬性讀取;右觸發器的值是從RightTrigger
屬性讀取。
float leftTrigger = reading.LeftTrigger; // returns a value between 0.0 and 1.0
float rightTrigger = reading.RightTrigger; // returns a value between 0.0 and 1.0
double leftTrigger = reading.LeftTrigger; // returns a value between 0.0 and 1.0
double rightTrigger = reading.RightTrigger; // returns a value between 0.0 and 1.0
正在讀取按鈕
每個遊戲板按鈕:D-pad、左右減震墊、左右拇指搖桿按壓、A、B、X、Y、View 和 A 四個方向。 為提高效率,按鈕讀數不會表示為單獨的布林值;相反,它們都封裝到由 GamepadButtons 列舉表示的單個位元欄位中。
按鈕值是從 Buttons
GamepadReading 結構的屬性讀取。 因為這個屬性是位元欄位,所以會使用位元遮罩來隔離您感興趣的按鈕值。 當設定相應的位時,按鈕被按下 (下);否則,會被釋放 (上)。
下列範例會決定是否按下 A 按鈕。
if (GamepadButtons::A == (reading.Buttons & GamepadButtons::A))
{
// button A is pressed
}
if (GamepadButtons.A == (reading.Buttons & GamepadButtons.A))
{
// button A is pressed
}
下列範例會決定是否釋放 A 按鈕。
if (GamepadButtons::None == (reading.Buttons & GamepadButtons::A))
{
// button A is released
}
if (GamepadButtons.None == (reading.Buttons & GamepadButtons.A))
{
// button A is released
}
有時您可能想要判斷按鈕何時從按壓轉換為鬆開或鬆開轉換為按壓,是否按多個按鈕或鬆開多個按鈕,或者是否以特定方式排列一組按鈕 (有些按下,有些不按下)。 如需如何偵測其中每種狀況的相關資訊,請參閱偵測按鈕轉換,以及 偵測複雜的按鈕配置。
執行遊戲板輸入範例
Gamepad UWP 範例 (github),示範如何連線至遊戲平台並讀取其狀態。
振動和脈衝觸發器概述
遊戲墊內的振動馬達用於向使用者提供觸覺反饋。 遊戲利用這種能力來創造更大的沈浸感,幫助傳達狀態資訊 (如損傷),表明接近重要的物體,或用於其他創造性的用途。
Xbox One 遊戲機總共配備四個獨立震動馬達。 其中兩個是位於遊戲墊本體中的大型馬達;左馬達提供粗糙的、高振幅的振動,而右馬達提供柔和、更微妙的振動。 另外兩個是小型電機,每個觸發器內部一個,直接給使用者的觸發器指提供尖銳的振動爆發;Xbox One 遊戲板的這種獨特能力是其觸發器被稱作脈衝觸發器。 透過將這些馬達協調在一起,可以產生多種觸覺感覺。
使用振動和脈衝
Gamepad 的震動是透過 Gamepad 類別的Vibration 屬性來控制。 Vibration
是由四個浮點值組成的 GamepadVibration ,每個值代表其中一個馬達的強度。
雖然可以直接修改屬Gamepad.Vibration
性的成員,但建議您將一個單獨的實GamepadVibration
例初始化為所需的值,然後將其複製到屬Gamepad.Vibration
性中以一次更改實際馬達強度。
下列範例示範如何一次變更所有馬達強度。
// get the first gamepad
Gamepad^ gamepad = Gamepad::Gamepads->GetAt(0);
// create an instance of GamepadVibration
GamepadVibration vibration;
// ... set vibration levels on vibration struct here
// copy the GamepadVibration struct to the gamepad
gamepad.Vibration = vibration;
// get the first gamepad
Gamepad gamepad = Gamepad.Gamepads[0];
// create an instance of GamepadVibration
GamepadVibration vibration = new GamepadVibration();
// ... set vibration levels on vibration struct here
// copy the GamepadVibration struct to the gamepad
gamepad.Vibration = vibration;
使用振動馬達
左右振動馬達的浮點值介於 0.0 (無振動) 到1.0 (最強烈振動) 之間。 左馬LeftMotor
達的強度由 GamepadVibration 結構的屬性設定;右馬達的強度由RightMotor
屬性設定。
下列範例會設定兩個振動馬達的強度,並啟動遊戲墊振動。
GamepadVibration vibration;
vibration.LeftMotor = 0.80; // sets the intensity of the left motor to 80%
vibration.RightMotor = 0.25; // sets the intensity of the right motor to 25%
gamepad.Vibration = vibration;
GamepadVibration vibration = new GamepadVibration();
vibration.LeftMotor = 0.80; // sets the intensity of the left motor to 80%
vibration.RightMotor = 0.25; // sets the intensity of the right motor to 25%
mainGamepad.Vibration = vibration;
請記住,這兩個馬達並不相同,因此將這些屬性設定為相同的值不會在一個馬達中產生與另一個馬達相同的振動。 對於任何值,左馬達在較低的頻率下產生較強振動,而右馬達在相同的值下,在較高的頻率下產生較溫和的振動。 即使在該最大值,左馬達也不能產生右馬達的高頻,右馬達也不能產生左馬達的高力。 然而,由於電機是由遊戲墊主體剛性連線的,即使電機具有不同的特性,並且能夠以不同的強度振動,玩家並不能完全獨立地體驗振動。 與電動機相同的情況相比,這種佈置允許產生更寬廣的、更有表現力的感覺範圍。
使用脈衝觸發程式
每個脈衝觸發馬達都有一個浮點值,介於 0.0 (無振動) 和 1.0 (最強烈振動) 之間。 左觸發電動機的強度由 LeftTrigger
GamepadVibration 結構的屬性設定;右觸發電動機的強RightTrigger
度由屬性設定。
下列範例會設定兩個脈衝觸發器的強度並啟動它們。
GamepadVibration vibration;
vibration.LeftTrigger = 0.75; // sets the intensity of the left trigger to 75%
vibration.RightTrigger = 0.50; // sets the intensity of the right trigger to 50%
gamepad.Vibration = vibration;
GamepadVibration vibration = new GamepadVibration();
vibration.LeftTrigger = 0.75; // sets the intensity of the left trigger to 75%
vibration.RightTrigger = 0.50; // sets the intensity of the right trigger to 50%
mainGamepad.Vibration = vibration;
與其它的馬達不同,觸發器內的兩個振動馬達是相同的,因此它們以相同的值在任一馬達中產生相同的振動。 然而,由於這些馬達並非以任何方式剛性地連線,因此玩家可以獨立體驗振動。 這種佈置允許完全獨立的感覺被同時導向兩個觸發器,並幫助它們傳遞比遊戲墊主體中的馬達更特定的資訊。
執行遊戲板震動樣本
GamepadVibration UWP 範例 (github),示範如何使用遊戲墊振動馬達和脈衝觸發器來產生各種效果。