Game Development Community

Client side TSStatic object

by Thomas \"Man of Ice\" Lund · 09/30/2004 (11:45 am) · 40 comments

Download Code File

What is this
With this code you can create 3d ingame objects that only exist in the scene of the client owning them. They do not exist on the server or other clients, and thus there is absolutely no overhead on the network when using these.

These objects are perfect for creating rich GUI's like waypoints, 3d reticles and other interfaces that you would otherwise have to do in 2d.

How to add
Simply unzip the files in the zip and dump into your game dir. Recompile. As simple as that

How to use
You can use this object from e.g. a GUI control. This example snippit below shows how to create the object on the client side. Remember to set a position.

ClientSideTSStatic* pWaypointMarker = new ClientSideTSStatic();
pWaypointMarker ->setField("shapeName", "game/data/shapes/waypoint.dts");
// Register the Object.
if (!pWaypointMarker ->registerObject())
{
   Con::errorf("Could not create waypoint marker");
   delete pWaypointMarker ;
}

There is no script interface. I'll leave that as an exercise to the reader ;-)

A special thanks to Ben for help on this resource.
Page«First 1 2 Next»
#21
02/06/2007 (11:34 pm)
If your shape is facing e.g. 0 degrees and then you feed it a value of e.g. 45 degrees, then the shape would instantly be rotated 45 degrees.

With interpolation and a "time to target" you can get the engine code to rotate it smoothly for you.

So it all depends on what you want.

The code is approx the same as interpolating between ticks. You calculate intermediate values over time.

You are the only one that knows if you need it or not
#22
02/07/2007 (12:11 am)
I see... I guess I'll have to test it to see what would work best. Thanx for the help. This will defenitely get me going in the right direction.
#23
02/08/2007 (3:48 am)
This resource helped me understand how objects are added to the client scenegraph. Thanks Thomas!
#24
02/08/2007 (5:50 am)
I'm having some fun with this too :-) Will release my code when I'm done.
#25
02/08/2007 (12:52 pm)
Super - I would love to see screenshots, movies and/or just hear about what you do with this :-D
#26
02/08/2007 (2:04 pm)
Will do... At the moment Im using it as target markers, or target waypoints. So when 2 teams have different objectives, the "objective" is only seen (or pointed at by a arrow above it) by the ness clients. Dunno, if i'm gonna use it, but ill share, as always.

My primary goal was to make a gui arrow, that points in the direction of the target, objective,waypoint... but I realised that this wasn't the correct way of doing it... So i made it to something useable.


