Authoring your first weapon entity
Contents
Overview
It should not come as a surprise that the Source engine has a very robust (although sometimes frustrating) weapons system. This article will cover creating a weapon entity from scratch.
For more information on the topic of weapons programming, consider visiting the weapons programming category.
Getting Started
To note, this tutorial expects the reader is using Source SDK 2013 Singleplayer. To get started, create a new .cpp file in the server project and call it weapon_tutorial.cpp. Type this into your newly created file.
#include "cbase.h"
#include "basehlcombatweapon.h"
#include "basecombatcharacter.h"
#include "player.h"
#include "gamerules.h"
#include "in_buttons.h"
#include "soundent.h"
#include "game.h"
#include "vstdlib\random.h"
#include "gamestats.h"
#include "npcevent.h"
#include "tier0\memdbgon.h"
These are all the files we need to include for our weapon to work. If we do not include most these, our weapon just simply would not compile.
Creating The Class
Now we will define the class and all of our functions, so it is easier on us down the line. Copy this code down into your file.
class CWeaponTutorial : public CBaseHLCombatWeapon
{
public:
DECLARE_CLASS( CWeaponTutorial, CBaseHLCombatWeapon );
DECLARE_DATADESC( );
DECLARE_ACTTABLE( );
CWeaponTutorial( );
DECLARE_SERVERCLASS( );
void Precache( );
void ItemPreFrame( );
void ItemBusyFrame( );
void ItemPostFrame( );
void PrimaryAttack( );
void AddViewKick( );
void DryFire( );
virtual bool Reload( void );
};
Here is a quick explanation of what is going on here. If you have read the My First Entity series of tutorials, you can see that instead of using CLogicalEntity
or CBaseAnimating
as our BaseClass
, we are using CBaseHLCombatWeapon
, which contains all of the functions and logic we will be needing to create this weapon. After this, we are simply doing the usual, and declaring our class and data description, and we are also defining an Acttable_t. This simply tells our weapon which animations to use. Finally, we are simply making our constructor, and making stubs for all of our functions. For more information on what any of these function stubs do, take a quick look at Authoring a weapon entity, but this is not required because later I will be giving a very simple explanation of what these are for and do. After this, type:
virtual const Vector& GetBulletSpread( )
{
static Vector cone = VECTOR_CONE_1DEGREES, npcCone = VECTOR_CONE_1DEGREES;
if (GetOwner( ) && GetOwner( )->IsNPC( )) //Always handle NPCs first
return npcCone;
else
return cone;
}
All this is doing is simply defining the bullet spread, which is one cone. After this, finally copy this down:
private:
float m_flRateOfFire;
All this is doing is defining a float named m_flRateOfFire
, which as you guessed, is our rate of fire. We will define this later.
Before Writing the Functions
Before we write the functions, there are a few simple things we have to do. First, we are going to link our weapon to an entity, so it can be spawned in game, either with the give command or by any other means, and precache it. Write this code down.
LINK_ENTITY_TO_CLASS( weapon_tutorial, CWeaponTutorial );
PRECACHE_WEAPON_REGISTER( weapon_tutorial );
Next, we will define our data description.
BEGIN_DATADESC( CWeaponTutorial )
DEFINE_FIELD( m_flRateOfFire, FIELD_FLOAT ),
END_DATADESC( )
If you have read the My First Entity series, you will know what this is doing. If we were feeling fancy, we could even link this value to a ConVar so we could define it in game. Finally, we will define the Acttable_t, so our weapon knows which animations to use. For this tutorial, we will be using the SMG animations.
acttable_t CWeaponTutorial::m_acttable[] =
{
{ ACT_RANGE_ATTACK1, ACT_RANGE_ATTACK_SMG1, true },
{ ACT_RELOAD, ACT_RELOAD_SMG1, true },
{ ACT_IDLE, ACT_IDLE_SMG1, true },
{ ACT_IDLE_ANGRY, ACT_IDLE_ANGRY_SMG1, true },
{ ACT_WALK, ACT_WALK_RIFLE, true },
{ ACT_WALK_AIM, ACT_WALK_AIM_RIFLE, true },
};
IMPLEMENT_ACTTABLE( CWeaponTutorial );
Programming the Functions
Now, we will finally write the code that will make everything come together. To start off, we will write the code for the constructor. What it does is fairly self-explanatory.
CWeaponTutorial::CWeaponTutorial( )
{
m_fMinRange1 = 24;
m_fMaxRange1 = 3000;
m_bFiresUnderwater = true;
}
Next, we will move on to the precache class. Since we are using the SMG models, we will precache those. This also sets our rate of fire to 10 rounds per second.
void CWeaponTutorial::Precache( )
{
m_flRateOfFire = 0.1f;
PrecacheModel( "models/weapons/v_smg1.mdl", true );
PrecacheModel( "models/weapons/w_smg1.mdl", true );
BaseClass::Precache( );
}
After this, we will define our DryFire
function. What this function does is also fairly self-explanatory.
void CWeaponTutorial::DryFire( )
{
WeaponSound( EMPTY );
SendWeaponAnim( ACT_VM_DRYFIRE );
m_flNextPrimaryAttack = gpGlobals->curtime + SequenceDuration( );
}
For this one, I am going to knock out three functions in one block, mainly because they do not do anything and they are just here so I can explain what they do and when you might need them.
void CWeaponTutorial::ItemPreFrame( )
{
BaseClass::ItemPreFrame( );
}
void CWeaponTutorial::ItemBusyFrame( )
{
BaseClass::ItemBusyFrame( );
}
void CWeaponTutorial::ItemPostFrame( )
{
BaseClass::ItemPostFrame( );
}
ItemPreFrame
is called before the player movement function. ItemBusyFrame
is called with the player movement function. ItemPostFrame
is called after the player movement function. Out of these three, ItemPostFrame
is the most useful. Now on to the most complicated of these functions, PrimaryAttack
. This one requires the most explanation, mainly because we are writing the firing logic ourselves.
void CWeaponTutorial::PrimaryAttack( )
{
CBasePlayer *pPlayer = ToBasePlayer( GetOwner( ) ); //This gets the current player holding the weapon
Vector vecSrc = pPlayer->Weapon_ShootPosition( ); //This simply just gets the current position of the player.
Vector vecAim = pPlayer->GetAutoaimVector( 0.0 ); //This gets where the player is looking, but also corrected by autoaim.
pPlayer->FireBullets( 1, vecSrc, vecAim, GetBulletSpread( ), MAX_TRACE_LENGTH, m_iPrimaryAmmoType, 1, entindex( ), 0, 30, GetOwner( ), true, true );
//This is a lengthy one. All of the args in here are data for our bullet. We could simply use
//BaseClass::PrimaryAttack, but this gives us no control over how our weapon is fired.
//So instead, we use this and give it all our info for our bullet.
//The next 2 are just where the bullet is being fired from and where it should go.
//Next is how far the bullet is fired, and after that is what type of ammo we use.
//After this is the tracer freq, which really doesnt matter.
//Next is the id of the entity firing the weapon, and the attachment id. These 2 dont really matter.
//Next is how much damage each bullet should do, which is 30.
//Next is what entity is firing the bullet, again.
//The final 2 define wether our first shot should be accurate, and wether this is the primary attack.
WeaponSound( SINGLE ); //This makes our weapon emit the single show sound.
SendWeaponAnim( ACT_VM_PRIMARYATTACK ); //This sends the animation for us shooting.
m_flNextPrimaryAttack = gpGlobals->curtime + m_flRateOfFire; //This defines when our next attack should be
AddViewKick( ); //Don't forget to add our viewkick
}
After this, we only have 2 more functions to go, so congratulations, you made it! We will now do the View Kick function, which is fairly simple.
void CWeaponTutorial::AddViewKick( )
{
CBasePlayer *pPlayer = ToBasePlayer( GetOwner( ) );
QAngle punch;
punch += QAngle( -0.2, 0.0, 0.0 );
pPlayer->ViewPunch( punch );
}
This just simply creates recoil straight up by adding .2 to our QAngle every time we fire. Now, for the final, and one of the simplest functions, reload.
bool CWeaponTutorial::Reload( )
{
bool fRet = DefaultReload( GetMaxClip1( ), GetMaxClip2( ), ACT_VM_RELOAD );
if (fRet)
WeaponSound( RELOAD );
return fRet;
}
All this does if checks if we must reload, and if we do, play the animation and sound, and if not, do nothing. Now, you can compile this and place the dll in your game bin folder. This won't work quite yet, because we have to do one more thing, and that thing is...
The Weapon Script
[Todo]