Utiliser l’Input System d’Unity 3D

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.

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.

Schéma du fonctionnement de l’Input System de Unity

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’élementTypeRôle
inputsettings.assetScriptable 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 assetsFichier contenant une liste d’actions associée à des contrôles sur les périphériques.
Player InputComposant, sur un GOLien entre un fichiers d’actions et
Input System UI Input ModuleComposant placé sur un GO aux côtés d’un Event SystemLien entre un Player Input et la navigation dans une interface utilisateur (UI).
Player Input ManagerComposant placé sur un GOGestionnaire 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.

Package Input System

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.

Choix du système d’Input

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)
Exemple de biding composite

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.

L’Input System d’Unity, permet de choisir les touches par position (Location of Key), ou par lettre associé à la touche

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 BehaviorDescriptionRécupération des infos dans les scripts
Send MessagesMé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 MessagesMé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 EventsDans 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.

Propriétés accessibles via un CallbackContext

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).

Exemple de PlayerInput assigné 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 :

Dans l’IDE Visual Studio, Keyboard.current dévoile le nom des touches du clavier qui peuvent être testées.

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.