Pratiques d’entrée pour les jeux
Cette rubrique décrit les modèles et techniques permettant d’utiliser efficacement des appareils d’entrée dans des jeux plateforme Windows universelle (UWP).
En lisant cette rubrique, vous allez apprendre :
- comment suivre les joueurs et les appareils d’entrée et de navigation qu’ils utilisent actuellement
- comment détecter les transitions de bouton (enfoncée à relâchée, mise en production enfoncée)
- comment détecter des dispositions de bouton complexes avec un seul test
Choix d’une classe d’appareil d’entrée
Il existe de nombreux types d’API d’entrée disponibles, tels que ArcadeStick, FlightStick et Gamepad. Comment décider de l’API à utiliser pour votre jeu ?
Vous devez choisir l’API qui vous donne l’entrée la plus appropriée pour votre jeu. Par exemple, si vous créez un jeu de plateforme 2D, vous pouvez probablement simplement utiliser la classe Gamepad et non pas vous déranger avec les fonctionnalités supplémentaires disponibles via d’autres classes. Cela limiterait le jeu à la prise en charge des boîtiers de commande uniquement et fournirait une interface cohérente qui fonctionnera sur de nombreux boîtiers de commande différents sans avoir besoin de code supplémentaire.
En revanche, pour les simulations de vol et de course complexes, vous pouvez énumérer tous les objets RawGameController comme base de référence pour vous assurer qu’ils prennent en charge tout appareil de niche que les joueurs passionnés peuvent avoir, y compris des appareils tels que des pédales distinctes ou des limitations qui sont toujours utilisés par un seul joueur.
À partir de là, vous pouvez utiliser la méthode FromGameController d’une classe d’entrée, telle que Gamepad.FromGameController, pour voir si chaque appareil a une vue plus organisée. Par exemple, si l’appareil est également un Boîtier de commande, vous pouvez ajuster l’interface utilisateur de mappage de bouton pour refléter cela et fournir des mappages de boutons par défaut sensibles à choisir. (Contrairement à exiger que le joueur configure manuellement les entrées du boîtier de commande si vous utilisez uniquementRawGameController.)
Vous pouvez également examiner l’ID de fournisseur (VID) et l’ID de produit (PID) d’un RawGameController (à l’aide de HardwareVendorId et hardwareProductId, respectivement) et fournir des mappages de boutons suggérés pour les appareils populaires tout en restant compatibles avec les appareils inconnus qui sortent à l’avenir via des mappages manuels par le lecteur.
Suivi des contrôleurs connectés
Bien que chaque type de contrôleur inclut une liste de contrôleurs connectés (par exemple, Gamepad.Gamepads), il est judicieux de conserver votre propre liste de contrôleurs. Consultez la liste des boîtiers de commande pour plus d’informations (chaque type de contrôleur a une section nommée de la même façon sur sa propre rubrique).
Toutefois, que se passe-t-il quand le joueur déconnecte son contrôleur ou qu’il se connecte à un nouveau ? Vous devez gérer ces événements et mettre à jour votre liste en conséquence. Pour plus d’informations, consultez Ajout et suppression de boîtiers de commande (à nouveau, chaque type de contrôleur a une section nommée de la même façon sur sa propre rubrique).
Étant donné que les événements ajoutés et supprimés sont déclenchés de façon asynchrone, vous pouvez obtenir des résultats incorrects lors du traitement de votre liste de contrôleurs. Par conséquent, chaque fois que vous accédez à votre liste de contrôleurs, vous devez placer un verrou autour de lui afin qu’un seul thread puisse y accéder à la fois. Cela peut être effectué avec le runtime d’accès concurrentiel, en particulier la classe critical_section, dans <ppl.h>.
Une autre chose à penser est que la liste des contrôleurs connectés sera initialement vide, et prend une seconde ou deux pour remplir. Par conséquent, si vous attribuez uniquement le boîtier de commande actuel dans la méthode de démarrage, il sera null !
Pour corriger ce problème, vous devez avoir une méthode qui « actualise » le boîtier de commande principal (dans un jeu à joueur unique ; les jeux multijoueurs nécessitent des solutions plus sophistiquées). Vous devez ensuite appeler cette méthode dans vos gestionnaires d’événements ajoutés et supprimés par votre contrôleur, ou dans votre méthode de mise à jour.
La méthode suivante retourne simplement le premier boîtier de commande dans la liste (ou nullptr si la liste est vide). Ensuite, vous devez simplement vous rappeler de vérifier nullptr chaque fois que vous effectuez n’importe quoi avec le contrôleur. C’est à vous que vous souhaitiez bloquer le gameplay lorsqu’il n’y a pas de contrôleur connecté (par exemple, en suspendant le jeu) ou simplement que le gameplay continue, tout en ignorant l’entrée.
#include <ppl.h>
using namespace Platform::Collections;
using namespace Windows::Gaming::Input;
using namespace concurrency;
Vector<Gamepad^>^ m_myGamepads = ref new Vector<Gamepad^>();
Gamepad^ GetFirstGamepad()
{
Gamepad^ gamepad = nullptr;
critical_section::scoped_lock{ m_lock };
if (m_myGamepads->Size > 0)
{
gamepad = m_myGamepads->GetAt(0);
}
return gamepad;
}
Mettez-le ensemble, voici un exemple de gestion des entrées à partir d’un boîtier de commande :
#include <algorithm>
#include <ppl.h>
using namespace Platform::Collections;
using namespace Windows::Foundation;
using namespace Windows::Gaming::Input;
using namespace concurrency;
static Vector<Gamepad^>^ m_myGamepads = ref new Vector<Gamepad^>();
static Gamepad^ m_gamepad = nullptr;
static critical_section m_lock{};
void Start()
{
// Register for gamepad added and removed events.
Gamepad::GamepadAdded += ref new EventHandler<Gamepad^>(&OnGamepadAdded);
Gamepad::GamepadRemoved += ref new EventHandler<Gamepad^>(&OnGamepadRemoved);
// Add connected gamepads to m_myGamepads.
for (auto gamepad : Gamepad::Gamepads)
{
OnGamepadAdded(nullptr, gamepad);
}
}
void Update()
{
// Update the current gamepad if necessary.
if (m_gamepad == nullptr)
{
auto gamepad = GetFirstGamepad();
if (m_gamepad != gamepad)
{
m_gamepad = gamepad;
}
}
if (m_gamepad != nullptr)
{
// Gather gamepad reading.
}
}
// Get the first gamepad in the list.
Gamepad^ GetFirstGamepad()
{
Gamepad^ gamepad = nullptr;
critical_section::scoped_lock{ m_lock };
if (m_myGamepads->Size > 0)
{
gamepad = m_myGamepads->GetAt(0);
}
return gamepad;
}
void OnGamepadAdded(Platform::Object^ sender, Gamepad^ args)
{
// Check if the just-added gamepad is already in m_myGamepads; if it isn't,
// add it.
critical_section::scoped_lock lock{ m_lock };
auto it = std::find(begin(m_myGamepads), end(m_myGamepads), args);
if (it == end(m_myGamepads))
{
m_myGamepads->Append(args);
}
}
void OnGamepadRemoved(Platform::Object^ sender, Gamepad^ args)
{
// Remove the gamepad that was just disconnected from m_myGamepads.
unsigned int indexRemoved;
critical_section::scoped_lock lock{ m_lock };
if (m_myGamepads->IndexOf(args, &indexRemoved))
{
if (m_gamepad == m_myGamepads->GetAt(indexRemoved))
{
m_gamepad = nullptr;
}
m_myGamepads->RemoveAt(indexRemoved);
}
}
Suivi des utilisateurs et de leurs appareils
Tous les appareils d’entrée sont associés à un utilisateur afin que son identité puisse être liée à son gameplay, à ses réalisations, à ses changements de paramètres et à d’autres activités. Les utilisateurs peuvent se connecter ou se déconnecter à volonté, et il est courant qu’un autre utilisateur se connecte à un appareil d’entrée qui reste connecté au système une fois que l’utilisateur précédent s’est déconnecté. Lorsqu’un utilisateur se connecte ou se déconnecte, l’événement IGameController.UserChanged est déclenché. Vous pouvez inscrire un gestionnaire d’événements pour cet événement afin de suivre les joueurs et les appareils qu’ils utilisent.
L’identité de l’utilisateur est également la façon dont un appareil d’entrée est associé à son contrôleur de navigation d’interface utilisateur correspondant.
Pour ces raisons, l’entrée du lecteur doit être suivie et corrélée avec la propriété User de la classe d’appareil (héritée de l’interface IGameController ).
L’exemple d’application UserGamepadPairingUWP sur GitHub montre comment vous pouvez suivre les utilisateurs et les appareils qu’ils utilisent.
Détection des transitions de bouton
Parfois, vous souhaitez savoir quand un bouton est appuyé ou libéré pour la première fois ; c’est-à-dire, précisément lorsque l’état du bouton passe de la sortie vers enfoncée ou de l’appui à la mise en production. Pour déterminer cela, vous devez mémoriser la lecture de l’appareil précédent et comparer la lecture actuelle à celle-ci pour voir ce qui a changé.
L’exemple suivant illustre une approche de base pour mémoriser la lecture précédente ; Les boîtiers de commande sont présentés ici, mais les principes sont les mêmes pour le stick arcade, la roue de course et les autres types d’appareils d’entrée.
Gamepad gamepad;
GamepadReading newReading();
GamepadReading oldReading();
// Called at the start of the game.
void Game::Start()
{
gamepad = Gamepad::Gamepads[0];
}
// Game::Loop represents one iteration of a typical game loop
void Game::Loop()
{
// move previous newReading into oldReading before getting next newReading
oldReading = newReading, newReading = gamepad.GetCurrentReading();
// process device readings using buttonJustPressed/buttonJustReleased (see below)
}
Avant d’effectuer autre chose, Game::Loop
déplace la valeur existante de newReading
(la lecture du boîtier de commande à partir de l’itération de boucle précédente) dans oldReading
, puis remplit newReading
avec une nouvelle lecture du boîtier de commande pour l’itération actuelle. Cela vous donne les informations dont vous avez besoin pour détecter les transitions de bouton.
L’exemple suivant illustre une approche de base pour détecter les transitions de bouton :
bool ButtonJustPressed(const GamepadButtons selection)
{
bool newSelectionPressed = (selection == (newReading.Buttons & selection));
bool oldSelectionPressed = (selection == (oldReading.Buttons & selection));
return newSelectionPressed && !oldSelectionPressed;
}
bool ButtonJustReleased(GamepadButtons selection)
{
bool newSelectionReleased =
(GamepadButtons.None == (newReading.Buttons & selection));
bool oldSelectionReleased =
(GamepadButtons.None == (oldReading.Buttons & selection));
return newSelectionReleased && !oldSelectionReleased;
}
Ces deux fonctions dérivent d’abord l’état booléen de la sélection newReading
de bouton, oldReading
puis effectuent une logique booléenne pour déterminer si la transition cible s’est produite. Ces fonctions retournent true uniquement si la nouvelle lecture contient l’état cible (appuyé ou libéré, respectivement) et que l’ancienne lecture ne contient pas non plus l’état cible ; sinon, ils retournent false.
Détection des dispositions de bouton complexes
Chaque bouton d’un appareil d’entrée fournit une lecture numérique qui indique s’il est enfoncé (bas) ou relâché (haut). Pour plus d’efficacité, les lectures de boutons ne sont pas représentées en tant que valeurs booléennes individuelles ; Au lieu de cela, ils sont tous emballés dans des champs de bits représentés par des énumérations spécifiques à l’appareil, telles que GamepadButtons. Pour lire des boutons spécifiques, le masquage au niveau du bit est utilisé pour isoler les valeurs qui vous intéressent. Un bouton est enfoncé (bas) lorsque son bit correspondant est défini ; sinon, elle est libérée (vers le haut).
Rappelez-vous que les boutons uniques sont déterminés à appuyer ou à libérer ; Les boîtiers de commande sont présentés ici, mais les principes sont les mêmes pour le stick arcade, la roue de course et les autres types d’appareils d’entrée.
GamepadReading reading = gamepad.GetCurrentReading();
// Determines whether gamepad button A is pressed.
if (GamepadButtons::A == (reading.Buttons & GamepadButtons::A))
{
// The A button is pressed.
}
// Determines whether gamepad button A is released.
if (GamepadButtons::None == (reading.Buttons & GamepadButtons::A))
{
// The A button is released (not pressed).
}
Comme vous pouvez le voir, la détermination de l’état d’un seul bouton est directement vers l’avant, mais parfois vous souhaiterez peut-être déterminer si plusieurs boutons sont enfoncés ou relâchés, ou si un ensemble de boutons est organisé d’une manière particulière , certains appuyés, certains non. Le test de plusieurs boutons est plus complexe que de tester des boutons uniques, en particulier avec le potentiel d’état de bouton mixte, mais il existe une formule simple pour ces tests qui s’appliquent à des tests simples et multiples.
L’exemple suivant détermine si les boutons du boîtier de commande A et B sont tous deux appuyés :
if ((GamepadButtons::A | GamepadButtons::B) == (reading.Buttons & (GamepadButtons::A | GamepadButtons::B))
{
// The A and B buttons are both pressed.
}
L’exemple suivant détermine si les boutons du boîtier de commande A et B sont tous deux libérés :
if ((GamepadButtons::None == (reading.Buttons & GamepadButtons::A | GamepadButtons::B))
{
// The A and B buttons are both released (not pressed).
}
L’exemple suivant détermine si le bouton du boîtier de commande A est enfoncé pendant que le bouton B est libéré :
if (GamepadButtons::A == (reading.Buttons & (GamepadButtons::A | GamepadButtons::B))
{
// The A button is pressed and the B button is released (B is not pressed).
}
La formule que cinq de ces exemples ont en commun est que la disposition des boutons à tester est spécifiée par l’expression du côté gauche de l’opérateur d’égalité tandis que les boutons à considérer sont sélectionnés par l’expression de masquage sur le côté droit.
L’exemple suivant illustre plus clairement cette formule en réécritant l’exemple précédent :
auto buttonArrangement = GamepadButtons::A;
auto buttonSelection = (reading.Buttons & (GamepadButtons::A | GamepadButtons::B));
if (buttonArrangement == buttonSelection)
{
// The A button is pressed and the B button is released (B is not pressed).
}
Cette formule peut être appliquée pour tester n’importe quel nombre de boutons dans n’importe quelle disposition de leurs états.
Obtenir l’état de la batterie
Pour tout contrôleur de jeu qui implémente l’interface IGameControllerBatteryInfo , vous pouvez appeler TryGetBatteryReport sur l’instance du contrôleur pour obtenir un objet BatteryReport qui fournit des informations sur la batterie dans le contrôleur. Vous pouvez obtenir des propriétés comme le taux de charge de la batterie (ChargeRateInMilliwatts), la capacité d’énergie estimée d’une nouvelle batterie (DesignCapacityInMilliwattHours) et la capacité d’énergie entièrement chargée de la batterie actuelle (FullChargeCapacityInMilliwattHours).
Pour les contrôleurs de jeu qui prennent en charge les rapports détaillés sur la batterie, vous pouvez obtenir ces informations et plus d’informations sur la batterie, comme détaillé dans Obtenir des informations sur la batterie. Toutefois, la plupart des contrôleurs de jeu ne prennent pas en charge ce niveau de rapports de batterie et utilisent plutôt du matériel à faible coût. Pour ces contrôleurs, vous devez tenir compte des considérations suivantes :
ChargeRateInMilliwatts et DesignCapacityInMilliwattHours seront toujours NULL.
Vous pouvez obtenir le pourcentage de batterie en calculant RemainingCapacityInMilliwattHours / FullChargeCapacityInMilliwattHours. Vous devez ignorer les valeurs de ces propriétés et traiter uniquement le pourcentage calculé.
Le pourcentage du point de puce précédent sera toujours l’un des éléments suivants :
- 100 % (complet)
- 70 % (moyen)
- 40 % (faible)
- 10 % (critique)
Si votre code effectue une action (comme l’interface utilisateur de dessin) en fonction du pourcentage de durée de vie de la batterie restante, assurez-vous qu’il est conforme aux valeurs ci-dessus. Par exemple, si vous souhaitez avertir le joueur lorsque la batterie du contrôleur est faible, faites-le quand elle atteint 10 %.