Game Development Community

dev|Pro Game Development Curriculum

3. MXG Weapon Mod - Structure and Armor Piercing Projectiles

by Marcus L · 07/07/2010 (2:28 am) · 8 comments

Tested on: 1.1b2
Note: I will only keep my resources up to date until next stable release (1.1 final)

This resource will make you able to define piercing power, material power and whether or not the bullets pierces though players.

Ok so I'm going to do it this way:
Where it says // Added, it means that the line is added.
Where it says // Removed, it means that the line is removed.
Where it says // Changed, it means that the line is changed.
Where it says ..., it means that there is code between what ever that was last written.
Whenever it says ..., there will be an comment below describing what function it is.

I recommend you using CTRL + F to find where to put the code blocks. Remember to backup before installing.

This one like the previous one, you'll have to install all my previous resources to install this one (Scroll down to see a list of my Weapon Mod resources). Again, this resource is modifiable if you for example only want this resource but no other I managed to do it.

First edit materials/materialDefinition.cpp:
...
//In Material::Material()
   mCustomDecal = NULL;
   mCustomExplosion = NULL;
   mThickness = 0.0; // Added
...
//In Material::initPersistFields()
	  addField( "customDecal",		   TYPEID< DecalData >(),    Offset( mCustomDecal, Material ) );
	  addField( "customExplosion",	   TYPEID< ExplosionData >(),Offset( mCustomExplosion, Material ) );
	  addField( "thickness",		   TypeF32,				Offset( mThickness, Material ) ); // Added

Then the header file, materials/materialDefinition.h:
...
//In public of class Material
   DecalData* mCustomDecal;
   const char* mCustomExplosion;
   F32 mThickness; // Added

Now modify T3D/projectile.cpp:
...
//In ProjectileData::ProjectileData()
   waterExplosion = NULL;
   waterExplosionId = 0;

   playerExplosion = NULL; // Added
   playerExplosionId = 0; // Added
...
//In ProjectileData::initPersistFields()
   addField("customDecal", TypeBool, Offset(customDecal, ProjectileData));
   addField("customExplosion", TypeBool, Offset(customExplosion, ProjectileData));
   addField("playerExplosion", TYPEID< ExplosionData >(), Offset(playerExplosion, ProjectileData)); // Added
   addField("piercingPower", TypeF32, Offset(piercingPower, ProjectileData)); // Added
   addField("armorPiercing", TypeBool, Offset(armorPiercing, ProjectileData)); // Added
...
//In ProjectileData::preload(bool server, String &errorStr)
      if (!waterExplosion && waterExplosionId != 0)
         if (Sim::findObject(waterExplosionId, waterExplosion) == false)
            Con::errorf(ConsoleLogEntry::General, "ProjectileData::preload: Invalid packet, bad datablockId(waterExplosion): %d", waterExplosionId);
	  
	  if (!playerExplosion && playerExplosionId != 0) // Added ->
		if (Sim::findObject(playerExplosionId, playerExplosion) == false)  
		  Con::errorf(ConsoleLogEntry::General, "ProjectileData::onAdd: Invalid packet, bad datablockId(playerExplosion): %d", playerExplosionId); // Added <-
...
//In ProjectileData::packData(BitStream* stream)
   if (stream->writeFlag(waterExplosion != NULL))
      stream->writeRangedU32(waterExplosion->getId(), DataBlockObjectIdFirst,
                                                      DataBlockObjectIdLast);

   if (stream->writeFlag(playerExplosion != NULL)) // Added ->
      stream->writeRangedU32(playerExplosion->getId(), DataBlockObjectIdFirst,  
                                                       DataBlockObjectIdLast); // Added <-
...
//In ProjectileData::unpackData(BitStream* stream)
   if (stream->readFlag())
      waterExplosionId = stream->readRangedU32(DataBlockObjectIdFirst, DataBlockObjectIdLast);

   if (stream->readFlag()) // Added
      playerExplosionId = stream->readRangedU32(DataBlockObjectIdFirst,  DataBlockObjectIdLast); // Added
...
//In Projectile::Projectile()
   mSourceObjectId( -1 ),
   mSourceObjectSlot( -1 ),
   mPiercing( 0 ), // Added
   mInsideMass( false ), // Added
...
//In Projectile::simulate( F32 dt )
   Point3F oldPosition;
   Point3F newPosition;
   Point3F rayDir; // Added
