top of page

L'autre

Jeu solo - Unreal Engine C++

L'autre est un jeu qui a été réalisé dans le cadre du cours 8GIF225 – Atelier de production de jeux vidéo I. Pour faire ce projet, nous étions une grande équipe de 4 programmeurs ainsi qu'une douzaine d'artistes du NAD-UQAC.

 

Ce jeu est un jeu sérieux de style platformer en 2.5D et qui a pour thème principal la bipolarité. Le jeu contient deux personnages, soit Juliette qui est contrôlée par le joueur et son copain Roméo qui est contrôlé par une intelligence artificielle. De plus, Roméo est une personne bipolaire qui doit traverser plusieurs épreuves. Juliette doit aider son copain Roméo à traverser les divers puzzles qui seront sur leurs chemins et ce, lors de différentes saisons. Chaque saison affectera Roméo différemment dans sa bipolarité comme la majorité des personnes bipolaires. Durant l'automne, Roméo est en état de "manie" et prendra les devants sans trop réfléchir et ce sera à Juliette de l'accompagner et de l'aider à poursuivre leur route ensemble. Puis, durant l'hiver, Roméo sera en état dépressif. Il sera lent et peu motivé. Il devra suivre Juliette et celle-ci devra encore une fois l'aider à traverser les divers obstacles sur leur route.    

​

En tant que programmeur, je me suis principalement occupé de la programmation de l'intelligence artificielle (AI) de Roméo dans les différentes saisons ainsi que des différentes actions réalisées par celui-ci comme: sauter, se pencher, attendre, marcher, etc. 

Le AI de Roméo

Initialisation du AI

Après avoir fait quelques prototypes avant de commencer le projet, notre équipe de programmeurs avons convenu d'utiliser un "behavior tree" ainsi que d'un "blackboard" pour faire le AI de Roméo en C++. Je me suis donc occupé de la configuration et de l'initialisation de ces éléments. Notamment en ajoutant les modules nécessaires dans le fichier build et en ajoutant les keys pour le blackboard. J'ai ensuite créé la "AI Controller class" de l'intelligence artificielle de Roméo ainsi que son "character class". J'ai également fait des fonctions qui consistent à initialiser le behavior tree et le blackboard au début du jeu.

Défis rencontrés

Le principal défi que j'ai rencontré pour l'initialisation est que je n'avais jamais travaillé avec les behavior tree ou les blackboard auparavant. J'ai donc dû faire quelques recherches en ligne pour savoir comment configurer tout cela en c++. Ce processus a été relativement difficile car la majorité des informations sur le sujet sont pour les systèmes en blueprint et non en c++.

Le AI de Roméo

Le système de PatrolPath et de Waypoints

Durant la saison de l'automne, Roméo doit se déplacer de façon énergique et semi-autonome. En effet, durant la période de manie de Roméo, Juliette doit pouvoir le suivre durant tout le niveau. C'est pourquoi j'ai développé un système de waypoints. Roméo aura un "patrolPath" qui lui sera assigné. Ce path, sera constitué de plusieurs points que j'ai nommé "waypoints". Les waypoints sont placés manuellement dans la map et constitueront le trajet de Roméo. De plus, lorsque le AI va atteindre le dernier waypoint, il va retourner au premier waypoint du trajet et refaire le parcours ou bien attendre au dernier si on le souhaite. Ce sera à Juliette de lui tenir la main pour l'aider à progresser et pour l'aider à poursuivre sa route en empruntant un path différent, car le niveau de l'automne aura plusieurs petits paths.

Premièrement, j'ai commencé par créer une classe pour le patrol path et j'ai fait une référence à celle-ci dans le AI character. Le patrol path a plusieurs fonctionnalités pour permettre de l'adapter aux différents puzzles directement dans l'éditeur. En effet, en plus de pouvoir ajouter autant de waypoint que l'on souhaite dans le tableau, il est aussi possible de décider si l'on souhaite faire "loop" ou non le AI à travers le path. Ceci est réalisé à l'aide d'une "checkbox" qui représente une valeur booléenne. Le patrol path a aussi une "sphere collision" qui se déplace à chaque emplacement de waypoint et elle se déplace au prochain path du AI quand le AI entre en collision avec elle. Le but étant que lorsque Roméo traversera l'une de ces "sphere collision" en tenant la main de Juliette, l'index du prochain waypoint à se rendre va s'incrémenter. Cela permet ainsi d'actualiser les waypoints lorsque Roméo exécute une autre tâche dans le behavior tree, soit de tenir la main. Sans une telle fonctionnalité, Roméo tenterait de revenir sur ses pas pour se rendre à la position du waypoint qu'il était rendu avant de tenir la main de Juliette. La taille de la "sphere collision" peut aussi être modifiée selon le path, pour l'adapter à l'environnement et optimiser les chances que le "event soit trigger" lorsque les deux personnages vont passer près d'un waypoint en se tenant la main. Finalement, il est aussi possible d'observer l'index courant du AI depuis l'éditeur pour pouvoir observer son parcours et bien comprendre sa prochaine destination en testant les paths lors de la production. Aussi, il est important de noter que j'ai décidé d'incrémenter l'index de path de 2 et non de 1 lors de la collision avec la prise de mains. J'ai fait cela afin de rendre le path plus permissif si le joueur n'entre pas en collision avec toutes les sphères. 

