Attention : cet article a été écrit pour le SDK 2.2 d'Half-Life 1 ! Il se peut qu'il ne soit pas entièrement compatible avec des versions antérieures ou supérieures du Kit.

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 vous 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 les DLL mp et client. 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 :

Voilà pour les fichiers. Maintenant où va-t-on coder ? dans la mp.dll et client.dll (ouais 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à ou vous voulez mais après la définition CBasePlayerWeapon, car notre arme en héritera, et placez le code suivant :

/////////////////////////////////////////////////////////////////////////////
//
// CMonArme - classe pour le ma super arme.
//
/////////////////////////////////////////////////////////////////////////////

#ifndef CLIENT_DLL

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

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

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

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 :

Les fonctions Spawn(), Precache(), GetItemInfo() et AddToPlayer() servent respectivement pour faire apparaître l'arme sur la map, précacher les ressources necessaires (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éléction 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 de 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 texture, 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'odre 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 dont 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 ici « Mon Arme ».

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

// --------------------------------------------------------------------------
// CMonArme::UseDecrement
// --------------------------------------------------------------------------

BOOL
CMonArme::UseDecrement ()
{ 
#if defined (CLIENT_WEAPONS)
  return TRUE;
#else
  return FALSE;
#endif
}

// --------------------------------------------------------------------------
// CMonArme::iItemSlot

// Retourne l'index du slot de l'arme dans le HUD.
// --------------------------------------------------------------------------

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

// --------------------------------------------------------------------------
// CMonArme::SecondaryAmmoIndex
//
// Facultatif si on utilise pas de second mode de tir.
// --------------------------------------------------------------------------

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

// --------------------------------------------------------------------------
// CMonArme::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é 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 utilié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 ne nous est inutile mais on est obligé de le garder à cause de la surcharge de fonction.

Pasons au sérieux :

// --------------------------------------------------------------------------
// CMonArme::Spawn
//
// Apparition de l'arme sur la map.
// --------------------------------------------------------------------------

void
CMonArme::Spawn ()
{
  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 necessaires
  SET_MODEL (ENT (pev), "models/w_monarme.mdl");   // modèle "world" de l'arme

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

La fonction Spawn() est appelée lorsque le modèle apparaît dans le monde. Elle initialise certaiens 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 :

// --------------------------------------------------------------------------
// CMonArme::Precache
//
// Précache toutes les ressources necessaires.
// --------------------------------------------------------------------------

void
CMonArme::Precache ()
{
  // 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 :

// --------------------------------------------------------------------------
// CMonArme::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 :

// --------------------------------------------------------------------------
// CMonArme::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) :

// --------------------------------------------------------------------------
// CMonArme::Deploy
//
// Déploiement de l'arme.
// --------------------------------------------------------------------------

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

// --------------------------------------------------------------------------
// CMonArme::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 quatre animations par « type » d'arme :

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.

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 :

// --------------------------------------------------------------------------
// CMonArme::PrimaryAttack
//
// Premier mode de tir.
// --------------------------------------------------------------------------

void
CMonArme::PrimaryAttack ()
{
  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 vérifie 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'animation « 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 (MA_SHOOT);

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

  // on ejecte 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 des 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 un 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 (la source du tir), vecDir est la direction pointée par le canon. vecShellVelocity est le vecteur vitesse initial 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 hein !) :

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 coment). Je voudrais également m'arrêter sur iDamage. Si ce paramètre est différent de 0, les skill cvar 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 ejecter 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 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'executer 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 :

// --------------------------------------------------------------------------
// CMonArme::SecondaryAttack
//
// Second mode de tir. Non implémenté.
// --------------------------------------------------------------------------

void
CMonArme::SecondaryAttack ()
{
}

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

// --------------------------------------------------------------------------
// CMonArme::Reload
//
// Recharge l'arme.
// --------------------------------------------------------------------------

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

// --------------------------------------------------------------------------
// CMonArme::WeaponIdle
// --------------------------------------------------------------------------

void
CMonArme::WeaponIdle ()
{
  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 rappler 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 trois 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 vu qu'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 placez le code suivant :

// Mon Arme
gSkillData.plrDmgMonArme = 30;

Vous pouvez bien évidemment changer la valeur. Voilà c'est terminé pour les dégats.

Coder des balles

Bon voilà, notre fling 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 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;

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;

Ça c'était pour les décals 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 degats de gSkillData.plrDmgMonArme seront attribués à la cible.

Coder des munitions

Quand je dis munitions je parle de chargeur. 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 (0 == 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 - classe chargeur pour mon arme.
//
/////////////////////////////////////////////////////////////////////////////

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

  void Precache ()
    {
      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 fonctions 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

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 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 » :

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

Maintenant vous avez enfin terminé, vous pouvez compiler 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à existance 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, explosions du PC, chien écrasé ou météore qui s'écrase sur votre maison. Si j'ai dis des trucs faux dans ce tutorial merci de me le signaler par mail (tfc.duke (CHEZ) gmail (POINT) com) ou autre ; si vous avez des questions, préférez les forums que le mail, d'autres pourrons aussi vous aider.

Creative Commons Logo Contrat Creative Commons

Cet article est mis à disposition sous un contrat Creative Commons (licence CC-BY-ND).