Game Development Community

dev|Pro Game Development Curriculum

Mounting ParticleEmitterNodes

by Daniel Buckmaster · 05/27/2008 (9:45 am) · 59 comments

Developed with TGE 1.5.2.

I decided I'd like the ability to mount particle emitters to a node in a ShapeBase object after thinking about flamethrowers. I think you get the idea.

Anyway, I'm not going to waffle: here are the code changes.

In particleEmitter.h:

This is just infrastructure - making sure the particle code 'knows' about the ShapeBase code.
Find:
#ifndef _GAMEBASE_H_
#include "game/gameBase.h"
#endif
And add beneath it:
//Dan's mods (mounting particles) ->
#ifndef _SHAPEBASE_H_
#include "game/shapeBase.h"
#endif
//<- Dan

Next, we're going to declare some simple additions to the ParticleEmitterNode class. ParticleEmitterNodes are basically invisible point objects that emit particles. What we want to do is add two things: an object and a mount node. So why three fields? Script compatability. It's complicated.
And why the method? onDeleteNotify is called on objects when another object is deleted. Here, we use it to see whether the parent object of our ParticleEmitterNode is deleted. If so, we'll want to do something.
Find:
ParticleEmitterNodeData* mDataBlock;
And add beneath:
//Dan's mods (mounting particles) ->
   S32 mMountObject;
   ShapeBase* mMount;
   S32 mMountNode;

   void onDeleteNotify(SimObject*);
   //<- Dan

processTick and interpolateTick are methods that allow an object to do something over time. They are called periodically on all objects.
Find:
// Time/Move Management
  public:
And add beneath:
//Dan's mods (mounting particles) ->
   void processTick(const Move* move);
   void interpolateTick(F32 dt);
   //<- Dan

In particleEmitter.cc

Again, administration.
Find:
#include "math/mathIO.h"
And add beneath:
(Thanks to Pifford for pointing out that netConnection.h needs to be included.)
//Dan's mods (mounting particles) ->
#include "game/shapeBase.h"
#include "sim/netConnection.h"
//<- Dan

Now, those fields that we added to ParticleEmitterNode need to be initialised, so they don't unexpectedly contain junk, which could cause bad things to happen.
Find:
mEmitterDatablock   = NULL;
   mEmitterDatablockId = 0;
   mEmitter            = NULL;
   mVelocity           = 1.0;
And add beneath:
//Dan's mods (mounting particles) ->
   mMountObject = -1;
   mMount = NULL;
   mMountNode = -1;
   //<- Dan

The initPersistFields method exposes a bunch of object fields to script. Note that only two of our three added fields are exposed to scripts.
Find:
addField("emitter",  TypeParticleEmitterDataPtr, Offset(mEmitterDatablock, ParticleEmitterNode));
   addField("velocity", TypeF32,                    Offset(mVelocity,         ParticleEmitterNode));
And add beneath:
//Dan's mods (particle mounting) ->
   addField("mountObject",TypeS32,Offset(mMountObject,ParticleEmitterNode));
   addField("mountNode",TypeS32,Offset(mMountNode,ParticleEmitterNode));
   //<- Dan

This is where that third field comes in. We use the integer we were passed from script to find an actual object pointer. Then we just perform some common-sense checks to make sure our data makes sense.
Find:
if (isClientObject())
   {
      ParticleEmitter* pEmitter = new ParticleEmitter;
      pEmitter->onNewDataBlock(mEmitterDatablock);
      if (pEmitter->registerObject() == false)
      {
         Con::warnf(ConsoleLogEntry::General, "Could not register base emitter for particle of class: %s", mDataBlock->getName());
         delete pEmitter;
         return false;
      }
      mEmitter = pEmitter;
   }