Ensuite, je devais faire 2 classes qui allaient se trouver dans mon behavior tree. Soit, une classe pour repérer le waypoint du path ainsi qu'une classe pour contrôler l'incrémentation de l'index de mon path à chaque fois que le AI atteint un waypoint pour ensuite lui indiquer le prochain. En effet, l'index correspond à un waypoint situé dans un tableau et c'est à ce waypoint que le AI doit se rendre.  Aussi, entre ces deux classes, j'utilise la fonction "move to" de unreal engine. 

behavior tree patrol path .png

Behavior tree pour le système de waypoints

Dans le code de ma classe servant à trouver la position dans le monde du prochain waypoint dans l'index, je me suis fait deux blackboard keys très utiles, soit la "patrol_path_index" qui est un "int" qui correspond à l'index courant dans mon behavior tree ainsi que la key "patrol_path_vector" qui est un "vector" qui correspond à la position en (x,y,z) du waypoint courant dans le monde. C'est la valeur de cette key qui est ensuite envoyée à la fonction "move to" pour que le AI se rende à cette position en utilisant le pathfinding de unreal. 

C'est finalement après avoir trouvé le waypoint et avoir effectué le "move to" du AI avec succès à la position du vecteur que j'incrémente mon path index pour le prochain waypoint. Lorsque le dernier waypoint est atteint, je regarde la valeur booléenne du path qui indique si le path "loop" ou pas. Si la valeur est fausse alors le AI va attendre à ce dernier index, car la direction sera désormais égale à "Wait" et cela fera en sorte que l'index ne sera plus incrémenté. Si elle est vraie, alors la direction reste égale à "forward" et l'index va continuer à être incrémenté.

Défis rencontrés

Les défis rencontré pour ce systèmes tournaient autour de la mécanique avec le tenage de main. En effet, je devais adapter ma mécanique en fonction d'une autre mécanique (le tenage de main) qui se trouvais dans le même behavior tree que ma mécanique de waypoint. Tel que mentionné plus tôt, pour résoudre ce problème, j'ai ajouté une "sphere collision" et je la déplace d'un waypoint à un autre. Le défi était ensuite de gérer si la collision du AI avec la sphère étais pendant sa navigation "standard" de patrol path, ou bien pendant un tenage de main avec Juliette. Pour ensuite effectuer le comportement voulu en conséquence. Le dernier défi était de bien gérer l'incrémentation de mon index pour ne pas créer de cas de "array out of bound" et que le path soit incrémenté correctement lors de la collision avec le tenage de main. Finalement, c'est ce système qui s'est avéré le plus difficile dans ce projet et qui m'a causé le plus de bug et d'ajustement. Toutefois, c'est également le système dont je suis le plus fier et que je trouve le plus polyvalent dans plusieurs situations différentes. Je suis donc très fier de ce que j'ai réussi à accomplir avec ce système. 

Le AI de Roméo

Le système de Wait gates

J'ai créé un système de "wait gates" qui permet de contrôler le temps d'atttente du AI lorsque celui-ci arrive à un endroit précis. J'ai créé ce système pour plusieurs raisons, telles que : pour faciliter le contrôle du comportement de Roméo dans les divers puzzles et pour donner l'occasion à Juliette de rattraper Roméo si celui-ci prend trop d'avance (ou si elle prend trop de retard). Les "wait gates" sont également gérés par le behavior tree et le blackboard du AI. 

Dans le header de ma classe "WaitAI", j'ai ajouté une "collision box" qui lance un "event" lorsqu'un personnage va entrer en collision avec elle. J'ai également mis un "float" qui permet de spécifier le temps d'attente du AI dans le "blueprint instance" de la "wait gate" dans l'éditeur. 

Wait AI capture set time.png

Dans le code source, j'initialise ma "collision box" ainsi que son event "onBeginOverlap". Si le AI entre en collision avec la "gate", alors j'accède au "controller" du AI et j'appelle la fonction "OnBeginTime" avec le temps souhaité en paramètre. Si Juliette entre dans la "collision box" et que le "controller" n'est pas égale à "NULL", cela signifie que Roméo est passé dans la "gate" avant elle.  Ensuite j'appelle la fonction "OnEndWaiting" avec la valeur "0" en paramètre pour arrêter de faire attendre Roméo. Si Juliette est la première à pénétrer dans la "collision box", alors rien ne se produit. 

