Créer une arme qui tire des projectiles

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

Introduction

Nous allons voir dans ce tutorial comment créer une arme qui tire des projectiles comme l'arbalète, l'hornetgun ou le rpg! Pour faire simple, ici nous allons coder un simple nailgun comme ceux de Team Fortress Classic ou DeathMatch Classic. Comme dans les tutos précédents, on appellera l'arme « mon arme » en reprenant la classe CMonArme du premier tutorial.

Pour cela, nous allons devoir créer une nouvelle entité qui sera le projectile en question, qui sortira du canon de votre arme.

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

L'entité « Nail »

Le projectile du nailgun, c'est le nail (une sorte de pic ou de « clou »). On va donc créer une nouvelle entité dont la classe sera CNail. Placez la définition de classe au début du fichier de votre arme, mais avant il va falloir désactiver la compilation côté client pour ce bout de code :

#ifndef CLIENT_DLL


// ============================================
// CNail - nail.
// ============================================

class CNail : public CBaseEntity
{
private:
    // fonctions
    void    Spawn( void );
    void    Precache( void );
    int     Classify( void );
    void    EXPORT BubbleThink( void );
    void    EXPORT NailTouch( CBaseEntity *pOther );


public:
    static  CNail *NailCreate( void );

};


// liaison entité->classe
LINK_ENTITY_TO_CLASS( nail, CNail );

La classe comporte cinq fonctions. Je ne vais pas parler de Spawn() et Precache() qui sont évidentes... On retrouve la fonction Classify() pour spécifier aucune relation entre entité et nail.

Deux fonctions plus interressantes : BubbleThink() et NailTouch(). La première est la fonction think principale et va être appelée en boucle jusqu'à ce que le nail touche un objet. À ce moment là c'est la seconde qui est appelée.

La troisième fonction — NailCreate() — est la seule qui sera publique. On l'appellera pour créer un nouveau nail lorsqu'on en aura besoin.

Passons aux définitions de fonctions :

// --------------------------------------------
// Spawn() - apparition de l'entité sur la map.
// --------------------------------------------

void CNail::Spawn( void )
{
    Precache();

    pev->classname  = MAKE_STRING( "nail" );
    pev->movetype   = MOVETYPE_FLY;
    pev->solid      = SOLID_BBOX;
    pev->gravity    = 0.5;

    SET_MODEL( ENT(pev), "models/nail.mdl" );

    UTIL_SetOrigin( pev, pev->origin );
    UTIL_SetSize( pev, Vector(0, 0, 0), Vector(0, 0, 0) );

    SetTouch( NailTouch );
    SetThink( BubbleThink );
    pev->nextthink = gpGlobals->time + 0.2;
}



// --------------------------------------------
// Precache() - précache toutes les ressources
// nécessaires.
// --------------------------------------------

void CNail::Precache( void )
{
    // modèle du nail
    PRECACHE_MODEL( "models/nail.mdl" );

    // sons
    PRECACHE_SOUND( "weapons/xbow_hitbod1.wav" );
    PRECACHE_SOUND( "weapons/xbow_hitbod2.wav" );
}



// --------------------------------------------
// Classify() - classification de l'entité.
// --------------------------------------------

int CNail::Classify( void )
{
    return   class_NONE;
}

Ici, rien de mystérieux. Spawn() initialise quelques paramètres de l'entité, Precache() charge les modèles et sons et Classify() renvoie CLASS_NONE.

On va directement passer au plus intéressant :

// --------------------------------------------
// BubbleThink() - traînée de bulles sous l'eau.
// --------------------------------------------

void CNail::BubbleThink( void )
{
    pev->nextthink = gpGlobals->time + 0.1;

    if( pev->waterlevel == 0 )
        return;

    // trainée de bulles sous l'eau
    UTIL_BubbleTrail( pev->origin - pev->velocity * 0.1, pev->origin, 1 );
}



// --------------------------------------------
// NailTouch() - appelée lorsque le nail touche
// un objet.
// --------------------------------------------

void CNail::NailTouch( CBaseEntity *pOther )
{
    SetTouch( NULL );
    SetThink( NULL );

    TraceResult tr = UTIL_GetGlobalTrace();


    if( pOther->pev->takedamage )
    {
        // on touche une entité

        entvars_t *pevOwner = VARS( pev->owner );
        ClearMultiDamage();

        if( pOther->IsPlayer() )
        {
            pOther->TraceAttack( pevOwner, gSkillData.plrDmgMonArme,
                pev->velocity.Normalize(), &tr, DMG_NEVERGIB );
        }
        else
        {
            pOther->TraceAttack( pevOwner, gSkillData.plrDmgMonArme,
                pev->velocity.Normalize(), &tr, DMG_BULLET | DMG_NEVERGIB );
        }

        ApplyMultiDamage( pev, pevOwner );

        // son aléatoire
        switch( RANDOM_LONG(0,1) )
        {
            case 0:
                EMIT_SOUND( ENT(pev), CHAN_BODY, "weapons/xbow_hitbod1.wav", 1, ATTN_NORM );
                break;

            case 1:
                EMIT_SOUND( ENT(pev), CHAN_BODY, "weapons/xbow_hitbod2.wav", 1, ATTN_NORM );
                break;
        }
    }
    else
    {
        // on touche un élément de la map

        // ricochet si l'impact n'est pas sous l'eau
        if( UTIL_PointContents( pev->origin ) != CONTENTS_WATER )
            UTIL_Sparks( pev->origin );

        // decal d'impact
        DecalGunshot( &tr, BULLET_PLAYER_MONARME );
    }

    // on détruit le nail
    SUB_Remove();
}

