Game Development Community

dev|Pro Game Development Curriculum

Torque3D Tracer Rounds

by Andrew Edmonds · 01/15/2010 (11:55 am) · 8 comments

I'd had 'tracer rounds' written on my to-do list for so long and after searching a lot, I still couldn't find anything that would fit the bill so I went ahead and wrote it myself. WARNING: It's not entirely realistic. In reality, a tracer round would be placed in a clip at (for example) every 5th position. This code makes an approximation of that - it allows you to set a percentage of your rounds that you wish to be tracers and uses a random number to decide whether to fire a tracer or not (so if you set a percentage of 20 in the datablock, roughly 1 on 5 rounds will be a tracer round). If you really want to do it properly, you could probably extend what I've done here. If that's enough for you, then let's continue!

EDIT: 'The People' demanded video, and who am I to argue? This shows the effect in action, but the muzzle velocity has been slowed right down (to 5) to show the effect. Also, the light has been brightened up massively - again to show the effect. In reality, our muzzle velocity is somewhere in the region of 400 for this weapon so it's a blink-and-you'll-miss-it effect.

Fields
The following fields are added to the ProjectileData datablock:
  • projectileTracerShapeName : The shape file for your tracer projectile.
  • tracerLightDesc : The LightDescription for your tracer. This allows you to have regular projectiles lit one way (or not at all) and your tracer projectiles lit another way (or not at all).
  • tracerPercent : The percentage of your rounds that you wish to be tracers

Source code changes

There are quite a few small source code changes so I apologise if it is a bit hard to read. This was done with a stock 1.1 Alpha codebase.

projectile.h

At around line 77, after
S32 armingDelay;  // the values are converted on initialization with
S32 fadeDelay;    // the IRangeValidatorScaled field validator

Add
// tracerRound >>>
const char* projectileTracerShapeName;
F32 tracerPercent;
LightDescription *tracerLightDesc;
S32 tracerLightDescId;
// tracerRound >>>

At around line 105, after
// variables set on preload:
Resource<TSShape> projectileShape;

Add
// tracerRound >>>
Resource<TSShape> projectileTracerShape;
// tracerRound <<<

At around line 179, after
// Rendering related variables
TSShapeInstance* mProjectileShape;
TSThread*        mActivateThread;
TSThread*        mMaintainThread;

Add
// tracerRound >>>
bool				mIsTracer;
// tracerRound <<<

That's it for projectile.h

projectile.cpp
At around line 55, after
ProjectileData::ProjectileData()
{
   projectileShapeName = NULL;

Add
// tracerRound >>>
projectileTracerShapeName = NULL;
tracerPercent = 0;
tracerLightDesc = NULL;
tracerLightDescId = 0;
// tracerRound <<<

At around line 125, after
addNamedField(projectileShapeName, TypeFilename, ProjectileData);
addNamedField(scale, TypePoint3F, ProjectileData);

Add
// tracerRound >>>
addNamedField(projectileTracerShapeName, TypeFilename, ProjectileData);
addNamedField(tracerPercent, TypeF32, ProjectileData);
addNamedField(tracerLightDesc, TypeLightDescriptionPtr, ProjectileData);
// tracerRound <<<

At around line 222, after
if (!lightDesc && lightDescId != 0)
   if (Sim::findObject(lightDescId, lightDesc) == false)
      Con::errorf(ConsoleLogEntry::General, "ProjectileData::preload: Invalid packet, bad datablockid(lightDesc): %d", lightDescId);

Add
// tracerRound >>>
if (!tracerLightDesc && tracerLightDescId != 0)
 if (Sim::findObject(tracerLightDescId, tracerLightDesc) == false)
  Con::errorf(ConsoleLogEntry::General, "ProjectileData::preload: Invalid packet, bad datablockid(tracerLightDesc): %d", tracerLightDescId); 
// tracerRound <<<

At around line 242, after
}
   activateSeq = projectileShape->findSequence("activate");
   maintainSeq = projectileShape->findSequence("maintain");
}

Add
// tracerRound >>>
if (projectileTracerShapeName && projectileTracerShapeName[0] != '')
{
   projectileTracerShape = ResourceManager::get().load(projectileTracerShapeName);
   if (bool(projectileTracerShape) == false)
   {
      errorStr = String::ToString("ProjectileData::load: Couldn't load shape "%s"", projectileTracerShapeName);
      return false;
   }
}
// tracerRound <<<

