Game Development Community

Object/Datablock methods

by Dumbledore · in Torque Game Engine · 07/17/2008 (6:25 pm) · 17 replies

Hi, I'm a big fan of OOP. In terms of maintainability and code reuse it would make sense if you could do client-side object related things in Torque Script. Here's an example:

When the user selects an object in the gameworld the user interface must:
- Display the appropriate menu for that object
- Display a profile picture and information on the object.


When the user unselects the object the user interface must:
- Hide the menu for that object
- Hide the profile area


Now for the OOP solution to this problem:

function ObjectData::updateUserInterface(%this, %isSelected)
{
     if ( %isSelected )
     {
           %this.displayMyMenu(); %this.displayMyProfile();
     }
     else 
     {
           %this.hideMyMenu();  %this.hideMyProfile(); 
     }
}

// when the mouse clicks on an object it generates the onMouseDown callback passing in the %obj clicked on.

function PlayGui::onMouseDown(%this, %obj)
{
     if ( isSelectable(%obj) )
     {  
           select(%obj);
     }
}

function select(%obk)
{
      %obj.getDataBlock().updateUserInterface(true); 
}

//  same thing for unselect except pass in false.

Unfortunately this won't work because the datablock method "updateUserInterface" is server-side. In a single player game this will work as expected and the correct updates to the UI will be seen. However, connect as a remote client to this server and the UI will not be updated.

The obvious way around that is to load your code with switch statements and other conditionals to get the right user interface updates for the right objects. Unfortunately this is a hacky solution and results in hard to maintain code.

What is the correct solution here? Or does Torque Script not offer one?

#1
07/17/2008 (7:21 pm)
Datablocks exist on both the server and client, sooo, if you need to call your datablock method "updateUserInterface" on the client, you should DEFINE it on the client side. It doesn't really sound like an appropriate method for the server side anyway, since UI/GUIs don't exist there.

Alternatively if it IS something that needs to be called on the server, but the client needs to notify the server when to do it, you can use a commandToServer.

The client just doesn't usually mess with datablocks because usually datablock methods are performing operations that have to affect all players (be ghosted) so they have to happen on the serverside object (owner) of the datablock.

If this is a UI change that really only effects one client and you just feel like putting it in the datablock namespace, I guess you could do that though.
#2
07/17/2008 (7:27 pm)
I just reread what you were really trying to do and I think thats a pretty good idea for eliminating switch statements.

Just make a cs file on the client side that defines the methods you want to call in the object(s)/datablock(s) namespaces.
#3
07/18/2008 (9:38 am)
Okay for some reason I have it stuck in my mind that you can only create datablock and object methods on the server. I will try to create them on the client and I'll prey it works. If it does work then I'll be back to loving Torque Script again!

Just to clarifiy, you are saying I can do this:

//[client/init.cs]
exec("~/scripts/aCoolObject.cs"); 

//[client/scripts/aCoolObject.cs]

new Datablock (ACoolObject)
{
     coolParamter = "Cool"; 
};

function ACoolObject::updateUI()
{
    echo("Cool!"); 
}

Thanks James.
#4
07/18/2008 (9:41 am)
Okay for some reason I have it stuck in my mind that you can only create datablock and object methods on the server. I will try to create them on the client and I'll prey it works. If it does work then I'll be back to loving Torque Script again!

Just to clarifiy, you are saying I can do this:

//[client/init.cs]
exec("~/scripts/aCoolObject.cs"); 

//[client/scripts/aCoolObject.cs]

new Datablock (ACoolObject)
{
     coolParamter = "Cool"; 
};

function ACoolObject::updateUI()
{
    echo("Cool!"); 
}

Thanks James.
#5
07/18/2008 (10:04 am)
No, I'm saying to just put THIS on the client side.

function DatablockName::updateUI()
{
   echo("Cool!");
}

You still want to allocate your datablocks on the server or they won't get ghosted to anyone.

Also this is invalid:
new Datablock( ACoolObject )
{
...
};

It should be...

datablock DatablockClass( DatablockName )
{
...
};

datablock is a keyword like new that should be used when you allocate datablocks or they will not be ghosted to all clients.
#6
07/18/2008 (10:11 am)
James, I don't think that will work because datablock names are not transmitted to clients. I tried it just for kicks and it didn't work. Can you confirm this?
#7
07/18/2008 (10:43 am)
Oh crap you are right...
#8
07/18/2008 (11:01 am)
Why not use a script object?

new ScriptObject(InterfaceController);
#9
07/18/2008 (12:07 pm)
I would update the networking code to transmit Datablock names.
#10
07/18/2008 (12:42 pm)
The problem Mark, is the namespace doesn't exist on the client since it hasn't been defined by the datablock. (So I misstated it above when I mentioned only that the datablock names themselves are not transmitted.) I don't know the nitty gritty details of how torque script gets compiled into byte code etc... so I wouldn't be able to add this functionality to torque script any time soon.

I noticed also in the RTSSK code that the GG employees themselves were not able to find an elegant solution like the one I presented above to this problem, and instead used the cluttering conditional approach which grows more and more unwieldy as you add new types of objects in your game.

