Guide rapide pour la création d'une arme

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

Introduction

Ce tutorial n'a pas pour but de vous apprendre le fonctionnement du code d'une arme, mais de vous permettre de recoder très rapidement le squelette d'une arme. Pour les explications, reportez-vous aux autres tutoriaux sur la création d'une arme. Si vous êtes débutant, je vous conseille fortement de commencer par les autres, pour comprendre ce qu'il ce passe. Si vous savez déjà comment ça marche et que vous ne voulez pas perdre de temps pour créer une nouvelle arme, alors go !

Ce tutorial est fait pour le sdk 2.2. On prendra ici comme nom d'arme « mon arme » et pour les munitions, « mes munitions ».

Fichiers requis

Tout d'abord, vous aurez besoin de plusieurs fichiers :

Coder une arme

Tout se passe côté serveur, c'est à dire mp.dll (ou hl.dll si vous préférez).

Dans weapons.h, la définition de classe et de quelques constantes :

#define WEAPON_MONARME              16  // ID de l'arme
#define MONARME_WEIGHT              15  // priorité dans la sélection automatique
#define MONARME_MAX_CARRY           35  // nombre maximum de munitions portables
#define MONARME_MAX_CLIP             7  // capacité maximal d'un chargeur
#define MONARME_DEFAULT_GIVE         7  // munitions données par l'arme
#define AMMO_MONARME_GIVE           14  // munitions données par un chargeur



// ============================================
// CMonArme - classe de "monarme"
// ============================================

class CMonArme : public CBasePlayerWeapon
{
public:
    // fonctions
    virtual bool UseDecrement( void );
    int     SecondaryAmmoIndex( void );
    int     iItemSlot( void );

    void    Spawn( void );
    void    Precache( void );
    int     GetItemInfo( ItemInfo *p );
    int     AddToPlayer( CBasePlayer *pPlayer );
    void    SendWeaponAnim( int iAnim, int skiplocal = 1, int body = 0 );

    void    PrimaryAttack( void );
    void    SecondaryAttack( void );
    bool    Deploy( void );
    void    Holster( int skiplocal = 0 );
    void    Reload( void );
    void    WeaponIdle( void );


public:
    // variables membres
    int     m_iShell;            // douille

private:
    unsigned short m_usMonArme;  // events
};

#endif  // CLIENT_DLL 

Dans monarme.cpp, les définitions de fonctions membres de la classe CMonArme :

//
//  monarme.cpp
//

#include    "extdll.h"
#include    "util.h"
#include    "cbase.h"
#include    "weapons.h"
#include    "player.h"

// création du lien entité->classe
LINK_ENTITY_TO_CLASS( weapon_monarme, CMonArme );


// liste des animations du world model (w_***.mdl)
enum monarme_e
{
    MA_IDLE1 = 0,
    MA_IDLE2,
    MA_IDLE3,
    MA_SHOOT,
    MA_SHOOT_EMPTY,
    MA_RELOAD,
    MA_DRAW,
    MA_HOLSTER,
};



// --------------------------------------------
// UseDecrement() - spécifique au sdk 2.2.
// --------------------------------------------

bool CMonArme::UseDecrement( void )
{
#if defined( CLIENT_WEAPONS )
    return true;
#else
    return false;
#endif
}



// --------------------------------------------
// iItemSlot() - retourne l'index du slot de
// l'arme dans le HUD.
// --------------------------------------------

int CMonArme::iItemSlot( void )
{
    return 2;
}



// --------------------------------------------
// SecondaryAmmoIndex() - facultatif si on
// utilise pas de second mode de tir.
// --------------------------------------------

int CMonArme::SecondaryAmmoIndex( void )
{
    return m_iSecondaryAmmoType;
}



// --------------------------------------------
// SendWeaponAnim() - joue l'animation iAnim de
// du modèle de l'arme.
// --------------------------------------------

