Créer une nouvelle arme (côté serveur)

Écrit le 05/07/2003 par DukeNukem
Dernière mise à jour : 29/05/2006

Introduction

Bon tout d'abord je tiens à préciser que je n'ai pas envie d'expliquer les fondements du C++ dans ce tutorial déjà bien assez long, donc si vous avez des lacunes sur le langage ou si vous ne le connaissez pas du tout, courrez à la FNAC ou aller faire un tour sur Amazon.fr pour payer une petite bible. Je présume également que vous connaissez un minimum le SDK d'Half-Life et que vous savez ce que sont la mp et la client dll. Voilà, les présentations sont faites, on peut commencer sérieusement :)

Ce tutorial a pour but de vous apprendre un peu commencer qu'on crée une arme avec le sdk 2.2. Pour le cas général on appellera l'arme monarme
Pour commencer, vous aurez besoin de plusieurs fichiers :
- le modèle w_monarme.mdl ("world", celui qu'on voit sur le sol)
- le modèle v_monarme.mdl ("view", celui qu'on voit quand on le tient)
- le modèle p_monarme.mdl ("player", celui qu'on voit sur les autres joueurs)

- un fichier weapon_monarme.txt (dossier sprites)
- modifications du fichier hud.txt (dossier sprites)

- un ou des sons "fire" (en général dossier sound/weapons)
- un ou des sons "reload" (en général dossier sound/weapons)
- d'autres sons comme le "shot empty" ou celui quand on ramasse des munitions (facultatif)

Voilà pour les fichiers. Maintenant où va-t-on coder ? dans la mp.dll et client.dll (oué c'est une nouveauté du SDK 2.2 et pour être franc, je préférais comme avant...). Allez, c'est partiii!!

Coder une arme

Lancez Visual C++ et direction la dll serveur!

On va commencer par créer une classe pour notre nouvelle arme. Rendez-vous donc dans weapon.h là où vous voulez mais après la définition CBasePlayerWeapon, car notre arme en héritera, et placez le code suivant :

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

#ifndef CLIENT_DLL

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

#endif  // CLIENT_DLL

À noter qu'autrefois on déclarait la classe dans le fichier source de l'arme (ici vous pouvez encore le faire) car nous n'en avions pas besoin côté client. C'est pour cela aussi que j'ai désactivé la compilation de ce bout de code avec #ifndef CLIENT_DLL.

En gros, comment ça marche une arme ?
Bien elle est composée de différentes fonctions appelées régulièrement pour différentes actions :

- une fonction pour le premier mode de tir : PrimaryAttack()
- une fonction pour le second mode de tir : SecondaryAttack()
- une fonction pour recharger l'arme : Reload()
- une fonction pour lorsqu'on fait rien : WeaponIdle()
- une fonction pour lorsqu'on sort l'arme : Deploy()
- une fonction pour lorsqu'on range l'arme : Holster()

Les fonctions Spawn(), Precache(), GetItemInfo() et AddToPlayer() servent pour faire apparaître l'arme sur la map, précacher les ressources nécessaires (modèles, sons, ...) et ajouter l'arme à l'inventaire du joueur.

Nous aurons ensuite besoin de quelques définitions, à déclarer toujours dans ce fichier :

#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

Le Mieux serait de les définir avec les autres du même genre des autres armes. Regardez en haut du weapon.h après la déclaration de CGrenade, les définitions sont rangées par catégories pour toutes les armes.

Voilà pour les déclarations ! Maintenant créez un nouveau fichier source pour votre arme et ajoutez-le au projet. Il contiendra les définitions des fonctions membres de la classe. Commencez par ajouter les #includes nécessaires :

//
//  monarme.cpp
//

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

Pour pouvoir utiliser votre arme dans Half-life, il faut dire au moteur que c'est une entité. Il suffit donc simplement d'utiliser la macro suivante :

LINK_ENTITY_TO_CLASS( weapon_monarme, CMonArme );

On va avoir besoin d'une fonction globale déclarée et définie dans weapons.cpp :

extern void EjectBrass( const Vector &vecOrigin,
                        const Vector &vecVelocity,
                        float rotation,
                        int model,
                        int soundtype );

Maintenant on va déclarer la liste des animations de l'arme, pour pouvoir dire ensuite au moteur quelle animation jouer au moment voulu. Ce sont les seules choses des modèles que l'on aura besoin pour coder notre arme, peu importe son squelette, son nombre de triangles, de vertices, de textures, de submodels ou autre. Pour les avoir, demandez à votre modeleur la liste ou prenez hlmviewer par exemple et regardez votre w_monarme.mdl. Le nom des anims n'est pas important non plus, seul l'ordre compte.

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

On va imaginer ici que le modèle est composé de trois animations idle, deux shoot donc une lorsque le chargeur est à cours de munitions, une reload lorsqu'on recharge et une lorsqu'on sort l'arme (draw) et une lorsqu'on la range (holster). "MA" signifie "Mon Arme" ici.

Ceci fait, on peut commencer à coder les fonctions. Débarrassons-nous d'abord des petites fonctions :

// --------------------------------------------
// UseDecrement() -
// --------------------------------------------

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 )
{
    MESSAGE_BEGIN( MSG_ONE, SVC_WEAPONANIM, NULL, m_pPlayer->pev );
        WRITE_BYTE( iAnim );
        WRITE_BYTE( body );
    MESSAGE_END();
}

Vite fait : UseDecrement() est un truc assez récent pour les armes côté client. Notre arme ne sera codée que côté serveur mais les autres ne le sont pas donc elle retournera TRUE quand même.

iItemSlot() retourne le numéro du slot où se trouve l'arme dans le HUD.

SecondaryAmmoIndex() retourne un index pour obtenir les bonnes munitions utilisées par le second mode de tir (utiliser ainsi : m_pPlayer->m_rgAmmo[ SecondaryAmmoIndex() ]). Cette dernière n'est pas du tout obligatoire si vous n'avez pas de second mode de tir ou si y vous utiliser les mêmes munitions.

Pour SendWeaponAnim(), c'est une fonction qu'on est obligé de surcharger depuis le SDK 2.2 pour éviter des problèmes dus à UseDecrement(). Elle sert à jouer une animation du modèle "view" de l'arme. Le paramètre skiplocal nous est inutile mais on est obligé de le garder à cause de la surcharge de fonction.

Passons au sérieux :

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

void CMonArme::Spawn( void )
{
    // nom de l'entité
    pev->classname = MAKE_STRING( "weapon_monarme" );

    // ID de l'arme
    m_iId = WEAPON_MONARME;

    // munitions que donne l'arme quand on la ramasse
    m_iDefaultAmmo = MONARME_DEFAULT_GIVE;

    // Précache des ressources nécessaires
    Precache();

    // modèle "world" de l'arme
    SET_MODEL( ENT(pev), "models/w_monarme.mdl" );

    // l'arme prête à tomber au sol (lorsque le joueur meurt)
    FallInit(); 
}

La fonction Spawn() est appelée lorsque le modèle apparaît dans le monde. Elle initialise certaines de ses variables membres puis charge son modèle à l'appel de SET_MODEL(). FallInit() prépare l'entité à être "libérée" dans le monde au cas ou le joueur se ferait tuer par exemple. Enfin, la fonction Precache() est obligatoire pour que le moteur charge les fichiers ressources nécessaires avant leur rendu. Si vous oubliez de précacher les ressources, vous n'aurez pas de modèles, sons, events ou sprites. La fonction se présente ainsi :

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

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

Je pense que vous l'avez compris : PRECACHE_MODEL() sert à précacher les .mdl, PRECACHE_SOUND() les sons et PRECACHE_EVENT() les fichiers .sc des events. Si vous avez des sprites, c'est PRECACHE_MODEL() aussi.
J'ai précaché 357_cock1.wav car c'est le son utilisé par toutes les armes en général lorsque le chargeur est vide.

Passons à la fonction suivante, GetItemInfo(), qui renvoie des infos sur l'arme :

// --------------------------------------------
// 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 munition type #1
    p->pszAmmo2 = NULL; // type de munitions pour le second mode de tire
    p->iMaxAmmo2 = -1; // nombre maximum de munition 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
}

Pour les types de munitions, ils sont identifiés par des chaînes de caractère. Ici, on a qu'un seul type donc pour le second on passe NULL au type et -1 à la capacité maximale. On verra un peu plus tard comment créer un nouveau type de munitions.
Pour les slots du HUD, rappelez-vous qu'ils commencent à 0 donc il faut soustraire 1 à la vraie valeur ! De même pour la position dans le slot.

On va maintenant passer à la fonction chargée d'ajouter l'arme à l'inventaire du joueur lorsqu'il la ramasse :

// --------------------------------------------
// AddToPlayer() -
// --------------------------------------------

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

C'est tout le temps la même : elle envoie un message au client après avoir appelé CBasePlayerWeapon::AddToPlayer().

Les deux fonctions suivantes sont appelées lorsque le joueur sort l'arme et lorsqu'il la range (quand il en sélectionne une autre quoi) :

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

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


// --------------------------------------------
// Holster() -
// --------------------------------------------

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

Dans la fonction Deploy() on appelle DefaultDeploy() qui va être chargée d'initialiser les deux modèles "view" et "player". Le troisième paramètre est l'animation à jouer lorsqu'on sort l'arme (donc à prendre dans l'enum du début) et le quatrième paramètre est une chaîne de caractère pour identifier le type d'animation que le modèle du joueur doit jouer. Vous pouvez en utiliser d'autres comme "python" (357), "mp5" (mp5) ou "trip" (tripmine) ou en créer vous même (voir ça avec votre modeleur ;)).

Il y'a 4 animations par "type" d'arme :
- "ref_aim_type"
- "ref_shoot_type"
- "crouch_aim_type"
- "crouch_shoot_type"

La fonction Holster(), elle, est appelée théoriquement lorsqu'on sélectionne une autre arme. Elle sert à "déinitialiser" l'arme si vous voulez. Par exemple, si vous aviez un mode zoom, vous pouvez le désactiver ici si ce n'est pas fait par le joueur avant. Vous pouvez également faire jouer une animation à votre modèle d'arme comme ici m'enfin on a pas souvent le temps de la voir :p

Voyons maintenant une des fonctions les plus importantes : PrimaryAttack() !

Dans cette fonction vous y mettez ce que vous voulez que votre arme fasse au moment de l'attaque. Ici, je vais vous montrer un exemple d'une simple arme à feu :

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

On check d'abord si le joueur est sous l'eau ou s'il n'a plus de munitions. Dans ce cas, impossible de tirer. PlayEmptySound() joue le son "weapons/357_cock1.wav" que l'on avait vu dans la fonction Precache(). La variable m_flNextPrimaryAttack contient le temps d'attente entre deux tirs. On utilise UTIL_WeaponTimeBase() pour obtenir le temps actuel auquel on ajoute le temps en secondes.

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

Bon ici y'a pas grand chose à dire. On ajuste quelques paramètres pour les "effets spéciaux", on décrémente le nombre de munitions du chargeur et on force le modèle du joueur à jouer l'anim "attack".

On va aborder maintenant la partie la plus intéressante :

    /////////////////////////////////////////////////////////////////////////////
    // variables locales
    TraceResult tr;
    float    flDistance         = 8192;
    int      rnd_seed           = m_pPlayer->random_seed;

    Vector  vecSrc              = m_pPlayer->GetGunPosition();
    Vector  vecDir              = m_pPlayer->GetAutoaimVector( AUTOAIM_5DEGREES );
    Vector  vecEnd              = vecSrc + vecDir * flDistance;

    Vector  vecShellVelocity    = m_pPlayer->pev->velocity 
                                + gpGlobals->v_right * UTIL_SharedRandomFloat( rnd_seed, 50, 70 )
                                + gpGlobals->v_up * UTIL_SharedRandomFloat( rnd_seed, 100, 150 )
                                + gpGlobals->v_forward * 25;

    Vector  vecShellOrigin      = pev->origin
                                + m_pPlayer->pev->view_ofs
                                + gpGlobals->v_up * -12
                                + gpGlobals->v_forward * 32
                                + gpGlobals->v_right * 6;


    // on applique les dommages à la cible
    m_pPlayer->FireBulletsPlayer(   1,
                                    vecSrc,
                                    vecDir,
                                    VECTOR_CONE_3DEGREES,
                                    flDistance,
                                    BULLET_PLAYER_MONARME,
                                    0,
                                    0,
                                    m_pPlayer->pev,
                                    rnd_seed );

    // on joue l'animation "shoot"
    SendWeaponAnim( DE_SHOOT );

    // joue un son de tir
    EMIT_SOUND( ENT(m_pPlayer->pev), CHAN_WEAPON, "weapons/monarme_fire.wav", 0.8, ATTN_NORM );

    // on éjecte une douille
    EjectBrass( vecShellOrigin, vecShellVelocity, pev->angles.y, m_iShell, TE_BOUNCE_SHELL );

    // on dessine un decal d'impact sur le mur
    UTIL_TraceLine( vecSrc, vecEnd, dont_ignore_monsters, ENT(pev), &tr );
    DecalGunshot( &tr, BULLET_PLAYER_MONARME );

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

J'ai mis toute cette partie du code entre dans slashs car elle est susceptible de changer selon si vous voulez coder votre arme côté serveur seulement, avec des events ou côté serveurs et client.

Bon on a d'abord une première partie avec plein de nouvelles variables locales. flDistance est la distance maximale que les projectiles de l'arme peuvent traverser (8192 est la taille d'une map). vecSrc est la position de l'arme dans l'espace, vecDir est la direction pointée par le canon. vecShellVelocity est le vecteur "vitesse" de la douille (on utilise des variables aléatoires pour plus de réalisme) et vecShellOrigin son point d'origine.

On arrive aux appels de fonctions.

FireBulletsPlayer() est responsable des dégâts causés par le tir. Voici sa déclaration (ne recopiez pas ce code !) :

Vector CBaseEntity::FireBulletsPlayer( ULONG cShots, // nombre de projectiles tirés
                                       Vector vecSrc, // source du tir
                                       Vector vecDirShooting, // vecteur direction
                                       Vector vecSpread, // dispersion des projectiles
                                       float flDistance, // distance maximale
                                       int iBulletType, // type de projectile
                                       int iTracerFreq, // (inutilisé)
                                       int iDamage, // domages causés
                                       entvars_t *pevAttacker, // attaquant
                                       int shared_rand ) // partage variables aléatoires avec client dll

L'argument vecSpread sert pour la dispersion des balles, une sorte d'offset d'inexactitude de l'arme. Différents vecteurs sont prédéfini pour, tous du style VECTOR_CONE_#DEGREES où # prend une valeur de 1 à 10 plus 15 et 20. Pour les curieux, ils sont définis dans weapons.h.

iBulletType est le type de projectile envoyé. Dans le code que je vous ai balancé j'ai mis BULLET_PLAYER_MONARME mais il va falloir le déclarer (on verra plus tard comment).

Je voudrait également m'arrêter sur iDamage. Si ce paramètre est différents de 0, les skill cvars ne seront pas utilisées c'est pour cela que je l'ai mis à 0, et il n'y a pas besoin ensuite d'appeler DecalGunshot(). Il nous faudra alors modifier la fonction FireBulletsPlayer() pour qu'elle accepte BULLET_PLAYER_MONARME et qu'elle cause quand même des dégâts à la cible.

On fait ensuite un simple appel à SendWeaponAnim() pour jouer une animation de tir puis EMIT_SOUND() pour jouer un son.

Pour éjecter une douille, on utilisera EjectBrass() qui prend en paramètres les vecteurs que nous avons calculés plus haut, la variable membre m_iShell contenant l'ID du modèle précaché de la douille et le type de son lorsque la douille tombe au sol.

Enfin, DecalGunshot() va nous dessiner un impact de balle contre le mur.

Reste à finir la fonction :

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

Pour m_flNextPrimaryAttack, on a vu plus haut et pour m_flTimeWeaponIdle, ça marche pareil sauf que c'est le temps d'attente avant d'exécuter la fonction WeaponIdle().

SecondaryAttack() fonctionne exactement de la même manière sauf qu'on utilise m_flNextSecondaryAttack à la place de m_flNextPrimaryAttack. Ici nous n'avons pas de second mode de tir, donc on laisse la fonction vide :

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

void CMonArme::SecondaryAttack( void )
{
}

Il nous reste deux fonctions. D'abord Reload() :

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

void CMonArme::Reload( void )
{
    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 );
    }
}

Elle est très simple. Si on n'a plus de munitions, on sort de la fonction. Sinon DefaultReload() se charge de remplir le chargeur et de faire jouer l'animation au modèle de l'arme. Son troisième paramètre est le temps en seconde avant de pouvoir tirer de nouveau.
Si le modèle de votre arme ne joue pas de son (voir les events dans le .qc) vous pouvez le faire ici avec EMIT_SOUND().

Et enfin la dernière fonction :

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

Pas grand chose à dire dessus. Elle est appelée lorsqu'on ne fait rien. Si vous avez plusieurs animations "idle", faites les jouer aléatoirement pour plus de réalisme.
Voilà c'est fini pour la classe de notre arme ! :)

Les dégats

Pour les dégâts on va utiliser les skill cvar.

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

    float plrDmgMonArme;

Cette variable contiendra les points de dommages de notre arme (enfin de ses projectiles plus exactement).
Maintenant 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" };

On crée en fait 3 objets cvar_t qu'on initialise immédiatement. Maintenant dans la fonction GameDLLInit() (toujours game.cpp) on va les enregistrer :

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

On va à présent définir la valeur de plrDmgMonArme. Dans gamerules.cpp, placez dans la fonction RefreshSkillData() l'instruction suivante :

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

Pour éviter la triche en multijoueur, et puisqu'il n'y a pas de niveaux de difficultés, on va définir plrDmgMonArme directement dans le code de notre dll. Déplacez vous dans multiplay_gamerules.cpp, dans la même fonction (RefreshSkillData()) et placer le code suivant :

    // Mon Arme
    gSkillData.plrDmgMonArme = 30;

Vous pouvez bien évidemment changer la valeur.

Voilà c'est terminé! c'était pas bien long cette fois :p

Coder des balles

Bon voilà, notre flingue il est là mais p'têtre bien qu'il tire quelque chose ! Alors on va coder des balles §§§

On a vu lorsqu'on a codé PrimrayAttack() qu'on utilisait BULLET_PLAYER_MONARME pour définir le type de projectile. On va donc maintenant le déclarer. Dans weapon.h vers les lignes 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;

Allez maintenant dans weapon.cpp et dans la 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;

Ca c'était pour les decals d'impact.

Dans combat.cpp on va modifier la fonction FireBulletsPlayer(). Ici aussi trouvez le switch de iBulletType et ajoutez le bloc de code suivant :

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

Ainsi lorsque PrimrayAttack() appellera FireBulletsPlayer() avec BULLET_PLAYER_MONARME, les dégâts de gSkillData.plrDmgMonArme seront attribués à la cible.

Coder des munitions

Quand je dis munitions je parle de chargeur hein ! (pour pas qu'on m'envoie des e-mails à ce sujet)

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

    int ammo_monarme;

Cette variable contiendra le nombre de munitions disponibles pour notre arme.

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

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

"mes munitions" souvenez, vous est la chaîne identifiant le type de munitions que notre arme a besoin.

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

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

Et maintenant, on va créer une nouvelle classe pour un nouveau type de munitions. 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 );

Bon je passe les fonction Spawn() et Precache() car elles fonctionnent comme pour notre arme. Pour AddAmmo(), elle n'est pas compliquée non plus. La fonction principale est GiveAmmo() qui prend comme paramètre le nombre de munitions à donner, la chaîne de caractère identifiant le type de munitions à donner et le nombre maximum de munitions que le joueur peut porter.

Pour finir...

Eh voilà on arrive à la fin ! Il reste cependant quelques petits trucs à ajouter.

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

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

Et 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" (cheat code « toutes les armes ») :

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

Maintenant vous avez enfin terminé, vous pouvez compiler la mp.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à existantes pour prendre exemple).

Conclusion

Voilà !! bon j'espère que c'était pas trop long et que vous en avez appris quelque chose (à faire des armes tiens!).
Je ne prends aucune responsabilité en cas de dommages, pertes, vols, PC qui explose, chien écrasé ou météore qui tombe sur votre maison.