P.S. If there ever there if there was an update to Torque Script, making methods declared for datablocks IN SCRIPT accessible to the client should be it. :)
#11
07/18/2008 (1:06 pm)
The datablock does not have to actually exist at the time you declare a method in the namespace of that datablock. I think if you did send the datablock name in its pack/unpack and set on the client side, you would be able to do this. When you set the name on the client side that becomes a part of the objects namespace and then when a method is called on the object it will search through the namespace and find the correct method at that time.

There could be a problem if you are running single-player though because then you would have the clientside and serverside datablock objects with the same name, (in fact I don't know if Torque will even allow this when you try to set the name on the client side object since one already exists with that name).

So unless you are sure to only do operations and pass around the object id itself, you might be accidently performing operations on the client-side datablock object when you meant to be operating on the server side object, or vice versa...

Couldn't you just add a field to the datablock like "clientCallbackNamespace" that is networked from server to client and then on the client side you would do...

%db.clientCallbackNamespace.methodName()

This way you can declare methods on either the server or the client for a datablock and there is definitely no collision between the two.
#12
07/18/2008 (1:59 pm)
I didn't realize it worked that way James but I think you are right. I will give this a try and report back.
#13
07/20/2008 (6:12 pm)
Well I just read the docs and I believe this:

The datablock does not have to actually exist at the time you declare a method in the namespace of that datablock. I think if you did send the datablock name in its pack/unpack and set on the client side, you would be able to do this. When you set the name on the client side that becomes a part of the objects namespace and then when a method is called on the object it will search through the namespace and find the correct method at that time.

is misstated.


If the datablock isn't created on the client, there exists no namespace, and thus the following:

function DataBlockName::methodName(%this)

declares a function named DataBlockName::methodName that takes one argument named %this.

I'll say it again. '::' has no intrinsic meaning. It can be used as part of a function name. The only time '::' scopes a method to a namespace is if that namespace already exists.

That is what I gathered from reading the docs anyways. So I'm back with having my little problem all over again.
#14
07/20/2008 (7:24 pm)
Quote:
Couldn't you just add a field to the datablock like "clientCallbackNamespace" that is networked from server to client and then on the client side you would do...

%db.clientCallbackNamespace.methodName()

I'm not sure exactly what you mean here, and I'm not sure how to update a datablock's name to the client. (Or if it is even possible.)

From what you are saying above, it sounds like you mean to add a string field to the datablock that is networked... unless there is a "TypeNamespaceName" that I don't know about.

I just don't see how this can work:

datablock (DB)
{
networkedField="MyNameSpace";
}

and then on the client

function MyNameSpace::f(%this) {}


It just can't be possible, even though I still don't exactly know what a namespace is, it just can't be possible lol. Even if I did something like:

new ScriptObject(MyNameSpace);
function MyNameSpace::f(%this) {}

on the client to ensure that "MyNameSpace" exists on the client, I doubt it would work because the "networkedField" is just a string.
#15
07/20/2008 (8:02 pm)
Actually it should work, but a mistake on my part, you would have to execute it with an eval or call, like...

eval ( %object.datablock.networkedField @ "::" @ "methodName();" );

Except you would probably want to also pass the datablock and object as arguments.

Or if this is something called from C++, it would be a bit simpler.

Con::executef( this->getDataBlock()->mClientCallbackNamespace, scriptThis() );

If you would like an example of how to add a new networked string to the Player class I can provide that.

I'm not sure if GameBaseData supports script class/superclass but if it doesn't its simply a matter of flipping the same flag ScriptObject does in its constructor, then maybe you could define your methods on both the client and server all in that namespace.

I don't know if if SimObject networks the script class/superclass or not, but you could add it if you needed. I think it would just be sending the string and then calling SimObject::setClassNamespace() on the client-side. The benefit of this would be no need for evals. Also I believe the method for setting an objects name is SimObject::setName()

By the way, you can use {quote}{/quote} to get a better format for quotations.
#16
07/21/2008 (2:11 pm)
James, I don't know if you are being creative with those function names or if they actually exist just not in my codebase...

Here is how I am setting the name of the datablock:

void RTSUnitData::packData(BitStream* stream)
{
	Parent::packData(stream);
	stream->writeString(this->getName(), 255);
}

void RTSUnitData::unpackData(BitStream* stream)
{
	char buff[256] = { 0 };
	Parent::unpackData(stream);
	stream->readString(buff);
	this->assignName(buff);
}

I still can't wrap my head around how to create the namespace on the client. (setClassNamespace() doesn't exist in my codebase). The eval solution isn't an option as I'd be better off just using switch statements in terms of readability. Thanks for all your help so far though James!

EDIT:

I spoke a little too soon. It appears you were correct when you said merely sending the datablocks name would work. The above code change makes it all work correctly meaning that the namespace is created on the client and the client-side datablock method is scoped correctly to that namespace.

Thanks man!
#17
07/21/2008 (2:49 pm)
Nice! Glad we finally got that worked out. Yeah, I was getting creative with those function names. I think setName is the name of the ConsoleMethod exposing AssignName to script. SetClassNamespace exists in, well, at least one of my codebases. Gah, working with too many difference versions of Torque at the same time...