void CMonArme::SendWeaponAnim( int iAnim, int skiplocal, int body )
{
    // fonction surchargée car quelques problèmes sinon depuis le sdk 2.2.
    // si quelqu'un trouve ce qui se passe, merci de me mailer : tfc_duke@hotmail.com

#ifndef CLIENT_DLL
    MESSAGE_BEGIN( MSG_ONE, SVC_WEAPONANIM, NULL, m_pPlayer->pev );
        WRITE_BYTE( iAnim );
        WRITE_BYTE( body );
    MESSAGE_END();
#endif
}



// --------------------------------------------
// Spawn() - Apparition de l'arme sur la map.
// --------------------------------------------

void CMonArme::Spawn( void )
{
    pev->classname = MAKE_STRING( "weapon_monarme" ); // nom de l'entité
    m_iId = WEAPON_MONARME; // ID de l'arme
    m_iDefaultAmmo  = MONARME_DEFAULT_GIVE; // munitions que donne l'arme quand on la ramasse

    Precache(); // précache des ressources nécessaires
    SET_MODEL( ENT(pev), "models/w_monarme.mdl" ); // modèle "world" de l'arme

    FallInit(); // prète à tomber au sol
}



// --------------------------------------------
// Precache() - Précache toutes les ressources
// necessaires.
// --------------------------------------------

void CMonArme::Precache( void )
{
    // modèles de l'arme
    PRECACHE_MODEL( "models/v_monarme.mdl" );
    PRECACHE_MODEL( "models/w_monarme.mdl" );
    PRECACHE_MODEL( "models/p_monarme.mdl" );

    // modèle douille
    m_iShell = PRECACHE_MODEL( "models/shell.mdl" );

    // sons
    PRECACHE_SOUND( "weapons/monarme_reload.wav" );
    PRECACHE_SOUND( "weapons/monarme_fire.wav" );
    PRECACHE_SOUND( "weapons/357_cock1.wav" );

    // event
    m_usMonArme = PRECACHE_EVENT( 1, "events/monarme.sc" );
}



// --------------------------------------------
// GetItemInfo() - Récupère les infos de l'arme.
// --------------------------------------------

int CMonArme::GetItemInfo( ItemInfo *p )
{
    p->pszName = STRING( pev->classname ); // nom de l'entité
    p->pszAmmo1 = "mes munitions"; // type de munitions pour le premier mode de tire
    p->iMaxAmmo1 = MONARME_MAX_CARRY; // nombre maximum de munitions type #1
    p->pszAmmo2 = NULL; // type de munitions pour le second mode de tire
    p->iMaxAmmo2 = -1; // nombre maximum de munitions type #2
    p->iMaxClip = MONARME_MAX_CLIP; // capacité maximale du chargeur
    p->iSlot = 1; // slot dans le HUD
    p->iPosition = 2; // position dans le slot
    p->iFlags = 0; // drapeau d'état
    p->iId = m_iId = WEAPON_MONARME; // ID de l'arme
    p->iWeight  = MONARME_WEIGHT; // priorité dans le choix automatique

    return 1; // tout s'est bien passé, on retourne 1
}



// --------------------------------------------
// AddToPlayer() - Ajoute l'arme à l'inventaire
// du joueur.
// --------------------------------------------

int CMonArme::AddToPlayer( CBasePlayer *pPlayer )
{
    if( CBasePlayerWeapon::AddToPlayer( pPlayer ) )
    {
        MESSAGE_BEGIN( MSG_ONE, gmsgWeapPickup, NULL, pPlayer->pev );
            WRITE_BYTE( m_iId );
        MESSAGE_END();

        return true;
    }

    return false;
}



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

bool CMonArme::Deploy( void )
{
    // paramètres : 1er -> view model
    //              2ème -> player model
    //              3ème -> animation "draw" du view model
    //              4ème -> animation du modèle du joueur (player.mdl)

    return DefaultDeploy( "models/v_monarme.mdl", "models/p_monarme.mdl",
                          MA_DRAW, "onehanded" );
}


// --------------------------------------------
// Holster() - Appelé lors d'un changement d'arme.
// Endroit idéal pour déinitialiser un zoom ou
// une visée laser.
// --------------------------------------------

