Projectile Particle Trails
by Paul /*Wedge*/ DElia · in Torque Game Engine · 10/28/2005 (4:46 am) · 28 replies
Does anybody know why, especially with fast moving particles (a few hundred velocity or so), the particle trail (and .dts) may show up a tick or so after where it was created (a significant distance that looks very awkward and makes it invisible at close range). I even added a value that is set to the projectiles position at onAdd and used as the starting point for emitParticles in projectile.cc, and it still will miss that area at times. I've tried messing with values in the interpolateTick to use for emitParticles, but haven't had any more luck with that. Given the projectile still hits even if the particles never show up, I have to imagine the data exists somewhere there to fix this.
About the author
#2
10/28/2005 (5:03 pm)
So if I prevented the projectile from moving for a few ticks when it's initiazlized, would this fix it? I'm not sure how noticable a delay it would cause on the projectile firing, but that sounds like that might be feasable if it's just a small range of delay? I'm sure I don't understand the networking enough to figure out how to get it to check if it's missed it's assigned start point when sent to the client and makeup the effects for that time.
#3
I suggest basing the delay check on mCurrTick because IIRC that value is sent in packUpdate. That way the client will delay the projectile (or not) in proper sync with the server.
10/28/2005 (5:51 pm)
I think that should work, yes. If the added delay isn't too bothersome. I suggest basing the delay check on mCurrTick because IIRC that value is sent in packUpdate. That way the client will delay the projectile (or not) in proper sync with the server.
#4
10/28/2005 (5:53 pm)
I've tried setting a timeout by ignoring the processTick, advanceTime, and interpolateTick in Projectile for a few ticks, but still running the parent in GameBase to give it time to update. However I think because of the way objects are prioritized to be updated, this doesn't help much. I _can_ skip a _few_ ticks without it being noticable, so I think I will try giving it a manual high priority. I'm not sure if this is a good idea or not though.
#5
10/28/2005 (6:03 pm)
Well yes, I added my own value into packUpdate so I can skip x amount of ticks. I forced getUpdatePriority in gamebase to always return 1.0f for projectile objects, but no matter how many ticks I skip before running the projectile sim, the position is still inconsistent...
#6
10/28/2005 (10:47 pm)
Maybe you could fire a second projectile behind the first, but slow enough to throw some particles the way you want them to look while still moving fast enough that the player wouldn't notice the speed difference. It sounds like the projectile you're firing is moving as fast as a bullet, but even half that speed might work for a particle trail behind it.
#7
10/29/2005 (7:26 pm)
No, because sometimes they fire perfectly normal at high speeds, or can still be off at lower speeds. I'm trying to debug what values are being sent to emitParticles in the processTick of the projectile, but I keep crashing it when I try to output values, probably coz' I don't know how to use the console output properley.Con::errorf("Projectile tick is %s", TickMs); //This causes a crash, no idea why . Also I saw this in particleEngine.cc if (mNextParticleTime > numMilliseconds) {
// Defer to next update
// (Note that this introduces a potential spatial irregularity if the owning
// object is accelerating, and updating at a low frequency)
//
mNextParticleTime -= numMilliseconds;
mInternalClock += numMilliseconds;
mLastPosition = end;
mHasLastPosition = true;
return;And I don't know if that could be related to it as well.
#8
btw, TickMs isn't going to tell you anything useful. There's no harm in printing it out, but it's a constant value: 32.
I don't think I'd worry too much about that block in particleEngine.cc.
You might want to check this out though:
www.garagegames.com/mg/forums/result.thread.php?qt=32826
10/29/2005 (8:59 pm)
%s if for passing string arguments. Use %f for floats and %i or %d for integers.btw, TickMs isn't going to tell you anything useful. There's no harm in printing it out, but it's a constant value: 32.
I don't think I'd worry too much about that block in particleEngine.cc.
You might want to check this out though:
www.garagegames.com/mg/forums/result.thread.php?qt=32826
#9
10/29/2005 (9:58 pm)
Ah thank you, I didn't understand there were type settings for the %'s in engine. I tried what you suggested there, however it doesn't fix anything. I don't see how the projectile can never miss it's collision check but not get the particle display right, since they are in the same portion of code. The fact that when I set the particles to always start from the first initialized position, and it was still offset, makes me almost certain this is a ghosting issue, and you would need to force the client to go back and render any data it's missed from the server projectile. Given the notes on explosion and onCollision the projectile calls, I can sort of understand what the problem is, though I'm still not really sure how to fix it.
#10
How are you determining the first position? For the server, the position should be initialized to the muzzle point of your weapon. For clients however, the projectile is initialized to the position that is sent in the first ghost update. This would be wherever the server sees the projectile at the time it executes packUpdate.
Problem is, as it stands the client has no way of knowing where the projectile started on the server. You would not want the client to start the projectiles at the same position they started at on the server, as this could leave the client seriously out of sync with the server. You could send two positions in the initial ghost update: the "current" and the "starting" positions. However, this would increase the number of bits required to ghost projectiles, thus reducing the number of projectiles that could be ghosted without overloading the connection. Is that worth it? Depends I guess on your particular game design.
I still think your idea about delaying the projectiles for a few ticks is worth further study. You said that didn't seem to help.. May I ask, are you sure you're delaying them properly? I would suggest something like this:
You could adjust the value 3 up or down, or add a datablock value to control that if you wish. You'll want to use mCurrTick however, since that value will be sent to the clients. This way they'll know what tick the projectile is supposed to be on when they recieve it, so they can keep their delay in sync with the server.
10/30/2005 (12:15 pm)
"...when I set the particles to always start from the first initialized position, and it was still offset..."How are you determining the first position? For the server, the position should be initialized to the muzzle point of your weapon. For clients however, the projectile is initialized to the position that is sent in the first ghost update. This would be wherever the server sees the projectile at the time it executes packUpdate.
Problem is, as it stands the client has no way of knowing where the projectile started on the server. You would not want the client to start the projectiles at the same position they started at on the server, as this could leave the client seriously out of sync with the server. You could send two positions in the initial ghost update: the "current" and the "starting" positions. However, this would increase the number of bits required to ghost projectiles, thus reducing the number of projectiles that could be ghosted without overloading the connection. Is that worth it? Depends I guess on your particular game design.
I still think your idea about delaying the projectiles for a few ticks is worth further study. You said that didn't seem to help.. May I ask, are you sure you're delaying them properly? I would suggest something like this:
void Projectile::processTick(const Move* move)
{
Parent::processTick(move);
mCurrTick++;
if(mSourceObject && mCurrTick > SourceIdTimeoutTicks)
{
mSourceObject = 0;
mSourceObjectId = 0;
}
// See if we can get out of here the easy way ...
if (isServerObject() && mCurrTick >= mDataBlock->lifetime)
{
deleteObject();
return;
}
else if (mHidden == true)
return;
[b]// Initial delay to allow clients time to recieve ghosts
if (mCurrTick < 3)
return;[/b]
// ... otherwise, we have to do some simulation work.
// etc...You could adjust the value 3 up or down, or add a datablock value to control that if you wish. You'll want to use mCurrTick however, since that value will be sent to the clients. This way they'll know what tick the projectile is supposed to be on when they recieve it, so they can keep their delay in sync with the server.
#11
It occurs to me that since mCurrTick is already being sent to the clients, why not use that to determine where the projectile originated? This should be very easy if the projectile is moving at a constant speed. Simply check this value in unpackUpdate and use it to determine how far the projectile would have traveled on the server before being ghosted. Something like this:
(That's "off the cuff" and untested, btw)
Then in processTick when you go to emitParticles, check to see if this is the very first client tick. If it is, you'll want to:
instead.
Note that this does not account for ballistic projectiles. That will require a little more math.
10/30/2005 (12:36 pm)
Another (possibly better?) idea:It occurs to me that since mCurrTick is already being sent to the clients, why not use that to determine where the projectile originated? This should be very easy if the projectile is moving at a constant speed. Simply check this value in unpackUpdate and use it to determine how far the projectile would have traveled on the server before being ghosted. Something like this:
void Projectile::unpackUpdate(NetConnection* con, BitStream* stream)
{
Parent::unpackUpdate(con, stream);
//... (more code here) ...//
mCurrTick = stream->readRangedU32(0, MaxLivingTicks);
[b]// Determine starting point from ticks gone by.
// Move back along velocity by number of ticks.
mStartingTick = mCurrTick;
mStartingPoint = mCurrPosition - mCurrVelocity * TickSec * mStartingTick;[/b]
if (stream->readFlag())
{
mSourceObjectId = stream->readRangedU32(0, NetConnection::MaxGhostCount);
mSourceObjectSlot = stream->readRangedU32(0, ShapeBase::MaxMountedImages - 1);
NetObject* pObject = con->resolveGhost(mSourceObjectId);
if (pObject != NULL)
mSourceObject = dynamic_cast<ShapeBase*>(pObject);
//... (more code here) ...//(That's "off the cuff" and untested, btw)
Then in processTick when you go to emitParticles, check to see if this is the very first client tick. If it is, you'll want to:
emitParticles(mStartingPoint, newPosition, mCurrVelocity, TickMs * mStartingTick);
instead.
Note that this does not account for ballistic projectiles. That will require a little more math.
#12
So what I am thinking I have to do is update a start and end position based on the server values only, use these for the client particle emission, and the add a particle emission inside the collision state of the projectile, so it updates if it's already collided when the client gets it.
[Edit]Damn not checking post updates... ehm yeah I'll have to try the second thing you mentioned too, would be less network intensive that what I was going to do[/Edit]
10/30/2005 (2:24 pm)
I realize now the ghosting issue is because of the speed involved in the projectile, and not intialization ticks being missed, since any number of delay time never changes things. I'm starting to understand the whole server/client duality a little better now, so I think I may start to be able to figure out how to fix it. I did the origin point test again, this time _only_ intializing the point if it was a server object. And it worked! However doing this ruins the actual particle effect, and the particles were still never displayed if the projectile hit at a close enough range.So what I am thinking I have to do is update a start and end position based on the server values only, use these for the client particle emission, and the add a particle emission inside the collision state of the projectile, so it updates if it's already collided when the client gets it.
[Edit]Damn not checking post updates... ehm yeah I'll have to try the second thing you mentioned too, would be less network intensive that what I was going to do[/Edit]
#13
To properly address this, the particle system would have to be extended to allow for the creation of prematurely aged particles. This could complicate matters significantly.
10/30/2005 (3:26 pm)
I just thought of a potential problem with that approach, unfortunately. While it would allow the particle trail to be drawn back to the projectile's point of origin, the age of the particles would be wrong. This may or may not be a problem, depending on the specific situation and the properties of the projectiles and particles. There is definitely the potential for visual anomalies under certain circumstances though. To properly address this, the particle system would have to be extended to allow for the creation of prematurely aged particles. This could complicate matters significantly.
#14
I get the initial position with this (the mask just updates mOriginPosition)
10/30/2005 (4:07 pm)
A short range going off the emitter origin isn't terribly noticable in the effect. I have something that half works now.I get the initial position with this (the mask just updates mOriginPosition)
if(isServerObject() && mCurrTick == 1){
mOriginPosition = mCurrPosition;
setMaskBits(EmitterMask);
}And then changed the particle emission to thisif(isClientObject() && mCurrTick <= 10){
//Con::errorf("intial client projectile tick");
emitParticles(mOriginPosition, newPosition, mCurrVelocity, TickMs);
updateSound();
}
else if(isClientObject()){
//Con::errorf("client projectile tick is %i", mCurrTick);
emitParticles(mCurrPosition, newPosition, mCurrVelocity, TickMs);
updateSound();
}And this somewhat fixes the issues with the range disparity, with maybe a slight hiccup in the particles, but not very noticable. It would be nice if I could figure out how to get the difference in the client update, to only apply this within the necessary range. The big problem now is getting it to check for very short distances when the projectile has already impacted something by the time the client gets it.
#15
And now the particle stream is perfect everytime _if_ the projectile hasn't hit by the time the client gets it. This will be trickier because emitParticles doesn't want to draw once the particle object is hidden... also I didn't show the code for the masks, values, stream settings, but if I get this all working I may submit it as a resource if I'm allowed to.
10/31/2005 (10:47 pm)
I just realized something, particles already can be "prematurely" aged. I now have a proper fix for the first half of this problem! First, I made a mask to update a new value called mParticleStart, and at the the top of Projectile::processTick, after mCurrTick++; I put this.bool makeupParticles = false;
if(isServerObject() && mParticleStart == Point3F(0, 0, 0)){
mParticleStart = mCurrPosition;
setMaskBits(InitialEmitterMask);
}
if(isClientObject() && mParticleStart != Point3F(0, 0, 0)){
makeupParticles = true;
}And then above the normal conditional for emitting particles at the bottom of the function I do this.if(isClientObject() && makeupParticles){
emitParticles(mParticleStart, mCurrPosition, mCurrVelocity, TickMs * mCurrTick);
mParticleStart.set(0, 0, 0);
updateSound();
}And now the particle stream is perfect everytime _if_ the projectile hasn't hit by the time the client gets it. This will be trickier because emitParticles doesn't want to draw once the particle object is hidden... also I didn't show the code for the masks, values, stream settings, but if I get this all working I may submit it as a resource if I'm allowed to.
#16
11/01/2005 (3:55 pm)
I've mucked about some more, and things about 75% working now. I removed all the bitmask stuff, as I realized there was an initial update I could use for the initial makeup fix. Particles are now drawing properly for half the cases where the projectile hits before the client gets anything. Explosions were already using a bitmask to makeup for when this issue came up, so I hacked the particle emissions into the explosion function. And that's where I'm stuck. I put this in at the top of projectile::explodeif(isClientObject() && pEnd != Point3F(0,0,0)){
Con::errorf("Fix goddamn particles %i", mHidden);
emitParticles(pStart, p, pVec, TickMs * mCurrTick);
}And verified it is being called and all the proper values are being passed to emitParticles, and emitParticles is not getting cancelled by mHidden. However no particles are drawing. Muffins.
#17
edit (adding):
11/01/2005 (4:25 pm)
Double check the value of mCurrTick? I think it might be possible for a projectile to explode client-side with mCurrTick at 0. If that did happen, TickMs * mCurrTick would be 0 and no particles would be emitted.edit (adding):
emitParticles(pStart, p, pVec, mCurrTick ? (TickMs * mCurrTick) : TickMs);
#18
11/01/2005 (4:33 pm)
Nope, I'd checked that. However I did find the problem. There were cases where onAdd hadn't been called when the client needed to emit particles, and thus the emitter had never been initialized. I fixed that and have this 100% working as far as I can tell. If anyone is interested I can make it into a resource I think.
#19
I'm curious... How did the engine miss calling onAdd? AFAIK that should be called as a result of the ghosting process, prior to any other functions for the projectile. This sounds like a bug with the potential to cause trouble elsewhere. I'd like to know more about what you've discovered here.
11/01/2005 (4:45 pm)
Cool. Glad you got it working. I'm curious... How did the engine miss calling onAdd? AFAIK that should be called as a result of the ghosting process, prior to any other functions for the projectile. This sounds like a bug with the potential to cause trouble elsewhere. I'd like to know more about what you've discovered here.
#20
People like pictures, right? I don't recommend using particles of this complexity en masse in actual gameplay =P
11/01/2005 (4:50 pm)
The engine did not miss onAdd, however it was being called after the projectile was set to be deleted and the explosion was created.People like pictures, right? I don't recommend using particles of this complexity en masse in actual gameplay =P
Torque 3D Owner Scott Richards