And add beneath:
//Dan's mods (mounting particles) ->
   //If we're a server object, resolve mount object
   else
   {
	   ShapeBase* ptr;
	   mMount = NULL;
	   if(mMountObject != -1)
		   if(Sim::findObject(mMountObject,ptr))
		   {
			   mMount = ptr;
			   deleteNotify(ptr);
			   //And while we're at it, check mount
			   if(mMountNode != -1)
				   if(mMountNode > ShapeBaseData::NumMountPoints)
				   {
					   Con::errorf("ParticleNodeEmitter::onAdd: mountNode too big!");
					   mMountNode = -1;
				   }
		   }
   }
   //<- Dan

This is the meat of the code. Here we declare the processTick, interpolateTick, and onDeleteNotify methods. Let's break them down:

processTick: every tick, if we're mounted to an object, we get the posiion and rotation of the node we're mounted to, and set our own position and rotation to be equal. I copied this code from StaticShape, which has simple mounting like this.

interpolateTick: this is called in the spaces between ticks. In this method, we do basically the same thing, but we use render transforms instead of regular ones. Don't ask me what the difference is.

onDeleteNotify: basically, if the object deleted was our mount object, we clear out the object from memory. Ideally, we should just delete the emitter. I'll add it to my to do list.

Find:
bool ParticleEmitterNode::onNewDataBlock(GameBaseData* dptr)
{
   mDataBlock = dynamic_cast<ParticleEmitterNodeData*>(dptr);
   if (!mDataBlock || !Parent::onNewDataBlock(dptr))
      return false;

   // Todo: Uncomment if this is a "leaf" class
   scriptOnNewDataBlock();
   return true;
}

//--------------------------------------------------------------------------
And add beneath:
//Dan's mods (mounting particles) ->
void ParticleEmitterNode::processTick(const Move* move)
{
	//If we have a mount object
	if(mMount)
	{
		MatrixF mat;
		//If we have a valid mount
		if(mMountNode != -1)
			mMount->getMountTransform(mMountNode,&mat);
		//Otherwise, use centre
		else
			mat = mMount->getTransform();
		//Set transforms
		Parent::setTransform(mat);
		Parent::setRenderTransform(mat);
	}
}

void ParticleEmitterNode::interpolateTick(F32 dt)
{
	//If we have a mount object
	if(mMount)
	{
		MatrixF mat;
		//If we have a valid mount
		if(mMountNode != -1)
			mMount->getRenderMountTransform(mMountNode,&mat);
		//Otherwise, use centre
		else
			mat = mMount->getRenderTransform();
		//Set render transform
		Parent::setRenderTransform(mat);
	}
}

void ParticleEmitterNode::onDeleteNotify(SimObject* obj)
{
	Parent::onDeleteNotify(obj);

	if(obj == (ShapeBase*)mMount)
	{
		mMount = NULL;
		mMountObject = -1;
		Con::executef(mDataBlock, 2, "onMountDeleted", scriptThis());
		deleteObject();
	}
}
//<- Dan
(Thanks to Pifford and Guimo for suggesting I just delete the object ;) and to Guimo for suggesting a script calback.)

