Game Development Community

RTSUnit Damage Particle Emitters

by BillF · in RTS Starter Kit · 03/16/2008 (10:15 am) · 11 replies

This resource adds progressive damage particles (puffs of smoke and fire) to an RTSUnit that will make a unit seem like its smoking or on fire due to damage. Its most useful for buildings and vehicle type units, although if you wanted to make a walking unit smoke or be on fire I guess it might come in handy :)

The code to do it comes directly from the standard engine's Vehicle class. I modified it a bit to make it....well work first, and I added a few other flags to have more control on the behavior. First, here are some screen shots

lh5.google.com/billfera/R91SoJvryrI/AAAAAAAAAZE/b3xFkSp6PKE/s800/smoke1.jpg

lh6.google.com/billfera/R91SoZvrysI/AAAAAAAAAZM/hPQO3xPhCb0/s800/smoke2.jpg

lh3.google.com/billfera/R91SopvrytI/AAAAAAAAAZU/ASwSjGxpgco/s800/smoke3.jpg

The basic idea is a particle emmiter is a point, in this case on a unit, that spawns particles defined by size, color, speed, lifetime, and a bunch of other attributes. The way the damage particles work is there are predefined thresholds on each level of damage (say 25%, 50%, 75%) and the damage smoke/fire/particle emmiter is registered for one particular level of damage. I expanded the standard code to allow the damage levels to either show only the active damage level, or all at the same time.

#1
03/16/2008 (10:16 am)
And now for the code, we'll modify RTSUnit.h/cc and one of your units's .cs implementation to define the particles.

In RTSUnit.h add

after
class RTSConnection;

add
class ParticleEmitter;
class ParticleEmitterData;

after
bool mDoLookAnimation;

add
enum RTSUnitEmitterConsts
   {
      VC_MAX_NUM_DAMAGE_EMITTERS = 5,
   };

   ParticleEmitterData *   damageEmitterList[ VC_MAX_NUM_DAMAGE_EMITTERS ];
   Point3F damageEmitterOffset[VC_MAX_NUM_DAMAGE_EMITTERS];
   S32 damageEmitterIDList[VC_MAX_NUM_DAMAGE_EMITTERS];
   F32 damageLevelTolerance[VC_MAX_NUM_DAMAGE_EMITTERS];
   F32 numDamageEmitters;
   bool bShowAllDamageEmmiters;

NOTE: This puts a limit on the number of damage emitters on a unit to 5. Feel free to change if you feel you need more.

after
RTSConnection* mControllingConnection;

add
ParticleEmitter *mDamageEmitterList[RTSUnitData::VC_MAX_NUM_DAMAGE_EMITTERS];

after
void setControllingConnection(RTSConnection* conn);

add
void updateDamageSmoke( F32 dt );


now in RTSUnit.cc

after
#include "game/RTS/visManager.h"

add
#include "game/fx/particleEngine.h"

in constructor, after
mDoLookAnimation = false;

add
dMemset( damageEmitterList, 0, sizeof( damageEmitterList ) );
   dMemset( damageEmitterIDList, 0, sizeof( damageEmitterIDList ) );
   dMemset( damageLevelTolerance, 0, sizeof( damageLevelTolerance ) );
   numDamageEmitters = 0;
   bShowAllDamageEmmiters = true;

in packData after
stream->writeString(this->className);

add
stream->write( numDamageEmitters );

   for (int i = 0; i < numDamageEmitters; i++)
   {
      if( stream->writeFlag( damageEmitterList[i] != NULL ) )
      {
         stream->writeRangedU32( damageEmitterList[i]->getId(), DataBlockObjectIdFirst,  DataBlockObjectIdLast );
      }
   }

   for (int j = 0;  j < numDamageEmitters; j++)
   {
      stream->write( damageEmitterOffset[j].x );
      stream->write( damageEmitterOffset[j].y );
      stream->write( damageEmitterOffset[j].z );
   }

   for (int k = 0; k < numDamageEmitters; k++)
   {
      stream->write( damageLevelTolerance[k] );
   }

  stream->writeFlag(bShowAllDamageEmmiters);


