Overcoming ITickable's Limitations
by David Wyand · in Torque Game Engine · 01/30/2006 (12:53 pm) · 15 replies
Greetings!
I've been working on a GameBase replacement class recently and am now looking into making my new class 'tickable'. This has of course lead me to look at TGE 1.4's ITickable class as an obvious choice for this. It is a great solution, but I've found a couple of limitations that don't make it a fit for all cases:
1. ITickable appears to really only be for client side objects. Its ITickable::advanceTime(timeDelta); method is called from the clientProcess() function in game.cc. This causes interpolateTick() to be called on all objects, which is unnecessary for server side objects.
One possible solution would be to call isClientObject() on each object to determine if interpolateTick() should be called at all. Of course this wouldn't stop the object from being processed during the client process side of DemoGame::processTimeEvent(), which may have unexpected consequences on the server.
2. If an object deletes itself during one of the ITickable methods, such as:
it is removed from the ITickable list as you would expect. The unexpected consequence is that the loop in ITickable::advanceTime() will still advance its pointer, so the object following the deleted one in the list is skipped. It will lose one tick.
Looking at how GameBase deals with this I see it uses an internal linked list and some interesting link list juggling to handle point #2. And it uses two lists for client and server, and an appropriate check in ::onAdd() to deal with point #1.
My question is: has anyone already worked out a solution to this and would be willing to share?
Thanks!
- LightWave Dave
I've been working on a GameBase replacement class recently and am now looking into making my new class 'tickable'. This has of course lead me to look at TGE 1.4's ITickable class as an obvious choice for this. It is a great solution, but I've found a couple of limitations that don't make it a fit for all cases:
1. ITickable appears to really only be for client side objects. Its ITickable::advanceTime(timeDelta); method is called from the clientProcess() function in game.cc. This causes interpolateTick() to be called on all objects, which is unnecessary for server side objects.
One possible solution would be to call isClientObject() on each object to determine if interpolateTick() should be called at all. Of course this wouldn't stop the object from being processed during the client process side of DemoGame::processTimeEvent(), which may have unexpected consequences on the server.
2. If an object deletes itself during one of the ITickable methods, such as:
void MyClass::processTick()
{
if(my test here)
{
this->deleteObject();
}
}it is removed from the ITickable list as you would expect. The unexpected consequence is that the loop in ITickable::advanceTime() will still advance its pointer, so the object following the deleted one in the list is skipped. It will lose one tick.
Looking at how GameBase deals with this I see it uses an internal linked list and some interesting link list juggling to handle point #2. And it uses two lists for client and server, and an appropriate check in ::onAdd() to deal with point #1.
My question is: has anyone already worked out a solution to this and would be willing to share?
Thanks!
- LightWave Dave
About the author
A long time Associate of the GarageGames' community and author of the Torque 3D Game Development Cookbook. Buy it today from Packt Publishing!
#2
Your idea of having a processing profile for an object is a great one. It would allow for the general solution.
What I've done here is written a solution that still assumes a client/server model as that solves my particular problem. Selfish, I know. :o)
I've called my class ICSTickable and you would place a call in clientProcess():
and serverProcess():
just as is done with the ProcessList class for GameBase objects. I'll share my source in the next two messages for you to tear apart. :o)
I'm starting to really like the idea of having a component-based system. Thanks for starting me down this path!
- LightWave Dave
02/10/2006 (12:49 pm)
Greetings Pat!Your idea of having a processing profile for an object is a great one. It would allow for the general solution.
What I've done here is written a solution that still assumes a client/server model as that solves my particular problem. Selfish, I know. :o)
I've called my class ICSTickable and you would place a call in clientProcess():
ICSTickable::advanceClientTime(timeDelta);
and serverProcess():
ICSTickable::advanceServerTime(timeDelta);
just as is done with the ProcessList class for GameBase objects. I'll share my source in the next two messages for you to tear apart. :o)
I'm starting to really like the idea of having a component-based system. Thanks for starting me down this path!
- LightWave Dave
#3
Too big to fit in one message...
02/10/2006 (12:51 pm)
ICSTickable.h:#ifndef _ICSTICKABLE_H_
#define _ICSTICKABLE_H_
#include "core/tVector.h"
/// This interface allows you to let any object be ticked, and is based on TGE 1.4's
/// ITickable class. The difference is ICSTickable allows for an object to be processed
/// on the server or client side. You use it like so:
/// @code
/// class FooClass : public SimObject, public virtual ICSTickable
/// {
/// // You still mark SimObject as Parent
/// typdef SimObject Parent;
/// private:
/// ...
///
/// protected:
/// // These three methods are the interface for ICSTickable
/// virtual void interpolateTick( F32 delta );
/// virtual void processTick();
/// virtual void advanceTime( F32 timeDelta );
///
/// public:
/// ...
/// };
/// @endcode
/// Please note the three methods you must implement to use ICSTickable, but don't
/// worry. If you forget, the compiler will tell you so. Also note that the
/// typedef for Parent should NOT BE SET to ICSTickable, the compiler will <i>probably</i>
/// also tell you if you forget that. Last, but assuridly not least is that you note
/// the way that the inheretance is done: public <b>virtual</b> ICSTickable
/// It is very important that you keep the virtual keyword in there, otherwise
/// proper behavior is not guarenteed. You have been warned.
///
/// The point of a tickable object is that the object gets ticks at a fixed rate
/// which is one tick every 32ms. This means, also, that if an object doesn't get
/// updated for 64ms, that the next update it will get two-ticks. Basically it
/// comes down to this. You are assured to get one tick per 32ms of simulation time
/// passing provided that isProcessingTicks returns true when ICSTickable calls it.
///
/// The processTick() method that provides the 32ms tick is called for both client
/// and server objects.
///
/// isProcessingTicks is a virtual method and you can (should you want to)
/// override it and put some extended functionality to decide if you want to
/// recieve tick-notification or not.
///
/// The remaining two methods of time notification are only called on the client.
/// advanceTime lets you know when time passes regardless of the return value
/// of isProcessingTicks. The object WILL get the advanceTime call every single
/// update. The argument passed to advanceTime is the time since the last call
/// to advanceTime. Updates are not based on the 32ms tick time. Updates are
/// dependant on framerate. So you may get 200 advanceTime calls in a second, or you
/// may only get 20. There is no way of assuring consistant calls of advanceTime
/// like there is with processTick. Both are useful for different things, and
/// it is important to understand the differences between them.
///
/// Interpolation is the last part of the ICSTickable interface on the client side.
/// It is called every update, as long as isProcessingTicks evaluates to true on the
/// object. This is used to interpolate between 32ms ticks. The argument passed to
/// interpolateTick is the time since the last call to processTick. You can see in the
/// code for ICSTickable::advanceClientTime that before a tick occurs it calls
/// interpolateTick(0) on every object. This is to tell objects which do interpolate
/// between ticks to reset their interpolation because they are about to get a new tick.
///
/// To place an object onto the client or server processing list, a call to
/// addObjectToTickList() is required. An object may only belong to one list at a time
/// and will automatically be removed from any existing list. This method may be called
/// at any time, but must be prior to the need for tick processing. A common location
/// would be in the class's constructor, or during the SimObject onAdd() method.
///
/// When an object is to be removed from a processing list, such as when it is deleted,
/// a call to removeObjectFromTickList() is required. This could be performed in the
/// class's destructor, or during the SimObject onRemove() method.
///
/// The following example illustrates how to perform this with a NetObject:
/// @code
/// bool FooClass::onAdd()
/// {
/// if (!Parent::onAdd())
/// return false;
///
/// addObjectToTickList(isClientObject());
///
/// return true;
/// }
///
/// void FooClass:onRemove()
/// {
/// removeObjectFromTickList();
///
/// Parent::onRemove();
/// }
/// @endcode
/// In the example, the NetObject's isClientObject() method is used to determine if it should
/// be added to the client list or the server list.
///
/// ICSTickable checks if an object deletes itself in the middle of tick processing and
/// ensures that no objects lose a tick due to the processing lists being reshuffled. The
/// assumption here is that an object will only delete itself during a processTick() or
/// advanceTime(). It would not make sense for an object to delete itself during an
/// interpolateTick().
///
/// @todo Support processBefore/After and move the GameBase processing over to use ICSTickable
class ICSTickable
{
private:
static U32 smLastClientTick; ///< Time of the last tick that occurred
static U32 smLastClientTime; ///< Last time value at which advanceTime was called
static U32 smLastClientDelta; ///< Last delta value for advanceTime
static U32 smLastServerTick; ///< Time of the last tick that occurred
static U32 smLastServerTime; ///< Last time value at which advanceTime was called
static U32 smLastServerDelta; ///< Last delta value for advanceTime
// This just makes life easy
typedef Vector<ICSTickable *>::iterator ProcessListIterator;
static Vector<ICSTickable *>& getClientProcessList(); ///< List of client tick controls
static Vector<ICSTickable *>& getServerProcessList(); ///< List of server tick controls
protected:
bool mProcessTick; ///< Set to true if this object wants tick processing
/// Determines which processing list the object has been placed on.
/// 0=Does not belong to a list (the default)
/// 1=Client processing list
/// 2=Server processing list
S32 mProcessingList;
/// Called within the object's onAdd() to place it in the
/// appropriate processing list.
/// @parm client True if this object should be placed on the client list
void addObjectToTickList(bool client);
/// Called within the object's onRemove() to remove it
/// from the processing lists.
void removeObjectFromTickList();
/// This method is called every frame for a client object and lets the object
/// interpolate between ticks so you can smooth things as long as
/// isProcessingTicks returns true when it is called on the object. An object
/// may not delete itself within this method, such as by using deleteObject().
virtual void interpolateTick( F32 delta ) = 0;
/// This method is called once every 32ms if isProcessingTicks returns true
/// when called on the object and is called on both client and server objects.
virtual void processTick() = 0;
/// This method is called once every frame for a client object regardless of
/// the return value of isProcessingTicks and informs the object of the passage
/// of time. This may be used to advance client side animations.
virtual void advanceTime( F32 timeDelta ) = 0;
public:
// Can let everyone look at these if they want to
static const U32 smTickShift; ///< Shift value to control how often Ticks occur
static const U32 smTickMs; ///< Number of milliseconds per tick, 32 in this case
static const F32 smTickSec; ///< Fraction of a second per tick
static const U32 smTickMask;Too big to fit in one message...
#4
02/10/2006 (12:51 pm)
ICSTickable.h continued:/// Constructor
/// Initialized to not belong to a processing list
ICSTickable();
/// Destructor
/// Remove this object from the processing list if it hasn't already done so.
virtual ~ICSTickable();
/// Is this object wanting to receive tick notifications
/// @returns True if object wants tick notifications
virtual bool isProcessingTicks() const { return mProcessTick; };
/// Sets this object as either tick processing or not
/// @parm tick True if this object should process ticks
virtual void setProcessTicks( bool tick = true );
//------------------------------------------------------------------------------
/// This is called in clientProcess to advance the time for all ICSTickable
/// client objects
/// @returns True if any ticks were sent
/// @see clientProcess
static bool advanceClientTime( U32 timeDelta );
/// This is called in serverProcess to advance the time for all ICSTickable
/// server objects
/// @returns True if any ticks were sent
/// @see serverProcess
static bool advanceServerTime( U32 timeDelta );
};
//------------------------------------------------------------------------------
inline void ICSTickable::setProcessTicks( bool tick /* = true */ )
{
mProcessTick = tick;
}
#endif // _ICSTICKABLE_H_
#5
Done.
PS: I've also included the fix by Paul Scott found here
PPS: It looks like some of my formatting has been mangled. Hopefully not enough to make it unreadable.
02/10/2006 (12:52 pm)
ICSTickable.cc:#include "core/iCSTickable.h"
// The statics
U32 ICSTickable::smLastClientTick = 0;
U32 ICSTickable::smLastClientTime = 0;
U32 ICSTickable::smLastClientDelta = 0;
U32 ICSTickable::smLastServerTick = 0;
U32 ICSTickable::smLastServerTime = 0;
U32 ICSTickable::smLastServerDelta = 0;
const F32 ICSTickable::smTickSec = ( F32( ICSTickable::smTickMs ) / 1000.f );
const U32 ICSTickable::smTickShift = 5;
const U32 ICSTickable::smTickMs = ( 1 << smTickShift );
const U32 ICSTickable::smTickMask = ( smTickMs - 1 );
Vector<ICSTickable *>& ICSTickable::getClientProcessList()
{
// This helps to avoid the static initialization order fiasco
static Vector<ICSTickable *> smClientProcessList; // List of client tick controls
return smClientProcessList;
}
Vector<ICSTickable *>& ICSTickable::getServerProcessList()
{
// This helps to avoid the static initialization order fiasco
static Vector<ICSTickable *> smServerProcessList; // List of server tick controls
return smServerProcessList;
}
//------------------------------------------------------------------------------
ICSTickable::ICSTickable()
{
mProcessingList = 0;
}
//------------------------------------------------------------------------------
ICSTickable::~ICSTickable()
{
// Make sure that the object has been removed from any processing list.
// Tsk, tsk. The object should clean up itself. Maybe do an assert?
removeObjectFromTickList();
}
//------------------------------------------------------------------------------
void ICSTickable::addObjectToTickList(bool client)
{
if(client)
{
// If we're already on the client list, return.
if(mProcessingList == 1)
return;
// If we're already on another list, first remove
// ourselves from it.
if(mProcessingList)
removeObjectFromTickList();
// Add to the client processing list
getClientProcessList().push_back(this);
mProcessingList = 1;
} else
{
// If we're already on the server list, return.
if(mProcessingList == 2)
return;
// If we're already on another list, first remove
// ourselves from it.
if(mProcessingList)
removeObjectFromTickList();
// Add to the server processing list
getServerProcessList().push_back(this);
mProcessingList = 2;
}
}
//------------------------------------------------------------------------------
void ICSTickable::removeObjectFromTickList()
{
if(!mProcessingList)
return;
if(mProcessingList == 1)
{
for( ProcessListIterator i = getClientProcessList().begin(); i != getClientProcessList().end(); i++ )
{
if( (*i) == this )
{
getClientProcessList().erase( i );
mProcessingList = 0;
return;
}
}
} else if(mProcessingList == 2)
{
for( ProcessListIterator i = getServerProcessList().begin(); i != getServerProcessList().end(); i++ )
{
if( (*i) == this )
{
getServerProcessList().erase( i );
mProcessingList = 0;
return;
}
}
}
}
//------------------------------------------------------------------------------
bool ICSTickable::advanceClientTime( U32 timeDelta )
{
U32 targetTime = smLastClientTime + timeDelta;
U32 targetTick = ( targetTime + smTickMask ) & ~smTickMask;
U32 tickCount = ( targetTick - smLastClientTick ) >> smTickShift;
// Storage to keep track of the last processed object
ICSTickable* obj;
// If we are going to send a tick, call interpolateTick(0) so that the objects
// will reset back to their position at the last full tick
if( smLastClientDelta && tickCount )
for( ProcessListIterator i = getClientProcessList().begin(); i != getClientProcessList().end(); i++ )
if( (*i)->isProcessingTicks() )
(*i)->interpolateTick( 0.f );
// Advance objects
if( tickCount )
for( ; smLastClientTick != targetTick; smLastClientTick += smTickMs )
for( ProcessListIterator i = getClientProcessList().begin(); i < getClientProcessList().end(); )
if( (*i)->isProcessingTicks() )
{
// Store the current object
obj = (*i);
// Process the object's tick
(*i)->processTick();
// Check if we should advance the iterator. Don't if the processed
// object has deleted itself.
if(obj == (*i))
++i;
} else
{
// Advance the iterator
++i;
}
smLastClientDelta = ( smTickMs - ( targetTime & smTickMask ) ) & smTickMask;
F32 dt = smLastClientDelta / F32( smTickMs );
// Now interpolate objects that want ticks
for( ProcessListIterator i = getClientProcessList().begin(); i != getClientProcessList().end(); i++ )
if( (*i)->isProcessingTicks() )
(*i)->interpolateTick( dt );
// Inform ALL objects that time was advanced
dt = F32( timeDelta ) / 1000.f;
for( ProcessListIterator i = getClientProcessList().begin(); i < getClientProcessList().end(); )
{
// Store the current object
obj = (*i);
// Process the object
(*i)->advanceTime( dt );
// Check if we should advance the iterator. Don't if the processed
// object has deleted itself.
if(obj == (*i))
++i;
}
smLastClientTime = targetTime;
return tickCount != 0;
}
//------------------------------------------------------------------------------
bool ICSTickable::advanceServerTime( U32 timeDelta )
{
U32 targetTime = smLastServerTime + timeDelta;
U32 targetTick = ( targetTime + smTickMask ) & ~smTickMask;
U32 tickCount = ( targetTick - smLastServerTick ) >> smTickShift;
// Storage to keep track of the last processed object
ICSTickable* obj;
// Advance objects
if( tickCount )
for( ; smLastServerTick != targetTick; smLastServerTick += smTickMs )
for( ProcessListIterator i = getServerProcessList().begin(); i < getServerProcessList().end(); )
if( (*i)->isProcessingTicks() )
{
// Store the current object
obj = (*i);
// Process the object's tick
(*i)->processTick();
// Check if we should advance the iterator. Don't if the processed
// object has deleted itself.
if(obj == (*i))
++i;
} else
{
// Advance the iterator
++i;
}
smLastServerDelta = ( smTickMs - ( targetTime & smTickMask ) ) & smTickMask;
smLastServerTime = targetTime;
return tickCount != 0;
}Done.
PS: I've also included the fix by Paul Scott found here
PPS: It looks like some of my formatting has been mangled. Hopefully not enough to make it unreadable.
#6
Not giving you a hard time for the work--it's highly appreciated!
02/10/2006 (1:50 pm)
@David: you know better! This should be a resource so they can simply download it, hehe. Cut/paste from forums for large segments of code is quite difficult.Not giving you a hard time for the work--it's highly appreciated!
#7
I was actually intending that others would look this over and comment.... Especially Pat. Once it has some burn-in time, I do plan on making it a resource. Or maybe someone will want to add it to TGE 1.4.1? :o)
- LightWave Dave
02/10/2006 (2:22 pm)
Eeek! The Zepp Police! :o)I was actually intending that others would look this over and comment.... Especially Pat. Once it has some burn-in time, I do plan on making it a resource. Or maybe someone will want to add it to TGE 1.4.1? :o)
- LightWave Dave
#8
02/10/2006 (2:39 pm)
Maybe it would make sense to do a TickMaster that you can then spawn different tickers off of, and then subscribe to the tickers from your itickable. So you sort of have profiles but they're not a seperate thing.
#9
02/10/2006 (3:01 pm)
I like Ben's idea about the TickMaster, especially since this would also provide a point where you can provide selective ticks at different rates as well, but all under control of a master ticker. This isn't something people consider much, but in any extremely large application with tens of thousands of objects, ticking every single one every 32 milliseconds isn't going to work, and having the ability to subscribe for, say, getting a pulse every 10 ticks, or 100, or whatever would be the basics of a powerful and flexible priority ticking system.
#10
02/10/2006 (4:22 pm)
Dave, I'm trying to pull some other people into this thread but it's not working very well. I think this is an important thing to hammer out.
#11
If we include basic start/stop functionality, Tickability becomes similar to a java thread in style, and if we treat Tickmaster thus as a threadgroup, a Tickmaster can be set up to start/stop and manage a group of Tickables. The optimisation potential of this is enormous.
02/12/2006 (2:47 am)
I like where this is going. If we include basic start/stop functionality, Tickability becomes similar to a java thread in style, and if we treat Tickmaster thus as a threadgroup, a Tickmaster can be set up to start/stop and manage a group of Tickables. The optimisation potential of this is enormous.
#12
02/12/2006 (3:36 am)
But a totally different problem. Fibers are a seperate, well-known area of functionality; Tickable is for synchronous processing.
#13
02/12/2006 (5:06 am)
What about having iTickable recieve Move inputs? Or are we just going to have to use GameBase for anything we want to control?
#14
02/12/2006 (8:53 am)
I def. don't think iTickable should get Move inputs. This is supposed to be as lightweight as it can be general purpose.
#15
So, async aside, starting/stopping and being able to do so at the Tickmaster group level as well, makes tickability an extremely powerful mechanism, for preloading objects, for lateloading objects, and especially for gamelogic objects. (Think range triggered AI, for instance).
The optimisation potential of only conditionally using ticks, is what is exposed by the starting/stopping function.
Edit: I also agree with Pat that input moves has nothing to do with tickability. The class and it's group equivalent should have only Tickability characteristics. Move inputs may be appropriate for a particular implementation of these classes, but that is game dependent.
02/12/2006 (11:50 pm)
Ben: I didn't mean to imply async with the java thread analogy. That's why I said 'in style'. I agree these ticks are synchronous in TGE/TSE and as such async is a different problem altogether.So, async aside, starting/stopping and being able to do so at the Tickmaster group level as well, makes tickability an extremely powerful mechanism, for preloading objects, for lateloading objects, and especially for gamelogic objects. (Think range triggered AI, for instance).
The optimisation potential of only conditionally using ticks, is what is exposed by the starting/stopping function.
Edit: I also agree with Pat that input moves has nothing to do with tickability. The class and it's group equivalent should have only Tickability characteristics. Move inputs may be appropriate for a particular implementation of these classes, but that is game dependent.
Torque 3D Owner Pat Wilson
This is a really good usability test for iTickable. When I designed it, I had decided that it was stupid something needed to inheret from GameBase just to get tick functionality. The plan was to have some kind of tick solution that could even work off multiple process lists and such eventually. I am not sure how to solve #1 because I didn't want to tie it to any kind of game concept.
Maybe a better idea would be to have tick profiles, so basically when you created an iTickable you'd pass in a behavior profile. This would tell it if it needed to get interpolate calls and stuff, enable client/server checks. Does that sound reasonable?
I'm going to drag in some of the other engine guys at GG into this thread and see what they think.