Dans mon code, j'utilise un "FTimerHandle" dans la classe du "NPC_AIController" (la classe du "controller" de Roméo) pour gérer le temps d'attente du AI. En effet, je démarre et arrête le chronomètre dans mes fonctions selon le temps reçu en paramètre et je "set" le temps dans la blackboard key "is_waiting". Quand cette valeur change cela démarre l'événement dans le behavior tree. La séquence des waypoints se poursuit ensuite quand le temps de cette blackboard key est égale de nouveau à zéro. 

Défis rencontrés

Les défis avec ce système étaient de le rendre le plus fluide possible avec le comportement du joueur. En effet, selon la façon de jouer du joueur, le temps d'attente n'était jamais parfaitement adapté. Parce que si le joueur prenait son temps dans une section et que le AI attendait pendant un nombre X de secondes, alors le AI partait à la fin de son chronomètre et le joueur plus lent perdait Roméo de vue. Toutefois, si le joueur était plus rapide et qu'il arrivait à la "wait gate" cela retardait le joueur, car il devait attendre que Roméo termine "sa petite pause". Pour régler cela, j'ai fait en sorte que peu importe le temps d'attente de la "wait gate", celle-ci allait être détruite quand le joueur allait entrer en collision avec elle. De cette façon, le AI peut attendre très longtemps s'il le faut et aussitôt que le joueur arrive près de Roméo, celui-ci arrête d'attendre. Également, si le joueur arrive avant Roméo dans la "wait gate" celle-ci est détruite avant même que Roméo arrive, ce qui fera en sorte qu'il n'attendra pas à cette même section et poursuivera sa route sans encombre et sans avoir à faire patienter le joueur inutilement.

Le AI de Roméo

Le système de crouching du AI

J'ai également réalisé le code permettant à Roméo de se pencher ("crouch"). Pour faire cela, lorsque la fonction "StartCrouching" est appelée, je divise de moitié la taille de la "capsule collision" de Roméo et je réduis sa vitesse de moitié. J'appelle ensuite la fonction du même nom dans la classe de son parent, soit la classe "ActiveCharacter". Je fais ensuite le processus inverse pour le faire arrêter de se pencher.  

preuve crouching romeo.png
Défis rencontrés

Le seul défi notable de ce système a été de régler le bug ou le nav mesh ne se rendait pas en dessous des endroits plus bas sous lequel le AI devait se pencher. Puis, après quelques recherches sur des forums en ligne, j'ai découvert qu'il suffisait de décocher la checkbox "can ever affect navigation" qui est normalement cochée par défaut dans unreal engine. Évidemment, sans décocher cette case, le AI n'aurait pas pu passer en dessous car un AI ne peut marcher que sur le nav mesh.

Le AI de Roméo

Le système de changement de path du AI

Le niveau de l'automne est constitué de plusieurs petits "patrol path" avec des particularités qui leur sont propres. Toutefois, il fallait une mécanique pour changer le path de Roméo dynamiquement durant la partie lorsque Juliette l'amène à un certain endroit en lui tenant la main. C'est pourquoi j'ai créé la classe "ChangePatrolPathAI". Le principe de base est sensiblement le même que la classe de la "Wait gate" vu précédemment. Toutefois, au lieu de spécifier un temps, on spécifie un path à l'instance de cette classe dans l'éditeur tel que montré dans la capture ci-dessous. Le path indiqué remplacera le path courant du AI lorsque celui-ci entrera dans la "collision box" peu importe s'il tient la main ou non de Juliette. 

preuve changement path.png

Dans le code, après la collision, je "set" le nouveau path au NPC (à Roméo). Puis, à l'aide du "controller" du AI, je "set" la blackboard key de l'index courant du waypoint à 0. Cela fait en sorte que Roméo commencera par le premier index (premier waypoint) du nouveau path peu importe l'index dans lequel il était rendu dans le path précédent. Cela assure que le nouveau path est effectué dans l'ordre voulu.  

Défis rencontrés

Ce système a été le plus simple à réaliser durant ce projet. Le seul défi était de s'assurer que le AI se rende au premier index du nouveau path qui lui est assigné à chaque fois peu importe où il était rendu dans le path précédent. Même si au final ce système a été le plus simple, il demeure l'un des plus importants pour le jeu et l'un des plus utiles.  

Le AI de Roméo

Le AI de l'hiver de Roméo