..
//Same Projectile::simulate( F32 dt )
   if ( mDataBlock->isBallistic )
      mCurrVelocity.z -= 9.81 * mDataBlock->gravityMod * dt;

   newPosition = oldPosition + mCurrVelocity * dt;

   if(this->mInsideMass != true) // Added ->
	   rayDir = oldPosition + mCurrVelocity * dt;
   else
	   rayDir = oldPosition - mCurrVelocity * dt; // Added <-
...
//Same Projectile::simulate( F32 dt )
   //MXG: Note, this resource will not work with a physics plugin without some modifications'
   //     Send a mail to me if you want help with this.
   if ( mPhysicsWorld )
      hit = mPhysicsWorld->castRay( oldPosition, rayDir, &rInfo, Point3F( newPosition - oldPosition) * mDataBlock->impactForce );            
   else 
      hit = getContainer()->castRay(oldPosition, rayDir, csmDynamicCollisionMask | csmStaticCollisionMask, &rInfo); // Changed
...
//Same Projectile::simulate( F32 dt )
// Next order of business: do we explode on this hit?
      if ( mCurrTick > mDataBlock->armingDelay || mDataBlock->armingDelay == 0 )
      {
         MatrixF xform( true ); // Removed ->
         xform.setColumn( 3, rInfo.point );
         setTransform( xform );
         mCurrPosition    = rInfo.point;
         mCurrVelocity    = Point3F::Zero; // Removed <-
...
//Same Projectile::simulate( F32 dt )
         // re-enable the collision response on the source object since
         // we need to process the onCollision and explode calls
         //if ( mSourceObject )
            //mSourceObject->enableCollision();
		 
		 if(rInfo.object->getTypeMask() & PlayerObjectType)  // Changed ->
		 {
			 onCollision( rInfo.point, rInfo.normal, rInfo.object );

			 if(mDataBlock->armorPiercing == true)
			 {
				 Explosion* pExplosion = NULL;
				 if (mDataBlock->playerExplosion != NULL)
				 { 
					 pExplosion = new Explosion;
					 pExplosion->onNewDataBlock(mDataBlock->playerExplosion, false);
				 }
				 if( pExplosion )
				 {
					 MatrixF xform(true);
					 xform.setPosition(rInfo.point);
					 pExplosion->setTransform(xform);
					 pExplosion->setInitialState(rInfo.point, rInfo.normal);
					 if (pExplosion->registerObject() == false)
					 {
						 Con::errorf(ConsoleLogEntry::General, "Projectile(%s)::explode: couldn't register explosion",
										mDataBlock->getName() );
						 delete pExplosion;
						 pExplosion = NULL;
					 }
				 }
			 }else{
				 ExplosionData* explosion = mDataBlock->playerExplosion;
				 explode( rInfo.point, rInfo.normal, NULL, explosion, objectType );
			 }
		 }else{

			 // Get the rayCasts material info 
			 Material* material = ( rInfo.material ? dynamic_cast< Material* >( rInfo.material->getMaterial() ) : 0 );
			 DecalData* decal = NULL;
			 ExplosionData* explosion = NULL;
			 if(mDataBlock->piercingPower != NULL && this->mPiercing == 0)
				 this->mPiercing = mDataBlock->piercingPower;

			 if(this->mInsideMass != true)
				 this->mInsideMass = true;
			 else
				 this->mInsideMass = false;
			 
			 if(material != NULL)
			 {
				 if(mDataBlock->customDecal && material->mCustomDecal != NULL)
					 // Uses custom decal, initalizing custom decal
					 decal = material->mCustomDecal;
				 else
					 // Initalizing default decal
					 decal = mDataBlock->decal;
				 
				 if(mDataBlock->customExplosion && material->mCustomExplosion != NULL)
					 // Uses custom explosion, ititalizing custom explosion
					 explosion = material->mCustomExplosion;
				 else
					 // Initalizing default explosion
					 explosion = mDataBlock->explosion;

				if(material->mThickness >= this->mPiercing || material->mThickness == 0 || this->mPiercing == 0)
				{
					onCollision( rInfo.point, rInfo.normal, rInfo.object );
					explode( rInfo.point, rInfo.normal, decal, explosion, objectType );
				}else{
					onCollision( rInfo.point, rInfo.normal, rInfo.object );
					 
					Explosion* pExplosion = NULL;
					if (explosion != NULL)
					{  
						pExplosion = new Explosion; 
						pExplosion->onNewDataBlock(explosion, false);  
					}  

					if( pExplosion )
					{
						MatrixF xform(true);
						xform.setPosition(rInfo.point);
						pExplosion->setTransform(xform);
						pExplosion->setInitialState(rInfo.point, rInfo.normal);
						if (pExplosion->registerObject() == false)
						{
							Con::errorf(ConsoleLogEntry::General, "Projectile(%s)::explode: couldn't register explosion",
										mDataBlock->getName() );
							delete pExplosion;
							pExplosion = NULL;
						}
					}
				}
					 
				// Client (impact) decal.
				if (decal != NULL)
					gDecalManager->addDecal( rInfo.point,  rInfo.normal, 0.0f, decal );
				 
				// Client object
				updateSound();
			}
		 } // Changed <-
...
//Same Projectile::simulate( F32 dt )
            // Now, take elasticity into account for modulating the speed of the grenade
            mCurrVelocity *= mDataBlock->bounceElasticity;

            // Set the new position to the impact and the bounce
            // will apply on the next frame.
            //F32 timeLeft = 1.0f - rInfo.t;
            newPosition = oldPosition = rInfo.point + rInfo.normal * 0.05f;
         }
      }
	  }
   }

   if(this->mPiercing != 0) // Added
	   this->mPiercing -= 1; // Added

