Créer une arme de contact

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

Introduction

Ce tutorial a pour but de vous montrer de vous montrer comment créer une arme de contact (crowbar, couteau, clef de 12, ...) en modifiant la fonction PrimaryAttack() ou SecondaryAttack() . Nous admettrons dans ce tutorial que la classe de votre arme s'appelle CMonArme.

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

Corps à corps

Pour déterminer l'objet que l'on touche lorsque l'on frappe avec notre arme, on va utiliser la fonction FindHullIntersection(). Cette fonction existe déjà dans crowbar.cpp. Si vous comptez garder la crowbar, vous pouvez simplement déclarer la fonction (en haut du fichier de votre arme) :

// définition de fonction dans crowbar.cpp
void FindHullIntersection( const Vector &vecSrc,
                          TraceResult &tr,
                          float *mins,
                          float *maxs,
                          edict_t *pEntity );

Dans le cas contraire, il va falloir conserver cette fonction ailleurs. Le mieux est de l'ajouter comme fonction membre à la classe de votre arme :

// ============================================
// CMonArme - classe objet de MonArme
// ============================================

class CMonArme : public CBasePlayerWeapon
{
public:
    // ...

    void    FindHullIntersection( const Vector &vecSrc, TraceResult &tr,
                                  float *mins, float *maxs, edict_t *pEntity );

}; 

Voici maintenant la définition de la fonction FindHullIntersection() :

// --------------------------------------------
// FindHullIntersection() - Détermine le point
// d'impact de plus proche.
// --------------------------------------------

void FindHullIntersection( const Vector &vecSrc, TraceResult &tr,
                          float *mins, float *maxs, edict_t *pEntity )
{
    float      distance;
    float      *minmaxs[2] = { mins, maxs };
    TraceResult tmpTrace;
    Vector      vecHullEnd = tr.vecEndPos;
    Vector      vecEnd;

    distance = 1e6f;

    vecHullEnd = vecSrc + ((vecHullEnd - vecSrc) * 2);
    UTIL_TraceLine( vecSrc, vecHullEnd, dont_ignore_monsters, pEntity, &tmpTrace );

    if( tmpTrace.flFraction < 1.0 )
    {
        tr = tmpTrace;
        return;
    }

    for( int i = 0; i < 2; i++ )
    {
        for( int j = 0; j < 2; j++ )
        {
            for( int k = 0; k < 2; k++ )
            {
                vecEnd.x = vecHullEnd.x + minmaxs[i][0];
                vecEnd.y = vecHullEnd.y + minmaxs[j][1];
                vecEnd.z = vecHullEnd.z + minmaxs[k][2];

                UTIL_TraceLine( vecSrc, vecEnd, dont_ignore_monsters, pEntity, &tmpTrace );

                if( tmpTrace.flFraction < 1.0 )
                {
                    float thisDistance = (tmpTrace.vecEndPos - vecSrc).Length();

                    if( thisDistance < distance )
                    {
                        tr = tmpTrace;
                        distance = thisDistance;
                    }
                }
            }
        }
    }
}

La fonction calcule le point d'intersection le plus proche du coup de lame (ou autre, tout dépend de votre arme).

Passons maintenant à la fonction PrimaryAttack() (en admettant que c'est elle qui gère les coups au corps à corps). Dans un premier temps, on calcule une ligne partant du joueur vers où il regarde, puis s'il rencontre un obstacle, on calcule le point d'intersection de la ligne et de l'entité. Ensuite on déclenche l'event, puis on fait jouer une animation aux modèles de l'arme et du joueur. On applique les dommages, et on fait jouer un son selon le type d'objet percuté (et on oublie pas un ptit décal d'impact sur les murs :)).

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