in unpackData after
className = stream->readSTString();

add
stream->read( &numDamageEmitters );

   for (int i = 0; i < numDamageEmitters; i++)
   {
      if( stream->readFlag() )
      {
         damageEmitterIDList[i] = stream->readRangedU32( DataBlockObjectIdFirst, DataBlockObjectIdLast );
      }
   }

   for( int j=0; j<numDamageEmitters; j++ )
   {
      stream->read( &damageEmitterOffset[j].x );
      stream->read( &damageEmitterOffset[j].y );
      stream->read( &damageEmitterOffset[j].z );
   }

   for( int k=0; k<numDamageEmitters; k++ )
   {
      stream->read( &damageLevelTolerance[k] );
   }

   bShowAllDamageEmmiters = stream->readFlag();

in initPersistFields after
addField("doLookAnimation",      TypeBool,   Offset(mDoLookAnimation,    RTSUnitData));

add
addField("damageEmitter",        TypeParticleEmitterDataPtr,   Offset(damageEmitterList,     RTSUnitData), VC_MAX_NUM_DAMAGE_EMITTERS);
   addField("damageEmitterOffset",  TypePoint3F,                  Offset(damageEmitterOffset,   RTSUnitData), VC_MAX_NUM_DAMAGE_EMITTERS);
   addField("damageLevelTolerance", TypeF32,                      Offset(damageLevelTolerance,  RTSUnitData), VC_MAX_NUM_DAMAGE_EMITTERS);
   addField("numDamageEmitters",   TypeF32,                      Offset(numDamageEmitters,    RTSUnitData));
   addField("ShowAllDamageEmmiters",TypeBool,   Offset(bShowAllDamageEmmiters,    RTSUnitData));

in RTSUnit::RTSUnit() after
mControllingConnection = NULL;

add
dMemset( mDamageEmitterList, 0, sizeof( mDamageEmitterList ) );

in onAdd() after
if (!Parent::onAdd())
      return false;

add
U32 j;
	for( j=0; j<RTSUnitData::VC_MAX_NUM_DAMAGE_EMITTERS; j++ )
	{
		RTSUnitData *pDB = dynamic_cast<RTSUnitData*>(mDataBlock);

		if( pDB->damageEmitterList[j] )
		{
			mDamageEmitterList[j] = new ParticleEmitter;
			mDamageEmitterList[j]->onNewDataBlock( pDB->damageEmitterList[j] );
			if( !mDamageEmitterList[j]->registerObject() )
			{
				Con::warnf( ConsoleLogEntry::General, "Could not register damage emitter for class: %s", pDB->getName() );
				delete mDamageEmitterList[j];
				mDamageEmitterList[j] = NULL;
			}
		}
	}

in onRemove()
add at the top of the method
for( int i=0; i<RTSUnitData::VC_MAX_NUM_DAMAGE_EMITTERS; i++ )
   {
      if( mDamageEmitterList[i] )
      {
         mDamageEmitterList[i]->deleteWhenEmpty();
         mDamageEmitterList[i] = NULL;
      }
   }

in advanceTime(F32 dt) after
if (mImpactSound)
      playImpactSound();

add
updateDamageSmoke(dt);

after the entire setDirty() method, add a new method
void RTSUnit::updateDamageSmoke( F32 dt )
{
	RTSUnitData *pDB = dynamic_cast<RTSUnitData*>(mDataBlock);

	F32 damagePercent = mDamage / pDB->maxDamage;

	if (pDB->numDamageEmitters >= RTSUnitData::VC_MAX_NUM_DAMAGE_EMITTERS)
	{
		pDB->numDamageEmitters = RTSUnitData::VC_MAX_NUM_DAMAGE_EMITTERS;
	}

	for( int i=pDB->numDamageEmitters-1; i>=0; i-- )
	{
		if( damagePercent < pDB->damageLevelTolerance[i]) continue;

		Point3F offset = pDB->damageEmitterOffset[i];

		MatrixF trans = getTransform();
		trans.mulP( offset );

		if( mDamageEmitterList[i] )
		{
			Point3F emitterPoint = offset;
			mDamageEmitterList[i]->emitParticles( emitterPoint, emitterPoint, Point3F( 0.0, 0.0, 1.0 ), getVelocity(), (U32)(dt * 1000));
		}
  
		if (!pDB->bShowAllDamageEmmiters) break;
	}
}

