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

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 a été écrit pour le SDK 2.2. On prendra ici comme nom d'arme mon arme et pour le nom des 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). Dans weapon.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é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

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

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


// --------------------------------------------------------------------------
// CMonArme::UseDecrement
//
// Spécifique au sdk 2.2.
// --------------------------------------------------------------------------

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)
{
  // fonction surchargée car quelques problèmes sinon depuis le sdk 2.2.
  // si quelqu'un trouve ce qui se passe, merci de me le signaler.
  MESSAGE_BEGIN (MSG_ONE, SVC_WEAPONANIM, NULL, m_pPlayer->pev);
    WRITE_BYTE (iAnim);
    WRITE_BYTE (body);
  MESSAGE_END ();
}

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

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

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

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

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

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

BOOL
CMonArme::Deploy ()
{
  // 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");
}

// --------------------------------------------------------------------------
// CMonArme::Holster
// --------------------------------------------------------------------------

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

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

  // 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 de l'event
                       (float *)&g_vecZero,   // origine
                       (float *)&g_vecZero,   // angles
                       vecDir.x,              // paramètre #1
                       vecDir.y,              // paramètre #2
                       0, 0, 0, 0);           // Autres paramètres 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 aléatoirement l'animation "idle"
  m_flTimeWeaponIdle = UTIL_WeaponTimeBase () + UTIL_SharedRandomFloat (m_pPlayer->random_seed, 10, 15);
}

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

void
CMonArme::SecondaryAttack ()
{
}

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

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

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 evidemment changer la valeur.

Coder des balles

Côté serveur, 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;

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 (0 == 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 - 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);

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 lier 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 quatres déclarations de fonction la liste des animation 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 ejecte 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() et 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 elles n'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 dlls.

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).

Creative Commons Logo Contrat Creative Commons

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