Écrit par David Henry, le 7 septembre 2002
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.
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.
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
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.
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); } }
Cet article est mis à disposition sous un contrat Creative Commons (licence CC-BY-ND).