Intelligence Artificielle

Dans un jeu vidéo, l’intelligence artificielle joue une grande part, qu’elle soit très évoluée ou non. Retrouvez tous les articles vous permettant de créer vos propres systèmes intelligents.

Comment créer l’intelligence artificielle d’un garde qui poursuit le joueur

Jouer à un jeu qui gère des personnages non joueur dynamiquement, c’est sympa. Cela donne vie à vos univers, apporte du mouvement et de l’imprévu. Dans ce tutoriel, nous allons voir comment mettre en place une IA simpliste qui va poursuivre le joueur s’il le détecte.

Comment fonctionne une IA ?

Le terme Intelligence Artificielle est un peu exagéré (à moins de travailler sur des réseaux neuronaux). En effet, lorsque l’on parle d’IA c’est principalement pour donner une illusion d’intelligence.

Une IA fait ce qu’on lui a dit de faire. Ce n’est rien de plus qu’un programme pseudo-intelligent. Cette intelligence est programmée pour répondre à des stimuli externes et à réagir en conséquence. Par exemple, un garde va se mettre en chasse dès qu’il voit le joueur.

Simple non ?

Ce « programme », c’est ce qu’on appelle un arbre décisionnel. A chaque instant, notre IA va prendre en compte tous les stimuli qu’on lui aura fourni et « regardera » son arbre de décision pour exécuter une action (ou pas). Par exemple : si je vois le joueur (stimulus), alors je me mets en chasse (action 1). Sinon, je reste à mon poste (action 2).

Ce fonctionnement est assez simple sur papier, mais plus vous allez construire des IA « intelligentes » pouvant faire beaucoup d’actions, plus l’arbre de décision va mécaniquement se complexifier. Il est donc important de comprendre le fonctionnement théorique.

Si vous avez bien compris, vous aurez remarqué qu’avec une structure de ce genre, une IA va toujours répondre de la même manière à une ensemble de stimuli. Or, une IA prévisible qui fait toujours la même chose, c’est un peu chiant… C’est là que vous pouvez induire des erreurs volontaires. Lors d’un choix, vous allez donc calculer la réponse appropriée aux stimuli… et appliquer une petite part d’aléatoire qui va venir mettre son grain de sel. Nous n’aborderons pas ce point ici, mais gardez le en tête pour plus tard.

La Finite State Machine

Pour mettre en place cet arbre de décisions, nous allons utiliser une Final State Machine (FSM), ou une machine à états. Une FSM est composée de plusieurs états. La machine ne peut être que dans un seul état à la fois. Elle peut en revanche sauter d’un état à un autre selon des transitions que vous aurez défini.

Dans le cadre de la création de notre intelligence artificielle, cette FSM va nous permettre de matérialiser son arbre de décision. Pour chaque état, l’IA va avoir un arbre de décision différent. Voilà 2 exemples d’états répondant au même stimulus (joueur détecté) :

  • L’IA du garde est éveillée, elle voit le joueur et commence à le poursuivre (changement vers l’état « poursuite »)
  • L’IA du garde est endormie, elle « voit » le joueur mais ne réagit pas.

Mecanim à la rescousse

Nous devons donc mettre en place une FSM. Bonne nouvelle, pas besoin de le faire, c’est déjà disponible dans Unity sous le nom de Mecanim. Mais Mecanim c’est pas juste prévu pour faire de l’animation ?

Normalement… oui. Mais Mecanim est une FSM par définition. Il est certes prévu pour gérer des animations, mais vous pouvez utiliser tout ce système pour autre chose, comme gérer une IA.

La suite de ce tutoriel part du principe que vous savez utiliser les bases de Mecanim. Si ce n’est pas le cas, vous devriez jeter un oeil à ce tutoriel.

Attaquons donc à créer une IA de garde avec quelques états assez simples :

Nous avons maintenant nos états. Il nous faut ajouter les stimuli. Ici, nous en aurons besoin de 2 :

  • Un booléen : Est-ce que le joueur est en vue
  • Un trigger : Est-ce que le garde est de retour à son poste

Nous pouvons maintenant définir les transitions entre les états. Le terme « transition » en animation implique une gestion automatique des transformations de votre modèle 3D. Dans notre cas, c’est simplement un moyen de dire quel état peut aller vers quels autres états.

