En tant que moteur très complet, Unity dispose d’un système dédié pour gérer les contrôles des joueurs. Ce système est très exhaustif, car il est capable de gérer une grande variété de systèmes de contrôles sur différents appareils (clavier souris, gamepad, écrans tactiles, contrôles de VR), il y a une gestion des joueurs multiples, et il peut être intégré à votre application de plusieurs manières.
Dans cet article, nous abordons uniquement l’Input System, le système activement géré par Unity 3D. Nous n’aborderons pas l’ancien système, l’Input Manager, qui est un peu moins poussé.
Cet article aborde le principe de l’Input System et quelques exemples de mise en place de ce système pour votre projet.
Dans cet article
Comment est conçu l’Input System ?
Schéma de fonctionnement
L’Input System d’Unity est un système central qui monitore les différents systèmes de contrôle reliés à l’appareil. Il vous permet de créer une couche d’abstraction entre ces périphériques et la couche applicative.
En somme, plutôt que de tester explicitement les touches, tels que « touche Espace », « boutons A, B, X, Y », on va plutôt raisonner en terme d’Actions, de style « Move », « Look », « Fire »… Ce qui permet de facilement adapter les contrôles d’un périphérique à un autre, et aussi de donner la possibilité au joueur de personnaliser ses contrôles.
C’est finalement une logique présente dans la plupart des jeux PC depuis longtemps, ça correspond à la logique des jeux PC où les joueurs souhaitent généralement pouvoir personnaliser leurs contrôles (et c’est aussi une bonne pratique).
Composants de l’Input System
Voici une liste des éléments qu’on peut trouver dans l’Input System :
Nom de l’élement | Type | Rôle |
---|---|---|
inputsettings.asset | Scriptable Object (fichier dans le répertoire Assets) | Panneau de configuration du projet. Paramétrage fin des entrées (Deadzone, multitap…), choix des périphériques supportés… |
Fichier d’actions (Actions.inputactions) | Fichier .inputactions dans le répertoire assets | Fichier contenant une liste d’actions associée à des contrôles sur les périphériques. |
Player Input | Composant, sur un GO | Lien entre un fichiers d’actions et |
Input System UI Input Module | Composant placé sur un GO aux côtés d’un Event System | Lien entre un Player Input et la navigation dans une interface utilisateur (UI). |
Player Input Manager | Composant placé sur un GO | Gestionnaire de multiples Player Input (pour les jeux multijoueurs locaux) |
Ajouter l’Input System dans un projet Unity 3D
Installation et paramétrage du package Input System
Ouvrez le Package Manager, choisissez le repository de Unity Registry, et cherchez « Input ». Vous verrez apparaître l’Input System. Mettez à jour à la dernière version et installez le package dans votre projet Unity.
L’Input Manager est présent par défaut dans les projets Unity 2020. Il vous faudra activer le package en allant dans Project Settings, onglet « Player ». À la section « Active Input Handling », sélectionnez « Input System Package (New) ». L’éditeur va vous demander un redémarrage pour mettre à jour l’ensemble du projet et le rendre compatible avec l’Input System.
Une fois le projet ré-ouvert, allez dans la section « Input System Package » de Project Settings. À cet endroit, vous pouvez cliquer sur Create settings asset et choisir où placer le fichier (par exemple dans Assets/Source/Input).
C’est un fichier de configuration qui stocke la manière dont sont traités les évènements, les devices supportés, etc. Vous pouvez directement changer son contenu à ce même endroit, et ajouter explicitement quels sont les périphériques supportés par l’application.
C’est une approche conseillée de sélectionner explicitement quels éléments sont supportés, afin d’être sûr qu’ils sont testés et correctement supportés par l’application. Certains sont très spécifiques, à la réalité virtuelle ou aux supports tactiles.
Créer un fichier d’actions : la couche d’abstraction du système d’input
Le fichier d’actions vous permet de définir toutes les actions dont vous pourriez avoir besoin, et de leur associer des contrôles pour les différents types de périphérique.
Pour créer ce fichier, vous pouvez directement aller dans un de vos répertoires Assets et faire Create > Input Actions. Alternativement, vous pouvez cliquer sur le bouton « Create Actions... » dans un composant Player Input.
L’arborescence d’un fichier Input Actions est ainsi :
- Une Action Map contient un ensemble d’actions liées à un contexte. Par exemple vous pourriez avoir une Map pour le joueur à pied, une autre Map pour quand le joueur est dans un véhicule. Par défaut, il y a une Map UI et une map Player.
- L’Action Map contient plusieurs Actions. Leur nom est utilisé pour la programmation, typiquement ça seront des verbes (Move, Look, Fire, Jump, Take…). On peut paramétrer une action en définissant s’il faut la traiter comme un bouton I/O ou comme une valeur, et dans ce second cas, le type de valeur à récupérer (un Vector2 par exemple).
- Chaque Action contient un ou plusieurs Properties, les choix de touches.
- Ça peut être un simple Binding (choix d’une touche ou d’un élément physique spécifique).
- Ça peut être également un Vecteur Composite (plusieurs touches combinées pour faire un ensemble de contrôles)
En terme de Vecteur composite, typiquement, ce qui est un stick 2D sur la manette peut correspondre à 4 touches sur le clavier. Les contrôles par défaut contiennent un binding équivalent entre le stick gauche et les touches WQSD et les touches fléchées du clavier.
De plus, les actions et les propriétés peuvent contenir des paramètres d’Interactions (pression continue, brève, etc.), et de Processors (inverser ou normaliser une action ou un bouton, par exemple pour qu’une pression sur un contrôle analogique (stick, gâchette) et un contrôle binaire (bouton, touche de clavier) donne le même effet dans le jeu.
Une grande qualité de l’Input System est qu’il permet d’adresser les touches selon leur position et non selon leur nomage. C’est notamment intéressant pour notre particularité française des claviers AZERTY : les touches de direction classique WASD des claviers QWERTY sont directement reconnus à l’emplacement des touches ZQSD d’un Azerty.
De même, pour un gamepad, on ne va pas s’adresser spécifiquement au bouton A d’un contrôleur Xbox, croix d’un contrôleur PlayStation, ou le bouton équivalent d’une manette générique : c’est le bouton « Sud » de la manette.
Le composant Player Input et les types de comportement
Vous pouvez ajouter le composant Player Input sur un GameObject, typiquement, il correspond aux contrôles pour un joueur. Ça peut être sur le GameManager en singleton pour un jeu solo, ou sur le personnage pour un jeu multi local.
- Actions : référence au fichier InputActionsAsset
- UI Input Module : référence au composant correspondant pour l’UI
- Camera : utilisé uniquement pour le multi-écrans
- Behavior : détermine la manière dont Player Input communique avec le reste du programme (voir ci-dessous)
- Open Input Settings : ouvre directement la section dans Project Settings
- Open Input Debugger : ouvre une fenêtre de debug, affichant toutes les entrées connectées et tout ce qui est pressé sur ces entrées
La section Behavior vous permet d’utiliser Player Input de quatre manières différentes :
Nom du Behavior | Description | Récupération des infos dans les scripts |
---|---|---|
Send Messages | Méthode SendMessage() : Appelle la méthode sur les composants du GameObject contenant PlayerInput | Créer les méthodes nécessaires dans un composant placé sur le GameObject |
Broadcast Messages | Méthode BroadcastMessage() : Appelle la méthode sur les composants du GameObject contenant PlayerInput (et les GameObjects enfants) | Créer les méthodes dans un composant placé sur le GameObject ou un de ses GameObject enfants |
Invoke Unity Events | Dans l’Inspecteur Unity, adressage explicite de fonctions situés dans des composants et GameObject divers. | Créer une méthode sur le composant de votre choix, et l’assigner dans Player Input dans l’inspecteur (cf. ci-dessous). |
Invoke C# Events | Évènements C# de base disponibles sur l’API PlayerInput. | Enregistrer les callbacks sur les évènements directement dans les scripts C#. |
Les modes de fonctionnement « Send Messages » et « Broadcast Messages » appellent certaines méthodes par défaut et d’autres qui dépendent directement du fichier Action attaché.
Méthodes par défaut : OnDeviceLost, OnDeviceRegained, OnControlsChanged
Méthodes dépendant des actions : OnMove, OnLook, OnFire, etc.
Ces méthodes sont directement listées dans l’inspecteur.
Petit exemple du comportement « Invoke Unity Events »
Exemple avec « Invoke Unity Events » : dans Player Input, en sélectionnant « Invoke Unity Events », une section déroulable « Events » est présentée. Elle contient les Actions Maps et les actions, et pour chaque action, la possibilité d’assigner un composant et des méthodes de ce composant. Exemple :
Dans le composant « ShipController », nous pouvons créer une méthode cible MoveAxis() qui prend en argument un paramètre de type CallbackContext. Cet objet qui fait partie du namespace UnityEngine.InputSystem.InputAction dispose de plusieurs propriétés et méthodes accessibles directement.
En somme, quand le joueur appuie sur les contrôles assignés dans l’action « Move », la méthode est appelé et des infos liées à cette entrée sont transmises en paramètre sur cette méthode.
Par exemple, la méthode ci-dessous récupère les infos passés en argument (callbackAxis) et utilise la méthode ReadValue() pour convertir cette position en Vector2. Ainsi, nous déplaçons dans la scène le GameObject où le composant est présent.
public void MoveAxis(CallbackContext callbackAxis)
{
transform.position += (Vector3)callbackAxis.ReadValue<Vector2>();
}
Nous allons nous concentrer sur le dernier comportement, les évènements C#.
Intégration de l’Input System avec les évènements C# (composant Player Input)
L’utilisation des évènements C# repose sur le principe d’évènement C#. Le principe, c’est d’inscrire les méthodes de votre classe sur les évènements de PlayerInput, et à chaque fois que les évènements sont déclenchés, les méthodes seront appelées.
Lier un GameObject à un composant Player Input
Pour s’inscrire aux évènements d’un PlayerInput, il faut d’abord avoir accès à ce PlayerInput dans votre classe.
Première solution : définir une variable PlayerInput directement dans la classe liée à votre objet :
using UnityEngine.InputSystem;
public class ShipController : MonoBehaviour
{
[SerializeField] private PlayerInput playerInput;
}
Si le PlayerInput est sur le même composant, vous pouvez utiliser GetComponent<PlayerInput>().
Sinon, directement dans l’inspecteur, vous assignez à ce champ votre GameObjet contenant le composant PlayerInput (l’attribut [SerializeField] vous permet de rendre visible ce champ privé dans l’inspecteur).
Seconde solution : votre PlayerInput peut être géré au niveau du GameManager, et du coup vous pouvez y accéder par les différents objets qui en ont besoin. En utilisant le principe du GameManager Singleton, créez au niveau du GameManager une variable PlayerInput accessible publiquement. C’est une manière plutôt dédiée aux jeux solo.
public class GameManager : MonoBehaviour
{
private static GameManager instance = null;
public static GameManager Instance => instance;
public PlayerInput GetPlayerInput { get; private set; }
private void Awake()
{
// [...] Code d'initialisation du Singleton
GetPlayerInput = GetComponent<PlayerInput>();
}
}
Que vous utilisiez une méthode ou l’autre, vous aurez un objet playerInput, ou une référence via GameManager.Instance.GetPlayerInput, accessible pour s’inscrire aux évènements.
Inscription précise aux évènements « actions » du PlayerInput
void Awake()
{
playerInput.actions["move"].performed += MoveAxis;
playerInput.actions["fire"].performed += LaunchProjectile;
}
Les lignes dans la méthode Awake permettent d’inscrire les méthodes « MoveAxis » et « LaunchProjectile » sur les actions du Player Input, respectivement « move » et « fire ».
Les actions disposent de 3 callback possibles :
- action.started : début de l’action
- action.performed : quand l’action s’est déroulée en entier
- action.cancelled : quand l’action n’a pas été jusqu’au bout
De plus, lorsque les méthodes sont appelées, elles envoient des informations correspondant à CallbackContext. Ainsi, lorsqu’on a une action dont la valeur est un Vector2 (comme Move et Look par défaut), on peut directement utiliser la valeur dans la méthode :
private void MoveAxis(InputAction.CallbackContext callbackAxis)
{
transform.position += (Vector3)callbackAxis.ReadValue<Vector2>();
}
Inscription générale aux évènements du PlayerInput (onActionTriggered…)
Plutôt que de s’inscrire à toutes les actions manuellement, une autre manière de faire, c’est de s’inscrire à l’évènement « OnAction » de l’InputSystem, puis de tester l’action directement dans la méthode inscrite.
Ça donne ça, au niveau de l’inscription :
void Awake()
{
playerInput.onActionTriggered += OnAction;
playerInput.onDeviceLost += OnDeviceLost;
playerInput.onDeviceRegained += OnDeviceRegained;
playerInput.onControlsChanged += OnControlsChanged;
}
- onActionTriggered est l’évènement global pour prendre en compte une action. Le paramètre passé est un CallbackContext.
- onDeviceLost ,onDeviceRegained et onDeviceChanged correspondent aux comportement quand le périphérique associé au PlayerInput est déconnecté, reconnecté. Le paramètre passé est PlayerInput.
private void OnDeviceLost(PlayerInput playerInput)
{
// TODO : coder le comportement quand le périphérique est débranché
}
private void OnDeviceRegained(PlayerInput playerInput)
{
// TODO : coder le comportement quand le périphérique est rebranché
}
private void OnControlsChanged(PlayerInput playerInput)
{
// Todo : coder le comportement quand le périphérique change
}
private void OnAction(InputAction.CallbackContext context)
{
switch(context.action.name)
{
case "Accelerate":
transform.position += Vector3.forward;
break;
case "Brake":
transform.position += Vector3.back;
break;
case "Move":
transform.position += (Vector3)context.action.ReadValue<Vector2>();
break;
case "Fire":
if (ammunition > 0)
{
LaunchProjectile();
}
else
{
audioSource.PlayOneShot(emptyFire);
}
break;
default:
break;
}
}
Dans l’objet CallbackContext, nous pouvons récupérer l’action qui a déclenché l’évènement (ci-dessus, nous utilisons context). Avec context.action.name, nous obtenons son nom, et faire un test sur cette action. Enfin, dans le cas d’une Action qui contient une valeur (par exemple un axe), nous pouvons lire la valeur transmise par l’action avec context.action.ReadValue<T>.
Récupérer des informations sur les devices et communiquer avec ceux-ci (vibration d’un Gamepad)
La classe PlayerInput vous permet de récupérer des infos sur les appareils actuellement en cours d’utilisation.
Par exemple, avec l’évènement « OnControlsChanged », on récupère la propriété currentControlScheme, qui correspond aux schémas de contrôle définis dans l’Input Manager (typiquement, Gamepad ou Keyboard&Mouse.
Ça parait par exemple de modifier une UI lorsqu’on passe d’un schéma clavier souris à un gamepad ou l’inverse :
private void OnControlsChanged(PlayerInput newPlayerInput)
{
if (newPlayerInput.currentControlScheme == "Gamepad")
{
userInterface.UpdateDisplay("Gamepad");
}
else
{
userInterface.UpdateDisplay("Keyboard");
}
}
De même, InputSystem dispose de plusieurs classes correspondant aux types de devices existants. Par exemple, on a la classe Gamepad. Dans le code suivant, nous avons un objet playerGamepad de type Gamepad, et lorsqu’il n’est pas null, on peut exécuter les méthodes qui font vibrer le Gamepad.
private Gamepad playerGamepad = null;
private bool rumbleIsAllowed = true;
private void OnControlsChanged(PlayerInput newPlayerInput)
{
StopRumble();
playerGamepad = newPlayerInput.devices[0] as Gamepad;
// Stocke le gamepad dans playerGamepad s'il existe, sinon playerGamepad est null.
}
private void TakeHit()
{
if (playerGamepad != null && rumbleIsAllowed)
{
// Déclenche une vibration (50% de vitesse en basse fréquence, 20% de vitesse en haute fréquence)
playerGamepad.SetMotorSpeeds(.5f, .2f);
}
}
private void StopRumble()
{
if (playerGamepad != null)
{
// Stoppe toute les vibrations
playerGamepad.ResetHaptics();
}
}
Concrètement, on va chercher le premier élément de newPlayerInput.devices, et grâce au mot-clé « as », playerGamepad stocke ce périphérique si c’est un Gamepad, et est null dans tous les autres cas.
Dans le reste du code, il nous suffit de lancer les méthodes sur playerGamepad seulement s’il n’est pas null. Ce sont les suivantes :
- SetMotorSpeeds(float lowFrequency, float highFrequency) : fait vibrer les moteurs du Gamepad aux vitesses spécifiées (basse fréquence / moteur de gauche et haute fréquente / moteur de droite). Ce sont des valeurs normalisées de 0 à 1.
- ResetHaptics() : remet les moteurs à leur valeur par défaut (0, 0), ce qui est équivalent à stopper la vibration.
- PauseHaptics() : met en pause les moteurs de vibration du Gamepad
- ResumeHaptics() : reprend la vibration mise en pause par PauseHaptics().
Lorsqu’on gère la vibration, la bonne pratique est de la rendre activable ou non (d’où la présence d’un bool dans le code ci-dessus), et bien sûr de stopper toute vibration lorsqu’on change de périphérique.
Typiquement, les vibrations sont également limitées dans le temps, donc chaque lancement de vibration va être accompagné d’un timer qui coupera plus ou moins rapidement la vibration selon le contexte du jeu.
Propriétés statiques de InputSystem, pour du prototypage rapide
InputSystem dispose aussi d’objets statiques, qui permettent d’interroger directement les éléments reliés à l’application, sans passer par un PlayerInput. On perd la couche d’abstraction, mais ça permet tout de même de prototyper ou de tester rapidement des choses, donc c’est important de les connaître.
Les classes Keyboard, Mouse, Gamepad, contiennent une propriété « all » et une propriété « current ».
La propriété InputDevice.all est un tableau contenant tous les objets de type InputDevice connectés au système, donc on peut faire une comparaison dessus pour savoir combien d’éléments sont reliés au système (par exemple, Gamepad.all.Count > 1 ou Mouse.all.Count == 0)
La propriété current contient la représentation du device le plus récent utilisé. On peut y trouver des informations sur le device (son nom, comment il est représenté dans Unity, etc.), ainsi que des méthodes permettant d’agir sur le périphérique.
En somme, dans une fonction Update(), vous pouvez faire des tests sur une touche ou un bouton pour vérifier si iel a été pressé(e) ou relâché(e), avec le format suivant :
- Device.current.touche.wasPressedThisFrame
- Device.current.touche.wasReleasedThisFrame
Voici quelques exemples :
private void Update()
{
if(Gamepad.all.Count > 0)
{
Debug.Log("Il y a " + Gamepad.all.Count + " gamepad(s) relié(s) au système.");
}
if (Gamepad.current.buttonSouth.wasPressedThisFrame)
{
// Code lorsque le bouton du bas (A ou X) est pressé
}
if (Gamepad.current.startButton.wasPressedThisFrame)
{
// Code lorsque le bouton Start est pressé
}
if (Keyboard.current.f5Key.wasReleasedThisFrame)
{
// Lorsque la touche F5 du clavier est relachée
}
if (Mouse.current.leftButton.wasPressedThisFrame)
{
// Code lors du clic gauche sur la souris
}
}
Votre IDE vous indiquera le nom des différentes touches que vous pouvez tester :
D’autres méthodes et propriétés sont accessibles selon les devices. Voici une brève sélection, à titre d’exemple :
- Device Gamepad.current
- SetMotorSpeeds(), ResetHaptics, PauseHaptics, ResumeHaptics() : méthodes pour la vibration (cf. paragraphe « vibration d’un gamepad)
- Device Keyboard.current
- KeyCount : nombre de touches du clavier
- keyboardLayout :
- anyKey : permet de tester la pression sur n’importe quelle touche du clavier (anyKey.wasPressedThisFrame)
- Device Mouse.current
- position : position de la souris dans l’espace 2D
- clickCount : modélise le nombre de clics (pour gérer le simple ou double clic)
À retenir : l’Input System de Unity 3D est un système complet, qui permet de nombreuses approches différentes pour gérer les contrôles d’un jeu. Nous avons vu quelques unes de ces approches, mais le sujet est vaste et fera l’objet d’autres articles.