Now you can compile/link the RTS engine code
#2
03/16/2008 (10:16 am)
Next, in an RTSUnit of your choice in \server\scripts\avatars add the following to a units .cc file

add the top of the file add

datablock ParticleData(RTSUnitLightDamageParticle)
{
   textureName          = "~/data/shapes/particles/smoke";
   dragCoefficient      = 1.0;
   gravityCoefficient   = -0.01;
   inheritedVelFactor   = 0.1;
   constantAcceleration = 0.0;
   lifetimeMS           = 1000;
   lifetimeVarianceMS   = 500;
   useInvAlpha          = true;
   spinRandomMin        = -90.0;
   spinRandomMax        = 500.0;
   colors[0]     = "0.20 0.20 0.20 0.3";
   colors[1]     = "0.1 0.1 0.1 0.3";
   sizes[0]      = 1.0;
   sizes[1]      = 1.5;
};
datablock ParticleEmitterData(RTSUnitLightDamageEmitter)
{
   ejectionPeriodMS = 10;
   periodVarianceMS = 0;
   ejectionVelocity = 2;
   velocityVariance = 2.0;
   ejectionOffset   = 0.0;
   thetaMin         = 25;
   thetaMax         = 35;
   phiReferenceVel  = 0;
   phiVariance      = 90;
   overrideAdvances = false;
   particles = "RTSUnitLightDamageParticle";
};
datablock ParticleData(RTSUnitMediumDamageParticle)
{
   textureName          = "~/data/shapes/particles/smoke";
   dragCoefficient      = 1.0;
   gravityCoefficient   = -0.01;
   inheritedVelFactor   = 0.3;
   constantAcceleration = 0.0;
   lifetimeMS           = 5000;
   lifetimeVarianceMS   = 1000;
   useInvAlpha          = true;
   spinRandomMin        = -90.0;
   spinRandomMax        = 500.0;

   colors[0]     = "0.20 0.20 0.20 0.3";
   colors[1]     = "0.1 0.1 0.1 0.3";
   colors[2]     = "0.33 0.33 0.33 0.05";
   colors[3]     = "0 0 0 0";

   sizes[0]      = 2.0;
   sizes[1]      = 2.5;
   sizes[2]      = 3.25;
   sizes[3]      = 4.0;
};
datablock ParticleEmitterData(RTSUnitMediumDamageEmitter)
{
   ejectionPeriodMS = 10;
   periodVarianceMS = 0;
   ejectionVelocity = 5;
   velocityVariance = 2.0;
   ejectionOffset   = 0.4;
   thetaMin         = 0;
   thetaMax         = 45;
   phiReferenceVel  = 0;
   phiVariance      = 180;
   overrideAdvances = false;
   particles = "RTSUnitMediumDamageParticle";
};

datablock ParticleData(RTSUnitHeavyDamageParticle)
{
   textureName          = "~/data/shapes/particles/smoke";
   dragCoefficient      = 0.0;
   gravityCoefficient   = -0.35;
   inheritedVelFactor   = 0.1;
   constantAcceleration = 0.0;
   lifetimeMS           = 580;
   lifetimeVarianceMS   = 150;
   useInvAlpha = false;
   colors[0]     = "0.8 0.6 0.0 0.1";
   colors[1]     = "0.8 0.65 0.0 0.1";
   colors[2]     = "0.0 0.0 0.0 0.0";
   sizes[0]      = 1.0;
   sizes[1]      = 2.0;
   sizes[2]      = 4.0;

    times[0] = 0.1;
    times[1] = 0.4;
    times[2] = 1.0;

};
datablock ParticleEmitterData(RTSUnitHeavyDamageEmitter)
{
   ejectionPeriodMS = 15;
   periodVarianceMS = 5;
   ejectionVelocity = 0.35;
   velocityVariance = 0.20;
   ejectionOffset   = 0.0;
   thetaMin         = 25;
   thetaMax         = 35;
   phiReferenceVel  = 0;
   particles = "RTSUnitHeavyDamageParticle";
};

