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

Nous allons voir dans ce tutorial comment créer une arme qui tire des projectiles comme l'arbalète, l'hornetgun ou le RPG. Pour faire simple, ici nous allons coder un simple nailgun comme ceux de Team Fortress Classic ou DeathMatch Classic. Comme dans les tutoriaux précédents, on appelera ici cette arme « mon arme » en reprenant la classe CMonArme du premier tutorial. Nous allons devoir créer une nouvelle entité qui sera le projectile en question, qui sortira du canon de votre arme.

Nailgun

L'entité « nail »

Le projectile du nailgun, c'est le nail (une sorte de pic ou de clou). On va donc créer une nouvelle entité dont la classe sera CNail. Placez la définition de classe au début du fichier de votre arme, mais avant il va falloir désactiver la compilation côté client pour ce bout de code :

#ifndef CLIENT_DLL

/////////////////////////////////////////////////////////////////////////////
//
// CNail - classe pour un projectile de type "clou".
//
/////////////////////////////////////////////////////////////////////////////

class CNail : public CBaseEntity
{
private:
  // fonctions
  void Spawn ();
  void Precache ();
  int Classify ();
  void EXPORT BubbleThink ();
  void EXPORT NailTouch (CBaseEntity *pOther);

public:
  static CNail *NailCreate ();
};

// liaison entité-classe
LINK_ENTITY_TO_CLASS (nail, CNail);

La classe comporte cinq fonctions. Je ne vais pas parler de Spawn() et Precache() qui sont évidentes. On retrouve la fonction Classify() pour spécifier aucune relation entre entité et nail. Deux fonctions plus intéressantes : BubbleThink() et NailTouch(). La première est la fonction think principale et va être appelée en boucle jusqu'à ce que le nail touche un objet. À ce moment là c'est la seconde qui est appelée. La troisième fonction — NailCreate() — est la seule qui sera publique. On l'appelera pour créer un nouveau nail lorsqu'on en aura besoin (au moment de tirer en fait).

Passons aux définitions de fonctions :

// --------------------------------------------------------------------------
// CNail::Spawn
//
// Apparition de l'entité sur la map.
// --------------------------------------------------------------------------

void
CNail::Spawn ()
{
  Precache ();

  pev->classname = MAKE_STRING ("nail");
  pev->movetype = MOVETYPE_FLY;
  pev->solid = SOLID_BBOX;
  pev->gravity = 0.5;

  SET_MODEL (ENT(pev), "models/nail.mdl");

  UTIL_SetOrigin (pev, pev->origin);
  UTIL_SetSize (pev, Vector (0, 0, 0), Vector (0, 0, 0));

  SetTouch (NailTouch);
  SetThink (BubbleThink);
  pev->nextthink = gpGlobals->time + 0.2;
}

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

void
CNail::Precache ()
{
  // modèle du nail
  PRECACHE_MODEL ("models/nail.mdl");

  // sons
  PRECACHE_SOUND ("weapons/xbow_hitbod1.wav");
  PRECACHE_SOUND ("weapons/xbow_hitbod2.wav");
}

// --------------------------------------------------------------------------
// CNail::Classify
//
// Classification de l'entité.
// --------------------------------------------------------------------------

int
CNail::Classify ()
{
  return CLASS_NONE;
}

Ici, rien de mystérieux. Spawn() initialise quelques paramètres de l'entité, Precache() charge les modèles et sons et Classify() renvoie CLASS_NONE (aucune intéraction). On va directement passer au plus intéressant :

// --------------------------------------------------------------------------
// CNail::BubbleThink
//
// Trainée de bulles sous l'eau.
// --------------------------------------------------------------------------

void
CNail::BubbleThink ()
{
  pev->nextthink = gpGlobals->time + 0.1;

  if (pev->waterlevel == 0)
    return;

  // trainée de bulles sous l'eau
  UTIL_BubbleTrail (pev->origin - pev->velocity * 0.1, pev->origin, 1);
}

// --------------------------------------------------------------------------
// CNail::NailTouch
//
// Fonction appelée lorsque le nail touche un objet.
// --------------------------------------------------------------------------

void
CNail::NailTouch (CBaseEntity *pOther)
{
  SetTouch (NULL);
  SetThink (NULL);

  TraceResult tr = UTIL_GetGlobalTrace ();

  if (pOther->pev->takedamage)
    {
      // on touche une entité

      entvars_t *pevOwner = VARS (pev->owner);
      ClearMultiDamage ();

      if (pOther->IsPlayer ())
        {
          pOther->TraceAttack (pevOwner, gSkillData.plrDmgMonArme,
              pev->>velocity.Normalize (), &tr, DMG_NEVERGIB);
        }
      else
        {
          pOther->TraceAttack (pevOwner, gSkillData.plrDmgMonArme,
              pev->velocity.Normalize (), &tr, DMG_BULLET | DMG_NEVERGIB);
        }

      ApplyMultiDamage (pev, pevOwner);

      // son aléatoire
      switch (RANDOM_LONG (0, 1))
        {
        case 0:
          EMIT_SOUND (ENT (pev), CHAN_BODY, "weapons/xbow_hitbod1.wav", 1, ATTN_NORM);
          break;

        case 1:
          EMIT_SOUND (ENT (pev), CHAN_BODY, "weapons/xbow_hitbod2.wav", 1, ATTN_NORM);
          break;
        }
    }
  else
    {
      // on touche un élément de la map

      // ricochet si l'impact n'est pas sous l'eau
      if (UTIL_PointContents (pev->origin) != CONTENTS_WATER)
        UTIL_Sparks (pev->origin);

      // décal d'impact
      DecalGunshot (&tr, BULLET_PLAYER_MONARME);
    }

  // on détruit le nail
  SUB_Remove ();
}