Will talk about it in my plan (Where i'll pimp Air Ace too ;)
#27
04/18/2007 (1:21 pm)
Does anybody know if this can be extended to interiors, such as the interiorInstance class? Or would this require drastic changes for interiors.
#28
04/19/2007 (9:09 am)
i actually got this working first try without tweaking... but how does one remove a client side object?
Edit: figured that part out, .delete lol

also, is all of the collision related code in onAdd needed, as colliding with a client side object seems odd to me.

also, where would i look to add the stuff like interpolation and animation support.
#29
04/26/2007 (2:19 pm)
Does anyone know how to implement movement of these objects with the mouse? Thomas alluded to it earlier when he said: "What I would do in your case is to save a pointer to the object that you create in an array or hash array. Then generate id's yourself and use those as part of the console methods." I am having trouble grasping this. Any thoughts would be great, and thanks Thomas for this great resource.

Jim Breslin
#30
04/27/2007 (12:26 am)
If you create the object from c++, you will have it's pointer. A pointer is just the offset adres of where that thing is stored in in your memory (ram). The script doesnt know where the object is, and if it doesnt know, you cant do anything with it. So you need to let the script know where the object is. You *could* pass the pointer itself as a string back to the script so that it knows and you can use the returned pointer in other functions. So it would look something like this in script for example:

%someobject = addClientSideTSStatic(%dtsincludingpath, %position, %rotation, %scale);

Now %someobject holds the actual pointer to the object. Then you can create some other function that uses that pointer again to do something with it:
moveClientSideTSStatic(%someobject,%position, %rotation);
Or whatever you would like.

However, it is safer for various reasons to NOT pass the real pointer on to the script but keep track of all the objects and give them a number yourself to return to the script. The main reason being that you can then check if the script user actually send back a valid pointer. There's not really another way to check if the pointer is correct. And if it's not and you use it anyway, things will get very messy.
#31
04/27/2007 (12:34 pm)
with just the stock resource you can do %blah = new clientsidetsstatic() etc etc, and then do %blah.settransform, theres nothing special that needs to be done, cept itll move around all jittery without interpolation, which is what i want todo, and animation.
#32
04/30/2007 (5:46 am)
hmm... good point.
#33
04/30/2007 (7:00 pm)
today i finally figured out how to get animations to work. and i also got interpolation working(made a second settransform function with a optional interpolation duration argument). i might post the modifications, though the way i did interpolation probably isnt the best way in the world.
#34
05/02/2007 (9:14 pm)
Thanks Robin and Benj,

Yes I am able to move the object around with the mouse without any jitters now using Benj's --"%blah = new clientsidetsstatic() etc etc, and then do %blah.settransform".
I am doing this in default.bind: Where $pNPCMarker is the client side static...

$restof = "2 0 0 0 0";
function yaw(%val)
{
$mvYaw += getMouseAdjustAmount(%val);
$currentMarkerTransform = $pNPCMarker.getTransform();
$markerX = getWords($currentMarkerTransform,0,0);
$markerX -= $mvYaw;
$newTransform = $markerX SPC $markerY SPC $restof;
$pNPCMarker.setTransform($newTransform);
}
function pitch(%val)
{
$mvPitch += getMouseAdjustAmount(%val);
$currentMarkerTransform = $pNPCMarker.getTransform();
$markerY = getWords($currentMarkerTransform,1,1);
$markerY += $mvPitch;
$newTransform = $markerX SPC $markerY SPC $restof;
$pNPCMarker.setTransform($newTransform);
}

What I really want to do is have the $pNPCMarker move equally in the xy coordinates with the player and then have you be able to move the mouse to get the $pNPCMarker to move over and above that. I can do it but that is when it gets the jitters.

I will be looking into getting rid of the jitters also but thanks again for the input Robin and Benj it is much appreciated.
Let you know if I figure anything out about the interpolation.
#35
05/03/2007 (7:26 am)
heres the code to add animation and interpolation.
alot of the animation code is snagged from shapebase and changed to work with tsstatic, the interpolation part i wrote out by hand.
in clientsidetsstatic.h:

find:
#ifndef _TSSTATIC_H_
#include "game/tsStatic.h"
#endif
add after it:
#include "game/shapeBase.h"


find:
typedef TSStatic Parent;
   bool mHidden;
add after:
U32 lastrenderms;

find:
void onRemove();
   void setHidden(bool hidden);
   bool prepRenderImage(SceneState* state, const U32 stateKey, const U32 startZone, const bool modifyBaseZoneState);

add:
struct Thread {
      /// State of the animation thread.
      enum State {
         Play, Stop, Pause
      };
      TSThread* thread; ///< Pointer to 3space data.
      U32 state;        ///< State of the thread
                        ///
                        ///  @see Thread::State
      S32 sequence;     ///< The animation sequence which is running in this thread.
      U32 sound;        ///< Handle to sound.
      bool atEnd;       ///< Are we at the end of this thread?
      bool forward;     ///< Are we playing the thread forward? (else backwards)
   };
   Thread mScriptThread[ShapeBase::MaxScriptThreads];

	TSShapeInstance* getShapeInstance(){return mShapeInstance;}
   bool setThreadSequence(U32 slot,S32 seq,bool reset);
   void updateThread(Thread& st);
   bool stopThread(U32 slot);
   bool pauseThread(U32 slot);
   bool playThread(U32 slot);
   bool setThreadDir(U32 slot,bool forward);
   void advanceThreads(U32 curms);
   void setTransform(const MatrixF &mat);
   void setTransform2(const MatrixF &mat,U32 targettick = 32);
   void interpolate(F32);

   U32 interpolateTargetTick;
   U32 interpolateStartTick;
   Point3F TargetPos;
   QuatF TargetRot;
   Point3F oldpos;
   QuatF oldrot;

in clientsidetsstatic.cc:

find:
ClientSideTSStatic::ClientSideTSStatic()
{
   mTypeMask |= StaticShapeObjectType | StaticObjectType;
   mNetFlags.clear(Ghostable | ScopeAlways);
   mNetFlags.set(IsGhost);
   mHidden = false;

add:
S32 i;
   for (i = 0; i < ShapeBase::MaxScriptThreads; i++) {
      mScriptThread[i].sequence = -1;
      mScriptThread[i].thread = 0;
      mScriptThread[i].sound = 0;
      mScriptThread[i].state = Thread::Stop;
      mScriptThread[i].atEnd = false;
      mScriptThread[i].forward = true;
   }
   lastrenderms = Sim::getCurrentTime();

	TargetPos = oldpos = Point3F();
	TargetRot = oldrot = QuatF();
	interpolateTargetTick = interpolateStartTick = 0;

find:
bool ClientSideTSStatic::prepRenderImage(SceneState* state, const U32 stateKey, const U32 startZone, const bool modifyBaseZoneState) {
	if (mHidden) {
		return false;
	}

add:
U32 curms = Sim::getCurrentTime();
advanceThreads(curms);

	if(interpolateTargetTick > 0)
		if(interpolateTargetTick >= curms)
		{
            F32 t = (float)(curms-interpolateStartTick)/(interpolateTargetTick-interpolateStartTick);
			interpolate(t);
		}
		else
		{
			MatrixF mat;
			TargetRot.setMatrix(&mat);
			mat.setColumn(3,TargetPos);
            setTransform(mat);
		}

then you should be at the end of the file basicly, so add this section at the end.
bool ClientSideTSStatic::setThreadSequence(U32 slot,S32 seq,bool reset)
{
   Thread& st = mScriptThread[slot];
   if (st.thread && st.sequence == seq && st.state == Thread::Play)
      return true;

   if (seq < ShapeBase::MaxSequenceIndex) {
	   setMaskBits(ShapeBase::ThreadMaskN << slot);
      st.sequence = seq;
      if (reset) {
         st.state = Thread::Play;
         st.atEnd = false;
         st.forward = true;
      }
      if (mShapeInstance) {
         if (!st.thread)
            st.thread = mShapeInstance->addThread();
         mShapeInstance->setSequence(st.thread,seq,0);
         updateThread(st);
      }
      return true;
   }
   return false;
}

void ClientSideTSStatic::updateThread(Thread& st)
{
   switch (st.state) {
      case Thread::Stop:
         mShapeInstance->setTimeScale(st.thread,1);
         mShapeInstance->setPos(st.thread,0);
         // Drop through to pause state
      case Thread::Pause:
         mShapeInstance->setTimeScale(st.thread,0);
         break;
      case Thread::Play:
         if (st.atEnd) {
            mShapeInstance->setTimeScale(st.thread,1);
            mShapeInstance->setPos(st.thread,st.forward? 1: 0);
            mShapeInstance->setTimeScale(st.thread,0);
         }
         else {
            mShapeInstance->setTimeScale(st.thread,st.forward? 1: -1);
         }
         break;
   }
}

bool ClientSideTSStatic::stopThread(U32 slot)
{
   Thread& st = mScriptThread[slot];
   if (st.sequence != -1 && st.state != Thread::Stop) {
	   setMaskBits(ShapeBase::ThreadMaskN << slot);
      st.state = Thread::Stop;
      updateThread(st);
      return true;
   }
   return false;
}

bool ClientSideTSStatic::pauseThread(U32 slot)
{
   Thread& st = mScriptThread[slot];
   if (st.sequence != -1 && st.state != Thread::Pause) {
	   setMaskBits(ShapeBase::ThreadMaskN << slot);
      st.state = Thread::Pause;
      updateThread(st);
      return true;
   }
   return false;
}

bool ClientSideTSStatic::playThread(U32 slot)
{
   Thread& st = mScriptThread[slot];
   if (st.sequence != -1 && st.state != Thread::Play) {
	   setMaskBits(ShapeBase::ThreadMaskN << slot);
      st.state = Thread::Play;
      updateThread(st);
      return true;
   }
   return false;
}

bool ClientSideTSStatic::setThreadDir(U32 slot,bool forward)
{
   Thread& st = mScriptThread[slot];
   if (st.sequence != -1) {
      if (st.forward != forward) {
		  setMaskBits(ShapeBase::ThreadMaskN << slot);
         st.forward = forward;
         st.atEnd = false;
         updateThread(st);
      }
      return true;
   }
   return false;
}


void ClientSideTSStatic::advanceThreads(U32 curms)
{
	F32 dt;
	dt = (curms - lastrenderms)*0.001;
	lastrenderms = curms;

	for (U32 i = 0; i < ShapeBase::MaxScriptThreads; i++) {
      Thread& st = mScriptThread[i];
      if (st.thread) {
         if (!mShapeInstance->getShape()->sequences[st.sequence].isCyclic() && !st.atEnd &&
             (st.forward? mShapeInstance->getPos(st.thread) >= 1.0:
              mShapeInstance->getPos(st.thread) <= 0)) {
            st.atEnd = true;
            updateThread(st);
            if (!isGhost()) {
               char slot[16];
               dSprintf(slot,sizeof(slot),"%d",i);
               Con::executef(this,3,"onEndSequence",scriptThis(),slot);
            }
         }
         mShapeInstance->advanceTime(dt,st.thread);
      }
   }
}

ConsoleMethod( ClientSideTSStatic, playThread, bool, 3, 4, "(int slot, string sequenceName)")
{
   U32 slot = dAtoi(argv[2]);
   if (slot >= 0 && slot < ShapeBase::MaxScriptThreads) {
      if (argc == 4) {
         if (object->getShapeInstance()->getShape()) {
            S32 seq = object->getShapeInstance()->getShape()->findSequence(argv[3]);
            if (seq != -1 && object->setThreadSequence(slot,seq,true))
               return true;
         }
      }
      else
         if (object->playThread(slot))
            return true;
   }
   return false;
}

ConsoleMethod( ClientSideTSStatic, setThreadDir, bool, 4, 4, "(int slot, bool isForward)")
{
   int slot = dAtoi(argv[2]);
   if (slot >= 0 && slot < ShapeBase::MaxScriptThreads) {
      if (object->setThreadDir(slot,dAtob(argv[3])))
         return true;
   }
   return false;
}

ConsoleMethod( ClientSideTSStatic, stopThread, bool, 3, 3, "(int slot)")
{
   int slot = dAtoi(argv[2]);
   if (slot >= 0 && slot < ShapeBase::MaxScriptThreads) {
      if (object->stopThread(slot))
         return true;
   }
   return false;
}

ConsoleMethod( ClientSideTSStatic, pauseThread, bool, 3, 3, "(int slot)")
{
   int slot = dAtoi(argv[2]);
   if (slot >= 0 && slot < ShapeBase::MaxScriptThreads) {
      if (object->pauseThread(slot))
         return true;
   }
   return false;
}

void ClientSideTSStatic::setTransform(const MatrixF &mat)
{
	TargetPos = oldpos = Point3F();
	TargetRot = oldrot = QuatF();
	interpolateTargetTick = interpolateStartTick = 0;
	Parent::setTransform(mat);
}

void ClientSideTSStatic::setTransform2(const MatrixF &mat,U32 targettick)
{
	const MatrixF& oldmat = getTransform();
	oldmat.getColumn(3,&oldpos);
	oldrot = QuatF(oldmat);

	interpolateStartTick = Sim::getCurrentTime();
	interpolateTargetTick = interpolateStartTick+targettick;

	mat.getColumn(3,&TargetPos);
	TargetRot = AngAxisF(mat);
}

ConsoleMethod( ClientSideTSStatic, setTransform2, void, 3, 4, "Interpolated(Transform T,Optional TargetTickDelay = 32MS)")
{
   Point3F pos;
   const MatrixF& tmat = object->getTransform();
   tmat.getColumn(3,&pos);
   AngAxisF aa(tmat);

   dSscanf(argv[2],"%g %g %g %g %g %g %g",
           &pos.x,&pos.y,&pos.z,&aa.axis.x,&aa.axis.y,&aa.axis.z,&aa.angle);

   MatrixF mat;
   aa.setMatrix(&mat);
   mat.setColumn(3,pos);
   if(argc > 3)
	   object->setTransform2(mat,dAtoi(argv[3]));
   else
	   object->setTransform2(mat);

}

void ClientSideTSStatic::interpolate(F32 d)
{
	MatrixF mat;
	QuatF newrot;
	Point3F newpos;

	newpos.interpolate(oldpos,TargetPos,d);
	newrot.interpolate(oldrot,TargetRot,d);

	newrot.setMatrix(&mat);
	mat.setColumn(3,newpos);

	Parent::setTransform(mat);
}

it will overload settransform to blank out interpolation data when its used, and add settransform2, which interpolates. used like so.. $narf.settransform2(%position,2000); which makes it take 2000ms to move to the position, the 2nd argument is optional, and defaults to 32ms, which is the time it takes torque to do 1 processing tick, so if you have a function scheduled to run and update a clientsidetsstatic, make the 2nd argument in settransform2 be the interval the function runs at, and should be perfectly smooth.
#36
11/18/2007 (3:29 am)
I have a strange issue with this resource in TGEA.. The second I create one of these ClientSideTSStatic's, texture animations and glow effects on *all* other objects of the same type go away. For example, if I create a TSStatic of knot.dts (which animates and reflects), and then create a ClientSideTSStatic of the same dts, neither of them will animate or reflect. It's like the system suddenly decides to render the mesh w/out the shader.

I tried creating my own simple class to do "no-collision client-side only" meshes and it does the same thing the instant I make the object a ghost:

class NonCollidableShape : public TSStatic
...

IMPLEMENT_CO_NETOBJECT_V1(NonCollidableShape);

NonCollidableShape::NonCollidableShape()
{
mTypeMask = DefaultObjectType | StaticObjectType | StaticRenderedObjectType | ShadowCasterObjectType;

mNetFlags.clear(Ghostable | ScopeAlways);
mNetFlags.set(IsGhost);
}

If I remove the mNetFlags lines I get all of my texture animations and effects back.
#37
03/29/2008 (7:42 am)
I can think of some really cool GUI uses for this - how about 3D damage indicators for vehicles? 3D terrain maps... ooh...
#38
06/11/2008 (12:49 pm)
Dear Folks,

We managed to create a Tsstatic objec in the scene.. but we want to do something like what there is in Guitar Hero or Rock Band....

You have some elements that come falling on the screen (commands), and on the background you have a camera that goes around the scenary...

Would you have any idea on how to do that? :)

Thanks,

Americo
#39
08/14/2008 (11:31 pm)
Works great under TGEA+AFX 1.7.1 once you add in Tuanlucky's adjustments. I'm gonna have fun with this one!
#40
02/26/2009 (4:14 pm)
Just wondering if anyone got this up and working with an object that has datablocks (ie StaticShape). I had a good size talk a long time ago with Tomas about this and he pointed me in the correct direction, but I am still having some issues. I know this may sound odd, but we have no need for torques server so we are getting rid of as much of it as we can (I could tell you a little more if you really need to know, but trust me we don't need it).

Here is what I did so far:

Created my own class that inherits from StaticShape

added my own datablock type that inherits from StaticShapeData just
incase I need to do anything different in there and changed mDataBlock to be a pointer to my new datablock type.


added:
static const U32 sgAllowedDynamicTypes = 0xffffff;
to the top of the cpp file

added:
mNetFlags.clear(Ghostable | ScopeAlways); mNetFlags.set(IsGhost);
[code]
to the ctor

I also added a ctor that takes a pointer to a datablock to set mDataBlock

added an onAdd():
[code]
{
   if(!mDataBlock)
      return false;

   // We need to modify our type mask based on what our datablock says...
	mTypeMask |= (mDataBlock->dynamicTypeField & sgAllowedDynamicTypes);

   addToScene();

   Con::executef(mDataBlock, "onAdd",scriptThis());

   return true;
}

added onRemove() as well:
{
	if (mDataBlock)
      Con::executef(mDataBlock, "onRemove",scriptThis());
   removeFromScene();
}

and added a creation function:
ConsoleFunction( addClientSideStaticShape, S32, 2, 2, "(DataBlock db)")
{
	ClientSideStaticShapeData* data;
	if (Sim::findObject(argv[1],data)) {
		ClientSideStaticShape* pCSStatic = new ClientSideStaticShape(data);

		// register the object
		if (!pCSStatic->registerObject())
		{
			Con::errorf("Could not register a ClientSideStaticShape");
			delete pCSStatic;
			return -1;
		}

		SimObject *obj = Sim::findObject(pCSStatic->getId());
		if(obj)
			return obj->getId();
		else
			return -1;

		//return pCSStatic->getId();
	}

	Con::errorf("Datablock not found.");
	return -1;
}

The issues:

As you can see in my creation function I tried just returning getID on pCSStatic (commented out part) this gives me an id but if I try calling any console method like getClassName() it says that the console method can't be found. It does pass a isObject test. So then I commented that out and tried doing a lookup with that id just for the hell of it and it never finds it even though registerObject() passes and adds it to the library. If I try using new instead (like I would like to be able to) it just crashes and breaks at a bunch of net code where it is doing mission cleanup or something (can give you more details if you want).

Anyway if anyone has any help or an example class (much like Tomas did) that would be sweet, cause I am a bit stuck I am going to mess around with it some more though to see if I can figure it out just figured I would save myself possible trouble if someone else knew exactly what to do.

I should probably tell you I am doing this in TGEA 1.8.1.

Also this is sorta a side note, but I got a tsStatic client side only object up and running just fine in TGE & TGEA and even changed the container from server to client that all the collision... code uses so it is all done on the client rather than the server (not to hard).
Page«First 1 2 Next»