packUpdate and unPackUpdate deal with transferring data between client and server (if I'm not very much mistaken). In packUpdatek, we write all the relevant data to a stream. The stream is sent to the client, where it is unpacked to the client object.
Find:
U32 retMask = Parent::packUpdate(con, mask, stream);
And add beneath:
//Dan's mods (mounting particles) ->
   if(stream->writeFlag(mask & InitialUpdateMask))
   {
	   if(stream->writeFlag(mMountObject != -1))
	   {
		   S32 gIndex = con->getGhostIndex(mMount);
		   if(stream->writeFlag(gIndex != -1))
		   {
			   stream->writeInt(gIndex,NetConnection::GhostIdBitSize);
			   stream->writeInt(mMountNode,ShapeBaseData::NumMountPointBits);
		   }
	   }
   }
   //<- Dan

Find:
Parent::unpackUpdate(con, stream);
And add beneath:
//Dan's mods (mounting particles) ->
   if(stream->readFlag())
   {
	   if(stream->readFlag())
	   {
		   if(stream->readFlag())
		   {
			   S32 gIndex = stream->readInt(NetConnection::GhostIdBitSize);
			   mMount = dynamic_cast<ShapeBase*>(con->resolveGhost(gIndex));
			   mMountNode = stream->readInt(ShapeBaseData::NumMountPointBits);
		   }
	   }
   }
   //<- Dan

That's it - it should build and compile. Here's an example of script usage - just put it in the console. I'm using sandard starter.fps art content here, just so you know.
new ParticleEmitterNode(){
   dataBlock = SteamEmitterNode;
   emitter = SteamEmitter;
   mountObject = LocalClientConnection.player;
   mountNode = 0;
};
If you don't specify a node, or if the node is for some reason unavailable, the emitter is mounted at the object's position.

Planned additions:
-Mounting using node names rather than mount numbers.
-Delete emitter when object is deleted.

About the author

Studying mechatronic engineering and computer science at the University of Sydney. Game development is probably my most time-consuming hobby!

Page «Previous 1 2 3 Last »
#1
05/27/2008 (12:44 pm)
Very nice, thanks! I'll give this a try.
#2
05/27/2008 (5:31 pm)
Cool! Gotta try this!
#3
06/07/2008 (12:26 pm)
Added some explanation to the code changes. Nothing too complicated.
#4
06/13/2008 (9:55 am)
Any luck with this and TGEA?
#5
06/17/2008 (11:34 am)
I have no idea how different TGEA is to TGE; I work with TGE. I should note that in the resource. However, the code is pretty high-level stuff, relatively, so TGE and TGEA shouldn't differ too much in this respect.
#6
07/27/2008 (5:43 am)
Just a suggestion. To delete this node when parent is deleted, just invoke a callback on the objecct like:

void ParticleEmitterNode::onDeleteNotify(SimObject* obj) {
   if(obj == (ShapeBase*)mMount)	{
      mMount = NULL;
      mMountObject = -1;	
      Con::executef(this, 1, "onMountDeleted");
   }
   Parent::onDeleteNotify(obj);
}

and in the console you can use a small callback script:
function ParticleEmitterNode::onMountDeleted(%this) {
   %this.delete();
}

Awesome resource.
Luck!
Guimo
#7
08/11/2008 (12:52 am)
I commented by I don't think it went through...

To delete the emitter on mounted object deletion just add:

deleteObject();

after:
mMountObject = -1;

in ParticleEmitterNode::onDeleteNotify()

-r
#8
08/11/2008 (2:10 am)
Also, to compile with TGE 1.5.2 and GCC4 it's necessary to add:
#include "sim/netConnection.h"

to particleEmitter.cc
#9
08/20/2008 (9:31 am)
Thanks for that guys! I don't really know why I couldn't be bothered adding the deleteObject();... :P. I'll add that now.
Guimo - a script callback is an interesting idea, but I reckon the C++ should handle object deleting. I'll add in a callback, though, just for completeness ;P. On the datablock, of course :)

I'm not sure about the best practice for deleting objects - the parent function should be called last, but also the object should be deleted last! I've decided to play it a bit safe (and rely on the fact that nothing further up the line defines anything useful in onDeleteNotified :P).
#10
09/11/2008 (12:22 am)
I have made good use of this resource. Thank you for posting it.

One problem I have however, is that when running the flames from my flamethrower blow back into my face. I thought inheritedVelFactor would correct for this, but it doesn't appear to be working. If the emitter is mounted to a player, do I need to use the player's velocity in the calculation instead of the emitter? Any idea how I might do that? Thanks!
#11
09/11/2008 (8:17 am)
What I would do (I would do it now if I didn't have homework :P) is find all references to the velocity of a particle emitter. I would guess that when I set the transform of an emitter, the velocity is not altered as it should be. You can verify that, but it seems to be the case. So you'd simply have to change the particle emitter's velocity in processTick.
#12
09/11/2008 (9:00 am)
Yeah that seemed to do the trick. The particles now correctly inherit the emitter's velocity. This almost seems like a bug with the engine because it didn't technically have anything to do with your code :P

The change I made was :

void ParticleEmitterNode::advanceTime(F32 dt)
{
   Parent::advanceTime(dt);

   Point3F emitPoint, emitVelocity;
   Point3F emitAxis(0, 0, 1);
   getTransform().mulV(emitAxis);
   getTransform().getColumn(3, &emitPoint);
	if(mMount != 0)  // added this check to support inheriting the velocity of the mounted object
	{
		emitVelocity = ((emitAxis + mMount->getVelocity()) * mVelocity );
	}
	else
	{
		emitVelocity = emitAxis * mVelocity;
	}

   mEmitter->emitParticles(emitPoint, emitPoint,
                           emitAxis,
                           emitVelocity, (U32)(dt * mDataBlock->timeMultiple * 1000.0f));
}
#13
09/11/2008 (10:27 am)
Thanks for posting that up :) This is quite an important change - do I have your permission to add it to the resource? With credit, of course ;)
#14
12/20/2008 (12:46 pm)
nice resource!!

