Game Development Community

Explosion/damage effects on units

by Dave Young · in RTS Starter Kit · 02/02/2006 (6:27 am) · 14 replies

In an effort to get networked explosions on buildings...

I first took a look at RTSProjectile to make an RTSExplosion, and was going along fine but the RTSExplosion didn't need all the vectors/ticks/moves, and it was starting to confuse me.

So to try and make it simple, I created an RTSUnitExplosionEvent based off of RTSUnitAttackEvent, and stripped out the selection args/processing because there is no selection, only a target/victim.

I have to figure out how to call it and how to process it, and I'm stumbling with the client/server process.

RTSUnitAttackEvent gets processed when the client issues an attack command, but in our case the call will be based somewhere else, most likely in the building's onDamage function. So there's never a client-based call, only a server-based call.

The other thing that is bothering me is that yes, I created a new event, but won't it only get issued to the one client? In other words, other clients won't be able to see the explosion... so I don't know if making an RTSExplosion Event is the right thing to do, unless I send it to *all* clients.

The process should go like this:
1) Server realizes the health of the building is low enough to warrant the creation of an explosion/fire effect (This part is sorted out pretty well)
2) Server issues commands to all connected clients to display the effect on the unit (Maybe I know how to do this, loop through the connections and send the event?)
3) The clients recieve the commands, resolve the server's objectId to individual ghostIDs (Where would this be done?)
4) Clients show the particleemitter at the location of the ghostID object. (Where is 'client side' done?)

How will the client know the explosion should be visible? Does that get automatically done if I create things in a certain way? After all this, do I *still* need to make a 'networked explosion'?

I will of course continue to look through the networking docs to see if something clicks for me, and I have some other ideas to try out too.

Any guidance is appreciated, I will be turning this into a resource when I am done.

#1
02/02/2006 (7:50 am)
Which technique did you wind up using for your RTSUnitExplosionEvent--actually creating a new class, or commandToClient in script? It greatly controls the answers!
#2
02/02/2006 (8:53 am)
I created a new RTSUnitExplosion Event, which mirrors the RTSUnitAttack Event but only includes the target info, and not the selection info. I carefully removed all the selection info from anywhere (RTSUnit.cc, RTSConnection.cc) I saw it, and was able to compile the project.

Now it seems time for the script part, what should be the easier part hehe and I'm drawing some blanks.
#3
02/02/2006 (9:15 am)
I created a script function to try it out:

function serverCmdIssueExplosion(%client, %targetID)
{
   %target = %client.resolveObjectFromGhostIndex(%targetID);

   if (!isObject(%target))
   {
   	error("serverCmdSetTarget - Invalid target id" SPC %targetID SPC " cannot be resolved to object.");
   	return;
   }

   %cl.sendExplosionEvent(%target);
 
}


But again, this is client side initiated, not really what we're looking for.
#4
02/02/2006 (10:52 am)
This is the new Event declaration with its cronies, in RTSConnection.cc. It compiled fine. Right now I am working on getting the cmdIssueExplosion to run when called from script.


class RTSUnitExplosionEvent : public NetEvent
{
public:
//   SimSet* selection;
   //SimSet objects;
   NetObject* victim;

   RTSUnitExplosionEvent()
   {
//      selection = NULL;
//      objects.registerObject();
   }

   ~RTSUnitExplosionEvent()
   {
//      objects.unregisterObject();
   }

   void pack(NetConnection *con, BitStream *bstream)
   {
      bstream->writeInt(con->getGhostIndex(victim),NetConnection::GhostIdBitSize);
   }
   void write(NetConnection *con, BitStream *bstream)
   {
      pack(con, bstream);
   }
   void unpack(NetConnection *con, BitStream *bstream)
   {
      S32 victimIdx = bstream->readInt(NetConnection::GhostIdBitSize);

      victim = con->resolveGhost(victimIdx);

      if(!victim)
         return;
      
   }

   void process(NetConnection *con)
   {
      GameBase *rv = dynamic_cast<GameBase*>(victim);

      //really really lame, but this keeps us from crashing on disconnect
//      while (objects.size())
         //objects.removeObject(objects[0]);
   }
   DECLARE_CONOBJECT(RTSUnitExplosionEvent);
};