D'abord BubbleThink() qui est très simple : cette fonction est appelée toutes les 0.1 secondes et si le nail se trouve sous l'eau, on crée une traînée de bulles, sinon on sort sans rien faire.

Puis NailTouch() qui sera appelée au moment de l'impact. Deux cas possible : l'objet touché est une entité type monstre, ou joueur. Dans ce cas on applique les dommages à l'entité puis on joue un son « splotf » quand le nail rentre dans l'organisme. Deuxième cas : le nail percute un bloc de la map. Là, on crée une étincelle de ricochet si l'impact ne se trouve pas sous l'eau puis un petit decal. Enfin, pour les deux cas, on détruit l'entité en appelant SUB_Remove().

Il nous reste plus qu'une fonction à définir :

// --------------------------------------------
// NailCreate() - crée une nouvelle entité "nail".
// --------------------------------------------

CNail *CNail::NailCreate( void )
{
    CNail *pNail = GetClassPtr( (CNail *)NULL );
    pNail->Spawn();

    return pNail;
}

Ici c'est pas très long. On crée un nouvel objet CNail, on le spawn puis on retourne un pointeur vers l'entité.

Et enfin, on réactive la compilation côté client dll :

#endif  // CLIENT_DLL

La fonction PrimaryAttack()

On va maintenant s'occuper d'arranger PrimaryAttack() pour qu'elle crée des nails et qu'elle les envoie tout droit :

// --------------------------------------------
// PrimaryAttack() - premier mode de tir.
// --------------------------------------------

void CMonArme::PrimaryAttack( void )
{
    if( m_iClip <= 0 )
    {
        PlayEmptySound();
        return;
    }

    m_iClip--;


/********************* [EVENT] ************************/

    int flags;

#if defined( CLIENT_WEAPONS )
    flags = FEV_NOTHOST;
#else
    flags = 0;
#endif

    // on appelle l'event chez la cl_dll
    PLAYBACK_EVENT( flags, m_pPlayer->edict(), m_usMonArme );

/********************* [/EVENT] ***********************/

    // on joue l'animation "shoot"
    m_pPlayer->SetAnimation( PLAYER_ATTACK1 );

    Vector anglesAim = m_pPlayer->pev->v_angle + m_pPlayer->pev->punchangle;
    UTIL_MakeVectors( anglesAim );

    anglesAim.x     = -anglesAim.x;
    Vector vecSrc   = m_pPlayer->GetGunPosition() - gpGlobals->v_up * 2;
    Vector vecDir   = gpGlobals->v_forward;


#ifndef CLIENT_DLL

    CNail *pNail = CNail::NailCreate();
    pNail->pev->origin  = vecSrc;
    pNail->pev->angles  = anglesAim;
    pNail->pev->owner   = m_pPlayer->edict();

    if( m_pPlayer->pev->waterlevel == 3 )
    {
        // les nail sont moins vite sous l'eau...
        pNail->pev->velocity = vecDir * 1000;
        pNail->pev->speed = 1000;
    }
    else
    {
        // ... que dans l'air
        pNail->pev->velocity = vecDir * 2000;
        pNail->pev->speed = 2000;
    }

#endif


    m_flNextPrimaryAttack = UTIL_WeaponTimeBase() + 0.1;
}

Dans un premier temps, on vérifie qu'il y'a toujours des nails dans le chargeur, sinon on sort de la fonction en jouant un son de chargeur vide. On décrémente le compteur de munitions du chargeur (m_iClip) puis on appelle l'event, et on fait jouer l'animation « shoot » au modèle du joueur. Jusqu'à présent rien de nouveau.

C'est maintenant qu'on va créer le fameux nail à envoyer ! Tout simplement, on crée le crée avec CNail::NailCreate() et on récupère un pointeur dans *pNail. Avec ce pointeur, on va alors pouvoir accéder à l'entité et modifier ces paramètres : direction, vitesse, ... À noter ici que l'on check si l'environnement ou l'on est pour établir la vitesse du nail : si l'on est sous l'eau, sa vitesse sera réduite de moitié.

Cette partie ne doit pas être compilée côté client si vous ne voulez pas vous retrouver avec deux nails à chaque tir.

L'event

Avant de terminer je vais quand même vous récrire la fonction event pour ce nailgun, vu que nous n'avons pas besoin d'éjecter de douille ou de dessiner un decal d'impact (ce qui est fait dans NailTouch()). La fonction est très simple :

// --------------------------------------------
// EV_MonArmeFire() - fonction event.
// --------------------------------------------

void EV_MonArmeFire( struct event_args_s *args )
{
    vec3_t origin;
    VectorCopy( args->origin, origin );


    gEngfuncs.pEventAPI->EV_PlaySound( args->entindex, origin,
        CHAN_WEAPON, "weapons/monarme_fire.wav", 1, ATTN_NORM, 0, 100 );


    // animation du modèle de l'arme et léger recule à chaque tir
    if( EV_IsLocal( args->entindex ) )
    {
        gEngfuncs.pEventAPI->EV_WeaponAnimation( MA_SHOOT1, 1 );
        V_PunchAxis( 0, -0.5 );
    }
}