SecondaryFire
method in the weapon class to switch between full-auto and semi-auto. In this tutorial, we are going to redo the same weapon, but this time, we are going to use the secondary fire mode to switch between zoomed in and zoomed out, like a sniper rifle. To keep it simple, we are going to pretend that this is an M16 with a sniper scope on it so that you can compare this tutorial with the last. You will notice that much of this#ifndef TOD_M16_H
#define TOD_M16_H
class CM16 : public CBasePlayerWeapon
{
public:
virtual void Spawn( void );
virtual void Precache( void );
virtual int iItemSlot( void ) { return M16_SLOT; }
virtual int GetItemInfo(ItemInfo *p);
virtual int AddToPlayer( CBasePlayer *pPlayer );
virtual void PrimaryAttack( void );
virtual void SecondaryAttack( void ); // switch between scope and iron sights
virtual BOOL Deploy( void );
virtual void Holster( int skiplocal = 0 );
virtual void Reload( void );
virtual void WeaponIdle( void );
virtual BOOL UseDecrement( void ) { return TRUE; }
virtual float Accuracy(float acc);
private:
int m_iShell;
BOOL InZoom() { return m_pPlayer->pev->fov != 0; }
unsigned short m_event;
unsigned short m_event_z;
};
class CM16AmmoClip : public CBasePlayerAmmo
{
virtual void Spawn( void );
virtual void Precache( void );
virtual BOOL AddAmmo( CBaseEntity *pOther ) ;
};
#endif
I am using the SecondaryAttack
method to zoom in on like on a sniper rifle. The only change here is that I have replaced the m_bSingleFire
variable with a InZoom()
variable and added another event variable for the zoomed in event. Also notice that we are overriding the Holster method from the base class, you will see why later.#include "extdll.h"
#include "util.h"
#include "cbase.h"
#include "monsters.h"
#include "weapons.h"
#include "nodes.h"
#include "player.h"
#include "soundent.h"
#include "gamerules.h"
#include "m16.h"
enum m16a1_e
{
M16_IDLE_SIL = 0,
M16_IDLE,
M16_FIRE1,
M16_FIRE2,
M16_FIRE3,
M16_RELOAD,
M16_DEPLOY,
};
void CM16::Spawn( )
{
pev->classname = MAKE_STRING("weapon_m16"); // hack to allow for old names
Precache( );
SET_MODEL(ENT(pev), M16_MODEL_WORLD);
m_iId = WEAPON_M16;
m_iDefaultAmmo = M16_DEFAULT_AMMO;
FallInit();// get ready to fall down.
}
There are no changes in this section.
void CM16::Precache( void )
{
PRECACHE_MODEL(M16_MODEL_1STPERSON);
PRECACHE_MODEL(M16_MODEL_3RDPERSON);
PRECACHE_MODEL(M16_MODEL_WORLD);
m_iShell = PRECACHE_MODEL ("models/shell.mdl");// brass shell
PRECACHE_SOUND (M16_SOUND_FIRE1);
PRECACHE_SOUND (M16_SOUND_FIRE2);
m_event = PRECACHE_EVENT( 1, "events/m16.sc" );
m_event_z = PRECACHE_EVENT( 1, "events/m16z.sc" );
}
I have added the second event for the zoomed in fire, make sure you create the empty m16z.sc file in the events directory.
int CM16::GetItemInfo(ItemInfo *p)
{
p->pszName = STRING(pev->classname);
p->pszAmmo1 = "ammo_m16"; // The type of ammo it uses
p->iMaxAmmo1 = M16_MAX_AMMO; // Max ammo the player can carry
p->pszAmmo2 = NULL; // No secondary ammo
p->iMaxAmmo2 = -1;
p->iMaxClip = M16_DEFAULT_AMMO; // The clip size
p->iSlot = M16_SLOT - 1; // The number in the HUD
p->iPosition = M16_POSITION; // The position in a HUD slot
p->iFlags = ITEM_FLAG_NOAUTOSWITCHEMPTY | ITEM_FLAG_SELECTONEMPTY;
p->iId = m_iId = WEAPON_M16; // The weapon id
p->iWeight = M16_WEIGHT; // for autoswitching
return 1;
}
int CM16::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;
}
BOOL CM16::Deploy( )
{
return DefaultDeploy( M16_MODEL_1STPERSON, M16_MODEL_3RDPERSON,
M16_DEPLOY, "mp5" );
}
No changes in here, let's look at the primary attack since that is where most of our changes are.
void CM16::PrimaryAttack()
{
// This weapon is single shot when zoomed in, auto otherwise
if (!(m_pPlayer->m_afButtonPressed & IN_ATTACK) && InZoom())
return;
// don't fire underwater
if (m_pPlayer->pev->waterlevel == 3)
{
PlayEmptySound( );
m_flNextPrimaryAttack = 0.15;
return;
}
// don't fire if empty
if (m_iClip <= 0)
{
PlayEmptySound();
m_flNextPrimaryAttack = 0.15;
return;
}
// Weapon sound
m_pPlayer->m_iWeaponVolume = NORMAL_GUN_VOLUME;
m_pPlayer->m_iWeaponFlash = NORMAL_GUN_FLASH;
// one less round in the clip
m_iClip--;
// add a muzzle flash
m_pPlayer->pev->effects = (int)(m_pPlayer->pev->effects) | EF_MUZZLEFLASH;
// player "shoot" animation
m_pPlayer->SetAnimation( PLAYER_ATTACK1 );
// fire off a round
Vector vecSrc(m_pPlayer->GetGunPosition());
Vector vecAim(m_pPlayer->GetAutoaimVector(AUTOAIM_2DEGREES));
// Accuracy of the weapon in degrees, zoomed in is way more accurate
Vector vecAcc(InZoom() ? VECTOR_CONE_1DEGREES : VECTOR_CONE_3DEGREES;
Vector vecDir(m_pPlayer->FireBulletsPlayer( 1, // how many shots
vecSrc,
vecAim,
vecAcc, // accuracy
8192, // max range
BULLET_PLAYER_M16, // bullet type
0, // tracer frequency
0, // damage
m_pPlayer->pev,
m_pPlayer->random_seed ));
// Fire off the client side event
PLAYBACK_EVENT_FULL( FEV_NOTHOST, m_pPlayer->edict(),
(m_InZoom ? m_event_z :m_event), 0.0,
(float *)&g_vecZero, (float *)&g_vecZero,
vecDir.x, vecDir.y, 0, 0, (m_iClip ? 0 : 1), 0 );
// Add a delay before the player can fire the next shot
m_flNextPrimaryAttack = UTIL_WeaponTimeBase() + M16_FIRE_DELAY;
m_flTimeWeaponIdle = UTIL_WeaponTimeBase() +
UTIL_SharedRandomFloat(m_pPlayer->random_seed,
M16_FIRE_DELAY + 1, M16_FIRE_DELAY + 2);
}
Starting at the top of this function, I have changed the code that makes the weapon single shot so that it only does it when it is zoomed in. The only other changes are in the accuracy, zoomed in is way more accurate and the PLAYBACK_EVENT_FULL
chooses which event to send based on whether or not it is zoomed in.
// Zoom in/out
void CM16::SecondaryAttack( void )
{
if ( m_pPlayer->pev->fov != 0 )
{
m_pPlayer->pev->fov = m_pPlayer->m_iFOV = 0; // 0 means reset to default fov
}
else if ( m_pPlayer->pev->fov != 20 )
{
m_pPlayer->pev->fov = m_pPlayer->m_iFOV = 20;
}
pev->nextthink = UTIL_WeaponTimeBase() + 0.1;
m_flNextSecondaryAttack = UTIL_WeaponTimeBase() + 1.0;
}
Secondary attack is the biggest change. Here we change the field of view (fov) for the player. This simulates looking through a rifle scope. Changing m_pPlayer->pev->fov
automatically gets sent to the client.
void CM16::Holster( int skiplocal /* = 0 */ )
{
m_fInReload = FALSE;// cancel any reload in progress.
if ( InZoom() )
{
SecondaryAttack( );
}
m_pPlayer->m_flNextAttack = UTIL_WeaponTimeBase() + 0.5;
}
We must over-ride the Holster method so that we can reset the field of view to normal before the weapon gets changed. If we don't do this, the next weapon selected will still have the same fov (it will look zoomed in.)
void CM16::Reload( void )
{
DefaultReload( M16_DEFAULT_AMMO, M16_RELOAD, M16_RELOAD_TIME );
}
void CM16::WeaponIdle( void )
{
ResetEmptySound( );
m_pPlayer->GetAutoaimVector( AUTOAIM_5DEGREES );
if (m_flTimeWeaponIdle > UTIL_WeaponTimeBase())
return;
SendWeaponAnim( M16_IDLE );
m_flTimeWeaponIdle = UTIL_SharedRandomFloat(m_pPlayer->random_seed, 10, 15);
}
void CM16AmmoClip::Spawn( void )
{
Precache( );
SET_MODEL(ENT(pev), "models/w_9mmARclip.mdl");
CBasePlayerAmmo::Spawn( );
}
void CM16AmmoClip::Precache( void )
{
PRECACHE_MODEL ("models/w_9mmARclip.mdl");
PRECACHE_SOUND("items/9mmclip1.wav");
}
BOOL CM16AmmoClip::AddAmmo( CBaseEntity *pOther )
{
int bResult = (pOther->GiveAmmo(M16_DEFAULT_AMMO, "ammo_m16",
M16_MAX_AMMO) != -1);
if (bResult)
{
EMIT_SOUND(ENT(pev), CHAN_ITEM, "items/9mmclip1.wav", 1, ATTN_NORM);
}
return bResult;
}
These methods round out our weapon and are the same as before. Now make the same changes as before to weapons.h. I will review them here for completeness, but make sure you look through the last tutorial if you have any questions.
#define WEAPON_HANDGRENADE 12
#define WEAPON_TRIPMINE 13
#define WEAPON_SATCHEL 14
#define WEAPON_SNARK 15
#define WEAPON_M16 16
This must be a unique value.
// weapon weight factors (for auto-switching) (-1 = noswitch)
#define CROWBAR_WEIGHT 0
#define GLOCK_WEIGHT 10
#define PYTHON_WEIGHT 15
#define MP5_WEIGHT 15
#define M16_WEIGHT 15
Our M16 will have the same switching weight as the MP5
#define M16_MODEL_1STPERSON "models/v_m16a1.mdl"
#define M16_MODEL_3RDPERSON "models/p_m16a1.mdl"
#define M16_MODEL_WORLD "models/w_m16a1.mdl"
#define M16_SOUND_FIRE1 "weapons/m16a1_fire-1.wav"
#define M16_SOUND_FIRE2 "weapons/m16a1_fire-2.wav"
#define M16_SOUND_VOLUME 0.85
#define M16_FIRE_DELAY 0.085 // was: .15 For comparison, glock's is 0.2
#define M16_RELOAD_TIME 2.0
#define M16_DEFAULT_AMMO 30
#define M16_MAX_AMMO 180
#define M16_SLOT 2
#define M16_POSITION 1
Same stuff here.
// 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_M16,
BULLET_MONSTER_9MM,
BULLET_MONSTER_MP5,
BULLET_MONSTER_12MM,
} Bullet;
Everything in weapons.h was the same, now let's make the same changes The other server side files;
else switch(iBulletType)
{
default:
case BULLET_PLAYER_9MM:
pEntity->TraceAttack(pevAttacker, gSkillData.plrDmg9MM,
vecDir, &tr;, DMG_BULLET);
break;
case BULLET_PLAYER_MP5:
case BULLET_PLAYER_M16:
pEntity->TraceAttack(pevAttacker, gSkillData.plrDmgMP5,
vecDir, &tr;, DMG_BULLET);
break;
const char *CBreakable::pSpawnObjects[] =
{
NULL, // 0
"item_battery", // 1
"item_healthkit", // 2
"weapon_9mmhandgun",// 3
"ammo_9mmclip", // 4
"weapon_9mmAR", // 5
"ammo_9mmAR", // 6
"ammo_ARgrenades", // 7
"weapon_shotgun", // 8
"ammo_buckshot", // 9
"weapon_crossbow", // 10
"ammo_crossbow", // 11
"weapon_357", // 12
"ammo_357", // 13
"weapon_rpg", // 14
"ammo_rpgclip", // 15
"ammo_gaussclip", // 16
"weapon_handgrenade",// 17
"weapon_tripmine", // 18
"weapon_satchel", // 19
"weapon_snark", // 20
"weapon_hornetgun", // 21
"weapon_m16",
"ammo_m16",
};
// called by worldspawn
void W_Precache(void)
{
memset(CBasePlayerItem::ItemInfoArray, 0,
sizeof(CBasePlayerItem::ItemInfoArray));
memset(CBasePlayerItem::AmmoInfoArray, 0,
sizeof(CBasePlayerItem::AmmoInfoArray));
giAmmoIndex = 0;
// custom items...
// m16
UTIL_PrecacheOtherWeapon( "weapon_m16" );
UTIL_PrecacheOther( "ammo_m16" );
Now, if you want to automatically give your weapon to a player in a game, you can do something like this... Open up multiplay_gamerules.cpp and give the player the weapon when they spawn.
void CHalfLifeMultiplay :: PlayerSpawn( CBasePlayer *pPlayer )
{
BOOL addDefault;
CBaseEntity *pWeaponEntity = NULL;
pPlayer->pev->weapons |= (1<Touch( pPlayer );
addDefault = FALSE;
}
if ( addDefault )
{
pPlayer->GiveNamedItem( "weapon_crowbar" );
pPlayer->GiveNamedItem( "weapon_9mmhandgun" );
pPlayer->GiveAmmo( 68, "9mm", _9MM_MAX_CARRY );// 4 full reloads
pPlayer->GiveNamedItem( "weapon_m16" );
pPlayer->GiveAmmo( M16_MAX_AMMO, "ammo_m16", M16_MAX_AMMO );
}
}
crosshair 320 crosshairs 24 0 24 24
autoaim 320 crosshairs 0 72 24 24
zoom 320 ch_sniper 0 0 256 256
zoom_autoaim 320 ch_sniper 0 0 256 256
If you need more information on this, check out this tutorial on weapon HUD sprites.void EV_TripmineFire( struct event_args_s *args );
void EV_SnarkFire( struct event_args_s *args );
void EV_FireM16( struct event_args_s *args );
void EV_FireM16Zoomed( struct event_args_s *args );
The first event is the same;
//======================
// M16 START
//======================
void EV_FireM16( event_args_t *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;
float flSpread = 0.01;
idx = args->entindex;
VectorCopy( args->origin, origin );
VectorCopy( args->angles, angles );
VectorCopy( args->velocity, velocity );
AngleVectors( angles, forward, right, up );
if ( EV_IsLocal( idx ) )
{
// Add muzzle flash to current weapon model
EV_MuzzleFlash();
gEngfuncs.pEventAPI->EV_WeaponAnimation(M16_FIRE1 +
gEngfuncs.pfnRandomLong(0,2), 2);
// This gives it a bit of kick, adjust the numbers to your liking
V_PunchAxis( 0, gEngfuncs.pfnRandomFloat( -2, 2 ) );
}
// Eject shells, adjust the last three numbers to move the start point that the
// shells eject from to fit your model
shell = gEngfuncs.pEventAPI->EV_FindModelIndex ("models/shell.mdl");
EV_GetDefaultShellInfo( args, origin, velocity, ShellVelocity,
ShellOrigin, forward, right, up, 12, -10, 7 );
EV_EjectBrass(ShellOrigin,
ShellVelocity,
angles[ YAW ],
shell,
TE_BOUNCE_SHELL);
// Play the fire sound
switch( gEngfuncs.pfnRandomLong( 0, 1 ) )
{
case 0:
gEngfuncs.pEventAPI->EV_PlaySound( idx, origin, CHAN_WEAPON,
"weapons/m16a1_fire-1.wav",
1, ATTN_NORM, 0,
94 + gEngfuncs.pfnRandomLong( 0, 0xf ) );
break;
case 1:
gEngfuncs.pEventAPI->EV_PlaySound( idx, origin, CHAN_WEAPON,
"weapons/m16a1_fire-2.wav",
1, ATTN_NORM, 0,
94 + gEngfuncs.pfnRandomLong( 0, 0xf ) );
break;
}
// Fire off the bullets client side
EV_GetGunPosition( args, vecSrc, origin );
VectorCopy( forward, vecAiming );
EV_HLDM_FireBullets( idx,
forward,
right,
up,
1,
vecSrc,
vecAiming,
8192,
BULLET_PLAYER_M16,
0,
0,
args->fparam1, // These are the accuracy passed from
args->fparam2 ); // PLAYBACK_EVENT_FULL on the server
}
The zoomed event is nearly the same, with only a few things left out that aren't needed.
//======================
// M16 Zoomed START
//======================
void EV_FireM16Zoomed( event_args_t *args )
{
vec3_t origin;
vec3_t angles;
vec3_t vecSrc, vecAiming;
vec3_t up, right, forward;
float flSpread = 0.01;
int idx = args->entindex;
VectorCopy( args->origin, origin );
VectorCopy( args->angles, angles );
AngleVectors( angles, forward, right, up );
if ( EV_IsLocal( idx ) )
{
// Add muzzle flash to current weapon model
EV_MuzzleFlash();
// This gives it a bit of kick, adjust the numbers to your liking
V_PunchAxis( 0, gEngfuncs.pfnRandomFloat( -2, 2 ) );
}
// Play the fire sound
switch( gEngfuncs.pfnRandomLong( 0, 1 ) )
{
case 0:
gEngfuncs.pEventAPI->EV_PlaySound( idx, origin, CHAN_WEAPON,
"weapons/m16a1_fire-1.wav",
1, ATTN_NORM, 0,
94 + gEngfuncs.pfnRandomLong( 0, 0xf ) );
break;
case 1:
gEngfuncs.pEventAPI->EV_PlaySound( idx, origin, CHAN_WEAPON,
"weapons/m16a1_fire-2.wav",
1, ATTN_NORM, 0,
94 + gEngfuncs.pfnRandomLong( 0, 0xf ) );
break;
}
// Fire off the bullets client side
EV_GetGunPosition( args, vecSrc, origin );
VectorCopy( forward, vecAiming );
EV_HLDM_FireBullets( idx,
forward,
right,
up,
1,
vecSrc,
vecAiming,
8192,
BULLET_PLAYER_M16,
0,
0,
args->fparam1, // These are the accuracy passed from
args->fparam2 ); // PLAYBACK_EVENT_FULL on the server
}
The only big difference between this and the other event is that we don't bother to eject shells or do the weapon animation since the weapon is zoomed in and the player won't be able to see it anyway.void EV_HornetGunFire( struct event_args_s *args );
void EV_SnarkFire( struct event_args_s *args );
void EV_FireM16( struct event_args_s *args );
void EV_FireM16Zoomed( struct event_args_s *args );
Then further down;
void Game_HookEvents( void )
{
gEngfuncs.pfnHookEvent( "events/python.sc", EV_FirePython );
gEngfuncs.pfnHookEvent( "events/gauss.sc", EV_FireGauss );
gEngfuncs.pfnHookEvent( "events/gaussspin.sc", EV_SpinGauss );
gEngfuncs.pfnHookEvent( "events/crossbow1.sc", EV_FireCrossbow );
gEngfuncs.pfnHookEvent( "events/crossbow2.sc", EV_FireCrossbow2 );
gEngfuncs.pfnHookEvent( "events/shotgun1.sc", EV_FireShotGunSingle );
gEngfuncs.pfnHookEvent( "events/shotgun2.sc", EV_FireShotGunDouble );
gEngfuncs.pfnHookEvent( "events/egon_fire.sc", EV_EgonFire );
gEngfuncs.pfnHookEvent( "events/egon_stop.sc", EV_EgonStop );
gEngfuncs.pfnHookEvent( "events/firehornet.sc", EV_HornetGunFire );
gEngfuncs.pfnHookEvent( "events/snarkfire.sc", EV_SnarkFire );
gEngfuncs.pfnHookEvent( "events/rpg.sc", EV_FireRpg );
gEngfuncs.pfnHookEvent( "events/tripfire.sc", EV_TripmineFire );
gEngfuncs.pfnHookEvent( "events/m16.sc", EV_FireM16 );
gEngfuncs.pfnHookEvent( "events/m16z.sc", EV_FireM16Zoomed );
The only diffence here is the extra event hook. The remaining changes#include "extdll.h"
#include "util.h"
#include "cbase.h"
#include "monsters.h"
#include "weapons.h"
#include "nodes.h"
#include "player.h"
#include "usercmd.h"
#include "entity_state.h"
#include "demo_api.h"
#include "pm_defs.h"
#include "event_api.h"
#include "r_efx.h"
#include "../hud_iface.h"
#include "../com_weapons.h"
#include "../demo.h"
#include "m16.h"
Then add a global for your weapon near the top with the rest of the weapons.
CTripmine g_Tripmine;
CSqueak g_Snark;
CM16 g_M16;
Add a HUD_PrepEntity
for the weapon in HUD_InitClientWeapons()
.
HUD_PrepEntity( &g_Tripmine , &player; );
HUD_PrepEntity( &g_Snark , &player; );
HUD_PrepEntity( &g_M16 , &player; );
Now, add your weapon to the switch statement in HUD_WeaponsPostThink()
switch ( from->client.m_iId )
{
case WEAPON_CROWBAR:
pWeapon = &g_Crowbar;
break;
case WEAPON_GLOCK:
pWeapon = &g_Glock;
break;
case WEAPON_M16:
pWeapon = &g_M16;
break;
There is also what I think is a bug in the SDK that prevents the weapon from zooming properly. Open up hud.cpp and find the method;
int CHud::MsgFunc_SetFOV(const char *pszName, int iSize, void *pbuf)
and comment out these two lines near the beginning,
//if ( cl_lw && cl_lw->value )
// return 1;
That's it.
You must log in to post a comment. You can login or register a new account.