void RTSConnection::sendExplosionEvent(GameBase* victim)
{
   if (!isConnectionToServer())
   {
      if (getGhostIndex(victim) == -1)
         return;

      RTSUnitExplosionEvent* event = new RTSUnitExplosionEvent;
      event->victim = victim;
      postNetEvent(event);
   }
}


ConsoleMethod( RTSConnection, sendExplosionEvent, void, 4, 4, "(victim)"
              "Sends an update that an explosion has happened to the victim unit")
{
   GameBase *victim;
   if (!Sim::findObject(dAtoi(argv[3]), victim))
   {
      Con::errorf("RTSConnection::sendExplosionEvent - failed to find victim");
      return;
   }

   object->sendExplosionEvent((GameBase*)victim);
}
#5
02/02/2006 (11:01 am)
Let's re-describe the problem set here, and cover the things that need to be addressed:

We want the ability to have explosions (more accurately, particle emitters) networked to all clients that have a particular building that is destroyed in scope so that they can see particle effects (smoke, etc.). This breaks down into x areas:
  • We need to have client side particle emitters that are able to generate smoke. It sounds as if you have that part down
  • We need to have the ability for the client to know when to display smoke (note: this is a subset of the general discussion issue, which was originally "how do we network explosions". As we are defining the problem, as it turns out we actually don't need to network explosions--simply have the clients know when to display smoke)
This is broken down into two parts: detecting when a building is damaged enough to display smoke, and actually displaying the smoke.

Based on the above problem description, we have a couple of givens that help us out:
  • The server already takes care of knowing when to send ghosted objects based on their scope, so only the clients that can see the buildings have the possibility of displaying smoke if we do things right
  • The server already transmits damage state as part of normal building ghosting
  • The client is going to be calling RTSBuilding::processTick() on every building (both client, and server, since they use the same executable).
Given the above, we actually have a rather easy implementation that we can use: footpuff emitters. These are used in stock TGE to have the players leave puffs of dust as they walk through different types of terrain, and are very similar in several ways to what we want to do.

Major differences that will need re-implementation:
  • footpuff emitters emit based on where the feet of the object strike the ground. We'll need to be able to manually set where the emitter sits on our buildings.
  • footpuff emitters emit only when the foot strikes the ground (based on the animation itself). We want the ability to turn ours on, as well as set increasing levels of smoke (change emitter types most likely).
I'd suggest doing some research on the footpuff emitters (will be in the C++ code, not script), and see if a solution evolves from that!
#6
02/02/2006 (11:09 am)
Good thinking Stephen, I always try to start with something similar and modify, this is a fresh start.
#7
02/06/2006 (5:00 am)
OK, for anyone following this thread, here's an update.

Starting from scratch and using the engine code only, I followed Stephen's advice and looked at the processTick event as a place to throw in a damage effects. I used the footpuff code to create a type of explosion effect which I set off in processTick in RTSUnit.cc. I had to make some new code for proper packing/unpacking and allowing for the effect to be defined in the datablock. Looking at the footpuff code, it was easy to see where to do that. Right now the effect is always visible if the explosion parameters are defined in the unit's datablock. I plan on tying it to damage, and seeing if I can modify the effect based on the currentdamage/maxDamage for the building.

I'm still not clear what I will have to do to make sure that the effect is seen by the appropriate clients, but I haven't gotten that far yet. I am hoping that it will just sort of happen, because I surrounded the effect with an isGhost() check, just like footPuffs.

So remaining to do is:

1) Figure out which values to check for currentDamage/maxDamage inside the processTick event
2) I want to try and modify the emitter based on the damage, so it is more pronounced effect as the building is more damaged
3) The effect itself also needs to be more 'explosiony', it is based off the footpuff and not very firy looking yet
4) Also I will eventually build on this into a final explosion when the unit is officially dead, I am not sure yet if processTick is the right place to do it, but it may be.