At around line 268, after
Parent::packData(stream);

stream->writeString(projectileShapeName);

Add
// tracerRound >>>
stream->writeString(projectileTracerShapeName);
// tracerRound <<<

At around line 308, after
if ( stream->writeFlag(lightDesc != NULL))
   stream->writeRangedU32(lightDesc->getId(), DataBlockObjectIdFirst,
                                              DataBlockObjectIdLast);

Add
// tracerRound >>>
if ( stream->writeFlag(tracerLightDesc != NULL))
   stream->writeRangedU32(tracerLightDesc->getId(), DataBlockObjectIdFirst, DataBlockObjectIdLast);
// tracerRound <<<

At around line 338, after
Parent::unpackData(stream);

projectileShapeName = stream->readSTString();

Add
// tracerRound >>>
projectileTracerShapeName = stream->readSTString();
// tracerRound <<<

At around line 376, after
if (stream->readFlag())
   lightDescId = stream->readRangedU32(DataBlockObjectIdFirst, DataBlockObjectIdLast);

Add
// tracerRound >>>
if (stream->readFlag())
   tracerLightDescId = stream->readRangedU32(DataBlockObjectIdFirst, DataBlockObjectIdLast);
// tracerRound <<<

At around line 571, inside bool Projectile::onAdd()

After
// If we're on the server, we need to inherit some of our parent's velocity
   //
   mCurrTick = 0;
}
   else

Change:
if (bool(mDataBlock->projectileShape))
{
   mProjectileShape = new TSShapeInstance(mDataBlock->projectileShape, isClientObject());

   if (mDataBlock->activateSeq != -1)
   {
      mActivateThread = mProjectileShape->addThread();
      mProjectileShape->setTimeScale(mActivateThread, 1);
      mProjectileShape->setSequence(mActivateThread, mDataBlock->activateSeq, 0);
   }
}

To:

if (bool(mDataBlock->projectileShape))
{
// tracerRound >>>
   mProjectileShape = new TSShapeInstance(mDataBlock->projectileShape, isClientObject());
   S32 random_number = mRandI(0, 100);
   F32 theTracerPercent = mDataBlock->tracerPercent;
   if (theTracerPercent > 100)
      theTracerPercent = 100;
   if (theTracerPercent < 0)
      theTracerPercent = 0;
   if (random_number > 100-theTracerPercent) {
      // use tracer if one is defined in the datablock
      if (mDataBlock->projectileTracerShape) {
         mProjectileShape = new TSShapeInstance(mDataBlock->projectileTracerShape, isClientObject());
         mIsTracer = true;
      } else {
         mProjectileShape = new TSShapeInstance(mDataBlock->projectileShape, isClientObject());
         mIsTracer = false;
      }
   } else {
      // use normal projectile
      mProjectileShape = new TSShapeInstance(mDataBlock->projectileShape, isClientObject());
      mIsTracer = false;
   }
   // tracerRound <<<

   if (mDataBlock->activateSeq != -1)
   {
      mActivateThread = mProjectileShape->addThread();
      mProjectileShape->setTimeScale(mActivateThread, 1);
      mProjectileShape->setSequence(mActivateThread, mDataBlock->activateSeq, 0);
   }
}

At around line 687, change the entire void Projectile::submitLights( LightManager *lm, bool staticLighting ) function to

void Projectile::submitLights( LightManager *lm, bool staticLighting )
{
   // tracerRound >>>
   if (staticLighting || mHidden || (!mDataBlock->lightDesc && !mDataBlock->tracerLightDesc))
      return;
   
   if (this->mIsTracer) {
      if (mDataBlock->tracerLightDesc) {
         mDataBlock->tracerLightDesc->submitLight( &mLightState, getRenderTransform(), lm, this);
      }
   } else {
      if (mDataBlock->lightDesc) {
         mDataBlock->lightDesc->submitLight( &mLightState, getRenderTransform(), lm, this );
      }
   }
   // tracerRound <<<
}

