Créer un nouveau monstre

Écrit le 03/07/2003 par DukeNukem
Dernière mise à jour : 30/01/2006

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 monstre Pit Drone.
// ============================================

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


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


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é 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 bas) 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 quatre 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 groupe. 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étail chaque fonction...

La fonction Spawn()

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

// --------------------------------------------
// Spawn() - apparition du monstre dans le niveau.
// --------------------------------------------

void CPitDrone::Spawn( void )
{
    // 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 à dessiner
    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, etc.) 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éfinit 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 sur les Skill CVARs).

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 decals pour le sang ;p 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 (demander-le à votre modeleur ou décompilez le modèle pour l'obtenir). Pour le 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 a sept submodels (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.

Si vous n'avez qu'un groupe de modèle, vous pouvez utiliser pev->body que vous initialisez avec l'index du submodel à dessiner.

Dans le même genre, il existe pev->skin qui vous permet de choisir une texture parmi un groupe. Il faut que le groupe ait été préalablement défini dans le .qc du modèle ($texturegroup).

Avant de fermer 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 les ressources nécessaires au niveau du moteur de jeu. On utilise pour les modèles et sprites la macro PRECACHE_MODEL(), pour les sons individuels PRECACHE_SOUND() et pour les tableaux de sons PRECACHE_SOUND_ARRAY() :

// --------------------------------------------
// Precache() - précachage de toutes les
// ressources nécessaires pour ce monstre.
// --------------------------------------------

void CPitDrone::Precache( void )
{
    // 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 pour Half-life). 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 switch de m_Activity en énumérant le différents ACT_ auxquels on veut changer la vitesse de rotation et on assigne la vitesse voulue à pev->yaw_speed :

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

void CPitDrone::SetYawSpeed( void )
{
    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ées en différents types tel que « Alien ennemi », « Alien neutre », « Humain ennemi » 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 :

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 :

// --------------------------------------------
// Classify() - classification du monstre dans
// la table des relations.
// --------------------------------------------

int CPitDrone::Classify( void )
{
    return class_ALIEN_MONSTER;
} 

Les fonctions ****Sound()

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 inline 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 plutôt que de faire un switch pour le nombre sorti et 150 appels à EMIT_SOUND()).

Voici les définitions de ces fonctions (qui se ressemblent beaucoup) :

// --------------------------------------------
// DeathSound() - sons émis par le monstre
// quand il meurt.
// --------------------------------------------

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



// --------------------------------------------
// PainSound() - sons émis par le monstre
// quand il a des dégâts.
// --------------------------------------------

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



// --------------------------------------------
// IdleSound() - sons émis par le monstre
// quand il ne fait rien.
// --------------------------------------------

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



// --------------------------------------------
// AlertSound() - sons émis par le monstre
// quand il voit un ennemi.
// --------------------------------------------

void CPitDrone::AlertSound( void )
{
    EMIT_SOUND( ENT(pev),
                CHAN_VOICE,
                pAlertSounds[ 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 est 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 ;)

http://www.game-lab.com/images/tuts/hl1_npc_new/01.jpg http://www.game-lab.com/images/tuts/hl1_npc_new/02.jpg