Faire suivre le joueur à l'aide de la classe CTalkMonster

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

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 de copier-collez pensez à changer à chaque fois).

La classe CTalkMonster

La classe CTalkMonster possède des fonctions qui vont nous permettre 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 l'include 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 :

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( void ) { 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 « use » 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( int Type ); 
    Schedule_t *GetSchedule( void ); 

    CUSTOM_SCHEDULES; 
};

Nous n'aurons pas besoin de créer de nouveaux TASK, 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 fonction 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 :

// ----------------------------------------- 
// 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 ); 
} 


// ----------------------------------------- 
// 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 TASK de l'IA par défaut et de l'IA spécialisée de CTalkMonster, deux 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à sans ce « 2 ».

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 :

// ----------------------------------------- 
// GetSchedule 
// ----------------------------------------- 

Schedule_t * CMyMonster::GetSchedule( void ) 
{ 
    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() :

// ----------------------------------------- 
// 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.