At around line 1318, under
if (state->isObjectRendered(this))
   {
      if ( mDataBlock->lightDesc )
      {
         mDataBlock->lightDesc->prepRender( state, &mLightState, getRenderTransform() );
      }

Add
// tracerRound >>>
if (mDataBlock->tracerLightDesc)
{
   mDataBlock->tracerLightDesc->prepRender(state, &mLightState, getRenderTransform());
}
// tracerRound <<<

That's it for the source changes - recompile the engine and move over to your weapon.cs script file.

Script change example
I am using this with an XM8 weapon (from the modmaker weapon pack). I added a new LightDescription for my tracer light source and added the fields to my ProjectileData datablock as follows:

datablock LightDescription(xm8TracerLightDesc)
{
   range = 5.0;
   color = "1 1 0";
   brightness = 20.0;
};

datablock ProjectileData( xm8Projectile )
{
   projectileShapeName = "art/shapes/weapons/xm8/ammo/5_56.dts";
   directDamage = 30;
   radiusDamage = 30;
   damageRadius = 5;
   areaImpulse = 2500;
   
   explosion = BulletDirtExplosion;
   waterExplosion = BulletWaterExplosion;
   
   muzzleVelocity = 400;
   armingDelay = 0;
   lifetime = 4992;
   fadeDelay = 4992;
   isBallistic = false;
   gravityMod = 0.5;
   projectileTracerShapeName = "art/shapes/weapons/xm8/tracer.dts"; // The tracer .dts
   tracerPercent = 20; // approx 1 in 5 rounds
   //lightDesc = xm8BulletLightDesc; // no light source on normal bullets
   tracerLightDesc = xm8TracerLightDesc; // the tracer light source
};

I have uploaded an example tracer (programmer art!) here.

About the author

Formed in 2005, EiKON Games is an indie games development project based in the UK working on the tactical first person shooter "Epoch: Incursion". See the Join Us or Contact Us pages at http://www.eikon-games.com/


#1
01/15/2010 (1:00 pm)
Cool! But We, The People, demand video of it in action!

[edit]The People are sated!

Good idea to slow it down and amplify the effect for demonstration purposes.
#2
01/15/2010 (1:30 pm)
Well, OK. But only because the people demanded it... :o)
#3
01/15/2010 (2:45 pm)
hehe nice work
#4
01/16/2010 (12:09 pm)
This is just what i needed! Thanks for sharing!

but any word about multiplayer? I tried on multiplayer locally (on one PC) and it didn't work, but it might just be because i ran it on one PC.
#5
01/16/2010 (1:10 pm)
I tested multiplayer on a lan between my dev pc and my laptop. The problem being my laptop really isn't up to running torque3d so it was struggling. I need to test it properly with a couple of decent pc's. However - that said, I was seeing some tracer fire on my laptop when I was firing from my desktop pc...

In short - more testing required! I should be able to test in the next week or so.
#6
01/18/2010 (10:43 am)
Looks like the site is stripping the slashes from the code... Giving some compile errors about empty variables and whatnot. Hopefully the code won't be stripped again in this. This rocks by the way, nicely done. It certainly looks like tracer rounds; although, like you said it's not 100% accurate as far as timing is concerned. That's not a huge issue though. Thank you for the work!

from this

At around line 242, after
// tracerRound >>>
if (projectileTracerShapeName && projectileTracerShapeName[0] != '')
{
   projectileTracerShape = ResourceManager::get().load(projectileTracerShapeName);
   if (bool(projectileTracerShape) == false)
   {
      errorStr = String::ToString("ProjectileData::load: Couldn't load shape "%s"", projectileTracerShapeName);
      return false;
   }
}
// tracerRound <<<

to this

At around line 242, after

// tracerRound >>>
if (projectileTracerShapeName && projectileTracerShapeName[0] != '\0')
{
projectileTracerShape = ResourceManager::get().load(projectileTracerShapeName);
if (bool(projectileTracerShape) == false)
{
errorStr = String::ToString("ProjectileData::load: Couldn't load shape \"%s\"", projectileTracerShapeName);
return false;
}
}



#7
04/18/2010 (7:55 am)
Has anyone been able to get this to show up on both the client and server computers? When I was testing my game with a friend of mine the tracers showed up find on my machine (the host server) but never on his. When he hosted the server they showed up for him but not for me.
#8
08/19/2010 (5:35 am)
if (staticLighting || mHidden || (!mDataBlock->lightDesc && !mDataBlock->tracerLightDesc))

'mHidden' replace to 'isHidden()' on T3D 1.1...