Game Development Community

Applying damage only when projectiles actually hit

by Gareth Fouche · in RTS Starter Kit · 04/08/2005 (12:21 am) · 16 replies

Recently I've been tinkering with the RTSProjectile code. As the SK stands, when you tell a unit to attack, the damage is immediately subtracted from the enemy, even if the unit is a ranged one. So the riflemans missile damages the target before it hits him. Now, this isn't quite the kind of behavior we want (although there is a perfectly sound reason for it shipping like that).

In this thread I will discuss what I did, for anyone interested. I just want everyone to be aware that I am still very much new to Torque. My code could be faulty. Backup any work before changing it. It also could be a less than optimal solution.

To that end, I would appreciate any experienced Torquers having a look at my code, and checking I haven't gone astray. Anyone who has a better solution, please post it.

Additionally, I don't have a network connection at home, so I haven't tested this in multiplayer. If there are any flaws in the code, the most likely place they'll show up is in a multiplayer game, because of the nature of how projectiles are handled. Anyone who could test it and report back, it would be appreciated!

I'm going to go into a bit of detail explaining whats happening, for any noobs like myself ;-).

Ok, so first off, what's happening with normal RTSProjectiles? Well, the logic is that in a pitched RTS battle you could have huge amounts of projectiles flying around (a reasonable assumption, the more the merrier!). Now, in normal TGE, the projectiles are transmitted around, and not only that, they have a decent amount of processing going on. Physics, collision info etc.

In a RTS you don't need a lot of that info. For instance, the collision is expensive. And with a lot of objects on screen, it'll really add up. So the solution is to not pass that info over the wire. In fact, the server doesn't even simulate the projectiles. They are purely a client side happening. They have no ability to send their data across the network (empty pack and unpack methods).

What IS simulated on both sides is the RTSUnits. They are ghosted from Server to Client, and the server is the ultimate authority as to what happens to them. The damage gets applied to targets in the attackers (datablocks) onAttack method. Since the RTSUnit is on the server, this will apply on both sides.

Ok, before I present the code, I want to discuss my thoughts on this matter. I totally agree that we should avoid sending the projectile data over the network. However, if the client can handle simulating the projectiles, I'm sure the server can. In fact, examining the code reveals that they are pretty simple, so I think its quite safe to sim them server side. Not only that, I cannot really think of another way to get damage to apply only when (and if!) the projectile hits. The client could inform the server when a unit dies, but that destroys the point of having the server as the master game authority.

So I decided to sim them on both sides, and this has the added nice benefit of allowing projectiles to maybe miss their targets. If anyone can see any flaws with my concept, please don't hesitate to point them out.

#1
04/08/2005 (12:23 am)
Having a look at the code, this is the way it works :

In /client/game.cs is the function doClientAttackAnimation, which gets called when your guy attacks. In it we can see a call to playAttackAnimation, and the creation of the units projectile.

