Game Development Community

Custom object added to Group not reflecting that group in C++

by Edward Rotberg · in Torque 3D Professional · 02/24/2010 (3:32 pm) · 22 replies

Hi,

I have a very simple custom object (SceneObject) that I have instantiated in the World Editor. If I move that object to a named Group, and then check its getGroup() group in the debugger, it tells me the name of the SimGroup is "Server Connection". However, when I examine some TSStatic objects that are also in the same World Editor group using getGroup(), the name of the SimGroup is the actual name of the group as it is displayed in the World Editor. I was hoping to be able to determine if my custom object belongs to the same group as these TSStatics but I can't seem to do that.

I have guaranteed that I am looking at the client object for my custom object. None of this is making sense at the moment.

Any ideas? I'm sure I'm missing something simple here.

Thanks in advance,

= Ed =

EDIT: I should add that this is all client-side stuff. I'm dealing with clicking on an object and trying to find out if my custom object is a member of the same group as the object clicked on.
Page «Previous 1 2
#1
02/24/2010 (6:39 pm)
Hey Ed, the problem is that when your server object gets ghosted, the client object does not get placed in a SimGroup structure that corresponds to what exists on the server (which is what you see in the World Editor). So you need to call getGroup() on a server object to accomplish what you'd trying to do. If you're just doing local client-server stuff, you could just do this:

%group = %myClientObj.getServerObject().getGroup();

That should get the group that the TSStatics are in. You could also do a server container raycast instead of a client raycast, which would return your server object directly.

If you are doing this with a separated server-client, you'd have to take a different approach.

*** EDIT ***
Didn't catch that you wanted to do this in C++. The equivalent of above would be:

NetObject *obj = myCustomClientObj->getServerObject();
SimGroup *group = obj->getGroup();
#2
02/24/2010 (8:24 pm)
Thanks Ryan,

I'm afraid, however, that this will need to run on a separated server-client setup, so I'm not sure that this will help. Are the TSStatics that I'm looking at server objects then? If not, why are they getting placed in a SimGroup structure? I thought that they were client objects as I'm pretty sure that the raycast involved is happening on the client side - but I could be wrong about that.

There are other ways that we can accomplish what we are trying to do, but none would be as elegant or as easy for our artists and designers to use/understand. I figured they put in this nice organizational feature (SimGroups) for us and I could take advantage of that. I'm sure there are good reasons for not mirroring the SimGroups on the client side, but I'm hazy on what they might be.

Thanks again.

= Ed =

#3
02/25/2010 (10:18 am)
Well, one way to do it but maybe not optimal would be like this:

- perform raycast clientside
- send the server the hit object's ghost ID (either through script with "commandToServer" or in C++ using "sendRemoteCommand")
- resolve the ghost ID serverside
- get the server object's group
- loop through the group and see if your custom object is in there
- send the result back to the client

You said this is all clientside stuff, but sounds like you're actually looking at the TSStatics on the server. Are you testing this on a true client?
#4
02/25/2010 (11:02 am)
Ryan,

