Écrit par David Henry, le 23 juillet 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.
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.
Comme toute entité, on doit créer une classe qui devra hériter de l'une de ces trois classes :
CBaseMonster
(monsters.h, monsters.cpp).CSquadMonster
(squadmonster.h, squadmonster.cpp),
héritant elle même de CBaseMonster
.CTalkMonster
(talkmonster.h, talkmonster.cpp),
héritant elle même de CBaseMonster
.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...
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 :
DONT_BLEED
(pas de sang).BLOOD_COLOR_RED
(orangina sanguin bien rouge).BLOOD_COLOR_YELLOW
(sang alien jaunâtre).BLOOD_COLOR_GREEN
(pareil que le dernier, sang jaune).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 ;-)
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"); }
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.
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; }
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 ); }
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 ;)
Cet article est mis à disposition sous un contrat Creative Commons (licence CC-BY-ND).