Écrit par David Henry, le 2 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.
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.
Tout d'abord, vous aurez besoin de plusieurs fichiers :
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); }
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.
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;
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);
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;
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).
Cet article est mis à disposition sous un contrat Creative Commons (licence CC-BY-ND).