Simple Projectile Bullets
January 2024
You can help by adding links to this article from other relevant articles.
January 2024
Adding simple projectile bullets is easier than you'd think. If you've looked around the site, you might have found Realistic Simulated Bullets.
It's a great article, but there's many additional features that you might not want, and it overrides the existing hitscan method of firing bullets. This can be problematic, because you may want to keep the existing hitscan methods in-tact.
That's where this comes in. This is a very simple method of adding projectile bullets that utilizes parts of the existing bullet methods without overriding them. It fires an invisible bullet and uses tracers as a visual stand-in for the bullet.
The Code
Add both of the following files to your Server project.
actual_bullet.h
#ifndef ACTUALBULLET_H
#define ACTUALBULLET_H
#include "cbase.h"
#include "baseentity.h"
#include "baseentity_shared.h"
#include "baseanimating.h"
class CActualBullet : public CBaseAnimating
{
DECLARE_CLASS(CActualBullet, CBaseAnimating);
DECLARE_DATADESC();
public:
void Start(void);
void Think(void);
void Stop(void);
Vector m_vecDir;
int m_Speed;
FireBulletsInfo_t info;
};
///so this is the actual bullet creation function.
inline void FireActualBullet(FireBulletsInfo_t &info, int iSpeed, const char* tracertype)
{
if (!info.m_pAttacker)
{
Warning("ERROR: Firing an actual bullet without an attacker specified. This will crash the game without it. Cancelling.\n");
return;
}
int iShots = info.m_iShots;
for (int i = 0; i < iShots; i++)
{
Vector vecSpreadSrc = info.m_vecSpread;
Vector vecSpread = Vector(RandomFloat(-vecSpreadSrc[0], vecSpreadSrc[0]), RandomFloat(-vecSpreadSrc[1], vecSpreadSrc[1]), RandomFloat(-vecSpreadSrc[2], vecSpreadSrc[2]));
Vector vecShot = info.m_vecDirShooting + vecSpread;
Vector vecShotDir = vecShot.Normalized();
trace_t tr;
UTIL_TraceLine(info.m_vecSrc, info.m_vecSrc + (vecShotDir * MAX_TRACE_LENGTH), MASK_SHOT, info.m_pAttacker, COLLISION_GROUP_NONE, &tr);
CActualBullet *pBullet = (CActualBullet*)CBaseEntity::Create("actual_bullet", info.m_vecSrc, vec3_angle, info.m_pAttacker);
pBullet->m_vecDir = vecShotDir;
pBullet->m_Speed = iSpeed;
pBullet->SetOwnerEntity(info.m_pAttacker);
pBullet->SetAbsOrigin(info.m_vecSrc);
pBullet->info = info;
pBullet->Start();
UTIL_Tracer(info.m_vecSrc, tr.endpos, info.m_pAttacker->entindex(), -1, (float)iSpeed, false, tracertype, 0);
}
}
#endif //ACTUALBULLET_H
actual_bullet.cpp
#include "cbase.h"
#include "actual_bullet.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
ConVar debug_actual_bullet("debug_actual_bullet", "0", FCVAR_GAMEDLL);
LINK_ENTITY_TO_CLASS(actual_bullet, CActualBullet);
BEGIN_DATADESC(CActualBullet)
END_DATADESC()
void CActualBullet::Start(void)
{
SetThink(&CActualBullet::Think);
SetNextThink(gpGlobals->curtime);
SetOwnerEntity(info.m_pAttacker);
}
void CActualBullet::Think(void)
{
SetNextThink(gpGlobals->curtime + 0.05f);
Vector vecStart;
Vector vecEnd;
float flInterval;
flInterval = gpGlobals->curtime - GetLastThink();
vecStart = GetAbsOrigin();
vecEnd = vecStart + (m_vecDir * (m_Speed * flInterval));
float flDist = (vecStart - vecEnd).Length();
trace_t tr;
UTIL_TraceLine(vecStart, vecEnd, MASK_SHOT, this, COLLISION_GROUP_NONE, &tr);
if (debug_actual_bullet.GetBool() == true)
DebugDrawLine(vecStart, vecEnd, 0, 128, 255, false, 0.05f);
if (tr.fraction != 1.0)
{
FireBulletsInfo_t info2;
info2.m_iShots = 1;
info2.m_vecSrc = vecStart;
info2.m_vecSpread = vec3_origin;
info2.m_vecDirShooting = m_vecDir;
info2.m_flDistance = flDist;
info2.m_iAmmoType = info.m_iAmmoType;
info2.m_iTracerFreq = 0;
GetOwnerEntity()->FireBullets(info2);
Stop();
}
else
{
SetAbsOrigin(vecEnd);
}
}
void CActualBullet::Stop(void)
{
SetThink(NULL);
UTIL_Remove(this);
}
Now, find the following around line 1596:
void CBaseEntity::FireBullets( const FireBulletsInfo_t &info )
Right before this:
int rumbleEffect = pPlayer->GetActiveWeapon()->GetRumbleEffect();
Add this:
CBaseCombatWeapon *pWeapon = pPlayer->GetActiveWeapon();
if (!pWeapon)
return;
Usage
To use the projectile bullets, add the following to the top of your weapon file:
#include "actual_bullet.h"
In your PrimaryAttack() function, set up your FireBulletsInfo_t struct like you normally would. Example:
FireBulletsInfo_t info;
info.m_iAmmoType = m_iPrimaryAmmoType;
info.m_iShots = 1;
info.m_vecSrc = pPlayer->Weapon_ShootPosition();
info.m_vecDirShooting = pPlayer->GetAutoaimVector(AUTOAIM_SCALE_DEFAULT);
info.m_vecSpread = GetBulletSpread();
info.m_pAttacker = GetOwnerEntity();
But instead of doing FireBullets(), do this:
FireActualBullet(info, 12000, GetTracerType());
Change the 12000 to whatever speed you wish, or if you want, you can set it to a ConVar to fine tune it in game.
And that's it! You now have simple projectile bullets. This can be used for both player weapons AND NPC's.
You can see the effect in action here.