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 a pour but de vous montrer de vous montrer comment créer une arme de contact (crowbar, couteau, clef de 12, etc.) en modifiant la fonction PrimaryAttack() ou SecondaryAttack(). Nous supposeront dans ce tutorial que la classe de votre arme s'appelle CMonArme.

Clef à molette (1) Clef à molette (2)

Corps à corps

Pour déterminer l'objet que l'on touche lorsque l'on frappe avec notre arme, on va utiliser la fonction FindHullIntersection(). Cette fonction existe déjà dans crowbar.cpp. Si vous comptez garder la crowbar, vous pouvez simplement déclarer la fonction (en haut du fichier de votre arme) :

// définition de fonction dans crowbar.cpp
void FindHullIntersection (const Vector &vecSrc,
                           TraceResult &tr,
                           float *mins,
                           float *maxs,
                           edict_t *pEntity);

Dans le cas contraire, il va falloir conserver cette fonction ailleurs. Le mieux est de l'ajouter comme fonction membre à la classe de votre arme :

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

class CMonArme : public CBasePlayerWeapon
{
public:
  // ...

  void FindHullIntersection (const Vector &vecSrc, TraceResult &tr,
                             float *mins, float *maxs, edict_t *pEntity);

};

Voici maintenant la définition de la fonction FindHullIntersection() :

// --------------------------------------------------------------------------
// FindHullIntersection
//
// Détermine le point d'impact de plus proche.
// --------------------------------------------------------------------------

void
FindHullIntersection (const Vector &vecSrc, TraceResult &tr,
                      float *mins, float *maxs, edict_t *pEntity)
{
  float distance;
  float *minmaxs[2] = { mins, maxs };
  TraceResult tmpTrace;
  Vector vecHullEnd = tr.vecEndPos;
  Vector vecEnd;

  distance = 1e6f;

  vecHullEnd = vecSrc + ((vecHullEnd - vecSrc) * 2);
  UTIL_TraceLine (vecSrc, vecHullEnd, dont_ignore_monsters, pEntity, &tmpTrace);

  if (tmpTrace.flFraction < 1.0)
    {
      tr = tmpTrace;
      return;
    }

  for (int i = 0; i < 2; ++i)
    {
      for (int j = 0; j < 2; ++j)
        {
          for (int k = 0; k < 2; ++k)
            {
              vecEnd.x = vecHullEnd.x + minmaxs[i][0];
              vecEnd.y = vecHullEnd.y + minmaxs[j][1];
              vecEnd.z = vecHullEnd.z + minmaxs[k][2];

              UTIL_TraceLine (vecSrc, vecEnd, dont_ignore_monsters, pEntity, &tmpTrace);

              if (tmpTrace.flFraction < 1.0)
                {
                  float thisDistance = (tmpTrace.vecEndPos - vecSrc).Length ();

                  if (thisDistance < distance)
                    {
                      tr = tmpTrace;
                      distance = thisDistance;
                    }
                }
            }
        }
    }
}

La fonction calcule le point d'intersection le plus proche du coup de lame (ou autre, tout dépend de votre arme).

Passons maintenant à la fonction PrimaryAttack() (en admettant que c'est elle qui gère les coups au corps à corps). Dans un premier temps, on calcule une ligne partant du joueur vers où il regarde, puis s'il rencontre un obstacle, on calcule le point d'intersection de la ligne et de l'entité. Ensuite on déclenche l'event, puis on fait jouer une animation aux modèles de l'arme et du joueur. On applique les dommages, et on fait jouer un son selon le type d'objet percuté (et on oublie pas un ptit décal d'impact sur les murs).

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