void CMonArme::Holster( int skiplocal /* = 0 */ )
{
    SendWeaponAnim( MA_HOLSTER );
}



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

void CMonArme::PrimaryAttack( void )
{
    if( m_pPlayer->pev->waterlevel == 3 )
    {
        // on empêche de tirer si l'on est sous l'eau

        PlayEmptySound();
        m_flNextPrimaryAttack = UTIL_WeaponTimeBase() + 0.15;
        return;
    }

    if( m_iClip <= 0 )
    {
        // on empêche de tirer si l'on est à court de munitions

        PlayEmptySound();
        m_flNextPrimaryAttack = UTIL_WeaponTimeBase() + 0.15;
        return;
    }


    // volume du son/puissance du flash
    m_pPlayer->m_iWeaponVolume  = NORMAL_GUN_VOLUME;
    m_pPlayer->m_iWeaponFlash  = NORMAL_GUN_FLASH;

    // on décrémente le compteur de munitions
    m_iClip--;

    // muzzleflash
    m_pPlayer->pev->effects |= EF_MUZZLEFLASH;

    // on force le modèle du joueur à jouer l'animation "shoot"
    m_pPlayer->SetAnimation( PLAYER_ATTACK1 );


    ///////////////////////////////////////////////////////////////////////////////
    Vector  vecSrc      = m_pPlayer->GetGunPosition();
    Vector  vecAiming  = m_pPlayer->GetAutoaimVector( AUTOAIM_5DEGREES );
    Vector  vecDir      = m_pPlayer->FireBulletsPlayer( 1,
                                                        vecSrc,
                                                        vecAiming,
                                                        VECTOR_CONE_3DEGREES,
                                                        8192,
                                                        BULLET_PLAYER_MONARME,
                                                        0,
                                                        0,
                                                        m_pPlayer->pev,
                                                        m_pPlayer->random_seed );

    int flags;

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

    // on appelle l'event chez la client dll
    PLAYBACK_EVENT_FULL( flags,                // drapeaux d'état
                        m_pPlayer->edict(),    // *pInvoker
                        m_usMonArme,           // index event
                        0.0,                   // délai avant action event
                        (float *)&g_vecZero,   // origine
                        (float *)&g_vecZero,   // angles
                        vecDir.x,              // paramètre #1
                        vecDir.y,              // paramètre #2
                        0, 0, 0, 0 );          // Autres params inutilisés


    ///////////////////////////////////////////////////////////////////////////////


    // on ajuste le temps avant de pouvoir tirer un nouveau coup
    m_flNextPrimaryAttack += 0.25;

    if( m_flNextPrimaryAttack < UTIL_WeaponTimeBase() )
        m_flNextPrimaryAttack = UTIL_WeaponTimeBase() + 0.25;


    // on joue aléatoirement l'animation "idle"
    m_flTimeWeaponIdle = UTIL_WeaponTimeBase()
                      + UTIL_SharedRandomFloat( m_pPlayer->random_seed,
                                                10, 15 );
}



// --------------------------------------------
// SecondaryAttack() - Second mode de tir.
// --------------------------------------------

void CMonArme::SecondaryAttack( void )
{
}



// --------------------------------------------
// Reload() - Recharge l'arme.
// --------------------------------------------

void CMonArme::Reload( void )
{
    // on ne recharge pas s'il n'y a plus de munitions en réserve
    if( m_pPlayer->ammo_monarme <= 0 )
        return;

    // on recharge m_iClip
    if( DefaultReload( MONARME_MAX_CLIP, MA_RELOAD, 1.5 ) )
    {
        // on joue un son
        EMIT_SOUND( ENT(m_pPlayer->pev), CHAN_WEAPON,
                    "weapons/monarme_reload.wav", 0.8, ATTN_NORM );
    }
}



// --------------------------------------------
// WeaponIdle() -
// --------------------------------------------

