paint-brush
Comment créer un contrôleur de personnage 2D dans Unity : partie 1par@deniskondratev
544 lectures
544 lectures

Comment créer un contrôleur de personnage 2D dans Unity : partie 1

par Denis Kondratev15m2024/04/22
Read on Terminal Reader

Trop long; Pour lire

Cet article plonge dans le processus de création d'un contrôleur de personnage 2D dans Unity, en se concentrant sur l'intégration d'une nouvelle méthode « Slide » pour « Rigidbody2D » qui simplifie le mouvement des personnages. Il couvre la configuration, les comportements physiques, la gestion des collisions et les contraintes de mouvement, fournissant ainsi un guide de base pour le développement d'un contrôleur de personnage pouvant être adapté et étendu pour divers jeux de plateforme 2D.
featured image - Comment créer un contrôleur de personnage 2D dans Unity : partie 1
Denis Kondratev HackerNoon profile picture
0-item
1-item
2-item


L'un des éléments clés de tout jeu de plateforme 2D est le personnage principal. La façon dont il se déplace et est contrôlé façonne considérablement l'atmosphère du jeu, qu'il s'agisse d'un jeu old-school confortable ou d'un slasher dynamique. Par conséquent, la création d’un contrôleur de personnage est une première étape importante dans le développement d’un jeu de plateforme.


Dans cet article, nous examinerons en profondeur le processus de création d'un personnage à partir de zéro, en lui apprenant à se déplacer dans le niveau tout en respectant les lois de la physique. Même si vous avez déjà de l'expérience dans la création de contrôleurs de personnages, vous serez intéressé par les innovations de Unity 2023. À ma grande surprise, une méthode Slide tant attendue a été ajoutée pour le composant Rigidbody2D , ce qui simplifie grandement l'écriture d'un contrôleur de personnages en permettant une utilisation plus efficace de Rigidbody2D en mode cinématique. Auparavant, toutes ces fonctionnalités devaient être implémentées manuellement.


Si vous souhaitez non seulement lire l'article mais aussi l'essayer en pratique, je vous recommande de télécharger un modèle de niveau depuis le référentiel GitHub Treasure Hunters , où sont déjà inclus les atouts nécessaires et un niveau prêt pour tester votre personnage.


Poser les bases de notre caractère

Pour notre jeu de plateforme, nous avons fixé une règle selon laquelle le jeu n'aura que des surfaces verticales et horizontales, et la force de gravité sera dirigée strictement vers le bas. Cela simplifie considérablement la création du jeu de plateforme au stade initial, surtout si vous ne souhaitez pas vous plonger dans les mathématiques vectorielles.


À l'avenir, je pourrai peut-être m'écarter de ces règles dans mon projet Treasure Hunters, où j'explore la création de mécaniques pour un jeu de plateforme 2D sur Unity. Mais ce sera le sujet d'un autre article.


Sur la base de notre décision selon laquelle le mouvement s'effectuera le long de surfaces horizontales, la base de notre personnage sera rectangulaire. L’utilisation de surfaces inclinées nécessiterait de développer un collisionneur en forme de capsule et des mécanismes supplémentaires tels que le glissement.


Tout d’abord, créez un objet vide sur la scène et nommez-le Capitaine – ce sera notre personnage principal. Ajoutez les composants Rigidbody2D et BoxCollider2D à l'objet. Définissez le type Rigidbody2D sur Kinematic afin que nous puissions contrôler le mouvement du personnage tout en utilisant les capacités physiques intégrées de Unity. Verrouillez également la rotation du personnage le long de l’axe Z en activant l’option Geler la rotation Z.


Votre configuration devrait ressembler à l'illustration ci-dessous.


