/* ** Command & Conquer Renegade(tm) ** Copyright 2025 Electronic Arts Inc. ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . */ /*********************************************************************************************** *** Confidential - Westwood Studios *** *********************************************************************************************** * * * Project Name : Commando * * * * $Archive:: /Commando/Code/Combat/bullet.cpp $* * * * $Author:: Byon_g $* * * * $Modtime:: 3/13/02 5:22p $* * * * $Revision:: 164 $* * * *---------------------------------------------------------------------------------------------* * Functions: * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ #include "bullet.h" #include "projectile.h" #include "combat.h" // for collision groups (should be damage.h?) #include "pscene.h" #include "weaponmanager.h" #include "assets.h" #include "debug.h" #include "armedgameobj.h" #include "crandom.h" #include "surfaceeffects.h" #include "mesh.h" #include "wwaudio.h" #include "explosion.h" #include "building.h" #include "persistfactory.h" #include "wwphysids.h" #include "damageablestaticphys.h" #include "wwprofile.h" #include "part_emt.h" #include "realcrc.h" #include "soldier.h" #include "segline.h" #include "timeddecophys.h" #include "simplegameobj.h" #include "c4.h" /* ** Constants for this module */ const float _INSTANT_BULLET_THRESHHOLD = 400; // If speed is >= _INSTANT_BULLET_THRESHHOLD m/s, treat it an an instant bullet /** ** BeamEffectManagerClass ** This is a static object that simply manages the active beam effect. Its job is to cache un-used beams ** so that we aren't allocating them at run time and to add new beam effects to the scene when needed. */ class BeamEffectManagerClass : public CombatPhysObserverClass { public: void Init( void ) {} void Shutdown( void ); /* ** Create a beam effect and put it in the scene. Whenever possible, this function ** tries to recycle a previously created beam effect */ void Create_Beam_Effect(const AmmoDefinitionClass * ammo_def,const Vector3 & p0,const Vector3 & p1); /* ** PhysObserver Interface - whenever a beam expires, we put it into a list so we can ** recycle it next time we need a beam effect */ virtual void Object_Removed_From_Scene(PhysClass * observed_obj); private: void Internal_Get_New_Beam(SegmentedLineClass ** beam_model,TimedDecorationPhysClass ** beam_wrapper); RefMultiListClass UnusedBeamList; }; static BeamEffectManagerClass _TheBeamEffectManager; void BeamEffectManagerClass::Shutdown( void ) { while (UnusedBeamList.Peek_Head()) { UnusedBeamList.Release_Head(); } } void BeamEffectManagerClass::Create_Beam_Effect(const AmmoDefinitionClass * ammo_def,const Vector3 & p0,const Vector3 & p1) { SegmentedLineClass * beam_model = NULL; TimedDecorationPhysClass * beam_wrapper = NULL; Internal_Get_New_Beam(&beam_model,&beam_wrapper); // line effect from "start" to "data.Position" static Vector3 points[2]; points[0] = p0; points[1] = p1; const int BEAM_SUBDIVISION = 4; const float BASE_AMPLITUDE = 0.3f; const float AMPLITUDE_PER_METER = 0.04f; /* ** User controlled settings */ TextureClass * beam_texture = Get_Texture_From_Filename(ammo_def->BeamTexture); beam_model->Set_Texture(beam_texture); REF_PTR_RELEASE(beam_texture); beam_model->Set_Points(2,points); beam_model->Set_Width(ammo_def->BeamWidth); beam_model->Set_Color(ammo_def->BeamColor); beam_model->Set_UV_Offset_Rate(Vector2(0,1.0f / ammo_def->BeamTime)); if (ammo_def->BeamSubdivisionEnabled) { beam_model->Set_Noise_Amplitude((BASE_AMPLITUDE + AMPLITUDE_PER_METER * (p1-p0).Quick_Length()) * ammo_def->BeamSubdivisionScale); beam_model->Set_Subdivision_Levels(BEAM_SUBDIVISION); } else { beam_model->Set_Noise_Amplitude(0.0f); beam_model->Set_Subdivision_Levels(0); } /* ** Constants; I don't let the user control these for now */ beam_model->Set_Texture_Tile_Factor(1.0f); // Only relevant for TILED_TEXTURE_MAP mode beam_model->Set_Texture_Mapping_Mode(SegLineRendererClass::UNIFORM_WIDTH_TEXTURE_MAP); beam_model->Set_Opacity(1.0f); beam_model->Set_Shader(ShaderClass::_PresetAdditiveShader); beam_model->Set_Merge_Abort_Factor(1.0f); beam_model->Set_Merge_Intersections(false); beam_model->Set_Disable_Sorting(false); beam_model->Set_Freeze_Random(ammo_def->BeamSubdivisionFrozen); beam_model->Set_End_Caps(ammo_def->BeamEndCaps); beam_model->Reset_Line(); beam_wrapper->Set_Lifetime(ammo_def->BeamTime); beam_wrapper->Set_Transform(Matrix3D(1)); beam_wrapper->Enable_Is_Pre_Lit(true); COMBAT_SCENE->Add_Dynamic_Object(beam_wrapper); REF_PTR_RELEASE(beam_wrapper); REF_PTR_RELEASE(beam_model); REF_PTR_RELEASE(beam_texture); } void BeamEffectManagerClass::Object_Removed_From_Scene(PhysClass * observed_obj) { /* ** One of the "beams" that we are observing expired and was removed from the scene so ** lets put it in the list for future re-use */ UnusedBeamList.Add((TimedDecorationPhysClass *)observed_obj); } void BeamEffectManagerClass::Internal_Get_New_Beam(SegmentedLineClass ** set_beam_model,TimedDecorationPhysClass ** set_beam_wrapper) { if (UnusedBeamList.Is_Empty()) { SegmentedLineClass * beam_model = NEW_REF(SegmentedLineClass,()); TimedDecorationPhysClass * beam_wrapper = NEW_REF(TimedDecorationPhysClass,()); beam_wrapper->Enable_Dont_Save(true); beam_wrapper->Set_Collision_Group(UNCOLLIDEABLE_GROUP); beam_wrapper->Set_Model(beam_model); beam_wrapper->Set_Observer(this); *set_beam_model = beam_model; *set_beam_wrapper = beam_wrapper; // refs returned to the caller } else { *set_beam_wrapper = UnusedBeamList.Remove_Head(); // refs returned to the caller *set_beam_model = (SegmentedLineClass *)((*set_beam_wrapper)->Get_Model()); } } /* ** Core Bullet Data for simulation */ class BulletDataClass { public: BulletDataClass( const AmmoDefinitionClass * def = NULL, const ArmedGameObj * owner = NULL, const Vector3 & position = Vector3(0,0,0), const Vector3 & velocity = Vector3(0,0,0) ) : AmmoDefinition( def ), Owner( owner ), Position( position ), Velocity( velocity ), SoftPierceCount( 0 ), Destroy( false ), DidExplode( false ), LastHitID( 0 ), GrenadeSafetyTimer( 0 ) {} ArmedGameObj * Get_Owner( void ) const { return (ArmedGameObj *)Owner.Get_Ptr(); } CollisionReactionType Bullet_Collision_Occurred( const CollisionEventClass & event ); ExpirationReactionType Bullet_Expired(); const AmmoDefinitionClass *AmmoDefinition; GameObjReference Owner; Vector3 Position; Vector3 Velocity; int SoftPierceCount; bool Destroy; bool DidExplode; int LastHitID; float GrenadeSafetyTimer; }; /* ** Bullet_Collision_Occured */ CollisionReactionType BulletDataClass::Bullet_Collision_Occurred( const CollisionEventClass & event ) { WWPROFILE( "Bullet Collision Occurred" ); WWASSERT(event.OtherObj); WWASSERT(event.CollisionResult != NULL); // WWDEBUG_SAY(( "Bullet Collision\n" )); // // Pre-calculate some useful variables // CollisionReactionType return_value = COLLISION_REACTION_DEFAULT; OffenseObjectClass offense( AmmoDefinition->Damage, (int)AmmoDefinition->Warhead, Get_Owner() ); offense.EnableClientDamage = true; // Only bullets apply to client damage; const Vector3 & collision_point = event.CollisionResult->ContactPoint; const Vector3 & collision_normal = event.CollisionResult->Normal; PhysicalGameObj * other = NULL; BuildingGameObj * building = NULL; if ( event.OtherObj->Get_Observer() != NULL ) { other = ((CombatPhysObserverClass *)event.OtherObj->Get_Observer())->As_PhysicalGameObj(); building = ((CombatPhysObserverClass *)event.OtherObj->Get_Observer())->As_BuildingGameObj(); } // // If the bullet hits its owner or its owner's vehicle, allow it to continue // Also, if it is a simple "Hidden Object", allow the bullet to continue // if ( other != NULL && Get_Owner() != NULL ) { SimpleGameObj * simple = other->As_SimpleGameObj(); VehicleGameObj * vehicle = other->As_VehicleGameObj(); SoldierGameObj * soldier = Get_Owner()->As_SoldierGameObj(); if ( vehicle != NULL && soldier != NULL && vehicle == soldier->Get_Vehicle() ) { // Debug_Say(( "Bullet Hitting its owner's vehicle\n" )); return COLLISION_REACTION_NO_BOUNCE; } if ( other == Get_Owner() ) { // Debug_Say(( "Bullet Hitting its owner\n" )); return COLLISION_REACTION_NO_BOUNCE; } if ((simple != NULL) && (simple->Is_Hidden_Object())) { return COLLISION_REACTION_NO_BOUNCE; } } // // Apply Graphical Effects. // If the mesh is shatterable it is shattered, Apply a surface effect depending on the surface type // that was collided with. In the case that the mesh was shattered, we disable surface effect decals. // bool shattered = false; if ((event.CollidedRenderObj != NULL) && (event.CollidedRenderObj->Class_ID() == RenderObjClass::CLASSID_MESH)) { MeshClass * mesh = (MeshClass *)event.CollidedRenderObj; if ((mesh->Get_W3D_Flags() & W3D_MESH_FLAG_SHATTERABLE) && (mesh->Is_Not_Hidden_At_All())) { PhysicsSceneClass::Get_Instance()->Shatter_Mesh( mesh, collision_point, collision_normal, Velocity ); // remeber that we shattered the mesh so that we don't create decals later shattered = true; // Bullets always pass through shattered objects because we sometimes have two-layered // windows that have different materials on the two sides. We don't want bullets to ever // shatter only one "side" of a window! return_value = COLLISION_REACTION_NO_BOUNCE; } } // Ignore backfaces (after shattering for windows) float dot = Vector3::Dot_Product( event.CollisionResult->Normal, Velocity ); if ( dot > 0 ) { return COLLISION_REACTION_NO_BOUNCE; } // // Apply Damage // There are several types of objects that can be damaged: normal dynamic game objects, damageable static // physics objects, and buildings. A building is damaged whenever any of its meshes or aggregates are // hit by a bullet. // Also, we only apply damage if the mesh wasn't shattered. Bullets pretend like nothing happened // if they shatter something. // if (!shattered) { const char * collided_obj_name = NULL; if ( event.CollidedRenderObj != NULL ) { collided_obj_name = event.CollidedRenderObj->Get_Name(); } if ( other ) { // if hitting a game object, if ( other->Get_ID() == LastHitID ) { // Debug_Say(( "Double Hit\n" )); return COLLISION_REACTION_NO_BOUNCE; } if ( GrenadeSafetyTimer > 0 ) { // only if the other is not an enemy if ( other && Get_Owner() && !other->Is_Enemy( Get_Owner() ) ) { return COLLISION_REACTION_DEFAULT; // Just bounce } } LastHitID = other->Get_ID(); // Apply Bullet Damage to a game object // Allow it to happen for clients, so we can get the repair effect // if ( CombatManager::I_Am_Server() ) { other->Apply_Damage_Extended( offense, 1.0f, Velocity, collided_obj_name ); // If the bullet is hitting C4, also apply damage to the object the C4 is // attached to. This is a fix for the "C4 armor" exploit. if (other->As_C4GameObj() != NULL) { ScriptableGameObj * host = other->As_C4GameObj()->Get_Stuck_Object(); if ((host != NULL) && (host->As_PhysicalGameObj() != NULL)) { host->As_PhysicalGameObj()->Apply_Damage_Extended( offense, 1.0f, Velocity, NULL ); } } // } if ( ( !other->Is_Soft() ) || ( ++SoftPierceCount >= AmmoDefinition->SoftPierceLimit ) ) { if ( AmmoDefinition->ExplosionDefID ) { ExplosionManager::Create_Explosion_At( AmmoDefinition->ExplosionDefID, collision_point, Get_Owner(), -collision_normal, other ); } Destroy = true; return_value = COLLISION_REACTION_STOP_MOTION; } else { return_value = COLLISION_REACTION_NO_BOUNCE; } } else { // if hitting terain if ( GrenadeSafetyTimer > 0 ) { Debug_Say(( "Safety\n" )); return COLLISION_REACTION_DEFAULT; // Just bounce } // If hitting a building, apply damage to it if ( building != NULL ) { // && CombatManager::I_Am_Server() ) { building->Apply_Damage_Building( offense, event.OtherObj->As_StaticPhysClass() ); } // Check for damage to a DamageableStaticPhys object // For now I'm using the persist factory chunk-ID for RTTI... If a better solution // turns up we should change this. if ( event.OtherObj->Get_Factory().Chunk_ID() == PHYSICS_CHUNKID_DAMAGEABLESTATICPHYS // && CombatManager::I_Am_Server() ) { DamageableStaticPhysClass * damphys = (DamageableStaticPhysClass *)event.OtherObj; OffenseObjectClass offense( AmmoDefinition->Damage, (int)AmmoDefinition->Warhead, Get_Owner() ); offense.EnableClientDamage = true; // Only bullets apply to client damage; damphys->Apply_Damage_Static( offense ); } // check for a non-stopping surface if ( !SurfaceEffectsManager::Does_Surface_Stop_Bullets( event.CollisionResult->SurfaceType ) ) { return_value = COLLISION_REACTION_NO_BOUNCE; } else { // If the ammo explodes when it hits terrain, then create an explosion and kill the bullet if ( AmmoDefinition->TerrainActivated ) { if ( AmmoDefinition->ExplosionDefID ) { ExplosionManager::Create_Explosion_At( AmmoDefinition->ExplosionDefID, collision_point, Get_Owner(), -collision_normal, building ); DidExplode = true; } Destroy = true; return_value = COLLISION_REACTION_STOP_MOTION; } } } } // Only make a bullet hit if it doesn't make an explosion if ( !DidExplode ) { Matrix3D surface_tm; surface_tm.Look_At(collision_point,collision_point-collision_normal,FreeRandom.Get_Float(0,DEG_TO_RADF(360))); int my_type = AmmoDefinition->HitterType; bool allow_decals = (shattered == false); if ( (float)AmmoDefinition->Damage < 0.0f ) { allow_decals = false; } SurfaceEffectsManager::Apply_Effect( event.CollisionResult->SurfaceType, my_type, surface_tm, event.OtherObj, Get_Owner(), allow_decals ); } return return_value; } ExpirationReactionType BulletDataClass::Bullet_Expired( void ) { Destroy = true; if ( AmmoDefinition->TimeActivated && AmmoDefinition->ExplosionDefID && !DidExplode ) { ExplosionManager::Create_Explosion_At( AmmoDefinition->ExplosionDefID, Position, Get_Owner() ); } return EXPIRATION_DENIED; // Don't let it die, I'll pull it from the scene later } /* ** BulletClass (for non-instant bullets) */ class BulletClass : public CombatPhysObserverClass, public MultiListObjectClass, public PostLoadableClass { public: ~BulletClass( void ); void Init( const BulletDataClass & data, float progress_time, const Vector3 & target, DamageableGameObj * target_object = NULL ); void Shutdown( void ); void Think( void ); // Save / Load virtual bool Save( ChunkSaveClass & csave ); virtual bool Load( ChunkLoadClass & cload ); virtual void On_Post_Load(void); bool Is_Valid( void ) { return BulletData.AmmoDefinition != NULL; } // Collision virtual CollisionReactionType Collision_Occurred( const CollisionEventClass & event ); virtual ExpirationReactionType Object_Expired( PhysClass * observed_obj ); private: BulletDataClass BulletData; ProjectileClass * Projectile; // for physics Vector3 TargetVector; GameObjReference TargetObject; float TrackingErrorTimer; Vector3 TrackingError; long ModelNameCRC; BulletClass( void ); friend class BulletManager; }; BulletClass::BulletClass( void ) : Projectile( NULL ), TargetVector( 0,0,0 ), TrackingErrorTimer( 0 ), TrackingError( 0,0,0 ), ModelNameCRC( 0 ) { WWASSERT( Projectile == NULL ); Projectile = NEW_REF( ProjectileClass, () ); Projectile->Set_Collision_Group( BULLET_COLLISION_GROUP ); } BulletClass::~BulletClass( void ) { if ( Projectile != NULL ) { Projectile->Set_Observer(NULL); Projectile->Release_Ref(); Projectile = NULL; } } void BulletClass::Init( const BulletDataClass & data, float progress_time, const Vector3 & target, DamageableGameObj * target_object ) { WWPROFILE( "Bullet Init" ); BulletData.AmmoDefinition = data.AmmoDefinition; BulletData.Owner = data.Get_Owner(); BulletData.SoftPierceCount = data.SoftPierceCount; BulletData.Destroy = data.Destroy; BulletData.LastHitID = 0; // We haven't hit anyone yet. BulletData.DidExplode = false; BulletData.GrenadeSafetyTimer = data.GrenadeSafetyTimer; if ( data.AmmoDefinition ) { BulletData.GrenadeSafetyTimer = data.AmmoDefinition->GrenadeSafetyTime; } TargetVector = target; TargetObject = target_object; TrackingErrorTimer = 0; TrackingError = Vector3( 0,0,0 ); Projectile->Set_Observer( this ); Projectile->Set_Bounce_Count( BulletData.AmmoDefinition->MaxBounces ); if ( BulletData.AmmoDefinition ) { Projectile->Set_Gravity_Multiplier( BulletData.AmmoDefinition->Gravity ); Projectile->Set_Elasticity( BulletData.AmmoDefinition->Elasticity ); } Projectile->Enable_Is_Pre_Lit(true); // bullets don't get lighting. if ( Projectile->Peek_Model() != NULL && Projectile->Peek_Model()->Get_Name() != NULL && BulletData.AmmoDefinition->ModelName.Compare_No_Case( Projectile->Peek_Model()->Get_Name() ) == 0 ) { // We don't need to do anything } else { RenderObjClass * model = NULL; model = WW3DAssetManager::Get_Instance ()->Create_Render_Obj( BulletData.AmmoDefinition->ModelName ); // If no name is given, lets create the NULL render obj if ( model == NULL ) { Debug_Say(( "Bullet Not Found \"%s\" \n", BulletData.AmmoDefinition->ModelName )); model = WW3DAssetManager::Get_Instance ()->Create_Render_Obj( "NULL" ); } Projectile->Set_Model( model ); if (model) { if ( BulletData.AmmoDefinition->ModelName.Compare_No_Case( model->Get_Name() ) != 0 ) { Debug_Say(( "Possible bullet twiddler!! %s %s\n", BulletData.AmmoDefinition->ModelName, model->Get_Name() )); } // ModelNameCRC = CRC_Stringi( model->Get_Name() ); ModelNameCRC = CRC_Stringi( BulletData.AmmoDefinition->ModelName ); model->Release_Ref(); } else { ModelNameCRC = 0xFFFFFFFF; } } if (Projectile->Get_Gravity_Multiplier() < 0.1f) { Projectile->Set_Orientation_Mode_Aligned_Fixed(); } else { Projectile->Set_Orientation_Mode_Aligned(); } COMBAT_SCENE->Add_Dynamic_Object( Projectile ); float duration = (float) BulletData.AmmoDefinition->Range / (float) BulletData.AmmoDefinition->Velocity; if (duration <= 0.0) { Debug_Say(( "NULL DURATION\n" )); } Projectile->Set_Lifetime( duration * 1.2f ); Projectile->Set_Position( data.Position ); WWASSERT(data.Velocity.Is_Valid()); Projectile->Set_Velocity( data.Velocity ); // If there are any emitters in this model, reset them for our new position RenderObjClass * model = Projectile->Peek_Model(); model->Restart(); // Timestep last, incase it gets shutdown during this call if ( data.Get_Owner() != NULL ) { data.Get_Owner()->Peek_Physical_Object()->Inc_Ignore_Counter(); } Projectile->Timestep( progress_time ); if ( data.Get_Owner() != NULL ) { data.Get_Owner()->Peek_Physical_Object()->Dec_Ignore_Counter(); } } void BulletClass::Shutdown( void ) { if ( Projectile != NULL ) { COMBAT_SCENE->Remove_Object( Projectile ); Projectile->Set_Observer( NULL ); } BulletData.AmmoDefinition = NULL; BulletData.Owner = NULL; } /* ** BulletClass Save and Load */ enum { CHUNKID_VARIABLES = 910991544, CHUNKID_OWNER, CHUNKID_TARGET_OBJECT, MICROCHUNKID_AMMO_DEFINITION_ID = 1, MICROCHUNKID_PROJECTILE, MICROCHUNKID_SOFT_PIERCE_COUNT, MICROCHUNKID_DESTROY, MICROCHUNKID_TARGET_VECTOR, MICROCHUNKID_TRACKING_ERROR_TIMER, MICROCHUNKID_TRACKING_ERROR, MICROCHUNKID_MODEL_NAME_CRC, }; bool BulletClass::Save( ChunkSaveClass & csave ) { csave.Begin_Chunk( CHUNKID_VARIABLES ); WWASSERT( BulletData.AmmoDefinition != NULL ); int def_id = BulletData.AmmoDefinition->Get_ID(); WRITE_MICRO_CHUNK( csave, MICROCHUNKID_AMMO_DEFINITION_ID, def_id ); WRITE_MICRO_CHUNK( csave, MICROCHUNKID_PROJECTILE, Projectile ); WRITE_MICRO_CHUNK( csave, MICROCHUNKID_SOFT_PIERCE_COUNT, BulletData.SoftPierceCount ); WRITE_MICRO_CHUNK( csave, MICROCHUNKID_DESTROY, BulletData.Destroy ); WRITE_MICRO_CHUNK( csave, MICROCHUNKID_TARGET_VECTOR, TargetVector ); WRITE_MICRO_CHUNK( csave, MICROCHUNKID_TRACKING_ERROR_TIMER, TrackingErrorTimer ); WRITE_MICRO_CHUNK( csave, MICROCHUNKID_TRACKING_ERROR, TrackingError ); WRITE_MICRO_CHUNK( csave, MICROCHUNKID_MODEL_NAME_CRC, ModelNameCRC ); csave.End_Chunk(); if ( BulletData.Get_Owner() != NULL ) { csave.Begin_Chunk( CHUNKID_OWNER ); BulletData.Owner.Save( csave ); csave.End_Chunk(); } if ( TargetObject.Get_Ptr() != NULL ) { csave.Begin_Chunk( CHUNKID_TARGET_OBJECT ); TargetObject.Save( csave ); csave.End_Chunk(); } return true; } bool BulletClass::Load( ChunkLoadClass & cload ) { while (cload.Open_Chunk()) { switch(cload.Cur_Chunk_ID()) { case CHUNKID_VARIABLES: { int def_id = 0; while (cload.Open_Micro_Chunk()) { switch(cload.Cur_Micro_Chunk_ID()) { READ_MICRO_CHUNK( cload, MICROCHUNKID_AMMO_DEFINITION_ID, def_id ); READ_MICRO_CHUNK( cload, MICROCHUNKID_PROJECTILE, Projectile ); READ_MICRO_CHUNK( cload, MICROCHUNKID_SOFT_PIERCE_COUNT, BulletData.SoftPierceCount ); READ_MICRO_CHUNK( cload, MICROCHUNKID_DESTROY, BulletData.Destroy ); READ_MICRO_CHUNK( cload, MICROCHUNKID_TARGET_VECTOR, TargetVector ); READ_MICRO_CHUNK( cload, MICROCHUNKID_TRACKING_ERROR_TIMER, TrackingErrorTimer ); READ_MICRO_CHUNK( cload, MICROCHUNKID_TRACKING_ERROR, TrackingError ); READ_MICRO_CHUNK( cload, MICROCHUNKID_MODEL_NAME_CRC, ModelNameCRC ); default: Debug_Say(("Unhandled Micro Chunk:%d File:%s Line:%d\r\n",cload.Cur_Micro_Chunk_ID(),__FILE__,__LINE__)); break; } cload.Close_Micro_Chunk(); } WWASSERT( BulletData.AmmoDefinition == NULL ); BulletData.AmmoDefinition = WeaponManager::Find_Ammo_Definition( def_id ); WWASSERT( BulletData.AmmoDefinition != NULL ); WWASSERT( Projectile != NULL ); if ( Projectile != NULL ) { REQUEST_REF_COUNTED_POINTER_REMAP( (RefCountClass **)&Projectile ); } break; } case CHUNKID_OWNER: BulletData.Owner.Load( cload ); break; case CHUNKID_TARGET_OBJECT: TargetObject.Load( cload ); break; default: Debug_Say(("Unhandled Chunk:%d File:%s Line:%d\r\n",cload.Cur_Chunk_ID(),__FILE__,__LINE__)); break; } cload.Close_Chunk(); } SaveLoadSystemClass::Register_Post_Load_Callback(this); return true; } void BulletClass::On_Post_Load (void) { // Plug ourselves back into the physics object as an observer WWASSERT(Projectile != NULL); Projectile->Set_Observer(this); return ; } CollisionReactionType BulletClass::Collision_Occurred( const CollisionEventClass & event ) { // Copy the data from the Projectile Projectile->Get_Velocity( &BulletData.Velocity ); Projectile->Get_Position( &BulletData.Position ); CollisionReactionType result = BulletData.Bullet_Collision_Occurred( event ); if ( BulletData.Destroy ) { Shutdown(); } return result; } ExpirationReactionType BulletClass::Object_Expired(PhysClass * observed_obj) { // Copy the data from the Projectile Projectile->Get_Velocity( &BulletData.Velocity ); Projectile->Get_Position( &BulletData.Position ); ExpirationReactionType result = BulletData.Bullet_Expired(); if ( BulletData.Destroy ) { Shutdown(); } return result; } #define RANDOM_VECTOR( spread ) Vector3( FreeRandom.Get_Float( -(spread), (spread) ), \ FreeRandom.Get_Float( -(spread), (spread) ), \ FreeRandom.Get_Float( 0, (spread) ) ) void BulletClass::Think( void ) { WWPROFILE( "Bullet Think" ); // Count down safety timer BulletData.GrenadeSafetyTimer -= TimeManager::Get_Frame_Seconds(); if ( BulletData.GrenadeSafetyTimer < 0 ) { BulletData.GrenadeSafetyTimer = 0; } // Handle Homing Bullets if ( BulletData.AmmoDefinition->IsTracking ) { // Track your target object DamageableGameObj * target_obj = (DamageableGameObj *)TargetObject.Get_Ptr(); if ( target_obj != NULL ) { if ( target_obj->As_PhysicalGameObj() ) { TargetVector = target_obj->As_PhysicalGameObj()->Get_Bullseye_Position(); } else { target_obj->Get_Position( &TargetVector ); } } Vector3 target_vector = TargetVector; Vector3 obj_pos; Projectile->Get_Transform().Get_Translation( &obj_pos ); // Debug_Say(( "Bullet Tracking %f %f %f\n", obj_pos.X, obj_pos.Y, obj_pos.Z )); target_vector -= obj_pos; // Find distance to target float target_distance = target_vector.Length(); TrackingErrorTimer -= TimeManager::Get_Frame_Seconds(); if ( TrackingErrorTimer < 0 ) { TrackingErrorTimer = FreeRandom.Get_Float( 0.1f, 0.4f ); TrackingError = RANDOM_VECTOR( BulletData.AmmoDefinition->RandomTrackingScale * target_distance / 2 ); } target_vector += TrackingError; target_vector.Normalize(); Vector3 current_vector; Projectile->Get_Velocity( ¤t_vector ); current_vector.Normalize(); Vector3 cross = Vector3::Cross_Product( current_vector, target_vector ); // only re-aim if cross product not near zero if ( cross.Length() > WWMATH_EPSILON ) { float dot = Vector3::Dot_Product( target_vector, current_vector ); if ( WWMath::Fabs(dot) >= 1.0f ) { // No turning needed } else { float angle = WWMath::Fast_Acos( dot ); float allowed_turn = BulletData.AmmoDefinition->TurnRate * TimeManager::Get_Frame_Seconds(); if ( angle > allowed_turn ) { angle = allowed_turn; } cross.Normalize(); Matrix3D tm(cross,angle); Vector3 current_vector; Projectile->Get_Velocity( ¤t_vector ); WWASSERT(current_vector.Is_Valid()); current_vector = tm.Rotate_Vector( current_vector ); WWASSERT(current_vector.Is_Valid()); Projectile->Set_Velocity( current_vector ); } } } } /* ** Instant Bullet Code */ void Simulate_Instant_Bullet( BulletDataClass & data, float progress_time ) { WWPROFILE("Simulate_Instant_Bullet"); // WWASSERT(data.Position.Is_Valid()); // WWASSERT(data.Velocity.Is_Valid()); Vector3 start = data.Position; Vector3 end = data.Velocity; end.Normalize(); end = start + (end * data.AmmoDefinition->Range); // This is that last object we choose to ignore PhysClass *blocker = NULL; // Always ignore the owner if ( data.Get_Owner() != NULL ) { if ( data.Get_Owner()->Peek_Physical_Object() != NULL ) { data.Get_Owner()->Peek_Physical_Object()->Inc_Ignore_Counter(); } } bool done = false; int hit_counter = 0; const int MAX_HITS = 5; while ( !done && (hit_counter < MAX_HITS)) { hit_counter++; // Check for collisions in the path of the object CastResultStruct res; LineSegClass ray( data.Position, end ); PhysRayCollisionTestClass raytest(ray,&res,BULLET_COLLISION_GROUP,COLLISION_TYPE_PROJECTILE); {WWPROFILE("Cast_Ray"); PhysicsSceneClass::Get_Instance()->Cast_Ray(raytest); } // If the result was a "startbad", we are done if (raytest.Result->StartBad) { // Debug_Say(( "Simulate_Instant_Bullet Startbad\n" )); done = true; } else { if (raytest.Result->Fraction < 1.0f) { // If there was a collision, set us at the point of collision // Also, fill in the cast result struct. ray.Compute_Point(raytest.Result->Fraction,&(raytest.Result->ContactPoint)); data.Position = raytest.Result->ContactPoint; // Notify the parties involved WWASSERT(raytest.CollidedPhysObj != NULL); CollisionReactionType reaction = COLLISION_REACTION_DEFAULT; CollisionEventClass event; event.CollisionResult = raytest.Result; event.CollidedRenderObj = raytest.CollidedRenderObj; event.OtherObj = raytest.CollidedPhysObj; reaction |= data.Bullet_Collision_Occurred(event); if ( reaction & COLLISION_REACTION_NO_BOUNCE ) { // We were requested to fly through. Mark the current blocker as ignore // so we collide with him no more this pass if ( blocker ) { // Stop ignoring the last blocker blocker->Dec_Ignore_Counter(); } blocker = raytest.CollidedPhysObj; if ( blocker ) { // Start ignoring this blocker blocker->Inc_Ignore_Counter(); } } else { done = true; } } else { done = true; data.Position = end; } } } if ( blocker ) { // Stop ignoring the last blocker blocker->Dec_Ignore_Counter(); } // stop ignoring the owner if ( data.Get_Owner() != NULL ) { if ( data.Get_Owner()->Peek_Physical_Object() != NULL ) { data.Get_Owner()->Peek_Physical_Object()->Dec_Ignore_Counter(); } } // If not destroyed, Expire.. if ( !data.Destroy ) { data.Bullet_Expired(); } // If the definition requests a beam effect, launch it if (data.AmmoDefinition->BeamEnabled) { _TheBeamEffectManager.Create_Beam_Effect(data.AmmoDefinition,start,data.Position); } } /* ** BulletManager */ MultiListClass LiveBulletList; MultiListClass DeadBulletList; void BulletManager::Init( void ) { _TheBeamEffectManager.Init(); } void BulletManager::Shutdown( void ) { while ( !LiveBulletList.Is_Empty() ) { delete LiveBulletList.Remove_Head(); } while ( !DeadBulletList.Is_Empty() ) { delete DeadBulletList.Remove_Head(); } _TheBeamEffectManager.Shutdown(); } void BulletManager::Update( void ) { MultiListIterator it( &LiveBulletList );; while (!it.Is_Done()) { BulletClass * bullet = it.Peek_Obj(); if (bullet->BulletData.Destroy) { bullet->Shutdown(); it.Remove_Current_Object(); DeadBulletList.Add( bullet ); } else { bullet->Think(); it.Next(); } } } #define BULLET_SPEED_CHEAT 0 void BulletManager::Create_Bullet( const AmmoDefinitionClass * def, const Vector3 & position, const Vector3 & velocity, const ArmedGameObj * owner, float progress_time, const Vector3 & target, DamageableGameObj * target_obj ) { WWASSERT(velocity.Is_Valid()); BulletDataClass data( def, owner, position, velocity ); #if BULLET_SPEED_CHEAT if ( (float)def->Velocity >= _INSTANT_BULLET_THRESHHOLD/3 ) { #else if ( (float)def->Velocity >= _INSTANT_BULLET_THRESHHOLD ) { #endif Simulate_Instant_Bullet( data, progress_time ); return; } WWPROFILE( "Create Bullet" ); BulletClass * bullet = NULL; // Find a bullet long crc = CRC_Stringi( def->ModelName ); MultiListIterator it( &DeadBulletList );; while ( !it.Is_Done() && bullet == NULL ) { BulletClass * test_bullet = it.Peek_Obj(); if ( test_bullet->ModelNameCRC == crc ) { bullet = test_bullet; it.Remove_Current_Object(); } else { it.Next(); } } if ( bullet == NULL ) { bullet = new BulletClass(); } if ( bullet != NULL ) { bullet->Init( data, progress_time, target, target_obj ); LiveBulletList.Add( bullet ); } } /* ** Save & Load */ enum { CHUNKID_BULLET = 916991653, }; bool BulletManager::Save( ChunkSaveClass &csave ) { MultiListIterator it( &LiveBulletList );; while (!it.Is_Done()) { BulletClass * bullet = it.Peek_Obj(); // Only save valid bullets if ( bullet->Is_Valid() ) { csave.Begin_Chunk( CHUNKID_BULLET ); bullet->Save( csave ); csave.End_Chunk(); } it.Next(); } return true; } bool BulletManager::Load( ChunkLoadClass &cload ) { while (cload.Open_Chunk()) { switch(cload.Cur_Chunk_ID()) { case CHUNKID_BULLET: { BulletClass * bullet = new BulletClass(); bullet->Load( cload ); LiveBulletList.Add( bullet ); break; } default: Debug_Say(( "Unrecognized BulletManager chunkID %d\n", cload.Cur_Chunk_ID() )); break; } cload.Close_Chunk(); } return true; }