I'll be the first to admit that I'm not sure where this is getting executed. I am doing my testing on a stand-alone version, so in my case, I could well be doing this testing on the server objects. That would be unfortunate as this needs to happen on the client, but all of this is initiated from scripts, the the current Canvas (we've made modifications to guiCanvas.cpp). Does this mean the request is sent to the server's guiCanvas??

= Ed =
#5
02/25/2010 (3:57 pm)
Well, writing server-client code on a stand-alone machine can get tricky since variables like 'gClientContainer' and 'gServerContainer' can be accessed anywhere in code, whether clientside or serverside. Also, methods like Sim::findObject() can "see" both server and client objects. This gives you direct access to server objects in client code and visa versa, which obviously is not valid when you separate the server and client.

The Canvas only exists on the client. If you're doing everything in script, I'm assuming you're using the ConsoleFunction, containerRayCast(). Looking at it's C++ definition, you can see that it does a serverside raycast. So if you're calling this function in response to a mouse event (on the client) it will work in a stand-alone exe for the reasons above (and return a server object!) but will not work on a true client.

One way to separate the server/client stuff:

- in your mouse event script callback, do something like, commandToServer('MyServerRayCast', %start, %end); // clientside

- in function serverCmdMyServerRayCast(%client, %start, %end) // serverside
--- do the containerRayCast();
--- get the group of the hit object
--- loop through the group checking for your custom object's classname
--- if custom object is in group, do commandToClient(%client, 'CustomObjInGroup');

- in clientCmdCustomObjInGroup(), respond accordingly // clientside
#6
02/25/2010 (5:19 pm)
Thanks Ryan. I will agree with you that having both the server and client sides running in a stand alone environment gets tricky. It's far too awkward to determine where you are running, and in fact at object constructor time, you can't find out which side you are on - at least not with the isClientObject() method.

While I can clearly see why you would want to pass mouse-clicks up to the server in most situations, there are certainly some cases where you want to operate locally and determine yourself when you want to involve the server. Sadly that becomes problematic with Torque at times.

= Ed =
#7
03/09/2010 (8:52 am)
@Ryan -> You said:
Quote:The Canvas only exists on the client. If you're doing everything in script, I'm assuming you're using the ConsoleFunction, containerRayCast(). Looking at it's C++ definition, you can see that it does a serverside raycast. So if you're calling this function in response to a mouse event (on the client) it will work in a stand-alone exe for the reasons above (and return a server object!) but will not work on a true client.

For what we are doing, which is non-destructive, and non manipulative of any objects, couldn't we write a clentContainerRayCast ConsoleMethod to do its rayCast client-side by copying the code and it to use gClientContainer instead of gServerContainer? Since we are calling this from our own script anyhow, it would seem to be a simple fix.

I'd love to get your thoughts.

= Ed =
#8
03/09/2010 (3:22 pm)
In the context of the previous discussion, that would definitely be the easiest way to do it, assuming you have a purely client-side method of checking if the hit object "belongs to the same group" as your custom object, whether that be a simgroup or something else. If you don't have a client-side method figured out, I thought of a pretty easy engine change that would automatically place your objects into the proper simgroups on the client when they first ghosted. But I haven't tried it out yet. :)
#9
03/09/2010 (3:30 pm)
Ryan,

I've already written the client-side rayCast and it returns the ghost ID just fine. Unfortunately, I can't seem to create an array of ghostIDs for the objects I'm interested in. This is all explained in a new post I've started here:
http://www.torquepowered.com/community/forums/viewthread/112297

If you can explain to me how to get my objects into the proper simGroups on the client-side at ghost time, that would provide an alternative method for me as I can then use that to locate the object hit by the rayCast and more importantly, its "owning" object (explained in the other post).

= Ed =
#10
03/09/2010 (3:46 pm)
Working on it right now, Ed. Gonna see if I can implement it. The basic idea is extending SceneObject by giving it a clientGroup string which is the name of the SimGroup you want it to exist in on the client. When it's ghosted, in its client-side onAdd() method, it checks to see if there is a SimGroup with the name of its clientGroup variable. If it exists, it simply adds itself to the group. If it doesn't exist, it creates the group, and adds itself to it. All subsequent SceneObject-derived objects with the same clientGroup value will find the existing SimGroup upon ghosting and add themselves to it.
#11
03/09/2010 (3:55 pm)
Sounds good. Let me know if you finish this and how it works. If you can't get around to it, I'll give it a try myself.

Thanks so much!

= Ed =
#12
03/09/2010 (4:14 pm)
Just a thought, but when you create the SimGroup on the client-side, it will probably have to be added as a child of the ServerConnection group, IIRC.