In the actual RTSProjectile.cc code, in the processTick method, the missile just travels from its start position to its target position, ignoring anything along the way. When it reaches its target, the explode function is called. (This might seem a bit cheesy, but examine most RTS and you'll see a lot of them pull this trick.). Now the problem is that at no point do any damage functions get called. Not only that, it wouldn't help if you did! (I tried, the target gets to 0 health and just stands there, stubbornly refusing to die)

The projectile is a client side object, so any calls it does to apply damage on a RTSUnit will be on the ghosted client object. Since the server is responsible for maintaining this copied object, and on the server the object is still alive, the guy on our side doesn't get his destroy functions called. (I'm not too clued up on object ghosting, if I'm wrong chirp up guys!)

In /server/scripts/avatars/rifleman.cs you can see where the actual damage takes place. In onAttack, which take note is a function of the units datablock, not the actual unit (this one bit me). This function will apply the damage the moment the unit begins attacking, which isn't what we want. So comment out the body of that function, we don't need it for the rifleman (and any other ranged units you might have). I left it for the melee guys.

Likewise, in doClientAttackAnimation that I mentioned above, comment out all the code except for the call to %attacker.playAnimation, and the first 2 lines where we check whether the target is enabled. Lets separate the projectile code from the animation stuff.

Now, copy all of the stuff that you've just commented out into a new function in rifleman.cs. I called the function fireProjectile, it takes the same parameters that the onAttack function does, and it is also a member of the datablock, like so :

function riflemanBlock::fireProjectile(%this,%attacker,%target)
{ 
 if(%target.getDamageState() !$= "Enabled")
      return;   

   %projDB = %attacker.getProjectileDatablock();

   %vel = VectorSub(%target.getPosition(), %attacker.getPosition());
   %vel = VectorNormalize(%vel);
   %vel = VectorScale(%vel, 5);
   %vel = VectorScale(%vel, %projDB.muzzleVelocity / VectorLen(%vel));
   
   // Create the projectile object, only if there is a projectile datablock
   if( isObject( %projDB ) )
   {
      %p = new RTSProjectile() 
      {
         dataBlock        = %projDB;
         initialVelocity  = %vel;
         targetPos        = %target.getPosition();
         speed            = 2;
         initialPosition  = VectorAdd(%attacker.getPosition(), "0 0 1");
         sourceObject     = %attacker;
	   targetObject	  = %target;
      };
      MissionCleanup.add(%p);
   }


}

Some of you might have noticed that when I create the projectile there is a new variable called targetObject in the constructor, which stores the tagets ID. I do this so we know who to damage when our projectile hits its target.

To use this variable, we need to declare it in RTSProjectile.h as so :

F32 mSpeed;
S32      mTargetObjectId; // << add this in under speed.

But we also have to expose this to the console so we can access it from there. InitPersistFields() is where we do this, so add in this line after all the others :

addField("targetObject",     TypeS32,     Offset(mTargetObjectId, RTSProjectile));
#2
04/08/2005 (12:24 am)
Ok, back to fireProjectile. Now, how to get this function called? And how to call it on both client and server? Elementary my dear Watson. Pop open RTSUnit.cc, go to the processTick method, and scroll down until you find this part :

if(isServerObject())
                        {
                           // Do a proper method callback...
                           Con::executef(mDataBlock, 3, "onAttack", scriptThis(), mAimObject->getIdString());		
                        }
                        else
                        {
			   // Call a goofy global function to do the client side animation.
                           Con::executef(3, "doClientAttackAnimation", scriptThis(), mAimObject->getIdString());													
                        }

And change it to this :

if(isServerObject())
                        {
                             // !edit, I originally left this out, thanks Stephen!
                             Con::executef(mDataBlock, 3, "onAttack", scriptThis(), mAimObject->getIdString());		

   			     Con::executef(mDataBlock, 3, "fireProjectile", scriptThis(), mAimObject->getIdString());				
                        }
                        else
                        {
          	            //Con::executef(mDataBlock, 3, "fireProjectile", scriptThis(), mAimObject->getIdString());

                           // Call a goofy global function to do the client side animation.
                           Con::executef(3, "doClientAttackAnimation", scriptThis(), mAimObject->getIdString());				
                        }

Notice how I have a call to fireProjectile for the client, but I commented it out? I originally assumed that because RTSProjectile has empy pack/unpack methods, the server wouldn't ghost them over to the client. Turned out I was mistaken, so we don't need to explicitly create the projectile on the client, which I suppose makes sense. I'm a little uneasy though, because as I said I am no expert, and I haven't got a LAN to test for discrepancies. I think it works, since I see projectiles, but I would really appreciate some confirmation.

Note also that this also creates some network traffic as this ghosting takes place. However, there will be no updates passed after that, so hopefully that traffic is drastically reduced. But again, I would love some input from anyone in the know.

Is there a way to stop the object ghosting (and then we can just uncomment that line, there will be projectiles on both sides, but zero net traffic)? I've had a gander at the help files, and based on that I tried putting this line in RTSProjectiles constructor :

mNetFlags.clear(ScopeLocal | Ghostable);

From what I know this should prevent the object ghosting (even to the local machine, I did that to test). Yet I still see projectiles. Can anyone tell me why? Is it just that I'm seeing the servers objects? I thought ScopeLocal handles this? Hopefully someone can clear this up for me.

Anyway, so for the moment we'll leave it as is and without the Netflags call.
#3
04/08/2005 (12:24 am)
Ok, so now our projectile will fire when our dude attacks, we just have to handle what happens when it hits. Back to RTSProjectile.cc. First processTick. Find the line :

mSourceObjectId = 0;
and comment it out. This seems to be a bit of legacy code designed to prevent a projectile from detecting a collision with the unit that fired it, immediately as its fired. We can get rid of it. In fact we can probably get rid of the whole of that if block and the checks where we enable and disable our source Objects collision detection further down.

Now up to the explode() function. See the Con::executef call? This is calling a script function. Almost exactly what we want! But its doing nothing currently, because the function isn't defined in any script files. We'll do so, but first lets just add in a bit to make this more helpful. Change the call to :

Con::executef(mDataBlock, 6, "onClientExplode", scriptThis(), Con::getIntArg(mSourceObjectId),       
                                                                             Con::getIntArg(mTargetObjectId), buffer, "1.0");

So, we've passed in 2 more arguments. Notice we changed the number of arguments from 4 to 6. We only actually define 5 parameters here, in script "%this" gets added in first. Also note the "Con::getIntArg" call, to convert the ID into a string. It seems like all parameters must be strings. Don't forget! (This one also bit me in the a$$).

Right! Now we just need to define onClientExplode. I put this in server/scripts/items/crossbow.cs, because the default projectile that guys fire is a crossbowProjectile. Put this at the bottom :

function CrossbowProjectile::onClientExplode(%this, %obj, %attacker, %target, %position, %switch)
{   
   %dif = VectorSub(%target.GetTransform(),%position);
   echo("Diff :" @ %dif);
   %length = VectorLen(%dif);
   echo("Length :" @ %length);

   if(%length <0.1)
   {
   
   %damage = %attacker.getDataBlock().baseDamage;
   if(%attacker.getNetModifier().baseDamage)
      %damage *= %attacker.getNetModifier().baseDamage;

   %armor  = %target.getDataBlock().armor;
   if(%target.getNetModifier().armor)
      %armor *= %target.getNetModifier().armor;
      
   if(%damage > %armor)
      %damage -= %armor;
   else
      %damage = 0;

   %target.applyDamage(%damage);
   }

   ServerPlay3D(CrossbowExplosionSound, %attacker.GetTransform());   	
}

You'll recognise this as the onAttack code that we removed from rifleman.cs with a little extra. The code at the top just calculates the difference between the projectiles "explode" point, and the original targets current poition. If the difference is less than 0.1, we damage him.

Note, if we wanted to make a bomb explosion, we could call some function like RadiusDamage here as well.

You can also see the snippet I added to play an explosion sound, for anyone wondering how to do this, since the stock RTS is deathly quiet ;-).


