Game Development Community

Scripting a simple "switch" to learn TS

by Justin Woodman · in General Discussion · 12/07/2009 (3:52 pm) · 19 replies

I'm attempting to script a simple "switch" (an object which, when clicked on, performs a function).

So far, I've added a "switches" datablock to /art/datablocks:
datablock StaticShapeData(RockSwitch)
{
   category = "Switches";
   className = "BasicSwitch";

   // Basic Item properties
   shapeFile = "art/shapes/rocks/rock1.dts";
};

I've added a "switches" script to /server/scripts (containing the "activate" function of the class "BasicSwitch"):
//When the player interacts with the switch...
function BasicSwitch::Activate(%this, %obj)
{	
	//Change the switch visibly
	%obj.delete();
	
	//Display switch activation in console
	echo("Switch activated");
}

And then in "PlayGui.cs", I've added a method of calling the "Activate" function (by clicking on the object with the mouse):
function PlayGui::onMouseDown(%this, %pos, %start, %ray)
{   
   %ray = VectorScale(%ray, 1000);
   %end = VectorAdd(%start, %ray);
   
   // find target tag...
   %scanTarg = ContainerRayCast( %start, %end, 1, %this ); 
   echo(%scanTarg);
   
   %scanTarg.Activate();
}

needless to say, it doesn't work. Instead, I get the warning:
scripts/gui/playGui.cs (74): Unknown command activate.
Object (3228) StaticShape -> ShapeBase -> GameBase -> SceneObject -> NetObject -> SimObject

which I don't understand. I've tried to read through the scripting tutorials, but I learn "hands on" and am still quite confused here. I guess I am missing something about the way datablocks and classes work, but I can't seem to find it.
If somebody wouldn't mind sort of explaining what I'm doing wrong, and how this type of thing should be used, I would be quite thankful.

#1
12/07/2009 (5:34 pm)
Assuming that the object (3228) in question is indeed your Switch object, it looks like the only problem is that you have assigned the Activate function to your shape's DataBlock, and you are attempting to call Activate() on the shape object instance. DataBlocks, you see, are special objects containing data common to multiple object instances. They are not themselves the actual objects which you see in the game. There is nothing wrong with assigning the Activate function to a DataBlock, but you will need to call it like this:

%scanTarg.getDataBlock().Activate(%scanTarg);
#2
12/07/2009 (6:47 pm)
Okay, I get it. So is the datablock the best place to assign the "Activate" function, assuming there will be many of these throughout the game world?

I suppose I am having trouble with the concept of a datablock. In my code, I said "ClassName = BasicSwitch". I thought this meant that I have created a new class called "BasicSwitch". Therefore, the "Activate" function is part of the [u]class[/u] "BasicSwitch". So if a datablock contains data about an object, then is the function "Activate" considered part of that data? I never considered a function "data"...
#3
12/07/2009 (7:55 pm)
Well, a datablock is still a separate object, it's just not the object you see in your game. The idea is that you define one datablock, then you can create multiple objects that each get references to that same datablock. This way variables (and functions too) can be associated with multiple objects through the shared datablock, instead of having to define the same variable multiple times for each object.

TorqueScript doesn't actually care whether an object is a datablock or not (as far as defining functions goes). TorqueScript will let you create a function in any namespace, which includes class names (like StaticShapeData) and instance names (like your RockSwitch). Furthermore the variable "className" allows you to arbitrarily assign yet another namespace identifier to a given object. Using that approach, you could in theory create two different objects of different classes and assign them the same className thus allowing both of them to share a common function defined with that name.

As an example, in your code above RockSwitch is an instance of StaticShapeData assigned a className "BasicSwitch". Torque will check each of those names when looking for a function/variable associated with that object. So if you were to call RockSwitch.Activate(), Torque would look for
function RockSwitch::Activate()
or
function StaticShapeData::Activate()
or
function BasicSwitch::Activate()

Any of those would work. Which way you choose to set it up is up to you, and it depends on how you want to organize your objects and functions. Do you want Activate() defined for ALL objects of class StaticShapeData? Or for ONLY the one called RockSwitch? Or do you want to make it more flexible by defining Activate() for BasicSwitch, which is a name you can assign to whatever object you want? They will all work.