= Ed =
#13
03/09/2010 (8:21 pm)
Hey Ed, I think I got it working. It was actually a little bit trickier than I thought it would be, but not too bad. It's all automatic... whatever SimGroup you place a SceneObject-derived object in on the server, it gets placed in a SimGroup with the same name on the client. If you move the object to a new SimGroup on the server, the client object will automatically get moved to the corresponding SimGroup on the client. If you're on a LocalConnection, however, it will not do this because the client object will get put in the server SimGroup (because of local server-client stuff we've talked about) and you can actually see both the server and client objects in the World Editor tree at the same time. And if you click on the client object, it'll crash. So I set it up to behave this way only for separated client-servers. This way you can do level creation on a LocalConnection machine (or dedicated server), and then when you place it on the dedicated server, it will just work. If you needed to do testing on a LocalConnection machine, it's an easy change, and just be sure not to select the client object in the editor tree.

I haven't gotten to test this on a seperated client-server. I may try tomorrow. I'll try to post the code here tomorrow as well.
#14
03/09/2010 (9:14 pm)
Ryan,

On a LocalConnection, can't you put the object into it's SimGroup under the server SimGroup? This way I can still test for it on any connection, Local or separate Client. It won't solve the crashing issue in the World Editor, but it might still give me what I need on the client side in just a single version. I can always build a separate World Editor version for that purpose.

= Ed =
#15
03/10/2010 (5:19 am)
My ghost creates its SimGroup under the ServerConnection. If the Server object is in TestGroup, then when the ghost tries to see if TestGroup already exists client-side, with a LocalConnection, it actually finds the existing server-side TestGroup (because it can see everything) and places itself in there. That's why you can see it in the World Editor.

I think I have an easier solution for you. Sounds like you don't really care what SimGroup the ghosts exist in, as long as they're grouped like on the server. So I changed the code to append a "_C" to the client SimGroup name corresponding to the one on the server. So on the server you place objects in TestGroup. On the client they will all exist in TestGroup_C. That gets rid of the client objects showing up in the World Editor and should work for your dedicated server situation. So no special builds, and even better, everything is transparent to your artist.
#16
03/10/2010 (6:44 am)
Ok, here's the code (based on 1.1 Beta 1)...

In "sceneGraph/sceneObject.h", add a new maskbit to sceneObject:

enum SceneObjectMasks 
   {
      InitialUpdateMask = BIT(0),
      ScaleMask         = BIT(1),
      FlagMask          = BIT(2),
      MountedMask       = BIT(3),
      ClientGroupMask	= BIT(4),    // RDM: added mask bit
      NextFreeMask      = BIT(5)     // RDM: shifted next free mask up by one
   };

Add a new variable and some method declarations:

SimPersistID *mMountPID;

// RDM: start
protected:

   StringTableEntry mClientGroup;

   static const char* _getClientGroup( void* object, const char* data );
   static bool _setClientGroup( void *object, const char *index, const char *data );

public:

   const char* getClientGroup() { return mClientGroup; }
   void setClientGroup(const char* clientGroup);
   void onGroupAdd();
// RDM: end

protected:

   /// @name Transform and Collision Members
   /// @{

   ///
   Container* mContainer;

In "sceneGraph/sceneObject.cpp", initialize mClientGroup in the SceneObject constructor:

mSceneManagerLinks = NULL;
   
   mObjectFlags.set( RenderEnabledFlag | SelectionEnabledFlag );

   mClientGroup = NULL;			// RDM
}


Add a new protected field in SceneObject::initiPersistFields():

addProtectedField( "isSelectionEnabled", TypeBool, Offset( mObjectFlags, SceneObject ),
         &_setSelectionEnabled, &_getSelectionEnabled,
         "Can only be selected in editor if true (and if class is selection-enabled, too)." );
		
      // RDM: start
      addProtectedField( "clientGroup", TypeString, Offset( mClientGroup, SceneObject ),
         &_setClientGroup, &_getClientGroup,
         "The name of the SimGroup the ghost will reside in." );
      // RDM: end

   endGroup( "Misc" );

Add a new method, SceneObject::onGroupAdd(), to override the SimObject's virtual method:

