Gamepad e vibrazione
Questa pagina descrive le nozioni di base della programmazione per gamepad che usano [Windows.Gaming.Input.Gamepad][gamepad] e le API correlate per la piattaforma UWP (Universal Windows Platform).
Leggendo questa pagina, si apprenderà quanto segue:
- come raccogliere un elenco di gamepad connessi e i relativi utenti
- come rilevare che un gamepad è stato aggiunto o rimosso
- come leggere l'input da uno o più gamepad
- come inviare comandi di vibrazione e impulsi
- come i gamepad si comportano da dispositivi di spostamento interfaccia utente
Panoramica sui gamepad
I gamepad come il controller wireless Xbox e il controller wireless Xbox S sono dispositivi di input di gioco per uso generico. Rappresentano il dispositivo di input standard in Xbox One e sono una scelta comune per i giocatori Windows quando non preferiscono una tastiera e un mouse. I gamepad sono supportati nelle app UWP di Windows 10 o Windows 11 e Xbox tramite lo spazio dei nomi Windows.Gaming.Input.
I gamepad Xbox One sono dotati di un pad direzionale (o D-pad); Pulsanti A, B, X, Y, Visualizza e Menu, levetta sinistra e destra, bumper, grilletto e un totale di quattro motori di vibrazione. Entrambe le levette forniscono letture analogiche doppie negli assi X e Y e fungono anche da pulsante quando vengono premute verso l'interno. Ogni grilletti fornisce una lettura analogica che rappresenta la distanza di pressione.
Nota
Windows.Gaming.Input.Gamepad
supporta anche i gamepad Xbox 360, che hanno lo stesso layout di controllo dei gamepad Xbox One standard.
Trigger di vibrazione e impulsi
I gamepad Xbox One forniscono due motori indipendenti per vibrazioni forti e sottili, nonché due motori dedicati per fornire vibrazioni nitide a ogni grilletto (questa caratteristica unica è il motivo per cui i grilletti del gamepad di Xbox One sono definiti grilletti di impulso).
Nota
I gamepad Xbox 360 non sono dotati di grilletti di impulso.
Per maggiori informazioni, vedere Panoramica dei grilletti di vibrazione e impulsi.
Zone morte delle levette
Una levetta inattiva nella posizione centrale produce idealmente la stessa lettura neutra negli assi X e Y ogni volta. Tuttavia, a causa delle forze meccaniche e della sensibilità della levetta, le letture effettive nella posizione centrale approssimano solo il valore neutro ideale e possono variare tra letture successive. Pertanto è necessario usare sempre una piccola zona morta, ovvero un intervallo di valori vicino alla posizione centrale ideale ignorata, per compensare le differenze di produzione, l'usura meccanica o altri problemi del gamepad.
Le zone morte più grandi offrono una semplice strategia per separare l'input intenzionale da quello non intenzionale.
Per maggiori informazioni, vedere Lettura delle levette.
Esplorazione dell'interfaccia utente
Per alleviare l'onere di supporto dei diversi dispositivi di input per la navigazione dell'interfaccia utente e incoraggiare la coerenza tra giochi e dispositivi, la maggior parte dei dispositivi di input fisici agisce contemporaneamente come un dispositivo di input logico separato denominato controller di navigazione dell'interfaccia utente. Il controller di spostamento interfaccia utente fornisce un vocabolario comune ai comandi di spostamento interfaccia utente tra i vari dispositivi di input.
In quanto controller di spostamento interfaccia utente, i gamepad eseguono il mapping del set necessario di comandi di spostamento alla levetta sinistra, D-pad e pulsanti Visualizza, Menu, A e B.
Comando Navigazione | Input dei gamepad |
---|---|
Su | Levetta sinistra su/D-pad su |
Giù | Levetta sinistra giù/D-pad giù |
Sinistra | Levetta sinistra a sinistra/D-pad a sinistra |
Destra | Levetta sinistra a destra/D-pad a destra |
Visualizza | Pulsante Visualizza |
Menu | Pulsante Menu |
Accetta | Pulsante A |
Annulla | Pulsante B |
I gamepad eseguono il mapping di tutti i set opzionali di comandi di spostamento agli input rimanenti.
Comando Navigazione | Input dei gamepad |
---|---|
Pagina su | Trigger sinistro |
Pagina giù | Trigger destro |
Pagina a sinistra | Bumper sinistro |
Pagina a destra | Bumper destro |
Scorri verso l'alto | Levetta destra su |
Scorri verso il basso | Levetta destra giù |
Scorri a sinistra | Levetta destra a sinistra |
Scorri a destra | Levetta destra a destra |
Contesto 1 | Pulsante X |
Contesto 2 | Pulsante Y |
Contesto 3 | Pressione della levetta sinistra |
Contesto 4 | Pressione della levetta destra |
Rilevare e tenere traccia dei gamepad
I gamepad vengono gestiti dal sistema, pertanto non è necessario crearli o inizializzarli. Il sistema fornisce un elenco di gamepad connessi ed eventi per notificare quando un gamepad viene aggiunto o rimosso.
Elenco dei gamepad
La classe Gamepad fornisce una proprietà statica, Gamepad, che è un elenco di sola lettura di gamepad attualmente connessi. Poiché si potrebbe essere interessati solo ad alcuni gamepad connessi, consigliamo di mantenere una propria raccolta anziché accedervi tramite la proprietà Gamepads
.
Nell'esempio seguente tutti i gamepad connessi vengono copiati in una nuova raccolta. Si noti che poiché altri thread in background accederanno a questa raccolta (negli eventi GamepadAdded e GamepadRemoved), è necessario posizionare un blocco intorno a qualsiasi codice che legge o aggiorna la raccolta.
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);
}
}
}
}
Aggiunta e rimozione di gamepad
Quando un gamepad viene aggiunto o rimosso, vengono generati gli eventi GamepadAdded e GamepadRemoved. È possibile registrare i gestori per questi eventi per tenere traccia dei gamepad attualmente connessi.
Nell'esempio seguente viene avviato il rilevamento di un gamepad aggiunto.
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);
}
}
};
Nell'esempio seguente viene avviato il rilevamento di un gamepad rimosso. Sarà anche necessario gestire cosa succede ai gamepad che si stanno monitorando quando vengono rimossi; ad esempio, questo codice tiene traccia solo dell'input da un gamepad e lo imposta semplicemente su nullptr
quando viene rimosso. Dovrai controllare ogni fotogramma se il gamepad è attivo e aggiornare il gamepad da cui si sta raccogliendo l'input quando i controller vengono connessi e disconnessi.
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);
}
}
};
Per maggiori informazioni, vedere Procedure di input per i giochi.
Utenti e cuffie
Ogni gamepad può essere associato a un account utente per collegare la propria identità al gioco e può avere una cuffia collegata per facilitare la chat vocale o le funzionalità del gioco. Per maggiori informazioni sull'uso di utenti e cuffie, vedere Tenere traccia degli utenti e dei relativi dispositivi e Cuffia.
Lettura del gamepad
Dopo avere identificato il gamepad, sarà possibile raccoglierne l'input. Tuttavia, a differenza di altri tipi di input più comuni, i gamepad non comunicano la modifica dello stato generando eventi. È invece possibile leggere regolarmente lo stato corrente eseguendo il polling.
Polling del gamepad
Il polling acquisisce uno snapshot del dispositivo di navigazione in un momento preciso. Questo approccio alla raccolta di input è ideale con la maggior parte dei giochi essendo la logica eseguita generalmente in un ciclo deterministico anziché essere guidata dagli eventi; è anche più semplice interpretare i comandi del gioco da input raccolti contemporaneamente, anziché raccogliere molti input singoli nel corso del tempo.
Si esegue il polling di un gamepad chiamando GetCurrentReading. Questa funzione restituisce un GamepadReading che contiene lo stato del gamepad.
Nell'esempio seguente viene eseguito il polling di un gamepad per il relativo stato corrente.
auto gamepad = myGamepads[0];
GamepadReading reading = gamepad->GetCurrentReading();
Gamepad gamepad = myGamepads[0];
GamepadReading reading = gamepad.GetCurrentReading();
Oltre allo stato del gamepad, ogni lettura include un timestamp che indica esattamente quando è stato recuperato lo stato. Il timestamp è utile per la correlazione tra la tempistica delle letture precedenti o la tempistica della simulazione del gioco.
Lettura delle levette
Ogni levetta fornisce una lettura analogica tra -1.0 e +1.0 negli assi X e Y. Nell'asse X, il valore -1.0 corrisponde alla posizione della levetta più a sinistra; il valore +1.0 corrisponde alla posizione più a destra. Nell'asse Y, il valore -1.0 corrisponde alla posizione della levetta più a sinistra; il valore +1.0 corrisponde alla posizione più a destra. In entrambi gli assi, il valore è circa 0.0 quando il bastone si trova nella posizione centrale, ma è normale che il valore preciso possa variare, anche tra letture successive; più avanti in questa sezione vengono descritte le strategie per attenuare questa variazione.
Il valore dell'asse X della levetta sinistra viene letto dalla proprietà LeftThumbstickX
della struttura GamepadReading. Il valore dell'asse Y viene letto dalla proprietà LeftThumbstickY
. Il valore dell'asse X della levetta destra viene letto dalla proprietà RightThumbstickX
. Il valore dell'asse Y viene letto dalla proprietà RightThumbstickY
.
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
Quando si leggono i valori della levetta, si noterà che non producono in modo affidabile una lettura neutra di 0.0 quando la levetta è inattiva nella posizione centrale; producono invece valori diversi vicino a 0.0 ogni volta che levetta viene spostata e restituita alla posizione centrale. Per attenuare queste variazioni, è possibile implementare una piccola zona morta, ovvero un intervallo di valori vicino alla posizione centrale ideale che viene ignorato. Un modo per implementare una zona morta consiste nel determinare quanto lontano la levetta si è spostata dal centro e ignorare le letture che sono più vicine a una distanza scelta. È possibile calcolare la distanza approssimativamente; la distanza non è esatta perché le letture del joystick sono essenzialmente polari, e non planari, solo usando il teorema di Pitagora. In questo modo viene generato un zona morta radiale.
L'esempio seguente illustra una zona morta radiale di base usando il teorema di Pitagora:
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
}
Ogni levetta agisce anche come un pulsante quando viene premuto verso l'interno; Per maggiori informazioni sulla lettura di questo input, vedere Lettura dei pulsanti.
Lettura dei grilletti
I grilletti sono rappresentati come valori a virgola mobile compresi tra 0.0 (completamente rilasciato) e 1.0 (completamente premuto). Il valore del grilletto sinistro viene letto dalla proprietà LeftTrigger
della struttura GamepadReading. Il valore del grilletto destro viene letto dalla proprietà 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
Lettura dei pulsanti
Ognuno dei pulsanti del gamepad, ovvero le quattro direzioni del D-pad, i bumper sinistro e destro, la pressione della levetta sinistra e destra, i pulsanti A, B, X, Y, Visualizza e Menu, fornisce una lettura digitale che indica se viene premuta (giù) o rilasciata (su). Per una questione di efficienza, le letture dei pulsanti non sono rappresentate come singoli valori booleani, bensì sono tutti compressi in un singolo campo di bit rappresentato dall'enumerazione GamepadButtons.
I valori del pulsante vengono letti dalla proprietà Buttons
della struttura GamepadReading. Poiché questa proprietà è un campo di bit, viene usata la maschera bit per bit per isolare il valore del pulsante a cui si è interessati. Il pulsante viene premuto (giù) quando viene impostato il bit corrispondente; in caso contrario, viene rilasciato (su).
Nell'esempio seguente viene determinato se viene premuto il pulsante A.
if (GamepadButtons::A == (reading.Buttons & GamepadButtons::A))
{
// button A is pressed
}
if (GamepadButtons.A == (reading.Buttons & GamepadButtons.A))
{
// button A is pressed
}
Nell'esempio seguente viene determinato se viene rilasciato il pulsante A.
if (GamepadButtons::None == (reading.Buttons & GamepadButtons::A))
{
// button A is released
}
if (GamepadButtons.None == (reading.Buttons & GamepadButtons.A))
{
// button A is released
}
Talvolta si potrebbe voler determinare se un pulsante passa da premuto a rilasciato o da rilasciato a premuto, se più pulsanti vengono premuti o rilasciati o se un set di pulsanti è disposto in un modo particolare (alcuni premuti, altri no). Per informazioni su come rilevare queste condizioni, vedere Rilevamento delle transizioni dei pulsanti e Rilevamento di disposizioni complesse dei pulsanti.
Eseguire l'esempio di input del gamepad
L'esempio GamepadUWP (su GitHub) illustra come connettersi a un gamepad e leggerne lo stato.
Panoramica dei grilletti di vibrazione e impulsi
I motori di vibrazione all'interno di un gamepad sono dedicati a fornire feedback tattile. I giochi usano questa capacità per creare un maggiore senso di immersione, comunicare informazioni sullo stato (ad esempio danni ricevuti), segnalare la prossimità a oggetti importanti o altri usi creativi.
I gamepad Xbox One sono dotati di un totale di quattro motori di vibrazione indipendenti. Due sono grandi motori che si trovano nel corpo del gamepad; il motore sinistro fornisce vibrazioni grezze e ad alta ampiezza, mentre il motore destro fornisce una vibrazione più delicata e sottile. Gli altri due sono piccoli motori, uno all'interno di ogni grilletti, che forniscono picchi di vibrazione direttamente alle dita sul grilletto; questa capacità unica del gamepad di Xbox One è il motivo per cui i suoi grilletti vengono anche definiti grilletti di impulso. Orchestrando insieme questi motori, è possibile produrre una vasta gamma di sensazioni tattili.
Uso di vibrazioni e impulsi
La vibrazione dei gamepad viene controllata tramite la proprietà Vibration della classe Gamepad. Vibration
è un'istanza della struttura GamepadVibration costituita da quattro valori a virgola mobile; ogni valore rappresenta l'intensità di uno dei motori.
Anche se i membri della proprietà Gamepad.Vibration
possono essere modificati direttamente, è consigliabile inizializzare un'istanza GamepadVibration
separata con i valori desiderati e quindi copiarla nella proprietà Gamepad.Vibration
per modificare contemporaneamente le intensità motorie effettive.
Nell'esempio seguente viene illustrato come modificare contemporaneamente le intensità motorie.
// 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;
Uso dei motori di vibrazione
I motori di vibrazione sinistro e destro accettano valori a virgola mobile compresi tra 0.0 (nessuna vibrazione) e 1.0 (vibrazione più intensa). L'intensità del motore sinistro viene impostata dalla proprietà LeftMotor
della struttura GamepadVibration; l'intensità del motore destro viene impostata dalla proprietà RightMotor
.
L'esempio seguente imposta l'intensità dei motori di vibrazione e attiva la vibrazione del gamepad.
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;
Tenere presente che questi due motori non sono identici, quindi l'impostazione di queste proprietà sullo stesso valore non produrrà la medesima vibrazione in entrambi i motori. Per qualsiasi valore, il motore sinistro produce una vibrazione più forte a una frequenza inferiore rispetto al motore destro che, per lo stesso valore, produce una vibrazione più delicata a una frequenza più alta. Anche al valore massimo, il motore sinistro non può produrre le alte frequenze del motore destro, né il motore destro produce le alte forze espresse dal motore sinistro. Tuttavia, poiché i motori sono strettamente collegati tramite il corpo del gamepad, i giocatori non sperimentano le vibrazioni completamente indipendentemente pur se i motori hanno caratteristiche diverse e possono vibrare con intensità diverse. Questa disposizione consente di produrre una gamma più ampia ed espressiva di sensazioni rispetto a motori identici.
Uso dei grilletti di impulso
Ogni motore di grilletti di impulso accetta un valore a virgola mobile compreso tra 0.0 (nessuna vibrazione) e 1.0 (vibrazione più intensa). L'intensità del grilletto sinistro viene impostata dalla proprietà LeftTrigger
della struttura GamepadVibration; l'intensità del grilletto destro viene impostata dalla proprietà RightTrigger
.
L'esempio seguente imposta l'intensità di entrambi i grilletti di impulso e li attiva.
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;
A differenza degli altri, i due motori di vibrazione all'interno dei grilletti sono identici in modo da produrre la stessa vibrazione in entrambi i motori per lo stesso valore. Tuttavia, poiché questi motori non sono collegati rigidamente in alcun modo, i giocatori sperimentano le vibrazioni in modo indipendente. Questa disposizione consente di indirizzare contemporaneamente sensazioni completamente indipendenti a entrambi i grilletti e li aiuta a trasmettere informazioni più specifiche rispetto ai motori nel corpo del gamepad.
Eseguire l'esempio di vibrazione del gamepad
L'esempio GamepadVibrationUWP (github) illustra come vengono usati i motori di vibrazione del gamepad e i grilletti di impulsi per produrre una varietà di effetti.