Aside from the definition inside the datablock so far this is all engine mod. This is my first foray into C++, so the going is a little slow but picking up steam.
#8
02/06/2006 (7:59 am)
Another update, here is the code I am using so far. The basic flow to do this was to create another version of footPuff emitters, and then change them to be something else. So I looked for everything pertaining to footPuff emitters in player.c, player.h, etc and repurposed it.


As a persistent effect, this works great so far, all I have to do to get it running is to define a damageEffectEmitter, in the datablock for the unit that will be showing it. Example:

damageEffectEmitter = ChimneyFireEmitter;
damageEffectNumParts = 10;
damageEffectRadius = 2.25;

in the datablock for it. the effect only shows up on the rifleman.

in player.h

near line 192, find
ParticleEmitterData * footPuffEmitter;
   S32 footPuffID;
   S32 footPuffNumParts;
   F32 footPuffRadius;

and add underneath:

ParticleEmitterData * damageEffectEmitter;
   S32 damageEffectID;
   S32 damageEffectNumParts;
   F32 damageEffectRadius;

In player.cc

In playerData::PlayerData() near line 210, find
footPuffEmitter = NULL;
   footPuffID = 0;
   footPuffNumParts = 15;
   footPuffRadius = .25;

and add

damageEffectEmitter = NULL;
   damageEffectID = 0;
   damageEffectNumParts = 15;
   damageEffectRadius = .25;


near line 348, in PlayerData::preLoad, find
if (!footPuffEmitter && footPuffID != 0)
      if (!Sim::findObject(footPuffID, footPuffEmitter))
         Con::errorf(ConsoleLogEntry::General, "PlayerData::preload - Invalid packet, bad datablockId(footPuffEmitter): 0x%x", footPuffID);

and add underneath:
if (!damageEffectEmitter && damageEffectID != 0)
      if (!Sim::findObject(damageEffectID, damageEffectEmitter))
         Con::errorf(ConsoleLogEntry::General, "PlayerData::preload - Invalid packet, bad datablockId(damageEffect): 0x%x", damageEffect);

Near line 478, in Playerdata::initPersistFields, find

addField("footPuffEmitter",   TypeParticleEmitterDataPtr,   Offset(footPuffEmitter,    PlayerData));
   addField("footPuffNumParts",  TypeS32,                      Offset(footPuffNumParts,   PlayerData));
   addField("footPuffRadius",    TypeF32,                      Offset(footPuffRadius,     PlayerData));

and add underneath:

addField("damageEffectEmitter",   TypeParticleEmitterDataPtr,   Offset(damageEffectEmitter,    PlayerData));
   addField("damageEffectNumParts",  TypeS32,                      Offset(damageEffectNumParts,   PlayerData));
   addField("damageEffectRadius",    TypeF32,                      Offset(damageEffectRadius,     PlayerData));

in playerData::packData, near line 595, find:
if( stream->writeFlag( footPuffEmitter ) )
   {
      stream->writeRangedU32( footPuffEmitter->getId(), DataBlockObjectIdFirst,  DataBlockObjectIdLast );
   }

   stream->write( footPuffNumParts );
   stream->write( footPuffRadius );

and add underneath:
if( stream->writeFlag( damageEffectEmitter ) )
   {
      stream->writeRangedU32( damageEffectEmitter->getId(), DataBlockObjectIdFirst,  DataBlockObjectIdLast );
   }

   stream->write( damageEffectNumParts );
   stream->write( damageEffectRadius );



in playerdata""unpackData. near line 713, find:

if( stream->readFlag() )
   {
      footPuffID = (S32) stream->readRangedU32(DataBlockObjectIdFirst, DataBlockObjectIdLast);
   }

   stream->read(&footPuffNumParts);
   stream->read(&footPuffRadius);

and add underneath:
if( stream->readFlag() )
   {
      damageEffectID = (S32) stream->readRangedU32(DataBlockObjectIdFirst, DataBlockObjectIdLast);
   }

   stream->read(&damageEffectNumParts);
   stream->read(&damageEffectRadius);



In RTSUnit.cc, anear line 11, add in:
#include "dgl/dgl.h"
#include "dgl/materialPropertyMap.h"
#include "game/fx/particleEngine.h"