Ajoutons maintenant une apparence à notre capitaine. Recherchez la texture dans Assets/Textures/Treasure Hunters/Captain Clown Nose/Sprites/Captain Clown Nose/Captain Clown Nose with Sword/09-Idle Sword/Idle Sword 01.png et définissez sa valeur Pixel Per Unit sur 32. Nous le ferons souvent utilisez cette valeur dans ce cours car nos textures sont créées avec cette résolution. Réglez le mode Sprite sur Single et, pour plus de commodité, définissez le pivot sur Bottom. N'oubliez pas d'appliquer les modifications en cliquant sur le bouton Appliquer. Assurez-vous que tous les réglages sont effectués correctement.



Dans cet article, nous n'aborderons pas l'animation des personnages, donc pour l'instant, nous n'utiliserons qu'un seul sprite. Dans l'objet Captain, créez un objet imbriqué appelé Apparence et ajoutez-y un composant Sprite Renderer, en spécifiant le sprite précédemment configuré.



Lorsque j'ai zoomé sur l'image du capitaine, j'ai remarqué qu'elle était assez floue en raison de paramètres de sprite incorrects. Pour résoudre ce problème, sélectionnez la texture Idle Sword 01 dans la fenêtre Projet et définissez le mode de filtre sur Point (pas de filtre). Maintenant, l'image est bien meilleure.


Configuration du collisionneur de personnages

Pour le moment, la position du collisionneur et l'image de notre capitaine ne correspondent pas, et le collisionneur s'avère trop gros pour nos besoins.

Corrigeons cela et positionnons également correctement le pivot du personnage en bas. Nous appliquerons cette règle à tous les objets du jeu pour faciliter leur placement au même niveau quelle que soit leur taille.


Pour faciliter la gestion de ce processus, utilisez le jeu Basculer la position de la poignée de l'outil avec le pivot, comme indiqué ci-dessous.


L'étape suivante consiste à ajuster le collisionneur de notre héros pour que son point de pivotement soit exactement au centre en bas. La taille du collisionneur doit correspondre exactement aux dimensions du personnage. Ajustez les paramètres Offset et Size du collisionneur, ainsi que la position de l'objet Apparence imbriqué pour obtenir la précision nécessaire.


Portez une attention particulière au paramètre Offset.X du collisionneur : sa valeur doit être strictement 0. Cela garantira le placement symétrique du collisionneur par rapport au centre de l'objet, ce qui est extrêmement important pour les rotations ultérieures des personnages à gauche et à droite, où la valeur de Transform.Scale.X est remplacé par -1 et 1. Le collisionneur doit rester en place et la rotation visuelle doit paraître naturelle.


Introduction au comportement physique des personnages

Avant tout, il est essentiel d’apprendre aux personnages – qu’ils soient le héros principal, les PNJ ou les ennemis – à interagir avec le monde physique. Par exemple, les personnages doivent être capables de marcher sur une surface plane, d'en sauter et d'être soumis à la force de gravité, les ramenant au sol.


Unity dispose d'un moteur physique intégré qui contrôle le mouvement des corps, gère les collisions et ajoute l'effet des forces externes sur les objets. Tout cela est implémenté à l’aide du composant Rigidbody2D . Cependant, pour les personnages, il est utile de disposer d'un outil plus flexible qui interagit avec le monde physique tout en donnant aux développeurs plus de contrôle sur le comportement de l'objet.


Comme je l'ai mentionné plus tôt, dans les versions précédentes d'Unity, les développeurs devaient implémenter eux-mêmes toute cette logique. Cependant, dans Unity 2023, une nouvelle méthode Slide a été ajoutée au composant Rigidbody2D , permettant un contrôle flexible de l'objet physique tout en fournissant toutes les informations nécessaires sur le mouvement effectué.


Commençons par créer une classe CharacterBody , qui contiendra la logique de base pour déplacer les personnages dans le monde physique. Cette classe utilisera Rigidbody2D en mode Kinematic , que nous avons déjà ajouté à notre personnage. La première chose est donc d’ajouter une référence à ce composant.


 public class CharacterBody : MonoBehaviour { [SerializeField] private Rigidbody2D _rigidbody; }


