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

Créer un monstre est sans doute une des choses les plus compliquées à coder. Un monstre (ou NPC pour Non Player Character, puisqu'il peut s'agir d'humains ou machines) doit se comporter comme s'il existait vraiment. C'est ce qu'on appelle l'IA : l'Intelligence Artificielle. Dans ce tutorial nous n'allons pas tout de suite aborder l'IA mais nous allons d'abord voir comment créer une nouvelle entité pour un nouveau monstre. Pour cela, nous allons utiliser un exemple en recréant le Pit Drone de Opposing Force.

Les classes de base

Comme toute entité, on doit créer une classe qui devra hériter de l'une de ces trois classes :

La classe CSquadMonster a été conçue pour créer des NPC vivants en groupes, tels que les marines, les alien grunt ou les houndeyes, et possède des fonctions de comportement de groupe par défaut. La classe CTalkMonster elle, est utilisée par les scientifiques et les agents de sécurité (barney) comportant des fonctions permettant aux NPC de discuter entre eux ou avec le joueur, ainsi que d'être utilisé (avec la touche « use ») pour suivre le joueur. Dans notre exemple nous commencerons simplement avec CBaseMonster qui donnera à notre monstre un comportement de base très simple.

Commençons par créer un nouveau fichier source (.cpp) du nom de votre monstre en l'ajoutant au projet de votre mp dll. Là première chose à faire sont les includes :

#include "extdll.h"
#include "util.h"
#include "cbase.h"
#include "monsters.h"

Rien à dire dessus, assez basique. Seul monsters.h est nouveau. Et ensuite, la déclaration et définition de classe de l'entité :

/////////////////////////////////////////////////////////////////////////////
//
// CPitDrone - classe pour le monstre "Pit Drone".
//
/////////////////////////////////////////////////////////////////////////////

class CPitDrone : public CBaseMonster
{
public:
  // fonctions
  void Spawn ();
  void Precache ();
  void SetYawSpeed ();
  int Classify ();

  // sons émis par le monstre
  void DeathSound ();
  void PainSound ();
  void IdleSound ();
  void AlertSound ();

public:
  // variables membres

  // tableaux de sons
  static const char *pDeathSounds[];
  static const char *pPainSounds[];
  static const char *pIdleSounds[];
  static const char *pAlertSounds[];
};

// lien entité-classe
LINK_ENTITY_TO_CLASS (monster_pitdrone, CPitDrone);

Voyons un peu les éléments de cette classe.

Si vous connaissez déjà un peu le SDK, les deux premières fonctions ne vous seront pas infamilières. Spawn() est appelée au moment de l'apparition de l'entité sur la map, et Precache() est appelée par Spawn() pour précacher toutes les ressources nécessaires. La fonction suivante, SetYawSpeed(), sert à gérer la vitesse de rotation du NPC sur lui-même. Enfin, Classify() est une première fonction ayant un rapport avec L'IA puisqu'elle retourne le type de NPC (nous verrons plus loin) qui va servir lors des rencontres avec d'autres NPC pour savoir s'ils sont ennemis, alliés ou neutres.

Vient ensuite un petit jeu de quatres fonctions : DeathSound(), PainSound(), IdleSound() et AlertSound(). Ces fonctions sont censées faire émettre du NPC différents sons selon son état (mourant, attaqué, au repos, ...). Souvent, il existe plusieurs sons pour chacun des types pour éviter une bande sonore trop répétée. En général, on préfère ranger ces listes de sons dans des tableaux. Ce sont les 4 variables membres qui suivent : quatre tableaux de chaînes de caractères qui vont contenir les noms de fichiers sons de chaque groupes. Profitons-en pour les définir :

// --------------------------------------------------------------------------
// déclaration et initialisation des variables
// membres statiques.
// --------------------------------------------------------------------------

const char *CPitDrone::pDeathSounds[] =
  {
    "pitdrone/pit_drone_die1.wav",
    "pitdrone/pit_drone_die2.wav",
    "pitdrone/pit_drone_die3.wav",
  };

const char *CPitDrone::pPainSounds[] =
  {
    "pitdrone/pit_drone_pain1.wav",
    "pitdrone/pit_drone_pain2.wav",
    "pitdrone/pit_drone_pain3.wav",
    "pitdrone/pit_drone_pain4.wav",
  };

const char *CPitDrone::pIdleSounds[] =
  {
    "pitdrone/pit_drone_idle1.wav",
    "pitdrone/pit_drone_idle2.wav",
    "pitdrone/pit_drone_idle3.wav",
  };

const char *CPitDrone::pAlertSounds[] =
  {
    "pitdrone/pit_drone_alert1.wav",
    "pitdrone/pit_drone_alert2.wav",
    "pitdrone/pit_drone_alert3.wav",
  };

Maintenant que l'on a fait un tour d'horizon sur la chose, voyons plus en détails chaque fonction...

La fonction Spawn()

C'est la fonction qui fait tout démarrer. Voici sa définition :

// --------------------------------------------------------------------------
// CPitDrone::Spawn
//
// Apparition du monstre dans le niveau.
// --------------------------------------------------------------------------

void
CPitDrone::Spawn ()
{
  // précachage des ressources
  Precache ();

  // initialisation du modèle
  SET_MODEL (ENT (pev), "models/pit_drone.mdl");
  UTIL_SetSize (pev, Vector (-32, -32, 0), Vector (32, 32, 64));

  // infos entité
  pev->solid = SOLID_SLIDEBOX;
  pev->movetype = MOVETYPE_STEP;
  pev->health = 40;
  pev->view_ofs = Vector (28, 28, 0);

  // cône de champs de vision
  m_flFieldOfView = 0.5;

  m_MonsterState = MONSTERSTATE_NONE;
  m_bloodColor = BLOOD_COLOR_GREEN;

  // sélection du submodel à déssiner
  SetBodygroup (SPIKE_GROUP, SPIKE_FULL);

  // initialisation du monstre
  MonsterInit ();
}

Cette fonction commence à appeler Precache() (que nous allons voir juste après) pour précacher les ressources (modèles, sons, sprites, ...) qui seront utilisées. Juste après, on utilise la macro SET_MODEL() pour indiquer au moteur de jeu quel modèle on va utiliser pour représenter le monstre (pour le modèle d'exemple, prenez-le dans le fichier pak0.pak de Opposing Force, si vous ne l'avez pas, prenez un autre, n'importe lequel pour tester). La fonction appelée ensuite sert à définir un bloc qui équivaut à tout le volume occupé par le monstre (en gros). Les deux derniers paramètres pris sont les coordonnées (x,y,z) du coin bas et les coordonnées du coin opposé en haut (on peut ainsi construire la diagonale du bloc) par rapport à l'origine du monstre. Ensuite sont initialisées les variables pev.

pev->solid définie le type du bloc créé par UTIL_SetSize(). Les différentes macros possibles sont définies dans const.h (faisant parti des fichiers ressource). Celles qui sont le plus utilisées sont SOLID_SLIDEBOX pour la gestion des collisions et SOLID_NOT pour rendre l'entité traversable une fois morte. pev->movetype est le type de déplacement de l'entité. Les macros sont aussi définies dans const.h, juste au dessus de celles de pev->solid. Il y en a quand même un certains nombre (à vous de les essayer), mais les plus utilisées pour les NPC sont MOVETYPE_STEP (à pied/pattes) et MOVETYPE_FLY (air ou liquide). pev->health contient les points de vie du monstre. Ici j'ai mis une variable constante (40) mais vous pouvez (je vous le conseille même) utiliser une skill cvar (voir autre tutorial). Et enfin pev->view_ofs est la position des yeux du monstre (certains n'initialisent pas cette valeur).

m_flFieldOfView définie le cône de visibilité (le FOV) du monstre (ce n'est pas en degrés). m_MonsterState est le statut du monstre (on verra ça une autre fois), pour le moment, on l'initialise à MONSTERSTATE_NONE. m_bloodColor est la couleur dont il faudra teindre les décals pour le sang. Il existe quatre macros possibles :

Voilà pour les initialisations. Vous pouvez initialiser encore d'autres variables, provenant de la classe de base ou des nouvelles créées par vous-même dans la classe du monstre.

On fait ensuite appel à la fonction SetBodygroup(). Cette fonction va charger le sous-modèle (submodel) passé en arguments. Le premier paramètre est le groupe du modèle (qui peut être « body », « gun » ou « head »), le second l'index du submodel du groupe. Cette fonction est bien utile pour varier la tête de vos NPC par exemple, ou les armes de votre soldat, etc... Pour connaître l'ordre des groupes du modèle et les submodels, regardez dans le fichier .qc du modèle (demandez-le à votre modeleur ou décompilez le modèle pour l'obtenir). Pour pit_drone.mdl, ça ressemble à ça :

$bodygroup studio
{
studio "pit_drone_reference"
}

$bodygroup gun
{
blank
studio "pit_drone_horns01"
studio "pit_drone_horns02"
studio "pit_drone_horns03"
studio "pit_drone_horns04"
studio "pit_drone_horns05"
studio "pit_drone_horns06"
}

On a deux groupes (les $bodygroup). Dans le premier (nommé studio), on a qu'un submodel donc on ne pourra pas choisir d'autres modèles. Pour le second (gun), on en a septs (le blank en est un aussi, il indique que le submodel est vide). Les macros utilisées pour le Pit Drone sont donc celles-ci :

// groupes submodels
#define BODY_GROUP  0
#define SPIKE_GROUP 1

// submodels
#define SPIKE_NONE  0
#define SPIKE_FULL  1
#define SPIKE_5     2
#define SPIKE_4     3
#define SPIKE_3     4
#define SPIKE_2     5
#define SPIKE_1     6

BODY_GROUP ne nous servira pas (vu qu'il n'y a qu'un submodel) mais je le mets pour que ce soit plus logique dans l'indexation des groupes. Si vous n'avez qu'un groupe de modèle, vous pouvez utiliser la variable pev->body que vous initialiserez avec l'index du submodel à dessiner. Dans le même genre, il existe pev->skin qui vous permet de choisir une texture parmis un groupe. Il faut que le groupe ait été préalablement défini dans le .qc du modèle ($texturegroup). Avant de terminer la fonction, Spawn() fait un appel à MonsterInit(). Cette dernière va initialiser d'autres variables et lancer la machine en route ;-)

La fonction Precache()

C'est une fonction très classique que vous devez sûrement connaître. Elle sert à précacher (charger et mettre en cache) les ressources nécessaires au niveau du moteur de jeu. On utilise pour les modèles et sprites la macro PRECACHE_MODEL(), pour les fichiers sons individuels PRECACHE_SOUND() et pour les tableaux de sons PRECACHE_SOUND_ARRAY() :

// --------------------------------------------------------------------------
// CPitDrone::Precache
//
// Précachage de toutes les ressources necessaires pour ce monstre.
// --------------------------------------------------------------------------

void
CPitDrone::Precache ()
{
  // sons individuels
  PRECACHE_SOUND ("pitdrone/pit_drone_attack_spike1.wav");
  PRECACHE_SOUND ("pitdrone/pit_drone_attack_spike2.wav");

  PRECACHE_SOUND ("pitdrone/pit_drone_melee_attack1.wav");
  PRECACHE_SOUND ("pitdrone/pit_drone_melee_attack2.wav");

  // tableaux de sons
  PRECACHE_SOUND_ARRAY (pDeathSounds);
  PRECACHE_SOUND_ARRAY (pPainSounds);
  PRECACHE_SOUND_ARRAY (pIdleSounds);
  PRECACHE_SOUND_ARRAY (pAlertSounds);

  // modèles
  PRECACHE_MODEL ("models/pit_drone.mdl");
}

La fonction SetYawSpeed()

Cette fonction sert à réguler la vitesse de rotation sur l'axe vertical (axe Z). On peut choisir une vitesse de rotation suivant l'activité en cours du modèle. Cette activité est stockée dans la variable membre (par héritage) m_Activity. Les différentes variables possibles sont énumérées dans activity.h et commencent toutes par ACT_#.

On fait donc un simple test de m_Activity en énumérant les différents ACT_# auxquels on veux changer la vitesse de rotation et on assigne la vitesse voulue à pev->yaw_speed :

// --------------------------------------------------------------------------
// CPitDrone::SetYawSpeed
//
// vitesse de rotation sur l'axe vertical selon l'activité du monstre.
// --------------------------------------------------------------------------

void CPitDrone::SetYawSpeed ()
{
  switch (m_Activity)
    {
    case ACT_RUN:
      pev->yaw_speed = 100;
      break;

    case ACT_IDLE:
    default:
      pev->yaw_speed = 90;
    }
}

Ici le Pit Drone pourra tourner plus vite lorsqu'il sera en train de courir plutôt qu'en étant au repos. Par défaut, la valeur sera la même qu'au repos.

La fonction Classify()

Toutes les entités sont classés en différents types tel que « Alien Enemi », « Alien neutre », « Humain Enemi » ou « Humain allié ». La fonction Classify() est chargée de simplement retourner un int qui correspond au type du monstre. Pour faciliter la lecture du code, on retourne une macro définie dans cbase.h. Voici un petit tableau descriptif de ces différents types existants :

Macro Valeur Description
CLASS_NONE 0 Aucune classe particulière
CLASS_MACHINE 1 Machines (Sentry gun, tourelles, ...)
CLASS_PLAYER 2 Joueur
CLASS_HUMAN_PASSIVE 3 Humain non agressif (scientifiques)
CLASS_HUMAN_MILITARY 4 Humain ennemi (hgrunts, assassins, ...)
CLASS_ALIEN_MILITARY 5 Alien ennemi et agressif (agrunt, ...)
CLASS_ALIEN_PASSIVE 6 Alien non agressif
CLASS_ALIEN_MONSTER 7 Alien agressif (Zombie, Garg, ...)
CLASS_ALIEN_PREY 8 Petite proie (headcrab)
CLASS_ALIEN_PREDATOR 9 Attaque toute forme de vie (bullsquid)
CLASS_INSECT 10 Insecte. Ignoré des aliens et humains (leech, roach, rat, ...)
CLASS_PLAYER_ALLY 11 Allié au joueur
CLASS_PLAYER_BIOWEAPON 12 Armes aliens du joueur (snarks, hornets)
CLASS_ALIEN_BIOWEAPON 13 Armes aliens ennemis (snarks, hornets)
CLASS_BARNACLE 99 Spécial pour le Barnacle

Pour notre Pit Drone, ce sera un simple Alien agressif :

// --------------------------------------------------------------------------
// CPitDrone::Classify
//
// Classification du monstre dans la table des relations.
// --------------------------------------------------------------------------

int
CPitDrone::Classify ()
{
  return CLASS_ALIEN_MONSTER;
}

Les fonctions XXXSound()

Ces fonctions ont pour but de faire émettre un son du NPC quand il meurt (DeathSound()), quand il se fait blesser (PainSound()), quand il est au repos (IdleSound()) et quand il détecte un ennemi (AlertSound()). On utilise ici la fonction EMIT_SOUND(). Comme nous avons plusieurs sons par groupes de sons, on va en choisir un aléatoirement en spécifiant pour fichier son :

pDeathSounds[ RANDOM_LONG( 0, ARRAYSIZE( pDeathSounds ) -1 ) ]

ARRAYSIZE() retourne le nombre d'éléments du tableau, ce qui nous permet de choisir un index aléatoire (avec RANDOM_LONG()) dans le tableau de sons (d'où l'utilité d'utiliser des tableaux pour stocker les noms de fichiers .wav. Voici les définitions de ces fonctions (qui se ressemblent beaucoup) :

// --------------------------------------------------------------------------
// CPitDrone::DeathSound
//
// Sons émis par le monstre quand il meurt.
// --------------------------------------------------------------------------

void
CPitDrone::DeathSound ()
{
  EMIT_SOUND (ENT (pev), CHAN_VOICE,
      pDeathSounds[RANDOM_LONG (0, ARRAYSIZE (pDeathSounds) -1)], 1.0, ATTN_NORM );
}

// --------------------------------------------------------------------------
// CPitDrone::PainSound
//
// Sons émis par le monstre quand il a des dégats.
// --------------------------------------------------------------------------

void
CPitDrone::PainSound ()
{
  EMIT_SOUND (ENT (pev), CHAN_VOICE,
      pDeathSounds[RANDOM_LONG (0, ARRAYSIZE (pPainSounds) -1)], 1.0, ATTN_NORM );
}

// --------------------------------------------------------------------------
// CPitDrone::IdleSound
//
// Sons émis par le monstre quand il ne fait rien.
// --------------------------------------------------------------------------

void
CPitDrone::IdleSound ()
{
  EMIT_SOUND (ENT (pev), CHAN_VOICE,
      pDeathSounds[RANDOM_LONG (0, ARRAYSIZE (pIdleSounds) -1)], 1.0, ATTN_NORM );
}

// --------------------------------------------------------------------------
// CPitDrone::AlertSound
//
// Sons émis par le monstre quand il voit un ennemi.
// --------------------------------------------------------------------------

void
CPitDrone::AlertSound ()
{
  EMIT_SOUND (ENT (pev), CHAN_VOICE,
      pDeathSounds[RANDOM_LONG (0, ARRAYSIZE (pAlertSounds) -1)], 1.0, ATTN_NORM );
}

Et après ?

Voilà, nous avons créé notre Pit Drone. Reste à rajouter une ligne dans le fichier .fgd :

@PointClass base(Monster) size(-16 -16 0, 16 16 48) = monster_pitdrone : "Pit Drone" []

Si vous compilez que vous testez, vous verrez que le Pit Drone tentera de vous attaquer (à cause de la valeur retournée par la fonction Classify()) mais sans faire de dégâts - au corps à corps comme à distance - alors que nous n'avons défini aucune fonction pour ! Cela vient du fait que notre clase hérite de l'IA par défaut de CBaseMonster. Le monstre ira même jusqu'à émettre des sons lors de ses attaques. Cela vient du .qc du modèle, qui à certaines animations du modèle fait jouer des sons via les events :

$sequence bite "bite" fps 25 ACT_MELEE_ATTACK1 1 { event 2 12 } { event 6 12 } { event 2 14 } { event 1008 1 "pitdrone/pit_drone_melee_attack1.wav" } 
$sequence range "range" fps 30 ACT_RANGE_ATTACK1 1 { event 1 11 } { event 1008 1 "pitdrone/pit_drone_attack_spike1.wav" }

Ici nous n'avons fait que le plus simple, reste maintenant à installer et configurer derrière toute l'Intelligence Artificielle... mais ce sera pour une autre fois ;)

Pit Drone (1) Pit Drone (2)
Creative Commons Logo Contrat Creative Commons

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