Attention, il y a un point important à modifier ici. Lorsque vous ajoutez une transition entre deux états sur l’Animator, cette transition va effectuer un mix des deux états pendant un laps de temps. Ce fonctionnement par défaut permet d’avoir des transitions souples entre les différentes animations. Mais dans notre cas, ça ne nous intéresse pas. Pire, on ne veut surtout pas que deux états s’exécutent en même temps !

Modifiez chacune de vos transitions pour supprimer cette petite période d’entrelacement des animations (voir image ci-dessous). Sur chacune des transitions, on va également ajouter les conditions de transition.

  • Si le garde est au repos et qu’il voit le joueur, il se met en chasse
  • Si le garde est en chasse et qu’il voit le joueur, il le poursuit
  • Si le garde est en chasse et qu’il ne voit plus le joueur, il retourne à son point de garde et se mets au repos.

Créer le gestionnaire d’IA

Avant de créer nos comportements, il faut créer un orchestrateur. C’est lui qui va donner les informations utiles à l’Animator, qui à son tour se chargera de faire les transition entre les différents états. Chaque état exécutera alors son comportement.

Les comportements que nous allons créer plus bas sont totalement indépendants et n’ont pas la vision sur ce qui se passe en dehors d’eux même. Vous aurez besoin de définir des éléments persistants qui seront utilisés par ces comportements. Comme par exemple la position de garde.

Ce gestionnaire va donc stocker toutes les informations utiles à ses comportements. Par la suite, les comportements vont tout simplement aller chercher les infos dont ils ont besoin et s’en servir pour effectuer leurs actions.

Créez donc un nouveau script qui sera notre orchestrateur. Voilà de quoi nous aurons besoin :

public class Guard : MonoBehaviour
{
    private Vector3 _guardPosition;
    private Animator _animator;
    private int _playerOnSightHash;
    private int _reachedGuardPointHash;
    
    public Vector3 GuardPosition { get { return _guardPosition; } }
    public int ReachedGuardPointHash { get { return _reachedGuardPointHash; } }

    private void Start()
    {
        _guardPosition = transform.position;
        _animator = GetComponent(typeof(Animator)) as Animator;

        _playerOnSightHash = Animator.StringToHash("PlayerOnSight");
        _reachedGuardPointHash = Animator.StringToHash("ReachedGuardPoint");
    }

    private void OnTriggerEnter(Collider collider)
    {
        if(collider.CompareTag("Player"))
        {
            _animator.SetBool(_playerOnSightHash, true);
        }
    }

    private void OnTriggerExit(Collider collider)
    {
        if (collider.CompareTag("Player"))
        {
            _animator.SetBool(_playerOnSightHash, false);
        }
    }
}

Dans la méthode Start on met en cache les éléments utiles. On a également la prise en charge du traitement des triggers avec les deux dernières méthodes. Dès que le joueur entrera dans ce trigger, c’est qu’il est entré dans le rayon de détection de l’IA, on met donc à jour le booléen correspondant sur l’Animator. Inversement lorsque le joueur sort du trigger. Cela implique évidemment que vous ajoutiez un collider sur votre IA (un SphereCollider par exemple) et le marquiez bien comme étant un trigger.

Enfin, notez l’exposition de la position de départ du garde et l’exposition du hash calculé du paramètre « ReachedGuardPosition » via des propriétés. Ces éléments seront utilisés par nos comportements.

Attacher un comportement à un état

Nous avons maintenant notre FSM qui définit comment notre IA va réagir. Il est temps de créer les comportements associés à chacun de nos états. Pour pouvoir se déplacer, notre IA va utiliser un NavMesh et toutes ls mécaniques associées. Si vous ne voyez pas de quoi il est question, rendez-vous sur cet article.

Nous avons donc plusieurs comportements à créer :

  • Chasse : Le garde poursuit le joueur
  • Retour au poste : Le garde a perdu le joueur de vue et retourne à son poste de garde

Pour pouvoir attacher un comportement à une état, il faut créer un script qui va étendre de la classe StateMachineBehaviour. Le plus simple est de le créer via le bouton « Add Behaviour », depuis l’inspecteur d’un état sur l’Animator :

Comportement de chasse

Commençons par le comportement de chasse. L’IA devra prendre en chasse le joueur et le suivre. On va se servir du NavMesh pour ça en lui donnant comme destination… le joueur !

On va donc utiliser 2 des méthodes qui nous sont proposées : OnStateEnter et OnStateUpdate. Dans la première, on va cacher des infos histoire de ne pas tuer les perfs. On ne va rechercher nos références que lorsqu’on rentre dans cet état et qu’on ne les a pas déjà.