Parfois, pour la dynamique du mouvement des personnages, il est nécessaire que la gravité agisse plus fortement que d'habitude. Pour y parvenir, nous ajouterons un facteur d’influence gravitationnelle de valeur initiale de 1, et ce facteur ne peut être inférieur à 0.


 [Min(0)] [field: SerializeField] public float GravityFactor { get; private set; } = 1f;


Nous devons également définir quels objets seront considérés comme des surfaces infranchissables. Pour ce faire, nous allons créer un champ nous permettant de préciser les couches nécessaires.


 [SerializeField] private LayerMask _solidLayers;


Nous limiterons la vitesse du personnage pour éviter qu'il ne développe des vitesses trop élevées, par exemple en raison d'une influence externe, notamment la gravité. Nous fixons la valeur initiale à 30 et limitons la possibilité de définir une valeur inférieure à 0 dans l'inspecteur.


 [Min(0)] [SerializeField] private float _maxSpeed = 30;


Lorsqu'on se déplace sur une surface, nous voulons que le personnage s'y accroche toujours si la distance qui les sépare est suffisamment petite.


 [Min(0)] [SerializeField] private float _surfaceAnchor = 0.01f;


Bien que nous ayons décidé que les surfaces de notre jeu seraient uniquement horizontales ou verticales, au cas où, nous préciserons l'angle d'inclinaison maximum de la surface sur laquelle le personnage peut se tenir de manière stable, avec une valeur initiale de 45º.


 [Range(0, 90)] [SerializeField] private float _maxSlop = 45f;


Grâce à l'inspecteur, je souhaite également voir la vitesse actuelle du personnage et son état, je vais donc ajouter deux champs avec l'attribut SerializeField .


 [SerializeField] private Vector2 _velocity; [field: SerializeField] public CharacterState State { get; private set; }


Oui, j'ai introduit ici une nouvelle entité encore non définie CharacterState . Nous en discuterons plus loin.


États de caractères

Pour simplifier le développement de notre jeu de plateforme, définissons seulement deux états de personnages principaux.

Le premier est Grounded , un état dans lequel le personnage se tient solidement à la surface. Dans cet état, le personnage peut se déplacer librement sur la surface et en sauter.


Le second est Airborne , un état de chute libre, dans lequel le personnage est dans les airs. Le comportement du personnage dans cet état peut varier en fonction des spécificités du jeu de plateforme. Dans un cas général proche de la réalité, le personnage se déplace sous l'influence de l'élan initial et ne peut influer sur son comportement. Cependant, dans les jeux de plateforme, la physique est souvent simplifiée au profit de la commodité et de la dynamique de jeu : par exemple, dans de nombreux jeux, même en chute libre, on peut contrôler le mouvement horizontal du personnage. Dans notre cas, cela est également possible, ainsi que la mécanique populaire du double saut, permettant un saut supplémentaire dans les airs.


Représentons les états de notre personnage dans le code :


 /// <summary> /// Describes the state of <see cref="CharacterBody"/>. /// </summary> public enum CharacterState { /// <summary> /// The character stays steady on the ground and can move freely along it. /// </summary> Grounded, /// <summary> /// The character is in a state of free fall. /// </summary> Airborne }


Il convient de noter qu’il peut y avoir beaucoup plus d’États. Par exemple, si le jeu comprend des surfaces en pente, le personnage peut être dans un état de glissement où il ne peut pas se déplacer librement vers la droite ou la gauche mais peut glisser sur la pente. Dans un tel état, le personnage peut également sauter en s'éloignant de la pente, mais uniquement dans le sens de la pente. Un autre cas possible est celui de glisser le long d'un mur vertical, où l'influence de la gravité est affaiblie et où le personnage peut pousser horizontalement.


Limitation de la vitesse de déplacement

