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:
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:
processTick and interpolateTick are methods that allow an object to do something over time. They are called periodically on all objects.
Find:
In particleEmitter.cc
Again, administration.
Find:
(Thanks to Pifford for pointing out that netConnection.h needs to be included.)
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:
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:
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:
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:
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:
Find:
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.
Planned additions:
-Mounting using node names rather than mount numbers.
-Delete emitter when object is deleted.
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" #endifAnd 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));
//<- DanThis 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;
}
}
}
//<- DanThis 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);
}
}
}
//<- DanFind:
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);
}
}
}
//<- DanThat'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!
#22
07/14/2009 (11:28 pm)
Well for me the code works, but how do I go about implementing it so that it will work in Torque? I am an artist, not a very good programmer, so at the end of your resource you say put that code into the console, what do you mean? Where in scripting would I implement this? Thanks very much.
#23
For example, I originally mentioned I wanted to use them for flamethrowers; the idea was to mount particle emitters on players that had been hit by the flame. You can prototype this effect with the crossbow by placing my example script in the CrosbowProjectile::onCollision method. Just check whether %col is a player object, and if so, spawn a ParticleEmitterNode and set the mountObject to %col.
07/15/2009 (4:23 am)
For a start, you could put my script sample into the Armor::onAdd method in player.cs. By changing LocalClientCOnnection.player to %obj, you can have smoke emitters mounted on players by default. Any other usage of the code would be determined by what you want to use these particle emitters for.For example, I originally mentioned I wanted to use them for flamethrowers; the idea was to mount particle emitters on players that had been hit by the flame. You can prototype this effect with the crossbow by placing my example script in the CrosbowProjectile::onCollision method. Just check whether %col is a player object, and if so, spawn a ParticleEmitterNode and set the mountObject to %col.
#24
07/16/2009 (12:21 am)
Thank you very much. You are a very talented programmer. Also, do you think I could use this for spawning blood out of a player on a sword impact? (I really like rpg elements) Thanks.
#25
function Armor::onAdd(%this,%obj)
{
// Vehicle timeout
%obj.mountVehicle = true;
// Default dynamic armor stats
%obj.setRechargeRate(%this.rechargeRate);
%obj.setRepairRate(0);
new ParticleEmitterNode(){
dataBlock = TorchFireEmitterNode;
emitter = TorchFireEmitter;
mountObject = %obj;
mountNode = 0;
}
07/16/2009 (12:50 am)
Er it's not working. What could be wrong? I am probably calling something wrong. It won't load the level.function Armor::onAdd(%this,%obj)
{
// Vehicle timeout
%obj.mountVehicle = true;
// Default dynamic armor stats
%obj.setRechargeRate(%this.rechargeRate);
%obj.setRepairRate(0);
new ParticleEmitterNode(){
dataBlock = TorchFireEmitterNode;
emitter = TorchFireEmitter;
mountObject = %obj;
mountNode = 0;
}
#26
07/16/2009 (5:36 pm)
Check the console for script errors. It looks like you need another closing brace and a semicolon after mountNode = 0;.
#27
07/17/2009 (11:40 pm)
ok I got the emitter to work, but it doesn't follow the character around. Is it supposed to? when I edit the character by pressing F11 it will update the position and the emitter will spawn above it. Is it supposed to do that?
#28
Oh, and as for your question above (sorry I didn't see it), this is suitable for blood as well - for example, if you want a little 'fountain' attached to a player ;P. If you don't want a reasonably long-lasting emitter that follows a character, then it's not worth mounting it to an object - just create the particle emitter at the sword hit point and let it sit there and delete itself after a brief burst of particles.
07/18/2009 (4:50 am)
Hmm, no, it should update its position automatically, all the time. Are you sure you made all the code changes and recompiled the engine succesfully?Oh, and as for your question above (sorry I didn't see it), this is suitable for blood as well - for example, if you want a little 'fountain' attached to a player ;P. If you don't want a reasonably long-lasting emitter that follows a character, then it's not worth mounting it to an object - just create the particle emitter at the sword hit point and let it sit there and delete itself after a brief burst of particles.
#29
By the way, nice resource!
07/18/2009 (10:02 pm)
Yes the engine was compiled successfully, but I will go back and check just to be sure.By the way, nice resource!
#30
08/01/2009 (12:22 am)
Just to test it, I am using the standard melee resource. How would I create and delete a particle effect when "firing" the melee weapon for instance?
#31
08/01/2009 (5:57 am)
I'm not sure which resource you mean, but I think it's safe to say you can create the ParticleEmitterNode whenever you do damage to an object. Have a look through the resource and see where damage is applied, and add my example script to create a new emitter there.
#32
08/01/2009 (11:53 am)
So how would you delete the particle after the hit occurs? I was reading something about "lifetime" of the particle. Is this how you do it?
#33
08/01/2009 (3:32 pm)
You could either schedule a delete command (search for schedule, you'll get tons of help), or I think particle emitters will delete themselves after a certain amount of time. I'm not sure if tht applies to emitters mounted in ParticleEmitterNodes, though. To be safe, I'd schedule a delete, or try to find out more about ParticleEmitterNodes deleting themselves.
#34
Register object failed for object (NULL) of class ParticelEmitterNode.
Object SteamEmitterNode is not a member of the GameData block class.
Object SteamEmitter is not a member of the ParticleEmitterData block class.
Thanks!
08/13/2009 (3:40 pm)
Hi there. I am fairly new at all of this I just need an idea on what this error could be. I have done all of your steps and I am using TGE 3d 1.52 .It comes up in the console after I type your command code..Register object failed for object (NULL) of class ParticelEmitterNode.
Object SteamEmitterNode is not a member of the GameData block class.
Object SteamEmitter is not a member of the ParticleEmitterData block class.
Thanks!
#35
Are you using the starter.fps demo, and have you made any changes to it? The two second messages seem to indicate that the SteamEmitterNode and SteamEmitter datablocks haven't been created. They should be created at the bottom of starter.fps/server/scripts/sgExamples.cs. Check in game.cs to make sure that file is being exec'ed.
08/13/2009 (4:33 pm)
Quote:ParticelEmitterNodeI'll guess that was a typo, judging by the second two errors you got.
Are you using the starter.fps demo, and have you made any changes to it? The two second messages seem to indicate that the SteamEmitterNode and SteamEmitter datablocks haven't been created. They should be created at the bottom of starter.fps/server/scripts/sgExamples.cs. Check in game.cs to make sure that file is being exec'ed.
#36
08/13/2009 (6:45 pm)
Definitely a typo..sorry. I guess I can start with this: I am using the VS2005 solution in Visual Studio Pro 2008. I am assuming you need me to make sure that somewhere in one of the files the game that is loading up is the starter.fps demo. That leaves me a little confused. How do I direct this through Visual Studio 2008? Once I get this loaded up I can look for the starter.fps/server/scripts/sgExamples.cs issue. Thanks again..
#37
08/13/2009 (7:17 pm)
I went ahead and loaded those missing elements via the console. Great stuff! Thanks for the help. Is this something where you can simply replace the emitter name in the console as long as you have loaded it in the project some where?(different emitters)
#38
08/13/2009 (8:09 pm)
You don't load starter.fps directly from Visual Studio - it's loaded in scripts, in main.cs by default. Anyway, sounds like you figured out the problem.Quote:Is this something where you can simply replace the emitter name in the console as long as you have loaded it in the project some where?(different emitters)You can do just that. As long as you have emitter/node datablocks created, you can use them in place of 'SteamEmitter/Node'.
#39
Joel
08/14/2009 (11:05 am)
Hello there again Daniel. I have another small question. I am curious as to why when I load up a mission, only the tutorial.base folder is showing. I am trying to load up something with the orc as the player model to attach the emitters to instead of the blue guy model. Why is it that the the folders like starter.fps and starter.racing are not showing along side the tutorial.base structure? I tried to copy and paste the starter.fps folder and placing it inside of the tutorial.base folder but when I go to load up one of the mission in that folder(tutorial folder) it throws a sever error about the wrong version number or unable to load the art...the folder structures is smoking me for some reason. I guess there are so many with the same name it is hard to tell which ones need changing...thanks again for any insight you can give.Joel
#40
08/14/2009 (1:03 pm)
Once again I have figured it out on. Maybe I should just play with the possibilities a little more before I ask more questions ;) The folder structure does still throw me. I believe now that I have figured out that the C:\Torque\TGE_1_5_2\example\main.cs is the key to opening whatever mission you want as a starting point...thanks again..the emitter is way too cool. 
Torque Owner Ben McIntosh
Edit: I just posted a resource detailing how I manipulated the state machine to allow for a permanent weapon emitter to make a flaming sword. (Link)