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.

Introduction

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

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 !)

Les fonctions n'appartenant pas à l'IA

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.

Personnalisons l'IA

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.

Creative Commons Logo Contrat Creative Commons

Cet article est mis à disposition sous un contrat Creative Commons (licence CC-BY-ND).