Nous avons déjà défini un champ privé _velocity , mais nous devons pouvoir obtenir et définir cette valeur de l'extérieur tout en limitant la vitesse maximale du personnage. Cela nécessite de comparer le vecteur vitesse donné avec la vitesse maximale autorisée.


Cela peut être fait en calculant la longueur du vecteur vitesse ou, en termes mathématiques, son ampleur. La structure Vector2 contient déjà une propriété magnitude qui nous permet de faire cela. Ainsi, si l’amplitude du vecteur dépassé dépasse la vitesse maximale autorisée, nous devons maintenir la direction du vecteur mais limiter son amplitude. Pour cela, on multiplie _maxSpeed par le vecteur vitesse normalisé (un vecteur normalisé est un vecteur de même direction mais de magnitude 1).


Voici à quoi cela ressemble dans le code :


 public Vector2 Velocity { get => _velocity; set => _velocity = value.magnitude > _maxSpeed ? value.normalized * _maxSpeed : value; }


Examinons maintenant de près comment la magnitude d'un vecteur est calculée. Il est défini par la formule :



Le calcul de la racine carrée est une opération gourmande en ressources. Même si dans la plupart des cas la vitesse ne dépassera pas le maximum, nous devons quand même effectuer cette comparaison au moins une fois par cycle. Cependant, nous pouvons considérablement simplifier cette opération si nous comparons le carré de la norme du vecteur avec le carré de la vitesse maximale.


Pour cela, nous introduisons un champ supplémentaire pour stocker le carré de la vitesse maximale, et nous le calculons une fois dans la méthode Awake :


 private float _sqrMaxSpeed; private void Awake() { _sqrMaxSpeed = _maxSpeed * _maxSpeed; }


Le réglage de la vitesse peut désormais être effectué de manière plus optimale :


 public Vector2 Velocity { get => _velocity; set => _velocity = value.sqrMagnitude > _sqrMaxSpeed ? value.normalized * _maxSpeed : value; }


Ainsi, nous évitons les calculs inutiles et améliorons les performances de traitement de la vitesse de déplacement du personnage.


Méthode de mouvement du corps rigide

Comme je l'ai mentionné plus tôt, Unity a ajouté une nouvelle méthode Slide() , qui simplifiera grandement le développement de notre CharacterBody . Cependant, avant d'utiliser cette méthode, il est nécessaire de définir les règles selon lesquelles l'objet se déplacera dans l'espace. Ce comportement est défini par la structure Rigidbody2D.SlideMovement .


Introduisons un nouveau champ _slideMovement et définissons ses valeurs.


 private Rigidbody2D.SlideMovement _slideMovement; private void Awake() { _sqrMaxSpeed = _maxSpeed * _maxSpeed; _slideMovement = CreateSlideMovement(); } private Rigidbody2D.SlideMovement CreateSlideMovement() { return new Rigidbody2D.SlideMovement { maxIterations = 3, surfaceSlideAngle = 90, gravitySlipAngle = 90, surfaceUp = Vector2.up, surfaceAnchor = Vector2.down * _surfaceAnchor, gravity = Vector2.zero, layerMask = _solidLayers, useLayerMask = true, }; }



Il est important d'expliquer que maxIterations détermine combien de fois un objet peut changer de direction à la suite d'une collision. Par exemple, si le personnage est dans les airs à côté d'un mur et que le joueur essaie de le déplacer vers la droite alors que la gravité agit sur lui. Ainsi, pour chaque appel à la méthode Slide() , un vecteur vitesse dirigé vers la droite et le bas sera défini. En heurtant le mur, le vecteur de mouvement est recalculé et l'objet continuera à se déplacer vers le bas.


Dans une telle situation, si la valeur maxIterations était définie sur 1, l'objet heurterait le mur, s'arrêterait et y resterait effectivement coincé.


Les valeurs de maxIterations et layerMask ont été définies précédemment. Pour des informations plus détaillées sur les autres champs, consultez la documentation officielle de la structure .


Enfin, déplacer le personnage

