#define
statements for each weapon. Add a new line for the new weapon. The value should be unique per weapon, so just increment the number of the last weapon.
#define WEAPON_NONE 0
#define WEAPON_CROWBAR 1
// ... more lines here
#define WEAPON_SNARK 15
#define WEAPON_DESERT_EAGLE 16
Next, we'll define the weight. Just underneath the weapon IDs are the weapon weight constants. I'll make the weight for the Desert Eagle to be the same as the magnum.
#define CROWBAR_WEIGHT 0
#define GLOCK_WEIGHT 10
#define PYTHON_WEIGHT 15
// ... more lines here
#define TRIPMINE_WEIGHT 10
#define DESERT_EAGLE_WEIGHT 15
As we're not adding a new ammo type, we can skip the MAX_CARRY
block, so move on to the MAX_CLIP
block. Add the relevant line to the end of this group.
#define GLOCK_MAX_CLIP 13
#define PYTHON_MAX_CLIP 6
// ... more lines here
#define SNARK_MAX_CLIP WEAPON_NOCLIP
#define DESERT_EAGLE_MAX_CLIP 7
Same goes for the DEFAULT_GIVE
constant as well.
#define GLOCK_DEFAULT_GIVE 13
#define PYTHON_DEFAULT_GIVE 6
// ... more lines here
#define HIVEHAND_DEFAULT_GIVE 8
#define DESERT_EAGLE_DEFAULT_GIVE 7
Since we'll be reusing the magnum's ammo box, you can ignore the AMMO_GIVE
section. That's it for constants.
CSqueak
definition:
class CDesertEagle : public CBasePlayerWeapon
{
public:
void Spawn();
void Precache();
int iItemSlot() { return 2; }
int GetItemInfo(ItemInfo *p);
int AddToPlayer(CBasePlayer *pPlayer);
void PrimaryAttack();
void SecondaryAttack();
BOOL Deploy();
void Holster(int skiplocal = 0);
void Reload();
void WeaponIdle();
virtual BOOL UseDecrement()
{
#if defined( CLIENT_WEAPONS )
return TRUE;
#else
return FALSE;
#endif
}
private:
};
This is your standard weapon class definition. There's no code here, just overrides of the base class methods, as documented in the High-Level Overview article. Additional changes will be needed to this class later, but this is the basic outline that we'll need to get started.iItemSlot
will be 1 greater than the value used for iSlot
in GetItemInfo
later. Think of the iItemSlot
value being 1-indexed, and the iSlot
value being 0-indexed. Second, the UseDecrement
method is defined in that way in order to handle client predictions. It's odd, but that's how it is.
CDesertEagle
class. We'll start with a very empty implementation of each method:
#include "extdll.h"
#include "util.h"
#include "cbase.h"
#include "weapons.h"
#include "player.h"
// These correspond directly to the sequences in the weapon's view model
enum desert_eagle_e {
DESERT_EAGLE_IDLE1 = 0,
DESERT_EAGLE_IDLE2,
DESERT_EAGLE_IDLE3,
DESERT_EAGLE_IDLE4,
DESERT_EAGLE_IDLE5,
DESERT_EAGLE_SHOOT,
DESERT_EAGLE_SHOOT_EMPTY,
DESERT_EAGLE_RELOAD,
DESERT_EAGLE_RELOAD_NOT_EMPTY,
DESERT_EAGLE_DRAW,
DESERT_EAGLE_HOLSTER,
};
LINK_ENTITY_TO_CLASS(weapon_eagle, CDesertEagle)
void CDesertEagle::Spawn()
{
}
void CDesertEagle::Precache()
{
}
int CDesertEagle::GetItemInfo(ItemInfo* p)
{
return 0;
}
int CDesertEagle::AddToPlayer(CBasePlayer* pPlayer)
{
return FALSE;
}
void CDesertEagle::SecondaryAttack()
{
}
void CDesertEagle::PrimaryAttack()
{
}
BOOL CDesertEagle::Deploy()
{
return FALSE;
}
void CDesertEagle::Holster(int skiplocal)
{
}
void CDesertEagle::Reload()
{
}
void CDesertEagle::WeaponIdle()
{
}
That's a start - the project will compile. But it won't do much at this point, so we should add a bit more code.
W_Precache
method. You'll see a bunch of calls to UTIL_PrecacheOther
and UTIL_PrecacheOtherWeapon
. All you need to do is add a new line to this method for your new weapon's classname. Ignore the #if !defined...
lines, they're not important. Add this line in the same place as the other precache calls, underneath the weapon_357
line is a good place.
UTIL_PrecacheOtherWeapon("weapon_eagle");
Now go back to deserteagle.cpp - let's start implementing some methods!
Spawn
method is called when the weapon entity appears in the world - when the gun is lying on the ground, ready to be picked up. Unless the weapon is given to the player by default, this is how the player will get your weapon. The Spawn
method looks very similar for most weapons. See the comments for the details of each line in this function.
void CDesertEagle::Spawn()
{
// Define the classname of the entity
// This is the name you should use to reference this entity name in your code base.
pev->classname = MAKE_STRING("weapon_eagle");
// Precache the weapon models and sounds
// This might be called by the engine separately, but it's best to call it here as well just in case.
Precache();
// Set the weapon ID
m_iId = WEAPON_DESERT_EAGLE;
// Tell the engine about the weapon's world model
SET_MODEL(ENT(pev), "models/w_desert_eagle.mdl");
// Set the default ammo value for the weapon
m_iDefaultAmmo = DESERT_EAGLE_DEFAULT_GIVE;
// Set up some default behaviour for the weapon
// This will tell the engine that the weapon should "fall" to the ground when it spawns.
// It also sets up the behaviour so that the weapon is equipped when the player touches it.
FallInit();
}
Precache
method is responsible for telling the engine what resources (sounds and models, mostly) that the weapon needs to have loaded in order to function correctly. This usually means the models for the weapon (world, model, and player) and the sounds for the weapon (shoot, reload, etc). Hopefully everything here is pretty understandable without extra explanation.
void CDesertEagle::Precache()
{
// Precache models
PRECACHE_MODEL("models/v_desert_eagle.mdl");
PRECACHE_MODEL("models/w_desert_eagle.mdl");
PRECACHE_MODEL("models/p_desert_eagle.mdl");
PRECACHE_MODEL("models/shell.mdl");
// Precache sounds
PRECACHE_SOUND("weapons/desert_eagle_fire.wav");
PRECACHE_SOUND("weapons/desert_eagle_sight.wav");
PRECACHE_SOUND("weapons/desert_eagle_sight2.wav");
}
You'll notice that desert_eagle_reload.wav isn't precached here. This is because the reload sound is only played via a model event, and all sounds in model events are already precached by the PRECACHE_MODEL
call. You only need to precache models or sounds when you directly reference them somewhere in the code. The models/shell.mdl file is used by the client-side code later, to eject an empty shell after a shot.Precache
method are "events" - used for client prediction. We won't go into detail for events or client prediction yet - that's covered later in this article.
GetItemInfo
method populates a ItemInfo
struct in order to send information about the weapon to the client. Without this method, the weapon cannot be used in any way. The method should return TRUE
(or 1
) when it's done. Most of the values here should be fairly obvious, so just take a look at the implementation and comments below. Remember that we're reusing the 357 ammo, so it's referenced a few times here.
int CDesertEagle::GetItemInfo(ItemInfo* p)
{
// This should match the classname - the HUD uses it to find the matching .txt file in the sprites/ folder
p->pszName = STRING(pev->classname);
// The "primary" ammo type for this weapon and the maximum ammo of that type that the player can hold
p->pszAmmo1 = "357";
p->iMaxAmmo1 = _357_MAX_CARRY;
// Same as above, but for "secondary" ammo. This should be NULL and -1 for weapons with no secondary
p->pszAmmo2 = NULL;
p->iMaxAmmo2 = -1;
// The size of a full clip
p->iMaxClip = DESERT_EAGLE_MAX_CLIP;
// Special weapon flags - leave this as 0 for now, this is covered in a different article
p->iFlags = 0;
// The "slot" in the HUD that the weapon appears in. This is a pistol, so it goes into slot 1 with the others
p->iSlot = 1;
// The "position" in the HUD that the weapon is added to. We'll put this after the magnum (which is in slot 1)
p->iPosition = 2;
// Set the ID and auto-switching weights of the weapon
p->iId = m_iId = WEAPON_DESERT_EAGLE;
p->iWeight = DESERT_EAGLE_WEIGHT;
return TRUE;
}
CBasePlayerWeapon
, and then sends a message to the client so the HUD can show the weapon icon in the "history" section.
int CDesertEagle::AddToPlayer(CBasePlayer* pPlayer)
{
// AddToPlayer returns TRUE if the weapon was picked up
if (CBasePlayerWeapon::AddToPlayer(pPlayer))
{
// Send a message to the client so the pickup icon can be shown
MESSAGE_BEGIN(MSG_ONE, gmsgWeapPickup, NULL, pPlayer->pev);
WRITE_BYTE(m_iId);
MESSAGE_END();
return TRUE;
}
return FALSE;
}
Deploy
method is quite simple for most weapons - you just call DefaultDeploy
with the appropriate arguments. That's what we'll do for the Desert Eagle as well:
BOOL CDesertEagle::Deploy()
{
return DefaultDeploy(
"models/v_desert_eagle.mdl", // Weapon view model
"models/p_desert_eagle.mdl", // Weapon player model
DESERT_EAGLE_DRAW, // "Draw" animation index for the view model
"onehanded", // Third person animation set for the weapon. We'll use the generic "onehanded" animation set
UseDecrement(), // Flag whether or not to do client prediction or not
pev->body // The weapon model's "body" pointer
);
}
Note reference to onehanded
here - this string refers to the animation set that should be used when the weapon is being fired by another player. For weapons that are similar to the stock weapons, you can use an existing animation set, which is what we're doing here. For weapons that require a different pose, you'll need to edit all of the models in the models/player/ folder in order to add a new animation set. This only applies to multiplayer and even then, is an optional change since it involves a lot of work. You can see the existing animation sets by opening any player model in a viewer and looking at the sequences named ref_aim_something
, crouch_aim_something
, ref_shoot_something
, and crouch_shoot_something
, where something
is the name of the animation set.
Holster
is slightly more complex than Deploy
, but not by much. This method should cancel any reload that's currently taking place, and then hide the weapon from view. For most weapons, this is done with a "holster" animation, but you can also just use the default implementation of the Holster
method, which just hides the weapons from view.
void CDesertEagle::Holster(int skiplocal)
{
// Cancel any reload in progress
m_fInReload = FALSE;
// Delay the next player's attack for about the same time as the holster animation takes
m_pPlayer->m_flNextAttack = UTIL_WeaponTimeBase() + 0.5;
// Play the "holster" animation
SendWeaponAnim(DESERT_EAGLE_HOLSTER);
}
A note about UTIL_WeaponTimeBase()
- this is a client-side call that will return a set delay, if required by the client for prediction purposes. When setting delay values in weapons code, you should always add UTIL_WeaponTimeBase()
to the value to ensure the client prediction works properly.
// HLDM Weapon placeholder entities.
CGlock g_Glock;
// ... more code ...
CSqueak g_Snark;
CDesertEagle g_DesertEagle; // Add this line
Next, scroll down and find the HUD_InitClientWeapons
method. Here you'll see a series of HUD_PrepEntity
calls - one for each weapon. Add a new line for your weapon.
// Allocate slot(s) for each weapon that we are going to be predicting
HUD_PrepEntity( &g_Glock , &player );
// ... more code ...
HUD_PrepEntity( &g_Snark , &player );
HUD_PrepEntity( &g_DesertEagle, &player ); // Add this line
Finally, scroll down a little more to find HUD_WeaponsPostThink
. There's a switch
statement here that you'll need to add a case
to:
switch ( from->client.m_iId )
{
case WEAPON_CROWBAR:
pWeapon = &g_Crowbar;
break;
// ... more code ...
// Add this case statement
case WEAPON_DESERT_EAGLE:
pWeapon = &g_DesertEagle;
break;
}
give weapon_eagle
in the console to verify that the weapon is showing up. Run give item_suit
as well, if you don't have one, so you can see the weapon in the HUD. It won't shoot at the moment or do anything useful - but you should be able to select it and see the weapon model in-game.
In relation to the HUD: we've taken a shortcut with the HUD sprites by copying the files directly from Opposing Force. Take a short break from programming to look at the files you copied from the sprites folder - specifically, the sprites/weapon_eagle.txt file. This file should be named the same as your weapon's classname, and must be present in order for the weapon to appear in the HUD. The file format is explained in detail in the High-Level Overview article, so be sure to read through it.impulse 101
, you'll notice that your weapon isn't given to the player. Let's take care of that and one other small thing before we continue. Open player.cpp and search for the CheatImpulseCommands
function. Somewhere in the case 101:
section, add a line for your weapon:
case 101:
gEvilImpulse101 = TRUE;
// ... code ...
GiveNamedItem( "weapon_eagle" ); // Add this line
// ... more code ...
gEvilImpulse101 = FALSE;
break;
The other small thing to take care of here is to add your weapon to the list of func_breakable drop items. Open func_break.cpp and at the top, you'll see an array called CBreakable::pSpawnObjects
. Add your weapon classname to this array. This list corresponds to the FGD file - don't forget to add it to the FGD when you're making changes to it.
const char *CBreakable::pSpawnObjects[] =
{
NULL, // 0
// ... code ...
"weapon_hornetgun", // 21
"weapon_eagle", // 22 <- add this line
};
Precache
method. Before we can do that, however, we need a new variable to hold the event resource. Go to weapons.h and add this field to the private
section of your CDesertEagle
class:
class CDesertEagle : public CBasePlayerWeapon
{
public:
// no changes in the public section
private:
unsigned short m_usFireDesertEagle;
};
Now go into deserteagle.cpp and add this to the end of the Precache
method:
// Precache fire event
m_usFireDesertEagle = PRECACHE_EVENT(1, "events/eagle.sc");
There's that event.sc file you copied earlier. The secret of this file is.... the contents don't matter. It can be empty, and it makes no difference. All that matters is that this file exists - the name of the file is used, and nothing else.EV_FireDesertEagle
at the top of the file, and a pfnHookEvent
call for your event inside Game_HookEvents
:
extern "C"
{
// ... code ...
void EV_FireDesertEagle( struct event_args_s *args );
}
void Game_HookEvents( void )
{
// ... more code ...
gEngfuncs.pfnHookEvent("events/eagle.sc", EV_FireDesertEagle);
}
Next, we'll go into ev_hldm.cpp and add the same definition to the top of the file:
extern "C"
{
// ... code ...
void EV_FireDesertEagle( struct event_args_s *args );
}
Scroll down a little and add the function implementation somewhere in this file. This code is mostly borrowed from the glock fire event, with small tweaks:
//======================
// DESERT EAGLE START
//======================
// Exactly the same enum from deserteagle.cpp, these
// values correspond to sequences in the viewmodel file
enum desert_eagle_e {
DESERT_EAGLE_IDLE1 = 0,
DESERT_EAGLE_IDLE2,
DESERT_EAGLE_IDLE3,
DESERT_EAGLE_IDLE4,
DESERT_EAGLE_IDLE5,
DESERT_EAGLE_SHOOT,
DESERT_EAGLE_SHOOT_EMPTY,
DESERT_EAGLE_RELOAD,
DESERT_EAGLE_RELOAD_NOT_EMPTY,
DESERT_EAGLE_DRAW,
DESERT_EAGLE_HOLSTER,
};
void EV_FireDesertEagle( event_args_t *args )
{
// Just a bunch of variables and boilerplate copy/paste code
int idx;
vec3_t origin;
vec3_t angles;
vec3_t velocity;
int empty;
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);
empty = args->bparam1;
AngleVectors(angles, forward, right, up);
shell = gEngfuncs.pEventAPI->EV_FindModelIndex("models/shell.mdl");// brass shell
// If the entity firing this event is the player
if (EV_IsLocal(idx))
{
// Render a muzzleflash
EV_MuzzleFlash();
// Show the weapon animation (a different one if this was the last bullet in the clip)
gEngfuncs.pEventAPI->EV_WeaponAnimation(empty ? DESERT_EAGLE_SHOOT_EMPTY : DESERT_EAGLE_SHOOT, 0);
// Apply some recoil to the player's view
V_PunchAxis(0, -4.0);
}
// Eject an empty bullet shell (the numbers here are mostly magic, experiment with them or just use whatever, it's not too important)
EV_GetDefaultShellInfo(args, origin, velocity, ShellVelocity, ShellOrigin, forward, right, up, -9.0, 14.0, 9.0);
EV_EjectBrass(ShellOrigin, ShellVelocity, angles[YAW], shell, TE_BOUNCE_SHELL);
// Play the "shoot" sound
gEngfuncs.pEventAPI->EV_PlaySound(idx, origin, CHAN_WEAPON, "weapons/desert_eagle_fire.wav", gEngfuncs.pfnRandomFloat(0.92, 1), ATTN_NORM, 0, 98 + gEngfuncs.pfnRandomLong(0, 3));
// Fire some bullets (this will do some prediction stuff, show a tracer, play texture sound, and render a decal where the bullet hits)
EV_GetGunPosition(args, vecSrc, origin);
VectorCopy(forward, vecAiming);
EV_HLDM_FireBullets(idx, forward, right, up, 1, vecSrc, vecAiming, 8192, BULLET_PLAYER_357, 0, 0, args->fparam1, args->fparam2);
}
//======================
// DESERT EAGLE END
//======================
That's everything for ev_hldm.cpp. Go back to deserteagle.cpp for the next parts.
void CDesertEagle::PrimaryAttack()
{
// Don't fire underwater - waterlevel 3 indicates that the player's head is underwater
if (m_pPlayer->pev->waterlevel == 3)
{
// Play a "click" and don't allow another primary attack for a short time
PlayEmptySound();
m_flNextPrimaryAttack = UTIL_WeaponTimeBase() + 0.15;
return;
}
// Check if the clip is empty
if (m_iClip <= 0)
{
if (!m_fInReload && m_fFireOnEmpty)
{
// If the player has fired previously, but is still holding the attack button down,
// just play the empty "click" sound until the player releases the button.
PlayEmptySound();
m_flNextPrimaryAttack = UTIL_WeaponTimeBase() + 0.2;
}
return;
}
// If we get to this point - we're shooting!
m_pPlayer->m_iWeaponVolume = NORMAL_GUN_VOLUME;
m_pPlayer->m_iWeaponFlash = NORMAL_GUN_FLASH;
// Decrease the number of bullets in the clip
m_iClip--;
// Add a muzzleflash to the player effects
m_pPlayer->pev->effects |= EF_MUZZLEFLASH;
// Player "shoot" animation
m_pPlayer->SetAnimation(PLAYER_ATTACK1);
// Set global vectors in the engine (don't ask)
UTIL_MakeVectors(m_pPlayer->pev->v_angle + m_pPlayer->pev->punchangle);
// Shoot bullets!
Vector vecSrc = m_pPlayer->GetGunPosition();
Vector vecAiming = m_pPlayer->GetAutoaimVector(AUTOAIM_10DEGREES);
Vector vecDir = m_pPlayer->FireBulletsPlayer(
1, // Number of bullets to shoot
vecSrc, // The source of the bullets (i.e. the gun)
vecAiming, // The direction to fire in (i.e. where the player is pointing)
VECTOR_CONE_10DEGREES, // The accuracy spread of the weapon
8192, // The distance the bullet can go (8192 is the limit for the engine)
BULLET_PLAYER_357, // The type of bullet being fired
0, // Number of tracer bullets to fire (none in this case)
0, // Set to non-zero to override the amount of damage (usually, leave this as 0)
m_pPlayer->pev, // Attacker entity
m_pPlayer->random_seed // The random seed
);
int flags;
#if defined( CLIENT_WEAPONS )
flags = FEV_NOTHOST;
#else
flags = 0;
#endif
PLAYBACK_EVENT_FULL(flags, m_pPlayer->edict(), m_usFireDesertEagle, 0.0, (float*)&g_vecZero, (float*)&g_vecZero, vecDir.x, vecDir.y, 0, 0, (m_iClip == 0) ? 1 : 0, 0);
// If the clip is now empty and there's no more ammo available, update the HEV
if (!m_iClip && m_pPlayer->m_rgAmmo[m_iPrimaryAmmoType] <= 0)
{
// HEV suit - indicate out of ammo condition
m_pPlayer->SetSuitUpdate("!HEV_AMO0", FALSE, 0);
}
// The desert eagle can fire quite quickly with no laser spot, so use a 250ms delay
m_flNextPrimaryAttack = m_flNextSecondaryAttack = UTIL_WeaponTimeBase() + 0.25;
// Set the time until the weapon should start idling again
m_flTimeWeaponIdle = UTIL_WeaponTimeBase() + UTIL_SharedRandomFloat(m_pPlayer->random_seed, 10, 15);
}
DefaultReload
function.
void CDesertEagle::Reload()
{
// Don't reload if the player doesn't have any ammo
if (m_pPlayer->ammo_357 <= 0) return;
int iResult;
// The view model has two different animations depending on if there are any bullets in the clip
if (m_iClip == 0) iResult = DefaultReload(DESERT_EAGLE_MAX_CLIP, DESERT_EAGLE_RELOAD, 1.5);
else iResult = DefaultReload(DESERT_EAGLE_MAX_CLIP, DESERT_EAGLE_RELOAD_NOT_EMPTY, 1.5);
if (iResult)
{
// If the reload happened, then reset the weapon's idle time
m_flTimeWeaponIdle = UTIL_WeaponTimeBase() + UTIL_SharedRandomFloat(m_pPlayer->random_seed, 10, 15);
}
}
WeaponIdle
must be implemented to make sure idle animations are shown while the player isn't doing anything while holding the gun. It can also be used to randomly switch between different idle animations to keep things interesting. The Desert Eagle model has 5 idle animations, so we use the random number utility to switch between each of them:
void CDesertEagle::WeaponIdle()
{
// This is used in conjunction with the PlayEmptySound function.
// This resets a flag so the "click" for an empty weapon can be replayed after a short delay
ResetEmptySound();
// Despite the name, this will SET the autoaim vector.
// 10 degrees is what the magnum uses, so we'll use the same.
m_pPlayer->GetAutoaimVector(AUTOAIM_10DEGREES);
// Exit out of the method if the weapon time hasn't passed yet or if the clip is empty
if (m_flTimeWeaponIdle > UTIL_WeaponTimeBase() || m_iClip <= 0) return;
// Weapon idle is only called after the weapon hasn't been used (fired or reloaded)
// for a while. In this case we want to play one of the idle animations for the weapon.
// The desert eagle view model has 5 different idle animations, and we'll give each one
// a 20% chance of playing, using the random number util function.
int iAnim;
float flRand = UTIL_SharedRandomFloat(m_pPlayer->random_seed, 0, 1);
if (flRand <= 0.2)
{
// The numbers here (76.0 / 30.0) are a way to represent the time taken by the
// animation, so the next idle animation isn't played before the current one has
// been completed. This animation is 76 frames long, and runs at 30 frames per second.
iAnim = DESERT_EAGLE_IDLE1;
m_flTimeWeaponIdle = UTIL_WeaponTimeBase() + (76.0 / 30.0);
}
else if (flRand <= 0.4)
{
iAnim = DESERT_EAGLE_IDLE2;
m_flTimeWeaponIdle = UTIL_WeaponTimeBase() + (61.0 / 24.0);
}
else if (flRand <= 0.6)
{
iAnim = DESERT_EAGLE_IDLE3;
m_flTimeWeaponIdle = UTIL_WeaponTimeBase() + (50.0 / 30.0);
}
else if (flRand <= 0.8)
{
iAnim = DESERT_EAGLE_IDLE4;
m_flTimeWeaponIdle = UTIL_WeaponTimeBase() + (76.0 / 30.0);
}
else
{
iAnim = DESERT_EAGLE_IDLE5;
m_flTimeWeaponIdle = UTIL_WeaponTimeBase() + (61.0 / 30.0);
}
// Play the idle animation
SendWeaponAnim(iAnim, UseDecrement(), pev->body);
}
CDesertEagle
class. These changes are directly from the CRpg
class and are required for the laser spot to work. Note that the CLaserSpot
class is referenced here - so make sure your CDesertEagle
is defined after the CLaserSpot
class to avoid compile errors.#ifndef CLIENT_DLL
int Save(CSave& save);
int Restore(CRestore& restore);
static TYPEDESCRIPTION m_SaveData[];
#endif
And after WeaponIdle()
:
void UpdateSpot();
BOOL ShouldWeaponIdle() { return TRUE; };
CLaserSpot* m_pSpot;
int m_fSpotActive;
The Save
and Restore
methods need to be added now that we've added a custom field to the class - m_fSpotActive
. This will be a flag to detect when the spot is active. To maintain this state between saves, we need to implement save/restore for the class. This is also why the field needs to be public - so the save/restore code can access it. We won't need to save the actual laser spot object (m_pSpot
), since we can create this as-needed.UpdateSpot
method is where the logic will go to .... update the spot. The other change - ShouldWeaponIdle
is slightly more nuanced. By default, the WeaponIdle
method is only called when the player isn't doing something else (such as shooting or reloading). By overriding ShouldWeaponIdle
, we can force the WeaponIdle
method to always be called. We do this so we can always call UpdateSpot
so it can stay up-to-date.
TYPEDESCRIPTION CDesertEagle::m_SaveData[] =
{
DEFINE_FIELD(CDesertEagle, m_fSpotActive, FIELD_INTEGER),
};
IMPLEMENT_SAVERESTORE(CDesertEagle, CBasePlayerWeapon);
CRpg
class, so take a look there for more information.Precache
method. This simply ensures that the laser spot's resources are precached properly.
UTIL_PrecacheOther( "laser_spot" );
Holster
code so that the laser is killed when the weapon is unequipped:
#ifndef CLIENT_DLL
// If the laser spot exists, kill it
if (m_pSpot)
{
m_pSpot->Killed(NULL, GIB_NEVER);
m_pSpot = NULL;
}
#endif
UpdateSpot()
to the very top of the WeaponIdle
function, above ResetEmptySound()
. Next, if you take a close look at the idle animations for the Desert Eagle in Opposing Force, you'll notice that 3 of the idle animations only play when the laser is off, and the other 2 only play when the laser is on. We'll update this method so that this behaviour is the same as well. Here's the updated version of this function:
void CDesertEagle::WeaponIdle()
{
// Update the laser spot
UpdateSpot();
// This is used in conjunction with the PlayEmptySound function.
// This resets a flag so the "click" for an empty weapon can be replayed after a short delay
ResetEmptySound();
// Despite the name, this will SET the autoaim vector.
// 10 degrees is what the magnum uses, so we'll use the same.
m_pPlayer->GetAutoaimVector(AUTOAIM_10DEGREES);
// Exit out of the method if the weapon time hasn't passed yet or if the clip is empty
if (m_flTimeWeaponIdle > UTIL_WeaponTimeBase() || m_iClip <= 0) return;
// Weapon idle is only called after the weapon hasn't been used (fired or reloaded)
// for a while. In this case we want to play one of the idle animations for the weapon.
// The desert eagle view model has 5 different idle animations, 1-3 will play when the laser
// is off, and 4-5 will play when the laser is on.
int iAnim;
float flRand = UTIL_SharedRandomFloat(m_pPlayer->random_seed, 0, 1);
// When the laser is on, we'll add 1 to the random result so that laser off
// animations play when the value is between 0 and 1, and laser on animations
// play when the value is between 1 and 2.
if (m_fSpotActive) flRand += 1.01; // Add an extra .01 to ensure it's always > 1
// Laser off animations
if (flRand <= 0.333)
{
// The numbers here (76.0 / 30.0) are a way to represent the time taken by the
// animation, so the next idle animation isn't played before the current one has
// been completed. This animation is 76 frames long, and runs at 30 frames per second.
iAnim = DESERT_EAGLE_IDLE1;
m_flTimeWeaponIdle = UTIL_WeaponTimeBase() + (76.0 / 30.0);
}
else if (flRand <= 0.667)
{
iAnim = DESERT_EAGLE_IDLE2;
m_flTimeWeaponIdle = UTIL_WeaponTimeBase() + (61.0 / 24.0);
}
else if (flRand <= 1)
{
iAnim = DESERT_EAGLE_IDLE3;
m_flTimeWeaponIdle = UTIL_WeaponTimeBase() + (50.0 / 30.0);
}
// Laser on animations
else if (flRand <= 1.5)
{
iAnim = DESERT_EAGLE_IDLE4;
m_flTimeWeaponIdle = UTIL_WeaponTimeBase() + (76.0 / 30.0);
}
else
{
iAnim = DESERT_EAGLE_IDLE5;
m_flTimeWeaponIdle = UTIL_WeaponTimeBase() + (61.0 / 30.0);
}
// Play the idle animation
SendWeaponAnim(iAnim, UseDecrement(), pev->body);
}
if (iResult)
block in the Reload
function:
#ifndef CLIENT_DLL
// If the player is reloading, hide the laser until the reload is complete
if (m_pSpot && m_fSpotActive)
{
m_pSpot->Suspend(1.6);
m_flNextSecondaryAttack = UTIL_WeaponTimeBase() + 1.6;
}
#endif
PrimaryAttack
method: the accuracy should be better, shots should be slower, and the laser should be hidden for a short time during the fire animation. Let's take care of each of these.FireBulletsPlayer
call. We'll simply change VECTOR_CONE_10DEGREES
to VECTOR_CONE_1DEGREES
when the spot is active. This is what we end up with:
Vector vecDir = m_pPlayer->FireBulletsPlayer(
1, // Number of bullets to shoot
vecSrc, // The source of the bullets (i.e. the gun)
vecAiming, // The direction to fire in (i.e. where the player is pointing)
m_fSpotActive ? VECTOR_CONE_1DEGREES : VECTOR_CONE_10DEGREES, // The accuracy spread of the weapon
8192, // The distance the bullet can go (8192 is the limit for the engine)
BULLET_PLAYER_357, // The type of bullet being fired
0, // Number of tracer bullets to fire (none in this case)
0, // Set to non-zero to override the amount of damage (usually, leave this as 0)
m_pPlayer->pev, // Attacker entity
m_pPlayer->random_seed // The random seed
);
Making shots slower - this is simply a matter of changing the value of m_flNextPrimaryAttack
, towards the end of the method. We'll double the delay if the spot is active. Here's the result:
// The desert eagle can fire quite quickly with no laser spot, so use a 250ms delay
// When the spot is active, the delay should be 500ms instead.
m_flNextPrimaryAttack = m_flNextSecondaryAttack = UTIL_WeaponTimeBase() + (m_fSpotActive ? 0.5 : 0.25);
Hiding the laser - similar to the reload function, we'll simply hide the laser for a short time. This goes directly after the previous code:
#ifndef CLIENT_DLL
// Hide the laser until the player can shoot again
if (m_pSpot && m_fSpotActive)
{
m_pSpot->Suspend(0.6);
m_flNextSecondaryAttack = UTIL_WeaponTimeBase() + 0.6;
}
#endif
Last but not least, we'll call the UpdateSpot
method to make sure it stays up-to-date. This goes right at the end:
// Keep the laser updated
UpdateSpot();
SecondaryAttack
should look like:
void CDesertEagle::SecondaryAttack()
{
// Toggle the laser
m_fSpotActive = !m_fSpotActive;
#ifndef CLIENT_DLL
// If the laser is being turned off, kill the laser
if (!m_fSpotActive && m_pSpot)
{
m_pSpot->Killed(NULL, GIB_NORMAL);
m_pSpot = NULL;
}
#endif
if (m_fSpotActive)
{
// If the spot is being turned on, play the ON sound and delay for a short time
EMIT_SOUND(ENT(m_pPlayer->pev), CHAN_ITEM, "weapons/desert_eagle_sight.wav", 1, ATTN_NORM);
m_flNextSecondaryAttack = UTIL_WeaponTimeBase() + 0.25;
}
else
{
// If the spot is being turned off, play the OFF sound and delay for a slightly longer time
EMIT_SOUND(ENT(m_pPlayer->pev), CHAN_ITEM, "weapons/desert_eagle_sight2.wav", 1, ATTN_NORM);
m_flNextSecondaryAttack = UTIL_WeaponTimeBase() + 0.5;
}
}
UpdateSpot
. This is just a copy-paste from the CRpg
class. Here's the code:
void CDesertEagle::UpdateSpot(void)
{
#ifndef CLIENT_DLL
// If the spot is active
if (m_fSpotActive)
{
// Make sure the spot entity has been created
if (!m_pSpot)
{
m_pSpot = CLaserSpot::CreateSpot();
}
// Perform a TraceLine to find the point that the laser shines on
UTIL_MakeVectors(m_pPlayer->pev->v_angle);
Vector vecSrc = m_pPlayer->GetGunPosition();
Vector vecAiming = gpGlobals->v_forward;
TraceResult tr;
UTIL_TraceLine(vecSrc, vecSrc + vecAiming * 8192, dont_ignore_monsters, ENT(m_pPlayer->pev), &tr);
// Put the laser spot at the end of the trace
UTIL_SetOrigin(m_pSpot->pev, tr.vecEndPos);
}
#endif
}
Deploy
function to handle this, but there's a different way. Instead of playing the "laser on" sound inside SecondaryAttack
, we could play it when the laser is created in the UpdateSpot
method.SecondaryAttack
method, and cut the EMIT_SOUND
line for when the laser is being turned on. Keep it in the clipboard, because we'll be pasting it into UpdateSpot
next.SecondaryAttack
:
if (m_fSpotActive)
{
// If the spot is being turned on, play the ON sound and delay for a short time
// Note: The "on" sound is handled by the UpdateSpot function so it works on Deploy as well!
m_flNextSecondaryAttack = UTIL_WeaponTimeBase() + 0.25;
}
else
// ... more code ...
Now go over to UpdateSpot
and add the EMIT_SOUND
line in:
// Make sure the spot entity has been created
if (!m_pSpot)
{
m_pSpot = CLaserSpot::CreateSpot();
// Play the "laser on" sound here, so it plays after a Deploy as well as a SecondaryAttack
EMIT_SOUND(ENT(m_pPlayer->pev), CHAN_ITEM, "weapons/desert_eagle_sight.wav", 1, ATTN_NORM);
}
// ... more code ...
UpdateSpot
, when creating the laser spot, simply set it's scale to 0.5
. That's it!
// Make sure the spot entity has been created
if (!m_pSpot)
{
m_pSpot = CLaserSpot::CreateSpot();
m_pSpot->pev->scale = 0.5;
// Play the "laser on" sound here, so it plays after a Deploy as well as a SecondaryAttack
EMIT_SOUND(ENT(m_pPlayer->pev), CHAN_ITEM, "weapons/desert_eagle_sight.wav", 1, ATTN_NORM);
}
Now the size of the laser spot will match Opposing Force. You might notice that OP4 uses a different sprite for the laser spot - if you want to use it, simply copy the gearbox/sprites/laserdot.spr file into your mod. If you want you mod to use a different sprite for the Desert Eagle, you'll need to make further changes to the CLaserSpot
class in order to make the sprite it uses more configurable.
@PointClass base(Weapon, Targetx) studio("models/w_desert_eagle.mdl") = weapon_eagle : "Desert Eagle" []
And don't forget about the change to the func_breakable definition as well! It's in the Breakable
base class, just add your number and description for the weapon to the end:
@BaseClass base(Targetname,Global) = Breakable
[
...
spawnobject(choices) : "Spawn On Break" : 0 =
[
0: "Nothing"
...
21: "Hornet Gun"
22: "Desert Eagle"
]
...
]
You must log in to post a comment. You can login or register a new account.
@sadpepe: The classname in this tutorial is
weapon_eagle
, it's possible that in your console and in the impulse 101 code, you typed deagle by mistake. The names need to match or nothing will happen.@The Skeleton: If you are not willing to spend a bit of time to learn, then programming is not for you. This tutorial is intended to be useful, not to entertain you.
@Chris I Guess: Start with the Half-Life Programming - Getting Started guide. It will tell you how to get these files. Note that if you don't know how to code, editing the HL SDK is going to be a very painful learning experience.