L’intelligence artificielle (IA) en elle-même dans les jeux est quelque chose de plutot simple. Cela se reduit generallement a un abre a decision ou chaque branche est une decision differente. Ce qui est dur c’est surtout ce qui gravite autour de l’IA comme l’animation et le pathfinding (pour savoir comment se deplacer du point A au point B en evitant les obstacles). Mais on va oublier ce qui entour l’ia pour cette fois.
Concernant l’IA dans les jeu donc, je me rappelle d’une discussion que j’ai eu un jour avec Peter (Molyneux) ou il ma dit quelque chose du genre : ” Tu sais Jeremy, j’ai programme une bonne dizaine d’AI dans ma carriere et le secret pour une bonne IA est pourtant simple, c’est de gerer les desires “. A savoir, desire d’attaquer, desire d’aller manger, de se deplacer etc.
A l’epoque j’avais alors crée une AI pour la dame de pique (un jeu de carte downloadable sur mon site web) sur ce model histoire de voir que ca donnerait et j’avais trouve la chose bien sympa.
L’idee est en effet fort simple, chaque creature (ou jeur AI) a une serie de variables pour representer ses envies (generallement un float de 0 - pas envie du tout, a 1 - trop envie). Pour le jeu de carte, c’etait les envie de jouer du treffle ou la dame de pique.
Puis, a chaque fois que l’IA doit prendre une decision pour quoi faire a present, on regarde la valeur des envies differentes et on actionne celle qui est la plus haute.
Simple mais efficace.
C’est ce qui etait utilise pour gerer l’ia des villageois dans “Black and White” et en ce moment je suis en train de reimplementer ca pour Battle For Independence. D’ou ce post.
Le principe apres n’est guere complique mais je me suis toujours etonne de pas voir plus d’ia decrites sur ce model dans les articles sur comment programmer une intelligence artificielle. Generallement les discussion sur la programmation de l’IA abordent l’implementation soit avec un simple switch et deux trois ‘if’ en dur dans le code, soit ca pars dans des delires de neural network.
Certes, “Black and White” utilisait aussi un neural network mais seulement pour la creature. Il va de soit qu’il n’y a aucun interet a apprendre a des villageois comment aller couper du bois… aucun avantage question gameplay si le joueur n’en voit pas l’interet (j’y reviendrais avec mon article sur le game design bientot).
Bref, dans ce post, je vais vous exposer l’implementation choisie pour l’intelligence artificielle dans Battle For Independence. Elle est similaire a celle des villageois de “Black and White” qui suffit amplement (meme aujourd’hui) pour la plupart des jeux. L’implementation est en C++ et assume que vous sachiez ce qu’est la programmation objet.
Chaque entite (objet dans le jeu) qui est capable de decision va etre designe avec ce systeme.
L’idee est la suivante: on a pour chaque entite plusieurs etats possibles (un simple enum suffit amplement) et on a 3 fonctions par etat pour:
1. Lorsqu’on entre dans cet etat
2. Le temps qu’on reste dans cet etat
3. Lorsqu’on sort de cet etat
Il est generallement plus pratique de regroupper ces 3 fonctions dans une structure et c’est ce qu’on va faire:
struct AITABLE
{
const char *mState;
AIFUNCTION mEnterState;
AIFUNCTION mProcessState;
AIFUNCTION mExitState;
};
Comme vous pouvez le voir, j’en ai egalement profite pour ajoutter un string qui permettra de debugger notre ia plus facielement. Il permet de stocker le nom de l’etat pour lequel les fonctions sont appellees.
Pour ceux qui se demande ce qu’est AIFUNCTION comme type, il est definit comme suit et a l’avantage de specifier des fonctions de ma classe AI.
typedef bool (AI::*AIFUNCTION)();
A ce stage, il ne nous reste plus qu’a remplir la table qui contient toutes les informations.
Pour cela je conseille tout d’abord de mettre la definition de la structure AITABLE ainsi que le typedef dans la class AI.
Deux raisons a cela:
1. Aucun interet pour les autres classes de connaitre leur existance
2. Cela nous permet de la remplir avec des fonctions qui sont declaree en protected ou private dans la class AI
Dans notre fichiers cpp on remplit la table comme suit:
// ————————————–
AI::AITABLE AI::AITable[AI::eMAX] =
{
“IDLE”, &AI::OnEnterIdle, &AI::OnProcessIdle, &AI::OnExitIdle,
“STAY”, NULL, &AI::OnProcessStay, &AI::OnExitStay,
“MOVE”, &AI::OnEnterMove, &AI::OnProcessMove, &AI::OnExitMove,
“ATTACK”,&AI::OnEnterAttack, &AI::OnProcessAttack, NULL,
};
Biensur au fur et a mesure que le jeu progresse et qu’on a besoin d’autres etats, on remplit la table.
Pour AI::eMAX c’est notre enum qui est definit comme suit dans la class AI
enum eACTIONSTATE
{
eINVALID = -1,
eIDLE,
eSTAY,
eMOVE,
eATTACK,
eMAX
};
Voila les bases de mon ia classique.
L’avantage d’avoir une fonction d’entree permet de mettre a jour certaines variables dont on aura besoin pour l’etat dans leque on rentre. Pour attaquer par exemple il peut etre utile dans la fonction d’assigner la cible a un pointer.
Notez qu’il n’est pas necessaire d’avoir une fonction pour toutes les combinaisons possible et NULL est accetable si on a pas besoin de cette fonction (comme pour on enter stay dans notre table)
Le fait d’avoir une fonction d’exit vous donne l’opportunite de remettre aux propre certaines choses (comme remettre a NULL le pointer de celui que l’on attaquait).
Dans ma classe AI, j’ai une fonction SetState qui me permet de changer d’etat et c’est la ou la magie se passe.
Comme vous vous en doutez, cette fonction appelle la fonction exit de l’etat dans lequel l’objet etait, puis enter de l’etat dans lequel on rentre.
La fonction qui update l’ia a pour but de decider si l’entite peut rester ou pas dans cet etat et est appelle a chaque fois que l’ia est mise a jour. Par exemple si notre ennemi est mort, alors on a aucune raison de rester en mode attaque.
On appelle les fonctions de notre table comme ca:
if (AITable[mState].mProcessState != NULL)
{
(this->*AITable[mState].mProcessState)();
}
mState etant l’enum qui precise dans quel etat est mon entite.
Voila pour les bases. Si on pousse le concept un peu plus loin, on peut faire plusieurs choses.
Tout d’abord les fonctions peuvent retourner un bool. Si vous retournez ‘false’, cela veut dire que vous n’acceptez pas que l’entite change d’etat. Ca peut etre par exemple que l’entite est en train de voler dans les airs et veuille se mettre a croupis en meme temps.
L’autre idee un peu plus complexe que Black and White avait est qu’on a une pile de decision. Par exemple, pour un villageois il avait: se deplacer jusqu’a l’arbre, puis couper du bois, puis se deplacer jusqu’au centre villle deposer son bois. Si l’entite est pas derangee elle effectue ses actions dans l’ordre.
Et c’est ainsi que se conclue cet article sur l’IA. Ce system est adaptable a la plupart des jeux et a comme avantage d’etre simple a mettre en place en plus d’etre agreable a utiliser.
Je suis mort de rire, genre on a quelque chose de vachement ressemblant… bon j’appelle mon avocat on va te faire un procès.
Je crois que ceux de Peter frappent a ta porte…
Hehe, j’ai bossé sur un moteur de machines à état pour des interfaces graphiques dans des décodeurs tv numérique qui reprend exactement la même architecture.
Le modèle que j’ai utilisé apporte en plus une mécanique de messages traités en fonction des états. Est ce que ce genre de chose ne pourrait pas être utile dans un moteur d’IA (genre exécution d’ordres reçus par une entité supérieure) ?
Très bon article Jérémy, c’est sympa de voir que c’est pas si compliqué de faire de l’IA
Genre, chui pas programmeur, et j’ai (presque) tout compris !
Merci tonton Kamron !
Le fait de rajouter des exemple de ce que tu expliques est vraiment parlant, continue comme ça en tout cas.
Merci pour nous, et pour Startrip 2
Petit edit: J’ai renomme “Startrip2″ en “Battle For Independence”.
Dans les JV, y a dix milles manières de coder son IA… J’ai déjà vu des jeux professionnels fais sans aucune machine à états (je citerai pas de noms). Le pb ça donne du codes dégulasses, difficiles à débuger et pas du tout flexible.
Pour la programmation de machine à état, il y a plusieurs manière de la programmer. Certains utilisent des macros (pour cacher des switchs et des if /else) avec pour chaque états des macros du genre : OnInit, OnExit, OnUpdate, OnChange… (bien entendu chaque état peut possèdé un timeout et passer à l’état suivant). D’autres font des machines à états comme ce que tu as décrit. Par contre, une machine à état est quelque chose qui est souvent ammené à être changé. L’idéal serait qu’elle fasse partie des données de ton jeu et non qu’elle soit à l’interieur de ton code. Pour cela, tu peux faire appel à un langage script et implementer un bouton reset qui te permettra de changer ta machine à état ingame…ça te fera gagner du temps pour le dév de ton jeu.
Autant le dire tout de suite, utiliser un language script pour l’IA est loin d’etre la solution miracle.
Debugger est clairement pas la joie, selon le language script il est aussi difficile de faire ce que l’on veut avec, mais le plus dur est d’avoir une ligne clair entre code et script.
Si on fait un language script pour les designers il est minimaliste et au final un designer n’est pas un programmer et ca loupera pas, un programmeur devra s’y coller. Si c’est un programmeur qui s’y colle un language script minimaliste va le frustrer au plus haut point.
Bref je suis pas fan de language script.
L’usage le plus ridicule que j’ai vue d’un langage script c’etait avec Catwoman. Les pauvres programmeurs utilisaient un language script fait maison pour faire le gameplay.
Du coup le language etait inadapte, impossible a debugger, et le plus drole de tous il ne se relancait pas pendant le jeu. Du coup y’avait aucun avantage a etre un language script.
c’est pire que ce que j’avais imaginé à propos de catwoman et d’EA
ça a l’air d’être un pb récurrent dans les JV… y a toujours autant de mecs qui réfléchissent pas (avec un ou plusieurs responsables technique pour superviser le tout… en particulier dans les grosses boites)
Concernant les machines à états et leurs langages, regardes plutôt cette page :
http://www.lri.fr/~blanch/publications/ihm2002/ihm2002-hsm.html
(c’est destiné aux IHMs mais ça peut aussi être adapter à l’IA)
@Jeremy & msx:
Je viens de tomber sur cet article par hasard (excellent d’ailleurs, bien expliqué et très intéressant).
Je suis pas un expert mais quel est l’avantage de cette approche avec une table de pointeurs sur fonctions, comparée à une machine d’état qui serait implémentée façon OO: une classe abstraite “State” et la dériver pour chaque état spécifique? Les états pourraient eux-mêmes décider des transitions et ainsi éviter les transitions interdites (s’accroupir alors que l’entité vole). Les méthodes OnEnter(), OnExit(), OnProcess() etc seraient aussi appelées correctement car définies dans la classe de base.
Problème de performances? de facilité à modifier si nécessaire?