works perfectly in 1.7.1 (with minor changes)

funny seeing the player running around with smoke pouring out of him
#15
06/24/2009 (9:55 am)
I am trying to port this over to TGEA 1.8.1. I had to make a couple changes.

1. Instead of using 'particleEmitter.h' and 'particleEmitter.cc', I had to use 'particleEmitterNode.h' and 'particleEmitterNode.cpp', otherwise I was unable to find most of the code mentioned in the guide.

2. This line in both files: #include "game/shapeBase.h"
Needs to be: #include "T3D/shapeBase.h"

I tried to run a build and am getting one error causing a couple other errors. I am not too savvy with C++/C# yet so I don't know how to fix them. Here's the errors:

particleEmitterNode.cpp
..\..\..\..\..\engine\source\T3D\fx\particleEmitterNode.cpp(259) : error C2665: 'Con::executef' : none of the 20 overloads could convert all the argument types
        c:\Torque\TGEA_1_8_1\engine\source\console/console.h(536): could be 'const char *Con::executef(const char *,const char *,const char *,const char *)'
        c:\Torque\TGEA_1_8_1\engine\source\console/console.h(564): or       'const char *Con::executef(SimObject *,const char *,const char *,const char *)'
        while trying to match the argument list '(ParticleEmitterNodeData *, int, const char [15], const char *)'
#16
07/03/2009 (9:03 am)
BUMP!!! REALLY need help with this!
#17
07/03/2009 (10:53 am)
Ok, I got help from Picasso in the TGEA Engine boards.

Picasso's solution: www.garagegames.com/community/forums/viewthread/96147/1#comment-646301


However, now my game is crashing when I use this...


new ParticleEmitterNode(TelePiece1) {
         canSaveDynamicFields = "1";
         Enabled = "1";
         position = "453.774 1019.04 2.19296";
         rotation = "0.498257 -0.278039 0.821239 1.19352";
         scale = "1 1 1";
         dataBlock = "EmberNode";
         emitter = "TelePieceEmitter";
         velocity = "1";
         mountObject = "TP1";
         mountNode = 0;
      };

"TP1" is the name of the object I want my emitter mounted to. It is a static shape and I made sure to export it with a 'mount0' node.
#18
07/04/2009 (4:54 am)
I think the problem you're seeing now is that you're passing a name to mountObject, but I designed it for use with an ID number. Try getting the object's ID number and using that instead. That, or I could add another field for a mountObjectName.
#19
07/04/2009 (4:05 pm)
This looks like a great resource. Can this also be used to attach emitters to nodes defined in a weapon image as well? It seems like just looking at the code it should work but I came across this post, which references your resource. Maybe there is a problem with Sim::findObject when dealing with images? I thought I would ask before trying to implement it.
#20
07/04/2009 (5:20 pm)
Hm, I never saw that thread. I've replied to it now. Basically, I didn't design the resource with that function in mind, since I believe images already have built-in particle effects (do they?).
But either way, the method the poster used was incorrect. See my reply for details ;P.

I should really update this resource, I've just realised :P.
Page «Previous 1 2 3 Last »