Pratiques d’entrée pour les jeux

Cette rubrique décrit les modèles et techniques permettant d’utiliser efficacement les périphériques d’entrée dans les jeux plateforme Windows universelle (UWP).

En lisant cette rubrique, vous allez apprendre :

  • Comment suivre les joueurs et les périphériques d’entrée et de navigation qu’ils utilisent
  • Comment détecter les transitions de bouton (appuyé à relâché, relâché à appuyé)
  • Comment détecter les dispositions de boutons complexes à l’aide d’un seul et même 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 choisissez-vous 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 ne pas vous embêter avec les fonctionnalités supplémentaires disponibles via d’autres classes. Cela limiterait le jeu à la prise en charge uniquement des boîtiers de commande et fournirait une interface cohérente qui fonctionnera sur de nombreux boîtiers de jeu différents sans besoin de code supplémentaire.

D’autre part, 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 n’importe quel appareil de niche que les joueurs enthousiastes peuvent avoir, y compris des appareils tels que des pédales séparées ou une limitation 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 Gamepad, vous pouvez ajuster l’interface utilisateur de mappage de boutons pour qu’elle le reflète et fournir des mappages de boutons par défaut raisonnables parmi lesquels choisir. (Cela contraste avec l’obligation pour le joueur de configurer manuellement les entrées du boîtier de commande si vous utilisez uniquement RawGameController.)

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 inclue une liste de contrôleurs connectés (comme Gamepad.Gamepads), il est judicieux de conserver votre propre liste de contrôleurs. Pour plus d’informations, consultez La liste des boîtiers de commande (chaque type de contrôleur a une section nommée de la même façon sur sa propre rubrique).

Cependant, que se passe-t-il quand le joueur débranche sa manette ou en branche une nouvelle ? 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 (là encore, 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 manière 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 celle-ci 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 à considérer est que la liste des contrôleurs connectés sera initialement vide et prendra une seconde ou deux pour remplir. Par conséquent, si vous affectez uniquement le boîtier de commande actuel dans la méthode start, il sera null !

Pour y remédier, vous devez disposer d’une méthode qui « actualise » le main boîtier de commande (dans un jeu solo ; les jeux multijoueurs nécessitent des solutions plus sophistiquées). Vous devez ensuite appeler cette méthode dans les gestionnaires d’événements ajoutés et supprimés du contrôleur, ou dans votre méthode de mise à jour.

La méthode suivante retourne simplement le premier boîtier de commande de la liste (ou nullptr si la liste est vide). Ensuite, il vous suffit de vous rappeler de case activée pour nullptr chaque fois que vous faites quelque chose avec le contrôleur. C’est à vous de décider si vous voulez bloquer le jeu quand il n’y a pas de contrôleur connecté (par exemple, en suspendant le jeu) ou simplement continuer le jeu, tout en ignorant les entrées.

#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;
}

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 périphériques

Tous les périphériques d’entrée sont associés à un utilisateur afin que son identité puisse être liée à sa séquence de jeu, ses succès, ses modifications de paramètres et ses autres activités. Les utilisateurs peuvent se connecter ou se déconnecter à leur guise, et il est courant qu’un autre utilisateur se connecte sur 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 d’effectuer le suivi des joueurs et des périphériques qu’ils utilisent.

L’identité utilisateur est également la façon dont un périphérique 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 de transitions de boutons

Vous souhaiterez savoir parfois quand un bouton est d’abord enfoncé ou relâché, autrement dit lorsque l’état du bouton passe de relâché à appuyé, ou inversement. Pour le déterminer, vous devez mémoriser la lecture précédente du périphérique et la comparer à la lecture actuelle pour voir ce qui a changé.

L’exemple suivant illustre une approche de base pour la mémorisation de 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, le volant 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 toute autre action, Game::Loop déplace la valeur existante de newReading (lecture du boîtier de commande de l’itération de boucle précédente) dans oldReading, puis renseigne newReading avec une nouvelle lecture du boîtier de commande correspondant à l’itération actuelle. Vous disposez alors des informations nécessaires pour détecter les transitions de boutons.

L’exemple suivant illustre une approche de base pour la détection des transitions de boutons :

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 du bouton de newReading et oldReading, puis exécutent 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 relâché, respectivement) et si l’ancienne lecture ne contient pas également l’état cible. Dans le cas contraire, elles retournent false.

Détection des dispositions de boutons 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 entrées de bouton ne sont pas représentées individuellement sous forme de valeurs booléennes. Elles sont toutes regroupées dans des champs de bits représentés par des énumérations propres aux périphériques, par exemple GamepadButtons. Pour lire des boutons spécifiques, un masquage au niveau du bit est effectué pour isoler les valeurs qui vous intéressent. Un bouton est enfoncé (bas) lorsque son bit correspondant est défini ; sinon, il est libéré (up).

Rappelez-vous comment les boutons uniques sont déterminés pour être enfoncés ou libérés; Les boîtiers de commande sont présentés ici, mais les principes sont les mêmes pour le stick arcade, le volant 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 simple, mais vous pouvez parfois déterminer si plusieurs boutons sont enfoncés ou relâchés, ou si un ensemble de boutons sont organisés d’une manière particulière (certains appuyés, d’autres pas). Le test de plusieurs boutons est plus complexe que le test de boutons uniques, en particulier avec le potentiel d’état mixte des boutons, mais il existe une formule simple pour ces tests qui s’applique aux tests à bouton unique et à plusieurs boutons.

L’exemple suivant détermine si les boutons du boîtier de commande A et B sont tous deux enfoncé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 de manette de jeu 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 A du boîtier de commande 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).
}

Dans la formule que ces cinq exemples ont en commun, la disposition des boutons à tester est spécifiée par l’expression située à gauche de l’opérateur d’égalité tandis que les boutons à examiner sont sélectionnés par l’expression de masquage à droite.

L’exemple suivant illustre cette formule plus clairement 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, avec toutes les dispositions de leur état.

Obtenir l’état de la batterie

Pour tout contrôleur de jeu qui implémente l’interface IGameControllerBatteryInfo, vous pouvez appeler TryGetBatteryReport sur le contrôleur instance pour obtenir un objet BatteryReport qui fournit des informations sur la batterie dans le contrôleur. Vous pouvez obtenir des propriétés telles que la vitesse de charge de la batterie (ChargeRateInMilliwatts), la capacité énergétique 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 génération de rapports sur la batterie et utilisent plutôt du matériel à faible coût. Pour ces contrôleurs, vous devez garder à l’esprit les 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 d’autonomie 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 de la manette est faible, faites-le quand elle atteint 10 %.

Voir aussi