void
CMonArme::PrimaryAttack ()
{
  static int iSwing;
  int fDidHit = FALSE;
  TraceResult tr;

  // on trace une ligne du joueur vers l'objet qu'il regarde
  UTIL_MakeVectors (m_pPlayer->pev->v_angle);
  Vector vecSrc = m_pPlayer->GetGunPosition ();
  Vector vecEnd = vecSrc + gpGlobals->v_forward * 32;

  UTIL_TraceLine (vecSrc, vecEnd, dont_ignore_monsters, ENT (m_pPlayer->pev), &tr);

#ifndef CLIENT_DLL
  if (tr.flFraction >= 1.0)
    {
      UTIL_TraceHull (vecSrc, vecEnd, dont_ignore_monsters, head_hull, ENT (m_pPlayer->pev), &tr);

      if (tr.flFraction < 1.0)
        {
          // on calcule le point d'intersection de la ligne et de l'objet
          // qu'on a touché (approximation)
          CBaseEntity *pHit = CBaseEntity::Instance (tr.pHit);

          if (!pHit || pHit->IsBSPModel ())
            FindHullIntersection (vecSrc, tr, VEC_DUCK_HULL_MIN,
                                  VEC_DUCK_HULL_MAX, m_pPlayer->edict ());

          // ceci est le point sur la surface actuelle
          vecEnd = tr.vecEndPos;
        }
    }
#endif

  // on appelle l'event
  PLAYBACK_EVENT (FEV_NOTHOST, m_pPlayer->edict (), m_usMonArme);

  // force le joueur à jouer l'animation "attack"
  m_pPlayer->SetAnimation (PLAYER_ATTACK1);

  if (tr.flFraction >= 1.0)
    {
      // manqué
      m_flNextPrimaryAttack = UTIL_WeaponTimeBase () + 0.5;
    }
  else
    {
      // on joue une des trois animations du modèle de l'arme

      switch (((iSwing++) % 2) + 1)
        {
        case 0:
          SendWeaponAnim (MA_ATTACK1HIT);
          break;

        case 1:
          SendWeaponAnim (MA_ATTACK2HIT);
          break;

        case 2:
          SendWeaponAnim (MA_ATTACK3HIT);
          break;
        }


#ifndef CLIENT_DLL
      // touché
      fDidHit = TRUE;
      CBaseEntity *pEntity = CBaseEntity::Instance (tr.pHit);

      // on applique les domages
      ClearMultiDamage ();
      pEntity->TraceAttack (m_pPlayer->pev, gSkillData.plrDmgMonArme,
                            gpGlobals->v_forward, &tr, DMG_CLUB);
      ApplyMultiDamage (m_pPlayer->pev, m_pPlayer->pev);

      // on joue le son approprié (vouwt, slpatch, dong)
      float flVol = 1.0;
      int fHitWorld = TRUE;

      if (pEntity)
        {
          if ((pEntity->Classify () != CLASS_NONE) &&
              (pEntity->Classify () != CLASS_MACHINE))
            {
              // on a touché quelque chose d'organique
              switch (RANDOM_LONG (0, 2))
                {
                case 0:
                  EMIT_SOUND (ENT (m_pPlayer->pev), CHAN_ITEM,
                              "weapons/cbar_hitbod1.wav", 1, ATTN_NORM);
                  break;

                case 1:
                  EMIT_SOUND (ENT (m_pPlayer->pev), CHAN_ITEM,
                              "weapons/cbar_hitbod2.wav", 1, ATTN_NORM);
                  break;

                case 2:
                  EMIT_SOUND (ENT (m_pPlayer->pev), CHAN_ITEM,
                              "weapons/cbar_hitbod3.wav", 1, ATTN_NORM);
                  break;
                }

              m_pPlayer->m_iWeaponVolume = 128;

              if (!pEntity->IsAlive ())
                return;
              else
                flVol = 0.1;

              // ne pas jouer de son "obstacle bsp"
              fHitWorld = FALSE;
            }
        }

      // on joue un son selon la texture de l'obstacle heurté

      if (fHitWorld)
        {
          float fvolbar = TEXTURETYPE_PlaySound (&tr, vecSrc,
                                                 vecSrc + (vecEnd-vecSrc) * 2,
                                                 BULLET_PLAYER_MONARME);

          switch (RANDOM_LONG (0, 1))
            {
            case 0:
              EMIT_SOUND_DYN (ENT (m_pPlayer->pev), CHAN_ITEM, "weapons/cbar_hit1.wav",
                              fvolbar, ATTN_NORM, 0, 98 + RANDOM_LONG (0, 3)); 
              break;

            case 1:
              EMIT_SOUND_DYN (ENT (m_pPlayer->pev), CHAN_ITEM, "weapons/cbar_hit2.wav",
                              fvolbar, ATTN_NORM, 0, 98 + RANDOM_LONG (0, 3)); 
                    break;
            }
         }

      m_pPlayer->m_iWeaponVolume = flVol * 512
#endif
      m_flNextPrimaryAttack = UTIL_WeaponTimeBase () + 0.25;

      // on applique un décal d'impact sur le mur
      DecalGunshot (&tr, BULLET_PLAYER_MONARME);
      pev->nextthink = UTIL_WeaponTimeBase () + 0.2;
    }
}