Ok! That's it. Damn, it took longer to write this up than it did to do the coding! I hope I wasn't too long winded in my efforts to make this as Noob friendly as possible. And hopefully I haven't made any huge errors, given that this is actually my first foray into the TGE codebase.

Rock on!
#4
04/08/2005 (12:35 am)
Ok, so now our projectile will fire when our dude attacks, we just have to handle what happens when it hits. Back to RTSProjectile.cc. First processTick. Find the line :

mSourceObjectId = 0;
and comment it out. This seems to be a bit of legacy code designed to prevent a projectile from detecting a collision with the unit that fired it, immediately as its fired. We can get rid of it. In fact we can probably get rid of the whole of that if block and the checks where we enable and disable our source Objects collision detection further down.

Now up to the explode() function. See the Con::executef call? This is calling a script function. Almost exactly what we want! But its doing nothing currently, because the function isn't defined in any script files. We'll do so, but first lets just add in a bit to make this more helpful. Change the call to :

Con::executef(mDataBlock, 6, "onClientExplode", scriptThis(), Con::getIntArg(mSourceObjectId),       
                                                                             Con::getIntArg(mTargetObjectId), buffer, "1.0");

So, we've passed in 2 more arguments. Notice we changed the number of arguments from 4 to 6. We only actually define 5 parameters here, in script "%this" gets added in first. Also note the "Con::getIntArg" call, to convert the ID into a string. It seems like all parameters must be strings. Don't forget! (This one also bit me in the a$$).

Right! Now we just need to define onClientExplode. I put this in server/scripts/items/crossbow.cs, because the default projectile that guys fire is a crossbowProjectile. Put this at the bottom :