near line 318, in RTSUnit::processTick, find:
if (!isGhost())
      updateAnimation(TickSec);

and add underneath:
PROFILE_START(RTSUnit_DamageEffect);
      
    //Do this if unit is alive and is a ghost
   if ((getDamageState() == Enabled) && isGhost())  {
      if (mDataBlock->damageEffectEmitter != NULL)
      {
             // New emitter every time for visibility reasons
              ParticleEmitter * emitter = new ParticleEmitter;
              emitter->onNewDataBlock( mDataBlock->damageEffectEmitter );
              if( !emitter->registerObject() )
               {
                Con::warnf( ConsoleLogEntry::General, "Could not register damageEffect emitter");
                delete emitter;
                emitter = NULL;
                }
               else
               {
                emitter->emitParticles( location, Point3F( 0.0, 0.0, 1.0 ), mDataBlock->damageEffectRadius,
                Point3F(0, 0, 0), mDataBlock->damageEffectNumParts );
                emitter->deleteWhenEmpty();
               }
        }
   }

As noted in the above post, I will be moving this towards damage based effects, but I thought this snapshot could be useful for putting effects on units in general.

[Edited] updated code section for processTick
#9
02/06/2006 (10:32 am)
When you get this all put together, will you be releasing it as a resource with the damage based effects? I'm really digging this. Great job Dave!
#10
02/06/2006 (10:55 am)
Yes Chip, that's the plan. I'm hesitant right now, because this is highly experimental and I will need to come up with and test a variety of situations, otherwise I will get bombarded with questions/problems I can't answer!
#11
02/06/2006 (11:42 am)
Sounds good man. Keep up the good work.
#12
02/08/2006 (8:16 am)
Another update. In RTSUnit.cc, processTick, I replaced the damageEffect profile I created before with this one:

This completes one portion of the original problem: damage based effects. It enables scaling of the effect based on damage! I don't know too much about particle emitters at all yet, so this one is the footpuff emitter dynamics using the chimneyfire emitter particles.

PROFILE_START(RTSUnit_DamageEffect);
      
    //Do this if unit is alive and is a ghost
   if ((getDamageState() == Enabled) && isGhost())  {
      if (mDataBlock->damageEffectEmitter != NULL)
      {
	  F32 tmpDamageLevel;
	  tmpDamageLevel = getDamageValue();
		if(tmpDamageLevel > 0){
             // New emitter every time for visibility reasons
            //mIsDamageEffect = true;
              ParticleEmitter * emitter = new ParticleEmitter;
              emitter->onNewDataBlock( mDataBlock->damageEffectEmitter );
              if( !emitter->registerObject() )
               {
                Con::warnf( ConsoleLogEntry::General, "Could not register damageEffect emitter");
                delete emitter;
                emitter = NULL;
                }
               else
               {
                emitter->emitParticles( location, Point3F( 0.0, 0.0, 1.0 ),
				(mDataBlock->damageEffectRadius) * (tmpDamageLevel),
                Point3F(0, 0, 0),
				(mDataBlock->damageEffectNumParts) * (tmpDamageLevel)
				);
                emitter->deleteWhenEmpty();
               }
			 }
//placeholder for future explode code
//			   if((mIsExploding == false) && (tmpDamageLevel >= .85)){
//				explode(location, VectorF(0,0,1), GameBaseObjectType);
//        	     }
		  
        }
   }


Last step is to figure out the best way to trigger an explosion. I'd like to do it here in processTick, but my initial experiments show that it's not a good place, as it looks like a unit can die before processTick happens, and you can't accurately capture a 100% damaged state before it's too late. An explosion effect is only good if it happens at the right time.
#13
02/19/2006 (1:26 pm)
Can't wait for this to be done it will add a lot of eye candy to my game.
#14
03/31/2006 (1:04 pm)
I'm sorry. The name of function in RTSConnection.cc file is wrong:

void RTSConnection::sendExplosionEvent(GameBase* victim){
if (!isConnectionToServer())

Change to it since we use version 1.3:

if (!isServerConnection())