Simulated Bullets
Preface
Basically, with simulated bullets, the aspects of physics are going to tried to be captured by simulating them in batch simulation code. So far the code is all server-side and is later expected to be client-side simulated with similar code. Bullet simulation using individual entities is not the way to go because they can fill up the entity list and are just unconventional.

Alteration
The code creates two ConVars which allow the host to change the penetration allowed and the maximum speed at which a bullet is removed.
Body
This code is WIP by ts2do
bullet_manager.h
#include "ammodef.h" #define BUTTER_MODE -1 #define ONE_HIT_MODE -2 #ifdef CLIENT_DLL//----------------------- class C_BulletManager; extern C_BulletManager *g_pBulletManager; #define CBulletManager C_BulletManager #define CSimulatedBullet C_SimulatedBullet #else//----------------------------------- class CBulletManager; extern CBulletManager *g_pBulletManager; #endif//---------------------------------- inline CBulletManager *BulletManager() { return g_pBulletManager; } extern ConVar g_debug_bullets; class CSimulatedBullet { public: CSimulatedBullet(); CSimulatedBullet( CBaseEntity *pAttacker, CBaseEntity *pAdditionalIgnoreEnt, int ammoType, int nFlags, Vector &vecDirShooting, Vector &vecOrigin, int damageType #ifndef CLIENT_DLL , CBaseEntity *pCaller, int iDamage, float flLagCompensation #endif ); ~CSimulatedBullet(); inline float BulletSpeedRatio(void) { return m_flBulletSpeed/m_flInitialBulletSpeed; } inline bool IsInWorld(void) const { if (m_vecOrigin.x >= MAX_COORD_INTEGER) return false; if (m_vecOrigin.y >= MAX_COORD_INTEGER) return false; if (m_vecOrigin.z >= MAX_COORD_INTEGER) return false; if (m_vecOrigin.x <= MIN_COORD_INTEGER) return false; if (m_vecOrigin.y <= MIN_COORD_INTEGER) return false; if (m_vecOrigin.z <= MIN_COORD_INTEGER) return false; return true; } bool StartSolid(trace_t &ptr, Vector &vecNewRay); bool AllSolid(trace_t &ptr); bool EndSolid(trace_t &ptr); #ifdef CLIENT_DLL bool SimulateBullet(void); #else void EntityImpact(trace_t &ptr); bool SimulateBullet(float flLagCompensation=-1); #endif inline int AmmoIndex(void) const { return m_iAmmoType; } private: bool m_bWasInWater; CTraceFilterSkipTwoEntities *m_pTwoEnts; CTraceFilterSimpleList *m_pIgnoreList;//already hit #ifndef CLIENT_DLL CUtlVector<CBaseEntity *> m_CompensationConsiderations;//Couldn't resist #endif EHANDLE m_hCaller; FireBulletsFlags_t m_nFlags; float m_flBulletSpeed; float m_flEntryDensity; float m_flInitialBulletSpeed; int m_iAmmoType; #ifndef CLIENT_DLL int m_iDamage; #endif int m_nDamageType; Vector m_vecDirShooting; Vector m_vecOrigin; Vector m_vecEntryPosition; // Prevent copies private: CSimulatedBullet( const CSimulatedBullet& other ); CSimulatedBullet& operator=( const CSimulatedBullet& other ); }; class CBulletManager : public CBaseEntity { DECLARE_CLASS( CBulletManager, CBaseEntity ); public: #ifdef CLIENT_DLL void ClientThink(void); #else void Spawn(void); void Think(void); #endif int AddBullet(CSimulatedBullet *pBullet, float flLatency); int RemoveBullet(int index); void UpdateBulletStopSpeed(void); int BulletStopSpeed(void) { return m_iBulletStopSpeed; } private: int m_iBulletStopSpeed; CUtlLinkedList<CSimulatedBullet*,int> m_Bullets; };
bullet_manager.cpp
#include "cbase.h" #include "util_shared.h" #include "bullet_manager.h" #include "shot_manipulator.h" #include "tier0/vprof.h" #pragma warning(disable:4800) #ifdef CLIENT_DLL//------------------------------------------------- #include "c_te_effect_dispatch.h" #include "engine/ivdebugoverlay.h" ConVar g_cl_debug_bullets( "g_cl_debug_bullets", "0", FCVAR_CHEAT ); C_BulletManager *g_pBulletManager = (C_BulletManager *)CreateEntityByName("bullet_manager"); extern void FX_PlayerTracer( Vector& start, Vector& end); #else//------------------------------------------------------------- #include "soundent.h" #include "player_pickup.h" #include "ilagcompensationmanager.h" ConVar g_debug_bullets( "g_debug_bullets", "0", FCVAR_CHEAT ); CBulletManager *g_pBulletManager; #endif//------------------------------------------------------------ // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" static void BulletSpeedModifierCallback(ConVar *var, const char *pOldString) { if(var->GetFloat()==0.0f) var->Revert(); } ConVar sv_bullet_speed_modifier( "sv_bullet_speed_modifier", "700.000000", (FCVAR_ARCHIVE | FCVAR_REPLICATED), "Density/(This Value) * (Distance Penetrated) = (Change in Speed)", BulletSpeedModifierCallback ); static void BulletStopSpeedCallback(ConVar *var, const char *pOldString) { int val = var->GetInt(); if(val<ONE_HIT_MODE) var->Revert(); else if(BulletManager()) BulletManager()->UpdateBulletStopSpeed(); } ConVar sv_bullet_stop_speed( "sv_bullet_stop_speed", "40", (FCVAR_ARCHIVE | FCVAR_REPLICATED), "Integral speed at which to remove the bullet from the bullet queue\n-1 is butter mode\n-2 is 1 hit mode", BulletStopSpeedCallback ); LINK_ENTITY_TO_CLASS( bullet_manager, CBulletManager ); #ifndef CLIENT_DLL void CBulletManager::Spawn(void) { Think(); } #endif CSimulatedBullet::CSimulatedBullet() { m_vecOrigin.Init(); m_vecDirShooting.Init(); m_flInitialBulletSpeed = m_flBulletSpeed = 0; #ifndef CLIENT_DLL m_flLagCompensation = #endif m_flEntryDensity = 0.0f; m_nFlags = (FireBulletsFlags_t)0; #ifndef CLIENT_DLL m_iDamage = m_nDamageType = #endif m_iAmmoType = 0; } CSimulatedBullet::CSimulatedBullet( CBaseEntity *pAttacker, CBaseEntity *pAdditionalIgnoreEnt, int ammoType, int nFlags, Vector &vecDirShooting, Vector &vecOrigin, int damageType #ifndef CLIENT_DLL , CBaseEntity *pCaller, int iDamage, float flLagCompensation #endif ) { m_nFlags = (FireBulletsFlags_t)nFlags; m_iAmmoType = ammoType; m_vecOrigin = vecOrigin; m_vecDirShooting = vecDirShooting; m_flInitialBulletSpeed = m_flBulletSpeed = GetAmmoDef()->GetAmmoOfIndex(ammoType)->bulletSpeed; //m_flInitialBulletSpeed = m_flBulletSpeed = BULLET_SPEED; m_flEntryDensity = 0.0f; #ifndef CLIENT_DLL m_flLagCompensation = 100*flLagCompensation; m_iDamage = iDamage; m_hCaller = pCaller; #endif m_nDamageType = damageType; m_pIgnoreList = new CTraceFilterSimpleList(COLLISION_GROUP_NONE); m_pIgnoreList->AddEntityToIgnore(pAttacker); if(pAdditionalIgnoreEnt!=NULL) m_pIgnoreList->AddEntityToIgnore(pAdditionalIgnoreEnt); m_pTwoEnts = new CTraceFilterSkipTwoEntities(pAttacker,pAdditionalIgnoreEnt,COLLISION_GROUP_NONE); } CSimulatedBullet::~CSimulatedBullet() { delete m_pIgnoreList; delete m_pTwoEnts; } #ifdef CLIENT_DLL bool FX_AffectRagdolls(Vector vecOrigin, Vector vecStart, int iDamageType); bool C_SimulatedBullet::SimulateBullet(void) { VPROF( "C_SimulatedBullet::SimulateBullet" ); #else bool CSimulatedBullet::SimulateBullet(float flLagCompensation/*=-1 (seconds)*/) { VPROF( "CSimulatedBullet::SimulateBullet" ); #endif trace_t trace;//, trace_back; Vector vecTraceStart(m_vecOrigin); Vector vecNewRay = m_vecDirShooting * m_flBulletSpeed; #ifdef GAME_DLL if(flLagCompensation!=-1) { //TODO: call backtracking first vecNewRay *= flLagCompensation * 100; } #endif m_vecOrigin += vecNewRay; bool bInWater = UTIL_PointContents(m_vecOrigin)&MASK_SPLITAREAPORTAL; if(!IsInWorld()) { return false; } #ifdef CLIENT_DLL FX_PlayerTracer( vecTraceStart, m_vecOrigin ); #endif if(m_bWasInWater!=bInWater) { #ifdef CLIENT_DLL //TODO: water impact effect //CBaseEntity::HandleShotImpactingWater #endif //CLIENT_DLL } if(bInWater) { #ifdef CLIENT_DLL //TODO: 1 bubble clientside #endif //CLIENT_DLL } #ifdef CLIENT_DLL if(g_cl_debug_bullets.GetBool()) { debugoverlay->AddLineOverlay( vecTraceStart, m_vecOrigin, 255, 0, 0, true, 10.0f ); } #else if(g_debug_bullets.GetBool()) { NDebugOverlay::Line( vecTraceStart, m_vecOrigin, 255, 255, 255, true, 10.0f ); } #endif m_bWasInWater = bInWater; bool bulletSpeedCheck; do { bulletSpeedCheck = false; UTIL_TraceLine( vecTraceStart, m_vecOrigin, MASK_SHOT, m_pIgnoreList, &trace ); if(!(trace.surface.flags&SURF_SKY)) { if(trace.allsolid)//in solid { if(!AllSolid(trace)) return false; bulletSpeedCheck = true; } else if(trace.startsolid)//exit solid { if(!StartSolid(trace, vecNewRay)) return false; bulletSpeedCheck = true; } else if(trace.fraction!=1.0f)//hit solid { if(!EndSolid(trace)) return false; bulletSpeedCheck = true; } else { //don't do a bullet speed check for not touching anything break; } } if(bulletSpeedCheck) { if(m_flBulletSpeed<=BulletManager()->BulletStopSpeed()) { return false; } } vecTraceStart = trace.endpos + m_vecDirShooting; }while(trace.fraction!=1.0f); return true; } bool CSimulatedBullet::StartSolid(trace_t &ptr, Vector &vecNewRay) { #ifdef CLIENT_DLL //TODO: penetration surface impact stuff #endif //CLIENT_DLL Vector vecExitPosition = ptr.fractionleftsolid * vecNewRay + ptr.startpos; float flPenetrationDistance = m_vecEntryPosition.DistTo(vecExitPosition); switch(BulletManager()->BulletStopSpeed()) { case BUTTER_MODE: { //Do nothing to bullet speed break; } case ONE_HIT_MODE: { return false; } default: { m_flBulletSpeed -= flPenetrationDistance * m_flEntryDensity / sv_bullet_speed_modifier.GetFloat(); break; } } return true; } bool CSimulatedBullet::AllSolid(trace_t &ptr) { switch(BulletManager()->BulletStopSpeed()) { case BUTTER_MODE: { //Do nothing to bullet speed break; } case ONE_HIT_MODE: { return false; } default: { m_flBulletSpeed -= m_flBulletSpeed * m_flEntryDensity / sv_bullet_speed_modifier.GetFloat(); break; } } return true; } bool CSimulatedBullet::EndSolid(trace_t &ptr) { m_vecEntryPosition = ptr.endpos; #ifndef CLIENT_DLL int soundEntChannel = ( m_nFlags&FIRE_BULLETS_TEMPORARY_DANGER_SOUND ) ? SOUNDENT_CHANNEL_BULLET_IMPACT : SOUNDENT_CHANNEL_UNSPECIFIED; CSoundEnt::InsertSound( SOUND_BULLET_IMPACT, m_vecEntryPosition, 200, 0.5, m_hCaller, soundEntChannel ); #endif if(FStrEq(ptr.surface.name,"tools/toolsblockbullets")) { return false; } #ifdef CLIENT_DLL FX_AffectRagdolls(ptr.startpos, ptr.endpos, m_nDamageType); //TODO: surface impact stuff #endif //CLIENT_DLL m_flEntryDensity = physprops->GetSurfaceData(ptr.surface.surfaceProps)->physics.density; if(ptr.DidHitNonWorldEntity()) { if(m_pIgnoreList->ShouldHitEntity(ptr.m_pEnt,MASK_SHOT)) { m_pIgnoreList->AddEntityToIgnore(ptr.m_pEnt); DevMsg("Hit: %s\n",ptr.m_pEnt->GetClassname()); #ifndef CLIENT_DLL EntityImpact(ptr); #endif } } if(BulletManager()->BulletStopSpeed()==ONE_HIT_MODE) { return false; } return true; } #ifndef CLIENT_DLL void CSimulatedBullet::EntityImpact(trace_t &ptr) { //TODO: entity impact stuff /*CTakeDamageInfo dmgInfo( this, pAttacker, flActualDamage, nActualDamageType ); CalculateBulletDamageForce( &dmgInfo, info.m_iAmmoType, vecDir, tr.endpos ); dmgInfo.ScaleDamageForce( info.m_flDamageForceScale ); dmgInfo.SetAmmoType( info.m_iAmmoType ); tr.m_pEnt->DispatchTraceAttack( dmgInfo, vecDir, &tr );*/ if ( GetAmmoDef()->Flags(m_iAmmoType) & AMMO_FORCE_DROP_IF_CARRIED ) { // Make sure if the player is holding this, he drops it Pickup_ForcePlayerToDropThisObject( ptr.m_pEnt ); } } void CBulletManager::Think(void) #else void CBulletManager::ClientThink(void) #endif { int x = m_Bullets.Head(); while(m_Bullets.IsValidIndex(x)) { if(m_Bullets[x]->SimulateBullet()) x = m_Bullets.Next(x); else x = RemoveBullet(x); } #ifdef CLIENT_DLL SetNextClientThink( gpGlobals->curtime + 0.01f ); #else SetNextThink( gpGlobals->curtime + 0.01f ); #endif } #ifdef CLIENT_DLL void BulletShotCallback( const CEffectData &data ) { Vector vecDir(data.m_vNormal), vecOrigin(data.m_vOrigin); CSimulatedBullet *pBullet = new CSimulatedBullet(cl_entitylist->GetBaseEntity(data.m_nEntIndex), cl_entitylist->GetBaseEntity(data.m_nAttachmentIndex), data.m_nMaterial, data.m_fFlags, vecDir, vecOrigin, data.m_nDamageType); BulletManager()->AddBullet(pBullet); } DECLARE_CLIENT_EFFECT( "bulletshot", BulletShotCallback ); #endif void CBulletManager::UpdateBulletStopSpeed(void) { m_iBulletStopSpeed = sv_bullet_stop_speed.GetInt(); } int CBulletManager::AddBullet(CSimulatedBullet *pBullet, float flLatency) { if (pBullet->AmmoIndex() == -1) { DevMsg("ERROR: Undefined ammo type!\n"); return -1; } int index = m_Bullets.AddToTail(pBullet); #ifdef CLIENT_DLL DevMsg( "Client Bullet Created (%i)\n", index); #else DevMsg( "Bullet Created (%i) LagCompensation %f\n", index, flLatency ); if(flLatency!=0.0f) m_Bullets[index]->SimulateBullet(flLatency); #endif return index; } int CBulletManager::RemoveBullet(int index) { int retVal = m_Bullets.Next(index); #ifdef CLIENT_DLL DevMsg("Client "); #endif DevMsg( "Bullet Removed (%i)\n", index ); m_Bullets.Remove(index); return retVal; }
Implementation
gamerules.cpp
Add | #include "bullet_manager.h" |
After | #include "filesystem.h" |
Add | g_pBulletManager = (CBulletManager*)CBaseEntity::Create( "bullet_manager", vec3_origin, vec3_angle ); |
After | g_pPlayerResource = (CPlayerResource*)CBaseEntity::Create( "player_manager", vec3_origin, vec3_angle ); |
ammodef.h
Change the Ammo_t
structure to the following:
struct Ammo_t { char *pName; int nDamageType; int eTracerType; float physicsForceImpulse; float bulletSpeed; int nMinSplashSize; int nMaxSplashSize; int nFlags; // Values for player/NPC damage and carrying capability // If the integers are set, they override the CVars int pPlrDmg; // CVar for player damage amount int pNPCDmg; // CVar for NPC damage amount int pMaxCarry; // CVar for maximum number can carry const ConVar* pPlrDmgCVar; // CVar for player damage amount const ConVar* pNPCDmgCVar; // CVar for NPC damage amount const ConVar* pMaxCarryCVar; // CVar for maximum number can carry };
Change the first two AddAmmoType
function prototypes to the following:
void AddAmmoType(char const* name, int damageType, int tracerType, int plr_dmg, int npc_dmg, int carry, float bulletSpeed, float physicsForceImpulse, int nFlags, int minSplashSize = 4, int maxSplashSize = 8 ); void AddAmmoType(char const* name, int damageType, int tracerType, char const* plr_cvar, char const* npc_var, char const* carry_cvar, float bulletSpeed, float physicsForceImpulse, int nFlags, int minSplashSize = 4, int maxSplashSize = 8 );
ammodef.cpp
Add | float bulletSpeed, |
Before both | float physicsForceImpulse, |
Add | m_AmmoType[m_nAmmoIndex].bulletSpeed = bulletSpeed; |
Before both | m_AmmoType[m_nAmmoIndex].physicsForceImpulse = physicsForceImpulse; |
hl2mp_gamerules.cpp
Change GetAmmoDef
to the following:
#define BULLET_SPEED(ftpersec) 0.12*ftpersec//inches per hundredth of a second CAmmoDef *GetAmmoDef() { static CAmmoDef def; static bool bInitted = false; if ( !bInitted ) { bInitted = true; // Name Damage Tracer PlrDmg NPCDmg MaxCarry Bulletspeed Physics Force Impulse Flags def.AddAmmoType("AR2", DMG_BULLET, TRACER_LINE_AND_WHIZ, 0, 0, 60, BULLET_SPEED(1225), BULLET_IMPULSE(200, 1225), 0 ); def.AddAmmoType("AR2AltFire", DMG_DISSOLVE, TRACER_NONE, 0, 0, 3, 0, 0, 0 ); def.AddAmmoType("Pistol", DMG_BULLET, TRACER_LINE_AND_WHIZ, 0, 0, 150, BULLET_SPEED(1225), BULLET_IMPULSE(200, 1225), 0 ); def.AddAmmoType("SMG1", DMG_BULLET, TRACER_LINE_AND_WHIZ, 0, 0, 225, BULLET_SPEED(1225), BULLET_IMPULSE(200, 1225), 0 ); def.AddAmmoType("357", DMG_BULLET, TRACER_LINE_AND_WHIZ, 0, 0, 12, BULLET_SPEED(5000), BULLET_IMPULSE(800, 5000), 0 ); def.AddAmmoType("XBowBolt", DMG_BULLET, TRACER_LINE, 0, 0, 10, BULLET_SPEED(8000), BULLET_IMPULSE(800, 8000), 0 ); def.AddAmmoType("Buckshot", DMG_BULLET | DMG_BUCKSHOT, TRACER_LINE, 0, 0, 30, BULLET_SPEED(1200), BULLET_IMPULSE(400, 1200), 0 ); def.AddAmmoType("RPG_Round", DMG_BURN, TRACER_NONE, 0, 0, 3, 0, 0, 0 ); def.AddAmmoType("SMG1_Grenade", DMG_BURN, TRACER_NONE, 0, 0, 3, 0, 0, 0 ); def.AddAmmoType("Grenade", DMG_BURN, TRACER_NONE, 0, 0, 5, 0, 0, 0 ); def.AddAmmoType("slam", DMG_BURN, TRACER_NONE, 0, 0, 5, 0, 0, 0 ); } return &def; }
As you can see, the bullet speed is derived from the ftpersec of the bullet impulses.
In CBaseEntity::FireBullets
TODO
dlls
player_lagcompensation.cpp
Add the following function to this file:
static int GetTargetTick(CBasePlayer *player,CUserCmd *cmd) { static CBasePlayer *lastPlayer; static int lastTarget; if(player==lastPlayer) return lastTarget; // Get true latency // correct is the amout of time we have to correct game time float correct = 0.0f; INetChannelInfo *nci = engine->GetPlayerNetInfo( player->entindex() ); if ( nci ) { // add network latency correct+= nci->GetLatency( FLOW_OUTGOING ); } // calc number of view interpolation ticks - 1 int lerpTicks = TIME_TO_TICKS( player->m_fLerpTime ); // add view interpolation latency see C_BaseEntity::GetInterpolationAmount() correct += TICKS_TO_TIME( lerpTicks ); // check bouns [0,sv_maxunlag] correct = clamp( correct, 0.0f, sv_maxunlag.GetFloat() ); // correct tick send by player int targettick = cmd->tick_count - lerpTicks; // calc difference between tick send by player and our latency based tick float deltaTime = correct - TICKS_TO_TIME(gpGlobals->tickcount - targettick); if ( fabs( deltaTime ) > 0.2f ) { // difference between cmd time and latency is too big > 200ms, use time correction based on latency // DevMsg("StartLagCompensation: delta too big (%.3f)\n", deltaTime ); targettick = gpGlobals->tickcount - TIME_TO_TICKS( correct ); } lastPlayer = player; lastTarget = targettick; return targettick; }
In CLagCompensationManager::StartLagCompensation
Replace | // Get true latency // correct is the amout of time we have to correct game time float correct = 0.0f; INetChannelInfo *nci = engine->GetPlayerNetInfo( player->entindex() ); if ( nci ) { // add network latency correct+= nci->GetLatency( FLOW_OUTGOING ); } // calc number of view interpolation ticks - 1 int lerpTicks = TIME_TO_TICKS( player->m_fLerpTime ); // add view interpolation latency see C_BaseEntity::GetInterpolationAmount() correct += TICKS_TO_TIME( lerpTicks ); // check bouns [0,sv_maxunlag] correct = clamp( correct, 0.0f, sv_maxunlag.GetFloat() ); // correct tick send by player int targettick = cmd->tick_count - lerpTicks; // calc difference between tick send by player and our latency based tick float deltaTime = correct - TICKS_TO_TIME(gpGlobals->tickcount - targettick); if ( fabs( deltaTime ) > 0.2f ) { // difference between cmd time and latency is too big > 200ms, use time correction based on latency // DevMsg("StartLagCompensation: delta too big (%.3f)\n", deltaTime ); targettick = gpGlobals->tickcount - TIME_TO_TICKS( correct ); } |
With | int targettick = GetTargetTick(player); |
ilagcompensationmanager.h
Add | static int GetTargetTick(CBasePlayer *player,CUserCmd *cmd); |
Before | extern ILagCompensationManager *lagcompensation; |
player_command.cpp
In CPlayerMove::StartCommand
Comment out | lagcompensation->StartLagCompensation( player, cmd ); |
hl2mp_player.cpp
The bullets will know when they're being fired so in CHL2MP_Player::WantsLagCompensationOnEntity
Comment out | if ( !( pCmd->buttons & IN_ATTACK ) && (pCmd->command_number - m_iLastWeaponFireUsercmd > 5) ) return false; |
In CHL2MP_Player::FireBullets
Add | lagcompensation->StartLagCompensation(this, m_pCurrentCommand); modinfo.m_flLatency = TICKS_TO_TIME(GetTargetTick(this,m_pCurrentCommand)); |
Before | BaseClass::FireBullets( modinfo ); |
Add | lagcompensation->FinishLagCompensation(this); |
After | BaseClass::FireBullets( modinfo ); |