Last, edit T3D/projectile.h:
...
//In public of class ProjectileData
   ExplosionData* waterExplosion;      // Water Explosion Datablock
   S32 waterExplosionId;               // Water Explosion ID

   ExplosionData* playerExplosion;     // Player Explosion Datablock // Added
   S32 playerExplosionId;              // Player Explosion ID // Added
...
//Same
   S32 decalId;                        // (impact) Decal ID
   bool customDecal;
   bool armorPiercing; // Added
   F32 piercingPower; // Added
...
//In protected of class Projectile
   S32      mSourceObjectId;
   S32      mSourceObjectSlot;

   F32      mPiercing; // Added
   bool     mInsideMass; // Added

Ok, now I'll explain some things using this resource. Now you'll be able to define:
1. thickness of your material, this is defined in the material. Example:
singleton Material(wood)
{
   mapTo = "wood";
   diffuseMap[0] = "wood_d";
   normalMap[0] = "wood_n";
   specularPower[0] = "55";
   pixelSpecular[0] = "1";
   thickness = 50; // Materals piercing resistance (very dependent on the projectile)
};
2. piercingPower and armorPiercing of your projectile, this is defined in the projectiles datablock. Example:
datablock ProjectileData(projectile)
{
   projectileShapeName = "art/shapes/weapons/projectiles/9mm/tracer.dts";

   piercingPower = 55; // Piercing Power (very dependent on the material)
   armorPiercing = false; // Does the projectile pierce through players?
};
3. Note: Installing this resource will also give you a playerExplosion. This is the explosion that is applied when you hit a player. To define this add playerExplosion = explosion;, where explosion is the explosion you want applied when you hit a player.

With these variables you'll be able to define very customized piercing abilities. Here is a neat picture of how the mechanism works:
img816.imageshack.us/img816/8913/75480940.png
The green bullet in this picture is the one that goes through, while the red has to low power to pierce the wall. This mechanism will give the projectile the ability to go through many walls. Note that the green bullet will leave a decal and explosion at exit.

Issues:
1. There is a bug in the engine which is discussed in this thread(has fix as well), this fix is required to avoid lost explosion packets.
2. I've noticed that saving a material with customized variables will screw up the material. I haven't looked into this yet, but if some of you know the fix, please post it!

If you stumble upon some issues, let me know.
Also know that my methods might not be correct, nor the most professional. Therefore if you see some silly mistakes please leave a comment with the "fix".

Other resources in my Weapon Mod:
1. MXG Weapon Mod - Decals Depending on Material
2. MXG Weapon Mod - Explosion and SFX Depending on Material
3. MXG Weapon Mod - Structure and Armor Piercing Projectiles
4. MXG Weapon Mod - Advanced Crosshair

Thanks,
Marcus L.

#1
07/07/2010 (4:21 pm)
Nice idea.

