Game Development Community

Projectiles: Server vs. Client

by Robert Fritzen · in Torque 3D Professional · 04/08/2014 (5:38 pm) · 7 replies

This is kind of a break off from my thread from last week, however, this topic is more relevant for the needs of that thread and suits what I'm actually looking for answer wise better.

So, I've been tackling the job of adding new projectile types as of recently, and one of my stumbling blocks I've come across is in C++, where calculations need to be handled on either the server or the client side of the application.

So, first things first, let's get a list of functions that are relevant:

void advanceTime(F32 dt)

This one appears to be called at every frame update in the game, and it can be used to provide the highest precision in movements. However, I can't seem to figure out if this is called on the client or the server side of things, but my hunch is telling me that it's taking place on both.

void processTick(const Move *move)
Now, I have no idea what's going on here. I state this because of the following in projectile.cpp:

void Projectile::processTick( const Move *move )
{
   Parent::processTick( move );
   mCurrTick++;

   simulate( TickSec );
}

void Projectile::simulate( F32 dt )
{         
   if ( isServerObject() && mCurrTick >= mDataBlock->lifetime )
   {
      deleteObject();
      return;
   }
   .
   .
   .
   if ( isClientObject() )
   {
      emitParticles( mCurrPosition, newPosition, mCurrVelocity, U32( dt * 1000.0f ) );
      updateSound();
   }
}
So, what I see going on here is absolutely "silly". So, if we're on the server the object is just deleted right away, which is confusing to me because you would think the server should have the authority on how and when projectiles function the way they are, so that led me to believe this is "client-only", until I hit the little if block near the bottom, which implies that calls on the server can still be made. Either this is redundant code, or I have no idea what's going on here.

void interpolateTick( F32 delta )
This one just drives me nuts beyond all reason, because it seems to share functioning from all of the prior calls. Again, server versus client, no idea here.

I would honestly love for you guys to tell me what's going on with these three calls. But what I'm really after here is a little more important. I want to do projectile specific stuff, positional related and functioning related, so what kind of functioning belongs in which functions, and how do I ensure I'm doing the right operation in what function. Lastly, I need something about this:

SimObjectPtr<ShapeBase> mSourceObject; ///< Actual pointer to the source object, times out after SourceIdTimeoutTicks
This is defined in projectile.h, now, I need this object to be "server-side" so I can use object specific functions on the server that relate to whether or not the player firing the gun is holding down a trigger (see prior thread), but when I'm calling this, I get stuck with a client object, which is bad. But here's where things get really "good".

Check out how that is defined:
bool Projectile::onAdd() {
   .
   .
   if (isServerObject()) {
      ShapeBase* ptr;
      if (Sim::findObject(mSourceObjectId, ptr)) {
         mSourceObject = ptr;

         // Since we later do processAfter( mSourceObject ) we must clearProcessAfter
         // if it is deleted. SceneObject already handles this in onDeleteNotify so
         // all we need to do is register for the notification.
         deleteNotify( ptr );
      }
      else {
         if (mSourceObjectId != -1)
            Con::errorf(ConsoleLogEntry::General, "Projectile::onAdd: mSourceObjectId is invalid");
         mSourceObject = NULL;
      }

      // If we're on the server, we need to inherit some of our parent's velocity
      //
      mCurrTick = 0;
   }
   .
   .
}
Which implies that this should be a server-side source object. So why exactly, would I be getting my client-side player object as the source of my projectile, which bases from this same code? You tell me.

So, what it all comes down to here, is a matter of the naming convention of these functions versus where they're actually used server versus client, and how I can go about getting the server version of mSourceObject without needing to dabble in the volatile and untrustable ghosting system for projectiles.

#1
04/08/2014 (6:38 pm)
IIRC:
  • processTick is called on both server and client once every 32ms (or at 32Hz, I can never remember) and does simulationy stuff that needs to happen with a guaranteed time rate.
  • interpolateTick is called on client objects every frame with a time delta, allowing them to do rendering-related logic to make the game look smooth at frame rates above 32Hz.
  • advanceTime is called on client objects every frame to do... things.
More information.
#2
04/08/2014 (10:15 pm)
I believe advanceTime is called on both client and server objects.
#3
04/09/2014 (6:35 am)
Yes, advanceTime() is called on both process lists.
There is a torque define to use the client list only.
#4
04/09/2014 (6:57 am)
Without looking to see it myself looks that the first part of Projectile::simulate checks to see if the current tick is greater than the lifetime of the projectile. If it is then it deletes the object. Otherwise it does what is in the rest of simulate, so when the object is still alive on the server it does the rest of the stuff.

I would guess what you want to do just depends on how/where the object is created.
#5
04/09/2014 (9:10 am)
So I would be safe to do stuff on the server using advanceTime() so long as the server related stuff is under a isServerObject() call?

The reason I ask is because it seems like processTick() on my projectile calls once, and then it just dies.
#6
04/09/2014 (6:36 pm)
I would look into why Projectile::processTick() is only called once on the server. The server should (or rather, is and will be) the authority for projectiles so not getting serverside processTick() calls is an issue.

interpolateTick() is called every frame. The 'dt' value actually represents a percentage of time between the last engine 'tick' and the next 'tick'. It ranges from 0.0-1.0 (0.0 = 'last tick time', 1.0 = 'next tick time').

advanceTime() is called every frame as well but the 'dt' value represents the time difference between the last call of advanceTime() and the current (real) time. advanceTime() is usually where animation updates are done because animation playback uses real time, not percentages of time.
#7
04/09/2014 (6:45 pm)
Oh, well thank you guys for all the help. I should have mentioned I found out that the projectile was being deleted outside of processTick, hence the call happening only one and everythings working as intended now without crashing.