Écrit par David Henry, le 24 octobre 2002
Attention : cet article a été écrit pour le SDK 2.2 d'Half-Life 1 ! Il se peut qu'il ne soit pas entièrement compatible avec des versions antérieures ou supérieures du Kit.
Ce tutorial va nous permettre de découvrir une classe dérivée de
CBaseMonster
permettant de créer de nouveaux modèles de NPC
avec une IA plus spécifique, notamment au niveau du dialogue (les scientifiques
et les barneys discutent entre eux ou avec le joueur) et de leur interaction
avec le joueur (demander à un NPC de vous suivre). Cette classe c'est
CTalkMonster
.
Ici nous allons voir comment personnaliser l'IA d'un NPC pour qu'il suive le joueur lorsqu'on utilise la touche « utiliser », et qu'il s'arrête lorsqu'on rappuie sur cette touche ou que le joueur l'attaque. Ce tutorial propose une solution simple et peu évoluée, le monstre ne vous parlera pas, et n'aura pas de rancune lorsque vous l'agresserez, ce qui pourrait constituer un très bon exercice pour vous familiariser avec l'IA d'Half-life.
Je ne vais pas refaire le code de tout un monstre, vous êtes libre de
choisir votre support, un monstre à vous, un monstre déjà existant ou un
nouveau monstre. Ici, je ferai comme si la classe de votre monstre était
CMyMonster
(si vous faites des copier/collez, pensez à changer
à chaque fois).
La classe CTalkMonster
possède des fonctions qui vont nous
premettre de paramétrer la fonction « utiliser » de notre NPC. Au lieu
de faire hériter la classe de notre monstre de CBaseMonster
, on
va donc dériver à partir de CTalkMonster
. Commencez par ajouter
le fichier d'en-tête nécessaire pour CTalkMonster
:
#include "talkmonster.h"
Assurez vous aussi que vous avez les includes de defaultai.h
et schedule.h. Maintenant, nous allons modifier la classe de base.
Allez au début de votre définition de classe, et changez la classe de base
CBaseMonster
en CTalkMonster
:
///////////////////////////////////////////////////////////////////////////// // // CMyMonster - classe pour mon monstre qui suit le joueur. // ///////////////////////////////////////////////////////////////////////////// class CMyMonster : public CTalkMonster { // ...
Bien. Maintenant que notre monstre a hérité de toutes les fonctions de
CTalkMonster
publiquement, nous devons nous assurer d'avoir les
trois fonctions TakeDamage()
, Killed()
et
ObjectCaps()
. Prenez votre définition de classe et ajoutez les
déclarations de fonction manquantes :
virtual int ObjectCaps () { return CTalkMonster::ObjectCaps () | FCAP_IMPULSE_USE; } int TakeDamage (entvars_t* pevInflictor, entvars_t* pevAttacker, float flDamage, int bitsDamageType); void Killed (entvars_t *pevAttacker, int iGib);
TakeDamage()
va nous servir pour que le monstre arrête de
suivre le joueur lorsque ce dernier l'agressera, Killed()
pour
s'assurer que le joueur ne contrôle plus le monstre s'il est mort, et
ObjectCaps()
, qui déclaré et définit ici à la fois, sert à prévenir
le jeu que l'on peut utiliser la touche « utiliser » avec le monstre.
Tant qu'on est dans la définition de classe, ajoutez ces trois déclarations si elles n'y sont pas déjà :
// IA Personnalisée Schedule_t *GetScheduleOfType (); Schedule_t *GetSchedule (); CUSTOM_SCHEDULES; };
Nous n'aurons pas besoin de créer de nouveaux Taks, donc StartTask()
et RunTask()
nous seront inutiles (cependant si elles existent déjà
dans votre monstre, ne les retirez pas !)
Il va falloir que l'ont définisse maintenant les fonctions
TakeDamage()
et Killed()
, et d'ajouter une instruction
à la fonction Spawn()
. On va commencer tout de suite avec
Spawn()
:
// ... MonsterInit(); SetUse (FollowerUse); }
SetUse()
sert à assigner la fonction FolowerUse()
à l'action « utiliser le monstre » (voir talkmonster.cpp pour
sa définition). Ajoutez maintenant à la suite de votre code :
// -------------------------------------------------------------------------- // CMyMonster::TakeDamage // -------------------------------------------------------------------------- int CMyMonster::TakeDamage (entvars_t* pevInflictor, entvars_t* pevAttacker, float flDamage, int bitsDamageType) { if (pevAttacker->flags & FL_CLIENT) { // l'agresseur est le joueur if (IsFollowing ()) StopFollowing (TRUE); } return CTalkMonster::TakeDamage (pevInflictor, pevAttacker, flDamage, bitsDamageType); } // -------------------------------------------------------------------------- // CMyMonster::Killed // -------------------------------------------------------------------------- void CMyMonster::Killed (entvars_t *pevAttacker, int iGib) { SetUse (NULL); CTalkMonster::Killed (pevAttacker, iGib); }
La fonction TakeDamage()
appelle la fonction
StopFollowing()
qui comme son nom l'indique, ordonnera au monstre
d'arrêter de suivre le joueur. Cependant je n'ai mis là qu'un petit système,
le joueur pourra reprendre le contrôle du monstre juste après l'avoir attaqué
sans qu'il ne rechigne. Si vous voulez qu'il se souvienne à vie de votre traitement,
vous devez l'écrire dans sa variable qui lui sert de mémoire (regardez la fonction
TakeDamage()
de CScientist
pour voir comment c'est codé,
ou celle de CBarney
mais plus compliquée). La fonction
Killed()
, quant à elle, retire le contrôle du monstre lorsqu'il est
mort.
Nous arrivons maintenant à la partie où l'on va personnaliser l'IA. Nous allons
créer avec les Taks de l'IA par défaut et de l'IA spécialisée de
CTalkMonster
, deux objets Task_t
et Schedule_t
.
Rendez-vous à la partie de votre code qui traite l'IA customisée. S'il y'en a pas,
ajoutez le code suivant n'importe où dans le fichier :
// -------------------------------------------------------------------------- // AI Schedules Specific to this monster // -------------------------------------------------------------------------- Task_t tlFollow2[] = { { TASK_MOVE_TO_TARGET_RANGE, (float)128 }, { TASK_SET_SCHEDULE, (float)SCHED_TARGET_FACE }, }; Schedule_t slFollow2[] = { { tlFollow2, ARRAYSIZE (tlFollow2), bits_COND_NEW_ENEMY | // conditions qui forceront bits_COND_LIGHT_DAMAGE | // le monstre à arrêter de bits_COND_HEAVY_DAMAGE | // suivre le joueur. bits_COND_HEAR_SOUND | bits_COND_PROVOKED, bits_SOUND_DANGER, "Follow" }, }; Task_t tlFaceTarget2[] = { { TASK_SET_ACTIVITY, (float)ACT_IDLE }, { TASK_FACE_TARGET, (float)0 }, { TASK_SET_ACTIVITY, (float)ACT_IDLE }, { TASK_SET_SCHEDULE, (float)SCHED_TARGET_CHASE }, }; Schedule_t slFaceTarget2[] = { { tlFaceTarget2, ARRAYSIZE( tlFaceTarget2 ), bits_COND_CLIENT_PUSH | bits_COND_NEW_ENEMY | bits_COND_LIGHT_DAMAGE | bits_COND_HEAVY_DAMAGE | bits_COND_HEAR_SOUND | bits_COND_PROVOKED, bits_SOUND_DANGER, // nécessite l'include de soundent.h !!! "FaceTarget" }, }; DEFINE_CUSTOM_SCHEDULES (CMyMonster) { // ... slFollow2, slFaceTarget2, }; IMPLEMENT_CUSTOM_SCHEDULES (CMyMonster, CTalkMonster);
Ici j'ai rajouté un « 2 » au nom des Task_t
et
Schedule_t
car ils existent déjà et risquent d'entrer en conflit.
Notez ici aussi la classe CTalkMonster
pour spécifier la classe
de base à IMPLEMENT_CUSTOM_SCHEDULES()
.
À présent, allez voir la définition de la fonction GetSchedule()
.
Si votre monstre n'en possède pas encore, il est temps de la créer :
// -------------------------------------------------------------------------- // CMyMonster::GetSchedule // -------------------------------------------------------------------------- Schedule_t* CMyMonster::GetSchedule () { switch (m_MonsterState) { case MONSTERSTATE_ALERT: case MONSTERSTATE_IDLE: { if (m_hEnemy == NULL && IsFollowing ()) { if (!m_hTargetEnt->IsAlive ()) { // le joueur est mort, on arrête de le suivre StopFollowing (FALSE); break; } else { // gène le joueur if (HasConditions (bits_COND_CLIENT_PUSH)) { // se pousse et vous suit toujours. return GetScheduleOfType (SCHED_MOVE_AWAY_FOLLOW); } return GetScheduleOfType (SCHED_TARGET_FACE); } } // pousse toi if (HasConditions (bits_COND_CLIENT_PUSH)) { // ok, je me pousse return GetScheduleOfType (SCHED_MOVE_AWAY); } break; } // ... } return CTalkMonster::GetSchedule (); }
Si le joueur est mort, le monstre s'arrête de suivre le joueur avec la
fonction StopFollowing()
que l'on a déjà vu. Si le joueur cogne
dans le monstre (car il veut passer et le monstre lui barre le chemin), ce
dernier se poussera en suivant toujours le joueur s'il le suivait déjà. N'oubliez
pas de retourner GetSchedule()
de CTalkMonster
à la fin de la fonction pour les schedules qui devront être traités par défaut.
Il nous reste plus que la fonction GetScheduleOfType()
:
// -------------------------------------------------------------------------- // CMyMonster::GetScheduleOfType // -------------------------------------------------------------------------- Schedule_t* CMyMonster::GetScheduleOfType (int Type) { switch (Type) { // ... case SCHED_TARGET_FACE: return slFaceTarget2; case SCHED_TARGET_CHASE: return slFollow2; // ... } return CTalkMonster::GetScheduleOfType (Type); }
Je voudrais ajouter encore une chose importante, si vous votre classe est dérivée
de CTalkMonster
: au lieu d'utiliser les constantes
LAST_COMMON_SCHEDULE
et LAST_COMMON_TASK
, utilisez les
macros spécifiques à la classe de base CTalkMonster
:
LAST_TALKMONSTER_SCHEDULE
et LAST_TALKMONSTER_TASK
respectivement.
Cet article est mis à disposition sous un contrat Creative Commons (licence CC-BY-ND).