/* ** 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 . */ /*********************************************************************************************** *** C O N F I D E N T I A L --- W E S T W O O D S T U D I O S *** *********************************************************************************************** * * * Project Name : WWPhys * * * * $Archive:: /Commando/Code/wwphys/projectile.cpp $* * * * Author:: Greg_h * * * * $Modtime:: 12/20/01 5:14p $* * * * $Revision:: 60 $* * * *---------------------------------------------------------------------------------------------* * Functions: * * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ #include "projectile.h" #include "pscene.h" #include "lineseg.h" #include "physcon.h" #include "physcoltest.h" #include "rendobj.h" #include "persistfactory.h" #include "wwphysids.h" #include "simpledefinitionfactory.h" #include "wwdebug.h" #include "wwhack.h" #include "wwprofile.h" DECLARE_FORCE_LINK(projectile); /*********************************************************************************************** ** ** ProjectileClass Implementation ** ***********************************************************************************************/ /* ** Declare a PersistFactory for ProjectileClasses */ SimplePersistFactoryClass _ProjectileFactory; /* ** ChunkID's used by ProjectileClass */ enum { PROJECTILE_CHUNK_MOVEABLEPHYS = 0x03308765, // parent class data PROJECTILE_CHUNK_VARIABLES, PROJECTILE_VARIABLE_POSITION = 0x00, PROJECTILE_VARIABLE_VELOCITY, PROJECTILE_VARIABLE_COLLIDESONMOVE, PROJECTILE_VARIABLE_ORIENTATIONMODE, PROJECTILE_VARIABLE_TUMBLEAXIS, PROJECTILE_VARIABLE_TUMBLERATE, PROJECTILE_VARIABLE_LIFETIME, PROJECTILE_VARIABLE_BOUNCECOUNT, }; ProjectileClass::ProjectileClass(void) : CollidesOnMove(true), OrientationMode(ORIENTATION_ALIGNED), TumbleAxis(1,0,0), TumbleRate(DEG_TO_RADF(10.0f)), Lifetime(2.0f), BounceCount(0) { State.Position.Set(0,0,0); State.Velocity.Set(0,0,0); // turn off lighting for all projectiles Enable_Is_Pre_Lit(true); } void ProjectileClass::Init(const ProjectileDefClass & def) { MoveablePhysClass::Init(def); CollidesOnMove = def.CollidesOnMove; OrientationMode = def.OrientationMode; TumbleAxis = def.TumbleAxis; TumbleRate = def.TumbleRate; Lifetime = def.Lifetime; BounceCount = def.BounceCount; State.Position.Set(0,0,0); State.Velocity.Set(0,0,0); if (TumbleAxis.Length2() == 0.0f) { TumbleAxis.Set(0,0,1); } else { TumbleAxis.Normalize(); } // turn off lighting for all projectiles Enable_Is_Pre_Lit(true); } ProjectileClass::~ProjectileClass(void) { } const AABoxClass & ProjectileClass::Get_Bounding_Box(void) const { assert(Model); return Model->Get_Bounding_Box(); } const Matrix3D & ProjectileClass::Get_Transform(void) const { assert(Model); return Model->Get_Transform(); } void ProjectileClass::Set_Transform(const Matrix3D & tm) { tm.Get_Translation(&State.Position); assert(Model); Model->Set_Transform(tm); // TODO: don't use the actual bounds? Just use the position and a pre-calced extent Update_Cull_Box(); } void ProjectileClass::Set_Orientation_Mode_Tumbling(void) { OrientationMode = ORIENTATION_TUMBLING; TumbleAxis.Set(0,0,0); while (TumbleAxis.Length() <= 0.001f) { TumbleAxis.X = WWMath::Random_Float(); TumbleAxis.Y = WWMath::Random_Float(); TumbleAxis.Z = WWMath::Random_Float(); } TumbleAxis.Normalize(); TumbleRate = WWMath::Random_Float((float)DEG_TO_RAD(2.0f),(float)DEG_TO_RAD(10.0f)); } void ProjectileClass::Set_Orientation_Mode_Tumbling(const Vector3 & axis,float rate) { OrientationMode = ORIENTATION_TUMBLING; TumbleAxis = axis; TumbleAxis.Normalize(); TumbleRate = rate; } void ProjectileClass::Set_Orientation_Mode_Aligned_Fixed(void) { OrientationMode = ORIENTATION_ALIGNED_FIXED; if (OrientationMode == ORIENTATION_ALIGNED_FIXED) { Matrix3D tm; tm.Obj_Look_At(State.Position,State.Position + State.Velocity,0.0f); Model->Set_Transform(tm); } } void ProjectileClass::Set_Lifetime( float time ) { Lifetime = time; } float ProjectileClass::Get_Lifetime( void ) { return Lifetime; } void ProjectileClass::Set_Bounce_Count(int count) { BounceCount = count; } int ProjectileClass::Get_Bounce_Count(void) { return BounceCount; } void ProjectileClass::Integrate(float dt) { float accel = PhysicsConstants::GravityAcceleration.Z * GravScale; State.Position.X += State.Velocity.X * dt; State.Position.Y += State.Velocity.Y * dt; State.Position.Z += 0.5f * accel * dt * dt + State.Velocity.Z * dt; State.Velocity.Z += accel * dt; } void ProjectileClass::Timestep(float dt) { WWPROFILE("Projectile::Timestep"); const int MAX_BUMPS = 5; if (Is_User_Control_Enabled()) { return; } if (dt == 0.0f) { return; } WWASSERT(State.Position.Is_Valid()); WWASSERT(State.Velocity.Is_Valid()); if ( CollidesOnMove ) { WWPROFILE("Move and Collide"); /* ** Repeat until we eat all of the time */ float remaining_time = dt; float timestep; int bumps = 0; /* ** This is that last object we choose to ignore */ PhysClass *blocker = NULL; while ((remaining_time > 0.0f) && (bumps < MAX_BUMPS) && (BounceCount >= 0)) { timestep = remaining_time; bumps++; /* ** Integrate the state of the object for some amount ** of time: timestep */ ProjectileStateStruct oldstate = State; Integrate(timestep); /* ** Check for collisions in the path of the object */ CastResultStruct res; LineSegClass ray(oldstate.Position,State.Position); PhysRayCollisionTestClass raytest(ray,&res,Get_Collision_Group(),COLLISION_TYPE_PROJECTILE); { WWPROFILE("Raycast"); Inc_Ignore_Counter(); PhysicsSceneClass::Get_Instance()->Cast_Ray(raytest); Dec_Ignore_Counter(); } /* ** If the result was a "startbad", just do the whole step */ if (raytest.Result->StartBad) { remaining_time -= timestep; } else { /* ** If there was a collision, cut the timestep so that ** we integrate right up to the collision */ if (raytest.Result->Fraction < 1.0f) { timestep = raytest.Result->Fraction * timestep; remaining_time -= timestep; /* ** Compute the point of collision, stored in ContactPoint of the CastResStruct ** so that observers have it. */ ray.Compute_Point(raytest.Result->Fraction,&(raytest.Result->ContactPoint)); /* ** Reset and integrate up to that point */ State = oldstate; Integrate(0.99f * timestep); /* ** Notify the parties involved */ WWASSERT(raytest.CollidedPhysObj != NULL); CollisionReactionType reaction = COLLISION_REACTION_DEFAULT; CollisionEventClass event; event.CollisionResult = raytest.Result; event.CollidedRenderObj = raytest.CollidedRenderObj; { WWPROFILE("Callbacks"); event.OtherObj = raytest.CollidedPhysObj; reaction |= Collision_Occurred(event); event.OtherObj = this; reaction |= raytest.CollidedPhysObj->Collision_Occurred(event); } if ( !(reaction & COLLISION_REACTION_NO_BOUNCE) ) { /* ** Perform collision processing and loop */ float dot = Vector3::Dot_Product(State.Velocity,raytest.Result->Normal); State.Velocity -= (1.0f + Elasticity)*dot*raytest.Result->Normal; /* ** If we are using ORIENTATION_MODE_ALIGNED_FIXED, update the ** transform whenever we bounce */ if (OrientationMode == ORIENTATION_ALIGNED_FIXED) { Matrix3D tm; tm.Obj_Look_At(State.Position,State.Position + State.Velocity,0.0f); Model->Set_Transform(tm); } /* ** Decrement the bounce count! */ BounceCount--; } else { /* ** We were requested to fly through. Mark the current blocker as ignore ** so we collide with him no more this pass */ Integrate(0.02*timestep); if ( blocker ) { // Stop ignoring the last blocker blocker->Dec_Ignore_Counter(); } blocker = raytest.CollidedPhysObj; if ( blocker ) { // Start ignoring this blocker blocker->Inc_Ignore_Counter(); } } /* ** If requested to stop, progress no more */ if ( reaction & COLLISION_REACTION_STOP_MOTION ) { remaining_time = 0; State.Velocity = Vector3(0,0,0); } } else { remaining_time -= timestep; } } } if ( blocker ) { // Stop ignoring the last blocker blocker->Dec_Ignore_Counter(); } } else { Integrate(dt); } Update_Transform(dt); Update_Cull_Box(); DEBUG_RENDER_AXES(Get_Transform(),Vector3(1.0f,1.0f,1.0f)); /* ** Decrement our life, check our bounce count */ Lifetime -= dt; if ((Lifetime < 0.0f) || (BounceCount < 0)) { Expire(); } } bool ProjectileClass::Cast_Ray(PhysRayCollisionTestClass & raytest) { assert(Model); if (Model->Cast_Ray(raytest)) { raytest.CollidedPhysObj = this; return true; } return false; } void ProjectileClass::Update_Transform(float dt) { WWASSERT(Model); // Update the cached transformation matrix Matrix3D tm; switch(OrientationMode) { case ORIENTATION_ALIGNED: // compute a matrix aligned to the path tm.Obj_Look_At(State.Position,State.Position + State.Velocity,0.0f); Model->Set_Transform(tm); break; case ORIENTATION_ALIGNED_FIXED: case ORIENTATION_FIXED: // just update the position Model->Set_Position(State.Position); break; case ORIENTATION_TUMBLING: // update the position, tumble a little tm = Model->Get_Transform(); tm.Set_Translation(State.Position); Matrix3D::Multiply(tm,Matrix3D(TumbleAxis,TumbleRate*dt),&tm); Model->Set_Transform(tm); break; }; Update_Visibility_Status(); } const PersistFactoryClass & ProjectileClass::Get_Factory (void) const { return _ProjectileFactory; } bool ProjectileClass::Save (ChunkSaveClass &csave) { csave.Begin_Chunk(PROJECTILE_CHUNK_MOVEABLEPHYS); MoveablePhysClass::Save(csave); csave.End_Chunk(); csave.Begin_Chunk(PROJECTILE_CHUNK_VARIABLES); WRITE_MICRO_CHUNK(csave,PROJECTILE_VARIABLE_POSITION,State.Position); WRITE_MICRO_CHUNK(csave,PROJECTILE_VARIABLE_VELOCITY,State.Velocity); WRITE_MICRO_CHUNK(csave,PROJECTILE_VARIABLE_COLLIDESONMOVE,CollidesOnMove); WRITE_MICRO_CHUNK(csave,PROJECTILE_VARIABLE_ORIENTATIONMODE,OrientationMode); WRITE_MICRO_CHUNK(csave,PROJECTILE_VARIABLE_TUMBLEAXIS,TumbleAxis); WRITE_MICRO_CHUNK(csave,PROJECTILE_VARIABLE_TUMBLERATE,TumbleRate); WRITE_MICRO_CHUNK(csave,PROJECTILE_VARIABLE_LIFETIME,Lifetime); WRITE_MICRO_CHUNK(csave,PROJECTILE_VARIABLE_BOUNCECOUNT,BounceCount); csave.End_Chunk(); return true; } bool ProjectileClass::Load (ChunkLoadClass &cload) { while (cload.Open_Chunk()) { switch(cload.Cur_Chunk_ID()) { case PROJECTILE_CHUNK_MOVEABLEPHYS: MoveablePhysClass::Load(cload); break; case PROJECTILE_CHUNK_VARIABLES: while (cload.Open_Micro_Chunk()) { switch(cload.Cur_Micro_Chunk_ID()) { READ_MICRO_CHUNK(cload,PROJECTILE_VARIABLE_POSITION,State.Position); READ_MICRO_CHUNK(cload,PROJECTILE_VARIABLE_VELOCITY,State.Velocity); READ_MICRO_CHUNK(cload,PROJECTILE_VARIABLE_COLLIDESONMOVE,CollidesOnMove); READ_MICRO_CHUNK(cload,PROJECTILE_VARIABLE_ORIENTATIONMODE,OrientationMode); READ_MICRO_CHUNK(cload,PROJECTILE_VARIABLE_TUMBLEAXIS,TumbleAxis); READ_MICRO_CHUNK(cload,PROJECTILE_VARIABLE_TUMBLERATE,TumbleRate); READ_MICRO_CHUNK(cload,PROJECTILE_VARIABLE_LIFETIME,Lifetime); READ_MICRO_CHUNK(cload,PROJECTILE_VARIABLE_BOUNCECOUNT,BounceCount); } cload.Close_Micro_Chunk(); } break; default: WWDEBUG_SAY(("Unhandled Chunk: 0x%X File: %s Line: %d\r\n",cload.Cur_Chunk_ID(),__FILE__,__LINE__)); break; } cload.Close_Chunk(); } return true; } /*********************************************************************************************** ** ** ProjectileDefClass Implementation ** ***********************************************************************************************/ /* ** Declare a PersistFactory for ProjectileDefClasses, this allows them to save and load */ SimplePersistFactoryClass _ProjectileDefFactory; /* ** Declare a definition factory. This exposes ProjectileDef to the editor. */ DECLARE_DEFINITION_FACTORY(ProjectileDefClass, CLASSID_PROJECTILEDEF, "Projectile") _ProjectileDefDefFactory; /* ** Chunk ID's used by MoveablePhysDefClass */ enum { PROJECTILEDEF_CHUNK_MOVEABLEPHYSDEF = 0x01210011, // (parent class) PROJECTILEDEF_CHUNK_VARIABLES, PROJECTILEDEF_VARIABLE_COLLIDESONMOVE = 0x00, PROJECTILEDEF_VARIABLE_ORIENTATIONMODE, PROJECTILEDEF_VARIABLE_TUMBLEAXIS, PROJECTILEDEF_VARIABLE_TUMBLERATE, PROJECTILEDEF_VARIABLE_LIFETIME, PROJECTILEDEF_VARIABLE_BOUNCECOUNT, }; ProjectileDefClass::ProjectileDefClass(void) : CollidesOnMove(true), OrientationMode(ProjectileClass::ORIENTATION_ALIGNED), TumbleAxis(1,2,1), TumbleRate(DEG_TO_RADF(10.0f)), Lifetime(2.0f), BounceCount(0) { #ifdef PARAM_EDITING_ON // make our parameters editable! EDITABLE_PARAM(ProjectileDefClass, ParameterClass::TYPE_BOOL, CollidesOnMove); EnumParameterClass *param = new EnumParameterClass(&OrientationMode); param->Set_Name ("OrientationMode"); param->Add_Value("ALIGNED",ProjectileClass::ORIENTATION_ALIGNED); param->Add_Value("FIXED",ProjectileClass::ORIENTATION_FIXED); param->Add_Value("TUMBLE",ProjectileClass::ORIENTATION_TUMBLING); GENERIC_EDITABLE_PARAM(ProjectileDefClass,param) FLOAT_EDITABLE_PARAM(ProjectileDefClass, TumbleAxis.X, 0.0f, 10.0f); FLOAT_EDITABLE_PARAM(ProjectileDefClass, TumbleAxis.Y, 0.0f, 10.0f); FLOAT_EDITABLE_PARAM(ProjectileDefClass, TumbleAxis.Z, 0.0f, 10.0f); ANGLE_EDITABLE_PARAM(ProjectileDefClass, TumbleRate, DEG_TO_RADF(0.1f), DEG_TO_RADF(90.0f)); FLOAT_EDITABLE_PARAM(ProjectileDefClass, Lifetime, 0.01f, 100.0f); INT_EDITABLE_PARAM(ProjectileDefClass, BounceCount, 0, 32); #endif } const PersistFactoryClass & ProjectileDefClass::Get_Factory (void) const { return _ProjectileDefFactory; } uint32 ProjectileDefClass::Get_Class_ID (void) const { return CLASSID_PROJECTILEDEF; } PersistClass * ProjectileDefClass::Create(void) const { ProjectileClass * obj = NEW_REF(ProjectileClass,()); obj->Init(*this); return obj; } bool ProjectileDefClass::Save(ChunkSaveClass &csave) { csave.Begin_Chunk(PROJECTILEDEF_CHUNK_MOVEABLEPHYSDEF); MoveablePhysDefClass::Save(csave); csave.End_Chunk(); csave.Begin_Chunk(PROJECTILEDEF_CHUNK_VARIABLES); WRITE_MICRO_CHUNK(csave,PROJECTILEDEF_VARIABLE_COLLIDESONMOVE,CollidesOnMove); WRITE_MICRO_CHUNK(csave,PROJECTILEDEF_VARIABLE_ORIENTATIONMODE,OrientationMode); WRITE_MICRO_CHUNK(csave,PROJECTILEDEF_VARIABLE_TUMBLEAXIS,TumbleAxis); WRITE_MICRO_CHUNK(csave,PROJECTILEDEF_VARIABLE_TUMBLERATE,TumbleRate); WRITE_MICRO_CHUNK(csave,PROJECTILEDEF_VARIABLE_LIFETIME,Lifetime); WRITE_MICRO_CHUNK(csave,PROJECTILEDEF_VARIABLE_BOUNCECOUNT,BounceCount); csave.End_Chunk(); return true; } bool ProjectileDefClass::Load(ChunkLoadClass &cload) { while (cload.Open_Chunk()) { switch(cload.Cur_Chunk_ID()) { case PROJECTILEDEF_CHUNK_MOVEABLEPHYSDEF: MoveablePhysDefClass::Load(cload); break; case PROJECTILEDEF_CHUNK_VARIABLES: while (cload.Open_Micro_Chunk()) { switch(cload.Cur_Micro_Chunk_ID()) { READ_MICRO_CHUNK(cload,PROJECTILEDEF_VARIABLE_COLLIDESONMOVE,CollidesOnMove); READ_MICRO_CHUNK(cload,PROJECTILEDEF_VARIABLE_ORIENTATIONMODE,OrientationMode); READ_MICRO_CHUNK(cload,PROJECTILEDEF_VARIABLE_TUMBLEAXIS,TumbleAxis); READ_MICRO_CHUNK(cload,PROJECTILEDEF_VARIABLE_TUMBLERATE,TumbleRate); READ_MICRO_CHUNK(cload,PROJECTILEDEF_VARIABLE_LIFETIME,Lifetime); READ_MICRO_CHUNK(cload,PROJECTILEDEF_VARIABLE_BOUNCECOUNT,BounceCount); } cload.Close_Micro_Chunk(); } break; default: WWDEBUG_SAY(("Unhandled Chunk: 0x%X File: %s Line: %d\r\n",cload.Cur_Chunk_ID(),__FILE__,__LINE__)); break; } cload.Close_Chunk(); } return true; } bool ProjectileDefClass::Is_Type(const char * type_name) { if (stricmp(type_name,ProjectileDefClass::Get_Type_Name()) == 0) { return true; } else { return MoveablePhysDefClass::Is_Type(type_name); } }