void CMonArme::PrimaryAttack( void )
{
    static int iSwing;
    int fDidHit = false;
    TraceResult tr;

    // on trace une ligne du joueur vers l'objet qu'il regarde
    UTIL_MakeVectors( m_pPlayer->pev->v_angle );
    Vector vecSrc  = m_pPlayer->GetGunPosition();
    Vector vecEnd  = vecSrc + gpGlobals->v_forward * 32;

    UTIL_TraceLine( vecSrc, vecEnd, dont_ignore_monsters, ENT( m_pPlayer->pev ), &tr );

#ifndef CLIENT_DLL
    if( tr.flFraction >= 1.0 )
    {
        UTIL_TraceHull( vecSrc, vecEnd, dont_ignore_monsters, head_hull, ENT( m_pPlayer->pev ), &tr );

        if( tr.flFraction < 1.0 )
        {
            // on calcule le point d'intersection de la ligne et de l'objet
            // qu'on a touché (approximation)
            CBaseEntity *pHit = CBaseEntity::Instance( tr.pHit );

            if( !pHit || pHit->IsBSPModel() )
                FindHullIntersection( vecSrc, tr, VEC_DUCK_HULL_MIN,
                                      VEC_DUCK_HULL_MAX, m_pPlayer->edict() );

            // ceci est le point sur la surface actuelle
            vecEnd = tr.vecEndPos;
        }
    }
#endif

    // on appelle l'event
        PLAYBACK_EVENT( FEV_NOTHOST, m_pPlayer->edict(), m_usMonArme );

    // force le joueur à jouer l'animation "attack"
    m_pPlayer->SetAnimation( PLAYER_ATTACK1 );

    if( tr.flFraction >= 1.0 )
    {
        // manqué
        m_flNextPrimaryAttack = UTIL_WeaponTimeBase() + 0.5;
    }
    else
    {
        // on joue une des trois animations du modèle de l'arme

        switch( ((iSwing++) % 2) + 1 )
        {
            case 0:
                SendWeaponAnim( MA_ATTACK1HIT );
                break;

            case 1:
                SendWeaponAnim( MA_ATTACK2HIT );
                break;

            case 2:
                SendWeaponAnim( MA_ATTACK3HIT );
                break;
        }


#ifndef CLIENT_DLL
        // touché
        fDidHit = true;
        CBaseEntity *pEntity = CBaseEntity::Instance( tr.pHit );

        // on applique les dommages
        ClearMultiDamage();
        pEntity->TraceAttack( m_pPlayer->pev, gSkillData.plrDmgMonArme,
                              gpGlobals->v_forward, &tr, DMG_CLUB );
        ApplyMultiDamage( m_pPlayer->pev, m_pPlayer->pev );

        // on joue le son approprié (vouwt, slpatch, dong)
        float flVol = 1.0;
        int fHitWorld = true;

        if( pEntity )
        {
            if( (pEntity->Classify() != class_NONE) && (pEntity->Classify() != class_MACHINE) )
            {
                // on a touché quelque chose d'organique
                switch( RANDOM_LONG( 0, 2 ) )
                {
                    case 0:
                        EMIT_SOUND( ENT(m_pPlayer->pev), CHAN_ITEM,
                                    "weapons/cbar_hitbod1.wav", 1, ATTN_NORM );
                        break;

                    case 1:
                        EMIT_SOUND( ENT(m_pPlayer->pev), CHAN_ITEM,
                                    "weapons/cbar_hitbod2.wav", 1, ATTN_NORM );
                        break;

                    case 2:
                        EMIT_SOUND( ENT(m_pPlayer->pev), CHAN_ITEM,
                                    "weapons/cbar_hitbod3.wav", 1, ATTN_NORM );
                        break;
                }

                m_pPlayer->m_iWeaponVolume = 128;

                if( !pEntity->IsAlive() )
                    return;
                else
                    flVol = 0.1;

                // ne pas jouer de son "obstacle bsp"
                fHitWorld = false;
            }
        }

        // on joue un son selon la texture de l'obstacle heurté

        if( fHitWorld )
        {
            float fvolbar = TEXTURETYPE_PlaySound( &tr, vecSrc,
                                                  vecSrc + (vecEnd-vecSrc) * 2,
                                                  BULLET_PLAYER_MONARME );

            switch( RANDOM_LONG( 0, 1 ) )
            {
                case 0:
                    EMIT_SOUND_DYN( ENT(m_pPlayer->pev), CHAN_ITEM, "weapons/cbar_hit1.wav", fvolbar,
                                    ATTN_NORM, 0, 98 + RANDOM_LONG(0,3) );
                    break;

                case 1:
                    EMIT_SOUND_DYN( ENT(m_pPlayer->pev), CHAN_ITEM, "weapons/cbar_hit2.wav", fvolbar,
                                    ATTN_NORM, 0, 98 + RANDOM_LONG(0,3) );
                    break;
            }
                }

        m_pPlayer->m_iWeaponVolume = flVol * 512
#endif
        m_flNextPrimaryAttack = UTIL_WeaponTimeBase() + 0.25;

        // on applique un décal d'impact sur le mur
        DecalGunshot( &tr, BULLET_PLAYER_MONARME );
        pev->nextthink = UTIL_WeaponTimeBase() + 0.2;
    }
}