NOTE: make sure you have the image file "~/data/shapes/particles/smoke"; in the correct location, if you don't have it hunt around in the demo code for FPS and others and move it into the right spot

Next at the bottom of your datablock RTSUnitData definition after

boundingBox = "2.0 2.0 2.0";

add
damageEmitter[0] = RTSUnitLightDamageEmitter;
   damageEmitter[1] = RTSUnitMediumDamageEmitter;
   damageEmitter[2] = RTSUnitHeavyDamageEmitter;
   damageEmitterOffset[0] = "0.0 0.0 2.0";
   damageEmitterOffset[1] = "0.0 0.0 2.5";
   damageEmitterOffset[2] = "0.0 0.0 1.0";
   damageLevelTolerance[0] = 0.15;
   damageLevelTolerance[1] = 0.50;
   damageLevelTolerance[2] = 0.75;
   numDamageEmitters = 3;
   ShowAllDamageEmmiters = true;

NOTE: The damageLevelTolerance floats define the percentage levels at which each emitter will show. I made the first one 15% just for debugging purposes, would be more fitting to be .25.

And that's it! Feel free to play with the settings and colors to fit your needs. This code was used to generate the screen shots shown above which were specific to the type of smoke/fire I wanted.
#3
03/16/2008 (10:23 am)
Awesome!
Thanks for this Bill.
#4
04/03/2008 (6:20 pm)
Would these same code modifications work for buildings too if added to RTSBuilding.h and RTSBuilding.cc and added the script changes.
#5
04/03/2008 (7:04 pm)
I haven't tested it with buildings yet, but it should work when you add the changes to RTSUnit.h/cc, since it is the base class of RTSBuilding - so its handled already. You'll just need to add the changes to the building's cs file as shown, and ...it should work. Famous last words :)

If I get a chance I'll test it with a building in the next few days.
#6
04/05/2008 (7:13 pm)
I'm getting this to compile but when I go to run it crashes while loading. The last thing the log said was,

Fatal: (c:\rts\engine\core\bitstream.cc @ 246) Out of range read

It was coming from this in bitstream.cc:

inline bool BitStream::readFlag()
{
   if(bitNum > maxReadBitNum)
   {
      error = true;
      AssertFatal(false, "Out of range read");
      return false;
   }
   S32 mask = 1 << (bitNum & 0x7);
   bool ret = (*(dataPtr + (bitNum >> 3)) & mask) != 0;
   bitNum++;
   return ret;
}
#7
04/05/2008 (8:05 pm)
Jonathon, I double checked the code posted with my changes and everything looks correct.

Does every read in unpackData match every write in packData? I've seen things happen like this where if one readFlag is in the wrong position it will cause the assert you're seeing. The reads and writes happen squentially so they need to be in the exact same positions.

If everything looks correct in your code I'll apply these steps to a clean build and double check again.
#8
04/06/2008 (9:46 am)
Yes you were right. I was missing a write in packData. Everything works now.
#9
04/06/2008 (11:00 am)
Cool, I was about to try this to give a hand here, but glad to see this works. Im thinking on open a TDN section called "Code Snipets" with all this awesome resources.
#10
04/06/2008 (11:36 am)
I got it to work with the RTSBuilding with no changes. Just make sure to add a maxDamage field.
#11
04/06/2008 (12:53 pm)
Great! Glad it works.