Authoring a weapon entity

From Valve Developer Community
Jump to: navigation, search
Русский

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.

To do: How do NPCs attack?

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.

Tip:Don't lag compensate or predict this call - but do predict viewmodel animation, ammo depletion and reloading.

Further reading:

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.

Note:If your weapon has other particle effects you'll need stop the tracers with ParticleProp()->StopEmission(CNewParticleEffect* pEffect) instead, which is client-side.

See also UTIL_ParticleTracer.

Impacts

To do: CBaseEntity::DoImpactEffect()

Networking

To do: Shared code, prediction, predicted viewmodels, compensation...

Sample weapon

See AK47 weapon.