Dans la seconde méthode, on va donner une destination. Ici, on ne veut pas recalculer le chemin à chaque frame car cela peut être consommateur. On va placer un petit intervalle en plus pour ne recalculer le chemin que toutes les 0.3s. Ce qui devrait être suffisant pour avoir une précision correcte.

using UnityEngine;
using UnityEngine.AI;

public class ChasePlayer : StateMachineBehaviour
{
    private const float PathUpdateInterval = 0.3f;

    private GameObject _player;
    private NavMeshAgent _agent;

    private float _lastUpdateTime = 0f;
    
    override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        if(_player == null)
        {
            _player = GameObject.FindGameObjectWithTag("Player");
            _agent = animator.gameObject.GetComponent(typeof(NavMeshAgent)) as NavMeshAgent;
        }
    }
    
    override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        if(Time.time > _lastUpdateTime + PathUpdateInterval)
        {
            _lastUpdateTime = Time.time;
            _agent.SetDestination(_player.transform.position);
        }
    }
}

Ce script implique que votre garde a un NavMeshAgent pour pouvoir se déplacer sur un NavMesh et pourchasser le joueur.

Comportement de retour

Il nous reste à créer le comportement qui va ramener le garde à son poste. Créez un nouveau StateMachineBehaviour comme vu plus haut.

Cette fois ci, on va changer la destination du garde pour le ramener à son point de départ. Dans la mise à jour, on vérifie juste si l’IA a presque atteint sa destination. Si c’est le cas, on considère qu’elle est arrivée à bon port et on enclenche le trigger correspondant afin de tenir informé l’Animator.

using UnityEngine;
using UnityEngine.AI;

public class GoBackToGuardPoint : StateMachineBehaviour {

    private NavMeshAgent _agent;
    private Guard _manager;
    
    override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        if(_agent == null)
        {
            _agent = animator.gameObject.GetComponent(typeof(NavMeshAgent)) as NavMeshAgent;
            _manager = animator.gameObject.GetComponent(typeof(Guard)) as Guard;
        }
        _agent.SetDestination(_manager.GuardPosition);
    }

    public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        if (_agent.remainingDistance < 0.2f)
        {
            animator.SetTrigger(_manager.ReachedGuardPointHash);
        }
    }
}

L’animator reçoit ce trigger, le consomme, et ramène notre IA dans son état initial : la veille.

Aller plus loin

La boucle est bouclée ! Votre garde va courir après le joueur s’il le voit… et retournera à son poste s’il le perd de vue. Nous avons mis en place un comportement très basique. La détection se fait via une Sphere alors qu’un cône de vision serait plus approprié. Nous avons aussi utilisé cette même sphère pour la poursuite du joueur alors qu’un Raycast serait plus efficace. Le garde se déplace toujours à la même vitesse…

Ce n’est donc pas parfait. Mais vous avez pu voir la mécanique à utiliser.

Il ne tient qu’à vous de créer votre propre IA maintenant ! Des gardes, des PNJs qui naviguent en ville. Un compagnon qui suit le joueur peut-être ? A vous de jouer ! Pour créer une IA rapidement, l’utilisation de Mecanim est la manière la plus rapide, simple et efficace. En revanche, si vous envisagez de créer des comportements beaucoup plus complexes, vous devriez vous diriger vers des solutions spécialisées disponibles sur l’Asset Store.

Conclusion

Dans ce tutoriel, vous avez appris à créer une IA en utilisant Mecanim. Voilà ce qu’il faut retenir :

  • Une IA utilise un arbre de décision influencé par des stimuli externes pour prendre ses décisions
  • Une FSM permet de facilement mettre en place une IA
  • Mecanim peut être utilisé pour autre chose que des animations (eh ouais !)
  • Des comportements peuvent être attachés à des états de l’Animator pour exécuter du code custom

Partagez ce tutoriels à vos amis sur les réseaux sociaux et montez-leur comment créer une IA pour leurs propres projets !

Comment utiliser le NavMesh pour donner à vos PNJ la possibilité de se déplacer dans des environnements complexes

Nous en avons déjà parlé ici, rendre son univers vivant est important pour l’immersion du joueur. Cette immersion passe entre autres par une intelligence artificielle capable d’effectuer des actions naturellement, comme par exemple se déplacer dans des environnements complexes. Unity propose un système de NavMesh pour répondre à cette problématique.

Continuer la lecture