Voilà. J'ai encore quelques commentaires à apporter sur cette fonction : iSwing est déclarée comme statique car on a besoin de garder la valeur du PrimaryAttack() précédent (sinon elle ne servirait à rien). Elle sert après à jouer toutes les animations « hit » en boucle (ici on admet qu'il y'en a trois). fHitWorld indique si l'on a heurté un objet de la map ou une entité. Si c'est un bloc solide, on jouera un son relatif à la texture de ce dernier.

Côté event

Passez côté DLL client, ev_hldm.cpp pour adapter la fonction event à votre arme :

// --------------------------------------------------------------------------
// EV_MonArmeFire
//
// Fonction event de MonArme.
// --------------------------------------------------------------------------

void
EV_MonArmeFire (struct event_args_s *args)
{
  static int iSwing;
  vec3_t origin;
  vec3_t angles;

  VectorCopy (args->origin, origin);

  // joue un son "dans le vent"
  gEngfuncs.pEventAPI->EV_PlaySound (args->entindex, origin, CHAN_WEAPON,
                              "weapons/cbar_miss1.wav", 1, ATTN_NORM, 0, PITCH_NORM);

  if (EV_IsLocal (args->entindex))
    {
      // joue une animation "coup manqué"
      switch ((iSwing++) % 3)
        {
        case 0:
          gEngfuncs.pEventAPI->EV_WeaponAnimation (MA_ATTACK1MISS, 1);
          break;

        case 1:
          gEngfuncs.pEventAPI->EV_WeaponAnimation (MA_ATTACK2MISS, 1);
          break;

        case 2:
          gEngfuncs.pEventAPI->EV_WeaponAnimation (MA_ATTACK3MISS, 1);
          break;
        }
    }
}

Rien de compliqué. La fonction event ici sert surtout à jouer les sons/animations lorsque le joueur frappe dans le vide.

Quelques mots avant de finir

Avant de clore ce tutorial, je voudrais vous rappeler quelques petits trucs pour ce genre d'arme. Tout d'abord, n'oubliez pas de précacher les sons utilisés :

// --------------------------------------------------------------------------
// CMonArme::Precache
//
// Précache les ressources nécessaires
// --------------------------------------------------------------------------

void
CMonArme::Precache ()
{
  // sons
  PRECACHE_SOUND ("weapons/cbar_hit1.wav");
  PRECACHE_SOUND ("weapons/cbar_hit2.wav");
  PRECACHE_SOUND ("weapons/cbar_hitbod1.wav");
  PRECACHE_SOUND ("weapons/cbar_hitbod2.wav");
  PRECACHE_SOUND ("weapons/cbar_hitbod3.wav");
  PRECACHE_SOUND ("weapons/cbar_miss1.wav");

  // ...
}

Désactivez aussi les types de munitions pour le mode de tir utilisant le corps à corps dans GetItemInfo() :

p->pszAmmo1 = NULL;
p->iMaxAmmo1 = -1;
p->iMaxClip = WEAPON_NOCLIP;

Enfin, si votre arme se tiens à une main comme le pied de biche, dans la fonction Deploy(), passez en dernier paramètre de DefaultDeploy() la chaîne « crowbar » :

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

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

Voilà, c'est fini. Vous pouvez également combiner corps à corps et arme à feu (par exemple, un coup de crosse en second mode de tir).

Creative Commons Logo Contrat Creative Commons

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