void CMonArme::WeaponIdle( void )
{
    ResetEmptySound();

    m_pPlayer->GetAutoaimVector( AUTOAIM_5DEGREES );

    if( m_flTimeWeaponIdle > UTIL_WeaponTimeBase() )
        return;


    switch( RANDOM_LONG( 0, 2 ) )
    {
        // on joue une animation "idle" au hasard
        default:
        case 0: SendWeaponAnim( MA_IDLE1 ); break;
        case 1: SendWeaponAnim( MA_IDLE2 ); break;
        case 2: SendWeaponAnim( MA_IDLE3 ); break;
    }

    // temps aléatoire avant de rappeler cette fonction
    m_flTimeWeaponIdle = UTIL_WeaponTimeBase()
                      + UTIL_SharedRandomFloat( m_pPlayer->random_seed,
                                                10, 15 );
}

Les dégats

Toujours côté dll serveur.

Commencez par aller dans skill.h et ajoutez une variable membre à la structure skilldata_t :

    float plrDmgMonArme;

Allez dans game.cpp et ajoutez le code suivant :

// Mon Arme
cvar_t  sk_plr_monarme_bullet1 = { "sk_plr_monarme_bullet1", "0" };
cvar_t  sk_plr_monarme_bullet2 = { "sk_plr_monarme_bullet2", "0" };
cvar_t  sk_plr_monarme_bullet3 = { "sk_plr_monarme_bullet3", "0" };

Un peu plus loin dans la fonction GameDLLInit() (toujours game.cpp) :

    // Mon Arme
    CVAR_REGISTER( &sk_plr_monarme_bullet1 ); // { "sk_plr_monarme_bullet1", "0" };
    CVAR_REGISTER( &sk_plr_monarme_bullet2 ); // { "sk_plr_monarme_bullet2", "0" };
    CVAR_REGISTER( &sk_plr_monarme_bullet3 ); // { "sk_plr_monarme_bullet3", "0" };

Dans gamerules.cpp, dans la fonction RefreshSkillData() :

    // Mon Arme
    gSkillData.plrDmgMonArme = GetSkillCvar( "sk_plr_monarme_bullet" );

Dans multiplay_gamerules.cpp, dans la même fonction (RefreshSkillData()) :

    // Mon Arme
    gSkillData.plrDmgMonArme = 30;

Vous pouvez bien évidemment changer la valeur.

Coder des balles

Côté serveur,

Dans weapons.h vers la ligne 190/200 normalement (si vous avez pas trop transformé le fichier), dans l'enum de Bullet, ajoutez BULLET_PLAYER_MONARME :

// bullet types
typedef        enum
{
    BULLET_NONE = 0,
    BULLET_PLAYER_9MM, // glock
    BULLET_PLAYER_MP5, // mp5
    BULLET_PLAYER_357, // python
    BULLET_PLAYER_BUCKSHOT, // shotgun
    BULLET_PLAYER_CROWBAR, // crowbar swipe

    BULLET_PLAYER_MONARME, // mon arme

    BULLET_MONSTER_9MM,
    BULLET_MONSTER_MP5,
    BULLET_MONSTER_12MM,
} Bullet;