Durant la saison de l'hiver, Roméo est en état dépressif. Il a donc un système de AI complètement différent. En effet, au lieu de prendre les devants et de suivre des waypoints dans des paths prédéfinis, il est cette fois toujours en arrière de Juliette et il l'a suit tout le long du niveau. Il n'a donc aucun path d'assigné et ne peut pas sauter, ni se pencher. Il marche également beaucoup plus lentement qu'en automne et il attend sur place s'il perd Juliette de vue. C'est donc à elle d'aller le chercher si celui-ci s'est perdu. La seule ressemblance avec le AI de Roméo durant l'automne est qu'il peut toujours tenir la main de Juliette. Il a également une accélération de marche lorsque Juliette lui demande de lui tenir la main et que Roméo se trouve plus loin. Il est important de noter que lorsque les deux sont côte à côte durant la prise de mains, les deux personnages marchent à la même vitesse. 

​

Pour faire cela, je me sers du même behavior tree que les autres saisons, mais j'ai ajouté une condition qui vérifie si le AI à un patrol path au début du niveau. S'il n'en a pas, alors cela signifie qu'il est dans la saison d'hiver et c'est là que je lance les fonctions correspondantes à ce nouveau comportement. J'ai aussi ajouté une "sight percerption system" au AI controller de Roméo, pour qu'il détecte Juliette dans un cercle de 360 degrés autour de lui. Tant que Juliette se trouvera dans ce cercle de détection, Roméo se déplacera vers la position de Juliette. J'ai également ajouté une condition dans la classe correspondant à la prise de mains du AI qui vérifie si la saison actuelle est l'hiver ou non. Si la saison est l'hiver, je donne alors une vitesse d'accélération différente qu'en automne pour la prise de mains. De cette façon, j'ai également paramétré tous les états de changement de vitesse de Roméo (avant, pendant et après) pendant la prise de mains en hiver.

Défis rencontrés

Les défis pour ce nouveau comportement du AI étaient de bien l'incorporer dans le système pour qu'il puisse bien fonctionner dans le même behavior tree sans impacter le reste ainsi que de faire les modifications de vitesses nécessaires avec la même prise de mains que les autres saisons. Ce système a demandé beaucoup de calibrations pour s'assurer que tout cela fonctionnait bien. J'avais pensé au départ de devoir faire complètement un autre behavior tree ainsi qu'un autre controller pour cela. Toutefois, j'ai réussi à mettre les deux systèmes ensemble de façon efficace et cela m'a fait sauver énormément de temps de cette façon. J'ai également été content de pouvoir faire un style de AI plus conventionnel, soit un AI qui suit le déplacement d'un autre de façon continue. Finalement, je trouve que ce système rend mon AI encore plus polyvalent et je suis très satisfait du résultat final. 

Les défis de conceptions du AI de Roméo

Durant la saison de l'automne, le AI m'a apporté plusieurs défis. Premièrement, je n'avais jamais utilisé de behavior tree ou de blackboard. Ensuite, faire un AI qui se déplace "seul" sans suivre un personnage ou un objet amène beaucoup de complexités. Aussi, en plus de le faire déplacer seul, je devais faire en sorte que celui-ci puisse également avoir différents comportements et parcourir une série d'obstacles tout en conservant tout le système fonctionnel et dynamique. J'ai aussi dû adapter mes systèmes aux différents changements qui peuvent survenir dans le behavior tree ainsi qu'actualiser certains éléments du AI lors du respawn dans le niveau (le respawn a été créé par l'un de mes coéquipiers). J'ai aussi créé une classe pour faire sauter le AI à une destination spécifique, mais j'ai choisi de ne pas l'intégrer à mon portfolio car j'ai dû m'inspirer d'un blog en ligne pour parvenir à avoir un saut fonctionnel avec mon AI. 

​

J'ai également trouvé lors de la réalisation de mes différents systèmes, qu'il y avait très peu de documentation en c++ portant sur les sujets que j'ai travaillés. Cela m'a permis de développer mes compétences et à apprendre beaucoup de nouvelles choses par moi même. Aussi, l'AI a subit beaucoup de modifications de comportement au fil de la production, ce qui m'a pris beaucoup de temps afin de souvent modifier le AI pour les nouveaux besoins du projet, ou à imaginer des alternatives pour régler les nouveaux problèmes qui survenaient en cours de route. 

Durant la saison d'hiver, j'ai réussi à produire le nouveau comportement du AI en un temps record en le jumelant adéquatement à mon système de AI de l'automne. De cette façon j'ai pu utiliser les éléments déjà existant du AI et ignorer ceux que je n'avais pas besoin durant la saison de l'hiver. Cela m'a évité de repartir de zéro et m'a donc permis de pouvoir mettre plus de temps sur la correction des bugs de mes mécaniques. Cela m'a également fait prendre conscience de la puissance des behavior tree et comment une bonne utilisation de ceux-ci peut nous aider à régler de nombreux problèmes qui peuvent paraître complexes aux premiers abords.

bottom of page