Maintenant, tout est prêt pour faire bouger le capitaine. Nous ferons cela dans FixedUpdate , un rappel dans Unity conçu pour gérer la physique. Au cours des dernières années, l’équipe Unity a considérablement amélioré la gestion de la physique 2D. Actuellement, le traitement peut être effectué dans le rappel Update ou même en appelant vous-même la méthode requise.


Cependant, dans cet exemple, nous utiliserons la méthode traditionnelle et éprouvée FixedUpdate . Avant de continuer, il convient de mentionner quelques mots sur la valeur de Time.fixedDeltaTime .


Pour garantir la prévisibilité de la physique du jeu, la simulation est effectuée par itérations à intervalles de temps fixes. Cela garantit que les changements de FPS ou les décalages n'affectent pas le comportement de l'objet.


Au début de chaque cycle, nous prendrons en compte l'effet de la gravité sur l'objet. Puisque la gravité est donnée par le vecteur d'accélération de la chute libre, nous pouvons calculer la variation de la vitesse Δv de l'objet au fil du temps Δt par la formule :




a est l'accélération constante de l'objet. Dans notre cas, il s'agit de l'accélération due à la gravité, compte tenu du coefficient que nous avons introduit — Physics2D.gravity * GravityFactor . Par conséquent, Δv peut être calculé comme suit :


 Time.fixedDeltaTime * GravityFactor * Physics2D.gravity


Le résultat final, où nous modifions la vitesse, ressemble à ceci :


 Velocity += Time.fixedDeltaTime * GravityFactor * Physics2D.gravity;


Nous pouvons maintenant effectuer le mouvement du corps rigide du personnage :


 var slideResults = _rigidbody.Slide( _velocity, Time.fixedDeltaTime, _slideMovement);


La variable slideResults est une valeur de la structure SlideResults et stocke les résultats du mouvement. Les principaux champs de ce résultat pour nous sont slideHit , le résultat d'une collision avec la surface pendant le mouvement, et surfaceHit - le résultat d'une projection vers le bas, qui aidera à déterminer si le personnage se tient sur une surface stable.


Gérer les collisions

Lors d'une collision avec des surfaces, il est crucial de limiter la vitesse du personnage vers cette surface. Un exemple simple est que si le personnage est immobile au sol, il ne doit pas continuer à prendre de la vitesse sous l'influence de la gravité. A la fin de chaque cycle, leur vitesse doit être nulle. De même, lorsqu'il se déplace vers le haut et touche le plafond, le personnage doit perdre toute vitesse verticale et commencer à descendre.


Les résultats des collisions, slideHit et surfaceHit , sont représentés par les valeurs de la structure RaycastHit2D , qui inclut la normale de la surface de collision.


La limitation de vitesse peut être calculée en soustrayant la projection du vecteur vitesse d'origine sur la normale de collision du vecteur vitesse lui-même. Cela se fait en utilisant le produit scalaire . Écrivons une méthode qui effectuera cette opération :


 private static Vector2 ClipVector(Vector2 vector, Vector2 hitNormal) { return vector - Vector2.Dot(vector, hitNormal) * hitNormal; }


Intégrons maintenant cette méthode dans notre FixedUpdate . Ici, pour surfaceHit , nous limiterons la vitesse uniquement si elle est dirigée vers le bas, car le lancer déterminant si l'objet est à la surface est toujours effectué pour vérifier le contact avec le sol.


 private void FixedUpdate() { Velocity += Time.fixedDeltaTime * GravityFactor * Physics2D.gravity; var slideResults = _rigidbody.Slide( _velocity, Time.fixedDeltaTime, _slideMovement); if (slideResults.slideHit) { _velocity = ClipVector(_velocity, slideResults.slideHit.normal); } if (_velocity.y <= 0 && slideResults.surfaceHit) { var surfaceHit = slideResults.surfaceHit; _velocity = ClipVector(_velocity, surfaceHit.normal); } }