Dans weapon.cpp, fonction DecalGunshot(), ajoutez une ligne dans le switch de iBulletType :

        switch( iBulletType )
        {
        case BULLET_PLAYER_9MM:
        // ...
        case BULLET_PLAYER_357:

        case BULLET_PLAYER_MONARME:

        default:
            // smoke and decal
            UTIL_GunshotDecalTrace( pTrace, DamageDecal( pEntity, DMG_BULLET ) );
            break;

Dans combat.cpp on va modifier la fonction FireBulletsPlayer(). Trouvez encore le switch de iBulletType et ajoutez le listing suivant :

            case BULLET_PLAYER_MONARME:
                pEntity->TraceAttack( pevAttacker, gSkillData.plrDmgMonArme, vecDir,
                                      &tr, DMG_BULLET );
                break;

Coder des munitions

Commencez par aller dans cbase.h et à la fin de la classe CbaseEntity, ajoutez une variable membre :

    int ammo_monarme;

Dans TabulateAmmo() (player.cpp) ajoutez cette instruction :

    ammo_monarme = AmmoInventory( GetAmmoIndex( "mes munitions" ) );

Tout au début de stats.cpp, dans AmmoDamage(), ajoutez cette condition avant le return :

    if( !strcmp( pName, "mes munitions" ) )
        return gSkillData.plrDmgMonArme;

Retournez dans le fichier de votre arme (monarme.cpp) et tout à la fin, après la dernière fonction de CMonArme (WeaponIdle() normalement) ajoutez le code du chargeur :

// ============================================
// CMonArmeAmmoClip - Chargeur Mon Arme.
// ============================================

class CMonArmeAmmoClip : public CBasePlayerAmmo
{
    void Spawn( void )
    {
        Precache();
        SET_MODEL( ENT(pev), "models/w_mesmunitions.mdl" );
        CBasePlayerAmmo::Spawn();
    }

    void Precache( void )
    {
        PRECACHE_MODEL( "models/w_mesmunitions.mdl" );
        PRECACHE_SOUND( "items/9mmclip1.wav" );
    }

    bool AddAmmo( CBaseEntity *pOther )
    {
        int bResult = (pOther->GiveAmmo( AMMO_MONARME_GIVE, "mes munitions",
                                        MONARME_MAX_CARRY ) != -1);

        if( bResult )
            EMIT_SOUND( ENT(pev), CHAN_ITEM, "items/9mmclip1.wav", 1, ATTN_NORM );

        return bResult;
    }
};

LINK_ENTITY_TO_CLASS( ammo_monarme, CMonArmeAmmoClip );

Les events

On passe maintenant côté client (client.dll).

Dans hl_events.cpp et dans l'extern C ajoutez notre nouvelle fonction event à la suite des autres :

void EV_MonArmeFire( struct event_args_s *args );

Puis un peu plus bas dans la fonction Game_HookEvents(), on va relier l'event à cette nouvelle fonction :

    gEngfuncs.pfnHookEvent( "events/monarme.sc", EV_MonArmeFire );

Dans ev_hldm.cpp on va redéclarer notre fonction dans l'extern C au début du fichier :

void EV_MonArmeFire( struct event_args_s *args );

Allez dans ev_hldm.h et dans le premier enum (celui de Bullet normalement) ajoutez cette ligne :

    BULLET_PLAYER_MONARME,

Descendez à la fin du fichier et placez avant les quatre déclarations de fonction la liste des animations du modèle « view » de votre arme :

enum monarme_e
{
    MA_IDLE1 = 0,
    MA_IDLE2,
    MA_IDLE3,
    MA_SHOOT,
    MA_SHOOT_EMPTY,
    MA_RELOAD,
    MA_DRAW,
    MA_HOLSTER,
};

Retournez dans ev_hldm.cpp et allez dans la fonction EV_HLDM_DecalGunshot(). Cherchez le switch de iBulletType et ajoutez cette ligne juste avant le default :

            case BULLET_PLAYER_MONARME:

Descendez un peu jusqu'à la fonction EV_HLDM_FireBullets(). Une fois dedans, trouvez le switch de iBulletType et ajoutez ce bloc de code :

            case BULLET_PLAYER_MONARME:

                EV_HLDM_PlayTextureSound( idx, &tr, vecSrc, vecEnd, iBulletType );
                EV_HLDM_DecalGunshot( &tr, iBulletType );

                break;

Et à la fin de ev_hldm.cpp :

// --------------------------------------------
// EV_MonArmeFire() - Event du tir de Mon Arme.
// --------------------------------------------

void EV_MonArmeFire( struct event_args_s *args )
{
    int idx;
    vec3_t origin;
    vec3_t angles;
    vec3_t velocity;

    vec3_t ShellVelocity;
    vec3_t ShellOrigin;
    int shell;
    vec3_t vecSrc, vecAiming;
    vec3_t up, right, forward;

    idx = args->entindex;
    VectorCopy( args->origin, origin );
    VectorCopy( args->angles, angles );
    VectorCopy( args->velocity, velocity );

    AngleVectors( angles, forward, right, up );

    // modèle de la douille
    shell = gEngfuncs.pEventAPI->EV_FindModelIndex( "models/shell.mdl" );

    if( EV_IsLocal( idx ) )
    {
        // flash
        EV_MuzzleFlash();

        // on joue l'animation du modèle "view" de l'arme
        gEngfuncs.pEventAPI->EV_WeaponAnimation( MA_SHOOT, 2 );

        // l'angle de vue est décalé du à la force du tir (pour le réalisme ;))
        V_PunchAxis( 0, -2.0 );
    }

    // on récupère les infos sur la douille (origine, vitesse, ...)
    EV_GetDefaultShellInfo( args, origin, velocity, ShellVelocity, ShellOrigin,
                            forward, right, up, 20, -12, 4 );

    // on éjecte la douille
    EV_EjectBrass( ShellOrigin, ShellVelocity, angles[ YAW ], shell, TE_BOUNCE_SHELL );

    // on joue un son de tir
    gEngfuncs.pEventAPI->EV_PlaySound( idx, origin, CHAN_WEAPON, "weapons/monarme_fire.wav",
                                      gEngfuncs.pfnRandomFloat( 0.92, 1.0 ), ATTN_NORM, 0,
                                      98 + gEngfuncs.pfnRandomLong( 0, 3 ) );

    EV_GetGunPosition( args, vecSrc, origin );
    VectorCopy( forward, vecAiming );

    // on dessine un decal d'impact contre le mur
    EV_HLDM_FireBullets( idx, forward, right, up, 1,
                        vecSrc, vecAiming, 8192, BULLET_PLAYER_MONARME,
                        0, 0, args->fparam1, args->fparam2 );
}

Tant qu'on est côté client, deux/trois petits trucs à rajouter mais qui n'ont aucun rapport avec les events.

Dans hl_weapons.cpp, créez une nouvelle variable globale au début du fichier :

CMonArme g_MonArme;

Descendez jusqu'à un peu plus de la moitié du fichier et à la fin de HUD_InitClientWeapons() ajoutez cette instruction :

    HUD_PrepEntity( &g_MonArme, &player );

Cherchez le switch de from->client.m_iId dans la fonction HUD_WeaponsPostThink() et ajoutez-y le cas de votre arme :

        case WEAPON_MONARME:
            pWeapon = &g_MonArme;
            break;

Pour finir...

Il reste quelques petits trucs à ajouter.

Dans la fonction W_Precache() de weapon.cpp vous devez précacher les armes et munitions sinon elle apparaissent pas ou alors c'est le crash :

    UTIL_PrecacheOtherWeapon( "weapon_monarme" );
    UTIL_PrecacheOther( "ammo_monarme" );

Dans player.cpp, rajoutez ceci au cas 101 du switch de iImpulse (dans la fonction CheatImpulseCommands()) pour avoir votre arme en tapant dans la console « impulse 101 » :

        GiveNamedItem( "weapon_monarme" );
        GiveNamedItem( "ammo_monarme" );

Vous pouvez compiler les deux dll.

Il faut aussi modifier le fichier .fgd pour pouvoir ajouter votre nouvelle arme et votre nouveau type de munitions dans vos maps. Éditez le fichier avec un éditeur de texte (bloc note ou worpad) et ajoutez ceci :

@PointClass base(Weapon, Targetx) = ammo_monarme : "Mon Arme Ammo" []
@PointClass base(Weapon, Targetx) = weapon_monarme : "Mon Arme" []

Reste enfin hud.txt dans le dossier sprites. Ouvrez le, commencez par remplacer le 123 par 125 à la toute première ligne (c'est le nombre de sprites HUD déclarés dans ce fichier) puis rajoutez :

d_monarme            320    320hud1    0    224    32    16
d_monarme            640    640hud1    192    16    32    16

Et n'oubliez pas de créer un weapon_monarme.txt toujours dans le dossier sprites pour le HUD (regardez ceux des armes déjà existante pour prendre exemple).