function CrossbowProjectile::onClientExplode(%this, %obj, %attacker, %target, %position, %switch)
{   
   %dif = VectorSub(%target.GetTransform(),%position);
   echo("Diff :" @ %dif);
   %length = VectorLen(%dif);
   echo("Length :" @ %length);

   if(%length <0.1)
   {
   
   %damage = %attacker.getDataBlock().baseDamage;
   if(%attacker.getNetModifier().baseDamage)
      %damage *= %attacker.getNetModifier().baseDamage;

   %armor  = %target.getDataBlock().armor;
   if(%target.getNetModifier().armor)
      %armor *= %target.getNetModifier().armor;
      
   if(%damage > %armor)
      %damage -= %armor;
   else
      %damage = 0;

   %target.applyDamage(%damage);
   }

   ServerPlay3D(CrossbowExplosionSound, %attacker.GetTransform());   	
}

You'll recognise this as the onAttack code that we removed from rifleman.cs with a little extra. The code at the top just calculates the difference between the projectiles "explode" point, and the original targets current poition. If the difference is less than 0.1, we damage him.

Note, if we wanted to make a bomb explosion, we could call some function like RadiusDamage here as well.

You can also see the snippet I added to play an explosion sound, for anyone wondering how to do this, since the stock RTS is deathly quiet ;-).


Ok! That's it. Damn, it took longer to write this up than it did to do the coding! I hope I wasn't too long winded in my efforts to make this as Noob friendly as possible. And hopefully I haven't made any huge errors, given that this is actually my first foray into the TGE codebase.

Rock on!
#5
04/08/2005 (1:01 am)
Ok, so now our projectile will fire when our dude attacks, we just have to handle what happens when it hits. Back to RTSProjectile.cc. First processTick. Find the line :

mSourceObjectId = 0;
and comment it out. This seems to be a bit of legacy code designed to prevent a projectile from detecting a collision with the unit that fired it, immediately as its fired. We can get rid of it. In fact we can probably get rid of the whole of that if block and the checks where we enable and disable our source Objects collision detection further down.

Now up to the explode() function. See the Con::executef call? This is calling a script function. Almost exactly what we want! But its doing nothing currently, because the function isn't defined in any script files. We'll do so, but first lets just add in a bit to make this more helpful. Change the call to :

Con::executef(mDataBlock, 6, "onClientExplode", scriptThis(), Con::getIntArg(mSourceObjectId),       
                                                                             Con::getIntArg(mTargetObjectId), buffer, "1.0");

So, we've passed in 2 more arguments. Notice we changed the number of arguments from 4 to 6. We only actually define 5 parameters here, in script "%this" gets added in first. Also note the "Con::getIntArg" call, to convert the ID into a string. It seems like all parameters must be strings. Don't forget! (This one also bit me in the a$$).

Right! Now we just need to define onClientExplode. I put this in server/scripts/items/crossbow.cs, because the default projectile that guys fire is a crossbowProjectile. Put this at the bottom :

function CrossbowProjectile::onClientExplode(%this, %obj, %attacker, %target, %position, %switch)
{   
   %dif = VectorSub(%target.GetTransform(),%position);
   echo("Diff :" @ %dif);
   %length = VectorLen(%dif);
   echo("Length :" @ %length);

   if(%length <0.1)
   {
   
   %damage = %attacker.getDataBlock().baseDamage;
   if(%attacker.getNetModifier().baseDamage)
      %damage *= %attacker.getNetModifier().baseDamage;

   %armor  = %target.getDataBlock().armor;
   if(%target.getNetModifier().armor)
      %armor *= %target.getNetModifier().armor;
      
   if(%damage > %armor)
      %damage -= %armor;
   else
      %damage = 0;

   %target.applyDamage(%damage);
   }

   ServerPlay3D(CrossbowExplosionSound, %attacker.GetTransform());   	
}

You'll recognise this as the onAttack code that we removed from rifleman.cs with a little extra. The code at the top just calculates the difference between the projectiles "explode" point, and the original targets current poition. If the difference is less than 0.1, we damage him.

Note, if we wanted to make a bomb explosion, we could call some function like RadiusDamage here as well.

You can also see the snippet I added to play an explosion sound, for anyone wondering how to do this, since the stock RTS is deathly quiet ;-).


Ok! That's it. Damn, it took longer to write this up than it did to do the coding! I hope I wasn't too long winded in my efforts to make this as Noob friendly as possible. And hopefully I haven't made any huge errors, given that this is actually my first foray into the TGE codebase.

Rock on!
#6
04/08/2005 (6:46 am)
Not bad at all! There are a couple of assumptions that we can fix (an obvious one is that your code change to RTSUnit.cc/processTick() means that melee units will no longer work properly), but all in all nothing absolutely incorrect.