// RDM: start
void SceneObject::onGroupAdd()
{
   if( isServerObject() )
   {
      // Server ojbect changed SimGroups... set mClientGroup to new SimGroup name
      mClientGroup = getGroup()->getName();

      // Update the client with the new SimGroup name
      setMaskBits( ClientGroupMask );
   }
   else
   {
      // Client Object changed SimGroups... check to make sure its in the group
      // specified by its mClientGroup.  This takes care of the first transmission
      // at mission loading time.  The new ghost will automatically get put
      // into ServerConnection, so we catch it here and put it in the correct group.
      if( mClientGroup != NULL )
      {
         if( dStrcmp( mClientGroup, getGroup()->getName() ) )
         {
            SimGroup* group;

            Sim::findObject( mClientGroup, group );

            group->addObject( this );
         }
      }
   }
}
// RDM: end
#17
03/10/2010 (6:46 am)
Add set/get methods for new protected field:

bool SceneObject::_setSelectionEnabled( void *object, const char *index, const char *data )
{
   SceneObject* obj = reinterpret_cast< SceneObject* >( object );
   obj->setSelectionEnabled( dAtob( data ) );
   return false;
}

// RDM: start
//--------------------------------------------------------------------------
// ClientGroup

const char* SceneObject::_getClientGroup( void* object, const char* data )
{
   SceneObject* obj = reinterpret_cast< SceneObject* >( object );
   return obj->getClientGroup();
}

bool SceneObject::_setClientGroup( void *object, const char *index, const char *data )
{
   SceneObject* obj = reinterpret_cast< SceneObject* >( object );
   obj->setClientGroup( data );
	return false;
}

void SceneObject::setClientGroup( const char* clientGroup )
{ 
   mClientGroup = StringTable->insert( clientGroup );
   setMaskBits( ClientGroupMask );
}
// RDM: end

Modify SceneObject::packUpdate to send the new protected field:

if ( stream->writeFlag( mask & FlagMask ) )
      stream->writeRangedU32( (U32)mObjectFlags, 0, getObjectFlagMax() );

   // RDM: start
   if ( stream->writeFlag( mask & ClientGroupMask ) )
      if ( stream->writeFlag( mClientGroup != NULL ) )
         stream->writeString( mClientGroup );
   // RDM: end

   if ( mask & MountedMask ) 
   {

Modify SceneObject::unpackUpdate to read in the new protected field and handle adding the group to the client.

// FlagMask
   if ( stream->readFlag() )      
      mObjectFlags = stream->readRangedU32( 0, getObjectFlagMax() );

   // RDM: start
   // ClientGroupMask
   if ( stream->readFlag() )
   {
      if( stream->readFlag() )
      {
         char newGroup[256];
         stream->readString( newGroup );

         // Clientside group will have same name as serverside group + "_C"
         dStrcat( newGroup, "_C" );

         mClientGroup = StringTable->insert( newGroup );

         SimGroup *group;
         Sim::findObject( mClientGroup, group );

         // If the group doesn't exist clientside, create it and add it to ServerConnection
         if( group == NULL )
         {
            group = new SimGroup();
            group->registerObject();
            group->assignName( mClientGroup );

            SimGroup *serverConn;
            Sim::findObject( "ServerConnection", serverConn );
            serverConn->addObject( group );
         }

        // If ghost has already been created, go ahead and place it in its group.
        // Otherwise, onGroupAdd() will do this for a newly created ghost.
        if( isProperlyAdded() )
           group->addObject( this );
     }
   }
   // RDM: end

   // MountedMask
   if ( stream->readFlag() ) 
   {
#18
03/10/2010 (8:12 am)
Sweeeeet, Ryan! I'll hook this all in today and let you know how it goes. I don't know if I can finish it up this AM before I head out for an offsite meeting, but if not I'll finish up tonight before I head out tomorrow for GDC.

Thanks so much for doing this!

= Ed =
#19
03/10/2010 (8:40 am)
@Ryan: It Works. I had to make some adjustments to your code as it looks like you are running under a different version (1.1?), but it works like a champ! Now I can get back to debugging my own collision detection problems :)

Thanks again so very much for this work.

= Ed =
#20
03/10/2010 (8:44 am)
Oops. I may have been hasty there. Still working on it...

= Ed =
Page «Previous 1 2