Does "piercing power" drop by 1 each tick even if the thickness is too great? Or does it have a quick check and abort? Thus saving computing time.
#2
07/07/2010 (4:37 pm)
Quote:Does "piercing power" drop by 1 each tick even if the thickness is too great?
Hmm, didn't think of that when writing the code, but no, if the thickness is greater than the piercing power at hit, the projectile will explode right away pfew!.

Haven't actually tested this, but from reading my code, it seems to be working that way yes, i had to read my own code to figure this out O_o:
if(mDataBlock->piercingPower != NULL && this->mPiercing == 0)
                 // Example: piercingPower = 25
				 this->mPiercing = mDataBlock->piercingPower; 
...
//Then a little further down
                 // Example: if(50 >= 25){ EXPLODE }
				 if(material->mThickness >= this->mPiercing || material->mThickness == 0 || this->mPiercing == 0) 
				 {
					 onCollision( rInfo.point, rInfo.normal, rInfo.object );
					 explode( rInfo.point, rInfo.normal, decal, explosion, objectType );
				 }

Quote:Thus saving computing time.
Is this so much tho? We're only talking about seconds :D.
#3
07/08/2010 (12:26 pm)
Yes, and for the way you explain it, and tested it by the looks of things CPU isnt a problem until lots of multiple shots are fired at the player. If he is being hunted by 12 people because all his comrades are dead, and these 2 have fully auto weapons? That might be more along the line Steve is talking about(especially after Steve's last in-game video!)
But aside from that I think this is a great idea! and by the way a great resources, don't feel you need to include anything extra, we all appreciate your effort:O)
#4
07/09/2010 (7:34 pm)
It's interesting how there can be more than one solution to the same objective in programing. I kinda like the idea of the thickness approach. This might solve some of my issues. My solution was to defined levels of penetration for materials and projectiles.


Here's my snippet if you're interested:
// Pretty sure this can be optimized...
// Just after "hit = getContainer()->castRay(oldPosition, newPosition, csmDynamicCollisionMask | csmStaticCollisionMask, &rInfo);"
// And replace "if ( hit )
//----------------------------------------------------------------------
//------------- Gamer's High - Travis: Penetrable Objects

    bool processHit = false;

	if( hit )
	{
		Material* material = ( rInfo.material ? dynamic_cast< Material* >( rInfo.material->getMaterial() ) : 0 );

		if(material != NULL)
		{
			if(material->mPenetrationLevel > 0 && mDataBlock->mPenetrateLevel >= material->mPenetrationLevel)
			{
				mHitCount++;
				if(mHitCount > mDataBlock->mHitMax)
					processHit = true;
				else
					paintDecal(rInfo.point, rInfo.normal);
			}
			else
				processHit = true;
		}
	}
//----------------------------------------------------------------------

   if ( processHit )

With your thickness method, I could slow the projectile. Giving it more "realism". Just might have to implement that.

Great work Marcus!
#5
07/10/2010 (3:15 am)
Quote:we all appreciate your effort:O)
Thanks, I've always wanted to help this comunity (O: does it exist a nicer comunity?

@Travis
Quote:It's interesting how there can be more than one solution to the same objective in programing
It's always like that, by the way how did you deal with the exit effects (decals and explosion)?

Not sure how our methods differ in memory consumption, but i guess your method is less consuming. It always end up at the question between preformance and dynamicness =) thanks btw. I'll look at your code and see if it might come to use.
#6
07/10/2010 (11:01 am)
Nice work Marcus! It's always good to see a series of Resources that tie together, and yours is sort of like a logical evolution for enhancing projectile/explosions - which is definitely a cool thing :D
#7
07/10/2010 (12:29 pm)
I think the most taxing is the dynamic_cast, but this, I believe, the easiest way to get the material information from a TS Static.

Basically, if the projectile can penetrate the object, it uses the rInfo (ray cast information) to get the normal collision point. Once I have the point I'll paint the decal paintDecal(rInfo.point, rInfo.normal); but will not process the normal collision. From my testing, this works when the projectile exits the object too. I'm looking at your custom sounds, decals, and particles and wanting to integrate those into this system. Could be a simple function in place of my paintDecal, maybe processVirtualHit(rInfo);...

Might have to work on a new kind material class. One that handles decals, sounds, particles, etc.

You got me thinking again!
#8
07/10/2015 (9:45 pm)
@Marcus has your modify T3D/projectile.cpp been put into action?

I ask as a number of your reference points/code don't seem to be there.

Mainly in the Projectile::simulate( F32 dt ) area of the code.

Can you please advise.