Cette implémentation permet de gérer correctement le mouvement du personnage, d'éviter les accélérations indésirables lors d'une collision avec diverses surfaces et de garder le mouvement des personnages dans le jeu prévisible et fluide.


Déterminer l'état du personnage

A la fin de chaque cycle, il faut déterminer si le personnage est sur une surface solide (état Grounded) ou en chute libre (ou, comme nous l'avons défini, en chute contrôlée – état Airborne).


Pour considérer le personnage comme étant à l'état Grounded, principalement, sa vitesse verticale doit être nulle ou négative, que nous déterminons par la valeur de _velocity.y .


Un autre critère important est la présence d'une surface sous les pieds du personnage, que l'on identifie à partir des résultats de mouvement de Rigidbody, notamment grâce à la présence de surfaceHit .


Le troisième facteur est l'angle d'inclinaison de la surface, que nous analysons en fonction de la normale de cette surface, c'est-à-dire la valeur de surfaceHit.normal . Il est nécessaire de comparer cet angle avec _maxSlop — l'angle maximum possible de la surface sur laquelle le personnage peut se tenir de manière stable.


Pour une surface complètement verticale, la normale sera strictement horizontale, c'est à dire que sa valeur vectorielle sera (1, 0) ou (-1, 0). Pour une surface horizontale, la valeur de la normale sera (0, 1). Plus l'angle d'inclinaison est petit, plus la valeur de y est grande. Pour l'angle alpha , cette valeur peut être calculée comme suit :



Puisque notre angle est donné en degrés, et que la fonction $\cos$ nécessite des radians, la formule se transforme en :



Pour cela, introduisons un nouveau champ et calculons-le dans la méthode Awake .


 private float _minGroundVertical; private void Awake() { _minGroundVertical = Mathf.Cos(_maxSlop * Mathf.PI / 180f); //... }


Mettons maintenant à jour notre code dans FixedUpdate , en vérifiant toutes les conditions ci-dessus.


 if (_velocity.y <= 0 && slideResults.surfaceHit) { var surfaceHit = slideResults.surfaceHit; Velocity = ClipVector(_velocity, surfaceHit.normal); State = surfaceHit.normal.y >= _minGroundVertical ? CharacterState.Grounded : CharacterState.Airborne; } else { State = CharacterState.Airborne; }


Cette logique nous permettra de déterminer avec précision quand le personnage est au sol et de répondre correctement aux changements de son état.


Ajout de CharacterBody au capitaine

Maintenant que notre composant CharacterBody est prêt, la dernière étape consiste à l'ajouter à notre Captain. Sur la scène, sélectionnez l'objet Captain et ajoutez-y le composant CharacterBody .

N'oubliez pas de configurer le Rigidbody comme indiqué dans l'illustration ci-dessus. Définissez le facteur de gravité sur 3 et sélectionnez l'option par défaut pour le calque solide.


Vous pouvez maintenant démarrer le jeu et expérimenter en définissant différentes valeurs pour la vitesse afin d'observer comment notre personnage se déplace dans la scène.

Conclusion pour l'instant

Bien sûr, nous devons encore ajouter des contrôles de caractères. Cependant, cet article est déjà devenu assez long, je détaillerai donc le contrôle du personnage à l'aide du nouveau système de saisie dans le prochain article : « Création d'un contrôleur de personnage 2D dans Unity : Partie 2 ».


Vous pouvez télécharger le projet complet décrit dans cet article ici : Treasure Hunters et tout vérifier en pratique si vous rencontrez des difficultés. Le développement d'un contrôleur de personnage est un aspect clé de la création d'un jeu de plateforme 2D, car il détermine le développement ultérieur du jeu. Cela affecte la facilité avec laquelle de nouvelles fonctionnalités seront ajoutées au comportement du héros principal ou des ennemis. Il est donc très important d’en comprendre les bases pour pouvoir développer son propre jeu de manière indépendante.