D'abord BubbleThink(), qui est très simple : cette fonction est appelée toutes les 0,1 secondes et si le nail se trouve sous l'eau, on crée une traînée de bulles, sinon on sort sans rien faire. Puis NailTouch(), qui sera appelée au moment de l'impact. Deux cas possible : l'objet touché est une entité type monstre, ou joueur. Dans ce cas on applique les dommages à l'entité puis on joue un son du style « splotf » quand le nail rentre dans l'organisme. Second cas : le nail percute un bloc de la map. Là, on crée une étincelle de ricochet si l'impact ne se trouve pas sous l'eau puis un petit décal. Enfin, pour les deux cas, on détruit l'entité en appelant SUB_Remove().

Il nous reste plus qu'une fonction à définir :

// --------------------------------------------------------------------------
// CNail::NailCreate
//
// Crée une nouvelle entité "nail".
// --------------------------------------------------------------------------

CNail*
CNail::NailCreate ()
{
  CNail *pNail = GetClassPtr ((CNail *)NULL);
  pNail->Spawn ();

  return pNail;
}

Ici c'est pas très long. On crée un nouvel objet CNail, on le spawn puis on retourne un pointeur vers l'entité.

Et enfin, on réactive la compilation côté DLL client :

#endif // CLIENT_DLL

La fonction PrimaryAttack()

On va maintenant s'occuper d'arranger PrimaryAttack() pour qu'elle crée des nails et qu'elle les envoie tout droit :

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

void
CMonArme::PrimaryAttack ()
{
  if (m_iClip <= 0)
    {
      PlayEmptySound ();
      return;
    }

  m_iClip--;

/********************* [EVENT] ************************/

  int flags;

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

  // on appelle l'event chez la cl_dll
  PLAYBACK_EVENT (flags, m_pPlayer->edict (), m_usMonArme);

/********************* [/EVENT] ***********************/

  // on joue l'animation "shoot"
  m_pPlayer->SetAnimation (PLAYER_ATTACK1);

  Vector anglesAim = m_pPlayer->pev->v_angle + m_pPlayer->pev->punchangle;
  UTIL_MakeVectors (anglesAim);

  anglesAim.x = -anglesAim.x;
  Vector vecSrc = m_pPlayer->GetGunPosition () - gpGlobals->v_up * 2;
  Vector vecDir = gpGlobals->v_forward;

#ifndef CLIENT_DLL

  CNail *pNail = CNail::NailCreate ();
  pNail->pev->origin = vecSrc;
  pNail->pev->angles = anglesAim;
  pNail->pev->owner = m_pPlayer->edict ();

  if (m_pPlayer->pev->waterlevel == 3)
    {
      // les nail sont moins vite sous l'eau...
      pNail->pev->velocity = vecDir * 1000;
      pNail->pev->speed = 1000;
    }
  else
    {
      // ... que dans l'air
      pNail->pev->velocity = vecDir * 2000;
      pNail->pev->speed = 2000;
    }

#endif

  m_flNextPrimaryAttack = UTIL_WeaponTimeBase () + 0.1;
}

Dans un premier temps, on vérifie qu'il y'a toujours des nails dans le chargeur, sinon on sort de la fonction en jouant un son de chargeur vide. On décrémente le compteur de munitions du chargeur (m_iClip) puis on appelle l'event, et on fait jouer l'animation « shoot » au modèle du joueur. Jusqu'à présent rien de nouveau.

C'est maintenant qu'on va créer le fameux nail à envoyer ! Tout simplement, on le crée avec CNail::NailCreate() et on récupère un pointeur dans *pNail. Avec ce pointeur, on va alors pouvoir accéder à l'entité et modifier ces paramètres : direction, vitesse, etc. À noter ici que l'on vérifie l'environnement où l'on est pour établir la vitesse du nail : si l'on est sous l'eau, sa vitesse sera alors réduite de moitié.

Cette partie ne doit pas être compilée côté client si vous ne voulez pas vous retrouver avec deux nails à chaque tir.

L'event

Avant de terminer je vais quand même vous récrire la fonction event pour ce nailgun, vu que nous n'avons pas besoin d'éjecter de douille ou de dessiner un décal d'impact (ce qui est fait dans NailTouch()). La fonction est très simple :

// --------------------------------------------------------------------------
// EV_MonArmeFire
//
// Fonction event.
// --------------------------------------------------------------------------

void
EV_MonArmeFire (struct event_args_s *args)
{
  vec3_t origin;
  VectorCopy (args->origin, origin);

  gEngfuncs.pEventAPI->EV_PlaySound (args->entindex, origin,
      CHAN_WEAPON, "weapons/monarme_fire.wav", 1, ATTN_NORM, 0, 100);

  // animation du modèle de l'arme et léger recule à chaque tir 
  if (EV_IsLocal (args->entindex))
    {
      gEngfuncs.pEventAPI->EV_WeaponAnimation (MA_SHOOT1, 1);
      V_PunchAxis (0, -0.5);
    }
}
Creative Commons Logo Contrat Creative Commons

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