I'm going to work out a few ideas and then come back with a few suggestions!
#7
04/08/2005 (7:04 am)
Whoops! Good catch, thanks Stephen.


Hehe, ok, I've modified the post to put that onAttack() call back in, which should fix it. Putting it back won't break the rifle guy, since his onAttack() is empty.

#8
04/08/2005 (7:14 am)
Btw, you should be able to safely turn off the Ghostable flag on your RTSProjectiles as well. This is most probably artifact code left over from the move to client side only projectiles. Assuming that your simulation changes are accurate and mirrored client and server, you don't need them networked at all as the basic assumption for your mod suggests!

My only "concern" (and it's a minor one) is state tracking of what is going on with the three "standard" observer modes: attacker, defender, and observer, an observer being a "third party" that is neither of the other two categories, but just watching the event happen. I didn't see much of an indication of this third party in your code, and it's a hard one to test (need 3 clients logged in!), but it probably needs to be covered somehow.
#9
04/08/2005 (7:42 am)
Hmmm, I see your point.

I'll have to wait till I get home to examine the code, I only have a snippet with me, but it looks like the attacking in processTick() is driven by the mAimObject variable. I think it depends on whether this variable is updated (pack/unpack) for ghosts. If it isn't, putting it in might fix it. (Although I can only theorise until I organise a LAN ;-) )

However, looking at it, if this isn't already the case, then I think that not only would my projectile stuff fail, the observer wouldn't see the units attack animation at all, nor would it see the original RTS projectiles either. Anyone whos played with this with a couple of players in, do the attack animations play for other peoples units?

This does also bring me back to one of my previous questions, if I turn ghostable off, why do I still see projectiles even though the client never calls fireProjectile? Is it because I'm running it on one machine only? Do I need to put that call in to ensure it works in an actual network situation? Or is it still ghosting regardless?

If anyone can test this, it'd be great!
#10
04/08/2005 (7:55 am)
Yes, unfortunately your test cases are "tainted" due to the fact that your server and your client executables are "co-located" in one--therefore, you see everything that is displayed on either the server, OR the client. In fact, this can even lead to seeing things in duplicate in some cases!

I'll try to take a look at how things work in a full 3+ client environment as soon as I can, but currently our code base is frozen for new functionality (new milestone coming out very shortly for us), so I can't really play around with it too much right now. Maybe someone else has the environment to give it a look-see!
#11
04/08/2005 (8:09 am)
Thanks man. Hehe, almost makes me want to buy a new computer, just to test my theories. Tinkering with the code really is the best way of learning.

Its gonna be a great RTS filled weekend.
#12
04/10/2005 (2:53 pm)
Gareth, if you want to add some test cases to your work then feel free to contact me. (My AIM s/n is on my profile. There are many other ways to contact me, just ask.)
#13
07/16/2005 (2:51 pm)
Gareth, did you all reach a conclusion on whether this works as intended?
#14
08/05/2005 (4:07 am)
No I didn't, sorry. I eventually decided that I would be encountering this kind of problem the whole way through (ie the problem of not knowing whether my code would work in multiplayer unless someone else tested it). I decided it would be better to make a single player game. I didn't want there to that 'disconnect' between coding and being able to test that code, it would interrupt my workflow too much.


Hopefully I'll come back to it someday, but for now...
#15
04/19/2009 (11:19 am)
I am thinking in another aproach to the same problem:

1. The server calculate a random point near to the center box position of the target with formula: targetPoint+= (attackerLevelorAim * targetPoint).

2. Send play animation to clients to targetPoint, if targetPoint is inside the box of target then make damage.
#16
02/05/2010 (4:10 am)
This is interesting -- I added this an I can see (2) projectiles firing.

I changed the speed in rifleman.cs to 8 ... and I see one glowing projectile zip quickly to the target, overtaking slower moving projectiles moving at a speed of 5 or 6.

It must be the server / client animations blending together -- or something.

Also think I'm still not seeing the projectile, just the effects around it.

this doesnt seem to do the trick
mNetFlags.clear(ScopeLocal | Ghostable);

and this causes crashes;
mNetFlags.set(IsGhost); // client-side object

EDIT: yep, confirmed it --- the server is making its own projectile, while this code adds a 2nd client side projectile.