Now, like I said before, TorqueScript technically doesn't care whether an object is a datblock or not when it comes to defining functions. In the above examples, Activate() was defined multiple ways for an object that was a datablock. Nothing says you HAVE to use a datablock for defining functions. You could use
function StaticShape::Activate()
instead, if you wanted to. You probably don't though, because then Activate() is defined for ALL of your StaticShapes, whether they be switches or oil drums or chairs. That doesn't make a great deal of sense from a code-organization standpoint. Alternately, you could give any one of your StaticShape objects its own name. Say you have a switch (which is an instance of StaticShape class that's using your RockSwitch datablock) and you give it a name "BravoOutpostMasterPowerSwitch". You could define a
function BravoOutPostMasterPowerSwitch::Activate()
which would then be valid ONLY for that ONE switch object. That approach could have its use. But it's more limited. If you then decide you want a CharlieOutpost with a similar switch, you'll need to define the same function again for your CharlieOutpostMasterPowerSwitch. Not very efficient if both functions are the same.

That's where the datablocks come in. By defining Activate() for the RockSwitch datablock and setting your scripts up to call the function on the datablock, you get an automatic association between the image you see in-game and the Activate() function. In the editor now you just say "give me a new StaticShape of type RockSwitch" and you get a rock1.dts switch already linked to Activate(). Better still is the way you have it now where the function is defined through the className. That way if you decide you want a "rock2.dts" switch, you can create a second datablock using that dts and the same className "BasicSwitch". Now both RockSwitch and RockSwitch2 are associated with the same Activate() function.

In the end, you can set it up however you want. It's just a matter of organization.

Quote:So is the datablock the best place to assign the "Activate" function, assuming there will be many of these throughout the game world?

Essentially, yes. In this case that's probably best.

[edit: a few small additions]
#4
12/07/2009 (9:41 pm)
Thank you for being so patient!

I've read through every piece of documentation intended to help understand these concepts, and I finally get it!
Hopefully this thread will help more than just me in the future. I'm sure I'll be asking more questions, but this is a great start.
#5
12/08/2009 (12:57 am)
Alright, so the next step in this might be to allow the "switch" to call a function, much like a GuiButtonControl does (with it's "command" field).

This got me thinking, would it be better to make a general "ClickableObject" datablock with an "onClick" and "onDoubleClick" functions and a "command" field?

regardless, I have very little idea on how I would create such a field (never mind making it accessible through the editor).

Just thoughts. If anyone has input I'd love to hear it!
#6
12/08/2009 (1:37 am)
Quote:regardless, I have very little idea on how I would create such a field (never mind making it accessible through the editor).
You can override the onAdd method in your BasicSwitch namespace to add fields to objects. I think it could look something like this:
function BasicSwitch::onAdd(%this,%obj)
{
   //%this: the datablock
   //%obj: the object instance
   //Initialise this so it's visible in the editor
   %obj.command = "myFunction();";
}
onAdd is called whenever an object is created with a specific datablock. Now you should be able to create a StaticShape with the BasicSwitch datablock, select it in the editor, and edit its 'command' field.

EDIT: Note that you don't really need to do that. If you go into the editor and select any object, you can add 'dynamic fields' to it, which are basically properties like %obj.command or %obj.mySpecialThing. But by initialising it in onAdd, I think it will be visible in the editor when the object is selected - so you don't have to manually add a new field for all your objects of that type, just edit the field that's already been created. If that makes sense.
#7
12/08/2009 (2:02 pm)
So when I add a field to an object, how do I access it form another function in that objects datablock? aka. how would I "eval" that function from "BasicSwitch::Activate"?
#8
12/08/2009 (6:45 pm)
Alright, so adding a field like "%obj.command" makes it a member variable, so it can be accessed from any other "function BasicSwitch::Whatever(%this, %obj)", right?

I ask because I'm trying to do this:
function BasicSwitch::onAdd(%this, %obj)  
{
	%obj.mTestVar = "Hello";
}

//When the player interacts with the switch...
function BasicSwitch::Activate(%this, %obj)
{	
	//Display switch activation in console
	echo("Switch activated");
	echo(%obj.mTestVar);
}
However, when "BasicSwitch::Activate" gets called, the output is:
3228 2.37588 5.47508 241.46 0.316197 -0.948693 0
Switch activated
(where "Hello" should be, there is a blank space).
#9
12/08/2009 (7:21 pm)
Curious. echo(%obj.getId()); in onAdd and Activate. Ideally you should get an echo from both indicating the same id.
#10
12/08/2009 (7:32 pm)
Both echo the same ID: 3228.
#11
12/08/2009 (8:02 pm)
Well. That should work then. How very strange. Try echo(%obj.mTestVar) in onAdd? Also perhaps try echo(3228.mTestVar) in various locations. Or 3228.dump()

(keep in mind that id number may change if you modify your mission)
#12
12/08/2009 (8:09 pm)
Okay, I placed these echoes in "onAdd" and "Activate":
echo(%obj.getId());
echo(%obj.mTestVar);
echo(3228.mTestVar);

from "onAdd" I get:
3228
Hello
Hello

but from "Activate" I get:
3228

Hello

edit: When I do a 3228.dump(), mTestVar is listed under "Tagged Fields", not "Menber Fields"... But I don't know if that is relevant or not.

edit again: What does work, is using "echo(%obj.getId().mTestVar);" but it doesn't seem like I should have to do that :/
#13
12/08/2009 (9:03 pm)
o_O

You shouldn't have to do that, no. That is strange. I've never seen such behavior from the script engine.

Couple questions: What version of Torque are you working with?

And what do you get if you just echo(%obj) in both functions?
#14
12/08/2009 (9:12 pm)
I am using T3D v.1.0.1

And I think I see the problem now that you had me do that (but I still have no idea how to fix it...)

I added "echo(%obj)" to both functions, and from "onAdd" I get:
3228

but from "Activate" I get:
3228 0.258232 4.77524 241.512 -0.148013 -0.988985 0

all I know is that they don't match :\
#15
12/08/2009 (9:15 pm)
Ah, but of course.

You answered the question I was just going to ask.

ContainerRayCast returns a collection of values; the first of which is the hit object id. The rest of the values are the hit position and normal. That explains why the script engine is getting confused.

I should have seen that from the start. Where you call Activate, instead of

%scanTarg.getDataBlock().Activate(%scanTarg);

what you need to do is isolate the object id like this:

%obj = firstWord(%scanTarg);
%obj.getDataBlock().Activate(%obj);

(edit: wait, I think it's just "firstWord". fixed now.)

(also, you could use getWord(%scanTarg, 0) where 0 is the word number 0,1,2,3,etc. It's a zero index identifier)
#16
12/08/2009 (10:25 pm)
Okay, so when I "passed" the %scanTag value to BasicSwitch.Activate, every time it used %obj, it used the tag+coordinates to reference the object?

I'm glad this came about, it help me understand the way %obj works and how it's used and passed around. And I guess I recall that ContainerRayCast returns the coordinates of the object detected, but why? Couldn't you just do something like getLocation(%obj) to get that after? it almost seems combersome to have ContainerRayCast return anything other than the tag...

But anyways, Thanks again for the help! I don't know how people just pick up TS without a ridiculous amount of background knowledge of the engine.
#17
12/08/2009 (10:44 pm)
Quote:so when I "passed" the %scanTag value to BasicSwitch.Activate, every time it used %obj, it used the tag+coordinates to reference the object?

Yes, and that's why it was getting confused.

Quote:Couldn't you just do something like getLocation(%obj) to get that after?

No. Because that would get you the location of the object that was hit. What ContainerRayCast is giving you is the location of where the hit occurred (as well as the normal of the face that was hit). Consider that the location of an object is based on that object's local "origin" point. The 0,0,0 point of the object's local coordinate space. This is normally near the center or base of the object. That is not necessarily where the ray hits the object. A Player object, for example, may have a "location" that is by its feet, near to the ground. If the ray hits the Player in the head, the hit point is some distance from its feet.
#18
12/08/2009 (10:59 pm)
Ohhhh I see. So I imagine this would be useful in the case where you wanted to shoot at a water tank, and have water spray out from the point of impact, away from the direction of impact?

If that's the case, it seems like ShapeBase would have an "onMouseDown" function, for simple detection of mouseclicks on the object (for all I know it does...).
#19
12/09/2009 (12:35 am)
Quote:So I imagine this would be useful in the case where you wanted to shoot at a water tank, and have water spray out from the point of impact, away from the direction of impact?

Exactly right.

As far as an onMouseDown function goes, I could be wrong but I don't think such a function exists for 3d objects. Reason being that the 3d variants of Torque (flexible though they may be) are designed primarily for First Person Shooter style games where the only interaction you have with objects in the world is either shooting them or running into them, not generally clicking on them. I'm not very familiar with TGEA/T3D, but I rather doubt they're much different from TGE in that regard. The editor interface does implement onClick events for 3d objects, but those events are not accessible outside of the editor interface. Still, as you've discovered it is possible to add such a function through script.