Authoring a weapon entity
It should come as no surprise that Source has very robust weapon systems. This article covers the steps required to create both hitscan and projectile weapons, and how to correctly set them up for use in multiplayer.
Overview
- Most properties of a gun can be configured with a script file
- If all you want to do is spit out bullets at a constant rate the amount of actual programming required is minimal.
- Weaponsfire can be predicted by the client and lag-compensated by the server
- This is normally only done with hitscan bullets as rockets, grenades, and other visible projectiles materialising out of thin air on observers' computers is hard to pass off!
- The viewmodel is a separate entity
- It is created by the player and shared between all weapons.
- There is a wide selection of weapons in the SDK to learn from
- Valve provide source code from both singleplayer and multiplayer games.
Weapon scripts
A weapon script defines most of the properties of a weapon including its models, ammunition, sounds, magazine size, and UI name. Using, extending and encrypting weapon scripts is covered in their article.
A weapon script will automatically be loaded for any weapon with an assigned classname. To easily precache script-defined resources call PRECACHE_WEAPON_REGISTER(classname)
- no quotemarks around the classname.
Entry points
There are four functions on a weapon which are called each frame by the owning player entity:
ItemPreFrame()
- Called before the player has moved. In Episode Two, decides whether now is a good time to display a HUD hint on weapon usage.
ItemHolsterFrame()
- Called before the player has moved, if the weapon is not active. Does nothing by default.
ItemBusyFrame()
- Called after the player has moved, if the player cannot fire. Does nothing by default.
ItemPostFrame()
- Called after the player has moved. The most important function, as it leads to one of the following:
PrimaryAttack()
SecondaryAttack()
HandleFireOnEmpty()
Reload()
WeaponIdle()
Shooting
A gun will typically shoot when the +attack key is active and (m_flNextPrimaryAttack <= gpGlobals->curtime)
. For secondary fire, +attack2 and m_flNextSecondaryAttack
are checked instead.
Bullets
Hitscan bullets are fired by passing a FireBulletsInfo_t
object to GetOwner()->FireBullets()
.
If all you want is a primary fire attack that spits out a constant stream of bullets (no matter whether they are 2 or 0.2 seconds apart) CBaseCombatWeapon
is already up to the job and you only need write two functions:
float GetFireRate()
- Seconds between each bullet.
const Vector& GetBulletSpread()
- Cone within which bullets will be randomly spread. Use the
VECTOR_CONE_*
#defines to avoid doing the maths yourself.
Skip on to the networking section if this fits your bill; effects will be handled for you. Otherwise see FireBulletsInfo_t
for details on setting up your own hitscan attack.
Projectiles
Rockets, grenades, and other projectiles are entities in their own right which exist independently of the weapon they came from. The weapon should spawn them with Create()
:
CBaseEntity::Create( "my_projectile", GetOwner()->Weapon_ShootPosition(), GetOwner()->EyeAngles(), GetOwnerEntity() );
Note that the owner is the player who own the weapon (GetOwnerEntity()
), not the weapon itself.
Further reading:
- Projectiles (for projectile design issues)
- Authoring a Model Entity (for creating QPhysics entities)
CPhysicsProp
(for creating VPhysics entities)
Effects
This section assumes you are using the new particle system, which is driven by artists. The old, hard-coded particle tech was never documented.
Tracers
Tracers should be dispatched from CBaseEntity::MakeTracer()
. CBasePlayer
is set up to emit the old hard-coded tracer effects; if you want to make changes it's best to implement particles. Doing so is easy:
// ALL IN SHARED CODE
// always emit tracers; let the particle system determine volume
// (if you don't mind editing CBasePlayer, just change m_iTracerFreq there)
void CMyPlayer::FireBullets( const FireBulletsInfo_t &info )
{
FireBulletsInfo_t new_info(info);
new_info.m_iTracerFreq = 1;
BaseClass::FireBullets(new_info);
}
// pass through to weapon, if desired
void CMyPlayer::MakeTracer( const Vector &vecTracerSrc, const trace_t &tr, int iTracerType )
{
GetActiveWeapon()->MakeTracer(vecTracerSrc,tr,iTracerType);
}
// emit weapon-specific tracer
void CMyWeapon::MakeTracer( const Vector &vecTracerSrc, const trace_t &tr, int iTracerType )
{
// 1 is attachment #1, which should be the muzzle
UTIL_ParticleTracer("my_tracer",vecTracerSrc,tr.endpos,entindex(),1);
}
The code above will create a new particle system for each bullet fired, which is suitable for one-shot effects. If your weapon shoots rapidly consider instead dispatching an effect which spawns a stream of particles when the player starts firing and using StopParticleEffects(this)
to terminate it when they finish. Test (CBasePlayer::m_nButtons & IN_ATTACK)
to achieve this.
ParticleProp()->StopEmission(CNewParticleEffect* pEffect)
instead, which is client-side.See also UTIL_ParticleTracer
.
Impacts
CBaseEntity::DoImpactEffect()