Voilà. J'ai encore quelques commentaires à apporter sur cette fonction : iSwing est déclarée comme statique car on a besoin de garder la valeur du PrimaryAttack() précédent (sinon elle ne servirait à rien). Elle sert après à jouer toutes les anims « hit » en boucle (ici on admet qu'il y'en a trois).
fHitWorld indique si l'on a heurté un objet de la map ou une entité. Si c'est un bloc solide, on jouera un son relatif à la texture de ce dernier.

Côté event

Passez côté client.dll, ev_hldm.cpp pour adapter la fonction event à votre arme :

// --------------------------------------------
// EV_MonArmeFire() - Fonction event de MonArme.
// --------------------------------------------

void EV_MonArmeFire( struct event_args_s *args )
{
    static int iSwing;
    vec3_t origin;
    vec3_t angles;

    VectorCopy( args->origin, origin );

    // joue un son "dans le vent"
    gEngfuncs.pEventAPI->EV_PlaySound( args->entindex, origin, CHAN_WEAPON,
                                      "weapons/cbar_miss1.wav", 1, ATTN_NORM, 0, PITCH_NORM );

    if( EV_IsLocal( args->entindex ) )
    {
        // joue une animation "coup manqué"
        switch( (iSwing++) % 3 )
        {
            case 0:
                gEngfuncs.pEventAPI->EV_WeaponAnimation( MA_ATTACK1MISS, 1 );
                break;

            case 1:
                gEngfuncs.pEventAPI->EV_WeaponAnimation( MA_ATTACK2MISS, 1 );
                break;

            case 2:
                gEngfuncs.pEventAPI->EV_WeaponAnimation( MA_ATTACK3MISS, 1 );
                break;
        }
    }
}

Rien de compliqué. La fonction event ici sert surtout à jouer les sons/animations lorsque le joueur frappe dans le vide.

Quelques mots avant de finir

Avant de clore ce tutorial, je voudrais vous rappeler quelques petits trucs pour ce genre d'arme.

Tout d'abord, n'oubliez pas de précacher les sons utilisés :

// --------------------------------------------
// Precache() - Précache les ressources nécessaires
// --------------------------------------------

void CMonArme::Precache( void )
{
    // sons
    PRECACHE_SOUND( "weapons/cbar_hit1.wav" );
    PRECACHE_SOUND( "weapons/cbar_hit2.wav" );
    PRECACHE_SOUND( "weapons/cbar_hitbod1.wav" );
    PRECACHE_SOUND( "weapons/cbar_hitbod2.wav" );
    PRECACHE_SOUND( "weapons/cbar_hitbod3.wav" );
    PRECACHE_SOUND( "weapons/cbar_miss1.wav" );

    // ...
}

Désactivez aussi les types de munitions pour le mode de tir utilisant le corps à corps dans GetItemInfo() :

    p->pszAmmo1    = NULL;
    p->iMaxAmmo1   = -1;
    p->iMaxClip    = WEAPON_NOCLIP;

Enfin, si votre arme se tiens à une main comme le pied de biche, dans la fonction Deploy(), passez en dernier paramètre de DefaultDeploy() la chaîne « crowbar » :

// --------------------------------------------
// Deploy() - Déploiement de l'arme.
// --------------------------------------------

bool CMonArme::Deploy( void )
{
    return DefaultDeploy( "models/v_monarme.mdl", "models/p_monarme.mdl", MA_DRAW, "crowbar" );
}

Voilà, c'est fini :) Vous pouvez également combiner corps à corps et arme à feu (par exemple, un coups de crosse en second mode de tir !).