Learning Torque & Building AI
by Ben Sparks - Warspawn · in Torque Game Engine · 08/27/2008 (12:13 am) · 4 replies
In the spirit of this thread Torque 101: Intro to Torque I have decided to post some of my progress in understanding Torquescript and developing AI units.
I want to post this because it will help solidify my own understanding of TorqueScript. I'll also disclaim this posting that I am learning as well, and everything I say may or may not be 100% accurate, I'll leave it up to those who know more than me to correct me. :)
I think that some of the reason that TorqueScript can be hard to understand (I'm just now starting to myself) is because it is hard to tell the difference between things that exist in the engine as c++ classes and "classes" that exist only in script, they often intermingle.
I will expand upon the dissection of an aiplayer which needs to include talk about the player. First, we have to talk about datablocks. A datablock is simply a set of parameters or variables that can be transmitted over the network. Perhaps the name of it is very fitting because all it is is a "block of data". The reason that it does anything to a Player is because PlayerData is a member variable of the Player C++ class. The datablock contains all of the details about the stats of a player. Like how much health it has, how fast it can run, etc. but it doesn't contain anything about actually DOING anything. The only methods that it even has are InitPersistFields (setup default values for the fields of data), PackUpdate & UnPackUpdate (send / recieve the values across the network). The datablock is the "blueprint" of how the player should be created. The Player class itself does all the work, it is the instance of the actual object that exists in the world. It has methods on how to move in 3D space and everything about the instance of it. For instance, the PlayerData would contain MAXHEALTH, but the Player would contain the CURRENTHEALTH. When you create a Player, it reads the datablock and sets up the default values of the Player object. In other words, you give it the blueprint, and then it builds it to spec. I'm not a car guy, so I'll use the analogy of a clock. A clock is an object that tells time. Clocks can come in many different shapes and sizes from a grandfather clock to a bedside alarm clock. They all tell time using our system of time. But one is in a big wooden box and one is in a small plastic case. They are both clocks, but the blueprints tell us how they should be built, the blueprints dont need to know how to tell time. (hope this makes sense so far)
I want to post this because it will help solidify my own understanding of TorqueScript. I'll also disclaim this posting that I am learning as well, and everything I say may or may not be 100% accurate, I'll leave it up to those who know more than me to correct me. :)
I think that some of the reason that TorqueScript can be hard to understand (I'm just now starting to myself) is because it is hard to tell the difference between things that exist in the engine as c++ classes and "classes" that exist only in script, they often intermingle.
I will expand upon the dissection of an aiplayer which needs to include talk about the player. First, we have to talk about datablocks. A datablock is simply a set of parameters or variables that can be transmitted over the network. Perhaps the name of it is very fitting because all it is is a "block of data". The reason that it does anything to a Player is because PlayerData is a member variable of the Player C++ class. The datablock contains all of the details about the stats of a player. Like how much health it has, how fast it can run, etc. but it doesn't contain anything about actually DOING anything. The only methods that it even has are InitPersistFields (setup default values for the fields of data), PackUpdate & UnPackUpdate (send / recieve the values across the network). The datablock is the "blueprint" of how the player should be created. The Player class itself does all the work, it is the instance of the actual object that exists in the world. It has methods on how to move in 3D space and everything about the instance of it. For instance, the PlayerData would contain MAXHEALTH, but the Player would contain the CURRENTHEALTH. When you create a Player, it reads the datablock and sets up the default values of the Player object. In other words, you give it the blueprint, and then it builds it to spec. I'm not a car guy, so I'll use the analogy of a clock. A clock is an object that tells time. Clocks can come in many different shapes and sizes from a grandfather clock to a bedside alarm clock. They all tell time using our system of time. But one is in a big wooden box and one is in a small plastic case. They are both clocks, but the blueprints tell us how they should be built, the blueprints dont need to know how to tell time. (hope this makes sense so far)
About the author
I'm a web developer by day, hobbyist game developer by night.
#2
Ok, now later in the player.cs file we have some functions on the Player class itself like:
Ok, now that we know some things about how Players operate, we can now begin to disect the aiPlayer code.
the first thing we see in aiPlayer.cs is:
Well, using the : like that is a form of script inheritance. So this is saying that we are creating another datablock, but this one is called DemoPlayer, its Parent however is PlayerBody. So this is why there is only one variable definition inside. DemoPlayer is the same thing as PlayerBody, so it retains all of the previous setup from the player.cs file where PlayerBody was defined. The shootingDelay varaible was not something that was needed by the main PlayerBody because it was designed with only human players in mind, not AI. Also note that shootingDelay is not a variable that is defined anywhere in c++, it is a dynamic custom variable. Just like we can add any function that we want, whether it is in c++ or not, we can also add any variable that we want. Telling which variables are defined in c++ and which are not is again somewhat difficult to do without looking through the c++ source code. Just like functions that are exposed to script, variables must also be exposed to script, this is done in the InitPersistFields function of the datablock in c++. In addition to creating new variables, and inheriting all the old variables, we can also overwrite any of the old variables. This is why if you don't want your AI units to look the same as the player, you would change the shapeFile here to a different .dts file.
Ok, so next we have some functions that appear to be callbacks on our datablock:
Any function that is setup on Armor can also be used by DemoPlayer. So all of the callbacks that are there, and even the custom functions will work just fine. Just like with the variables however, you can choose to override the functions. In this case they have chosen not to, however, you could easily make a DemoPlayer::onCollision function, and put specific functionality for when it happens to a DemoPlayer. It won't affect the Player, because inheritance only goes one way. A DemoPlayer = PlayerBody (Armor) but a PlayerBody != DemoPlayer.
08/27/2008 (12:15 am)
So there are a bunch of callbacks in there, and there are also some other functions that are NOT callbacks. I can't really think of a way to know which are which besides looking through the C++ code and seeing when a callback is being made. This is because you can create any function you want, it doesn't have to be declared in c++ anywhere for it to exist. It does however need to be called from something. The callbacks get called from C++ when certain events happen, however they could also be executed via script just like any other custom function. The tricky part is that if you create Armor::onTouchFireball(%this) you might expect that once the player touches your fireball object this would be automatically called. At least, you might think that if you are just looking at the script in order to figure out how things work. It won't ever be called, since the c++ doesn't ever call that function (unless you added it in). However, you could put something in the script onCollision function like (pseudocode):if(%col == fireball)
{
%this.onTouchFireball(%obj);
}Ok, now later in the player.cs file we have some functions on the Player class itself like:
function Player::kill(%this, %damageType)
{
%this.damage(0, %this.getPosition(), 10000, %damageType);
}This is a custom function that can be used on a Player class. When you are dealing with any C++ class there are usually several of it's internal methods exposed to script. Kill is not one of them, nor is damage for that matter. What IS exposed to script is by ShapeBase, and it is applyDamage. So what this, and damage is, are custom functions that can be called in script or in the console. For instance if you have a player, you choose it's instance and call the function. Let's say that you know the in game instance ID of the player: 6852, you can say 6852.kill("Fire"); and it will kill that player instance with the damage type of fire. Ok, so to fully understand this, we should trace back to the origin, when we hit the engine, we know we're at the base level. Here is where the damage function for the Player is defined:function ShapeBase::damage(%this, %sourceObject, %position, %damage, %damageType)
{
// All damage applied by one object to another should go through this
// method. This function is provided to allow objects some chance of
// overriding or processing damage values and types. As opposed to
// having weapons call ShapeBase::applyDamage directly.
// Damage is redirected to the datablock, this is standard proceedure
// for many built in callbacks.
%this.getDataBlock().damage(%this, %sourceObject, %position, %damage, %damageType);
}Ok, so looking at this function, we notice that applyDamage is still not being called. What this function is doing (from the comments and the code) is calling a damage function on the DATABLOCK of the item. The reason that in player.kill we are able to call %this.damage even though we never defined Player::damage is because of inheritance again. Just like the AIPlayer is derived from Player and retains all of its functions, the Player is derived from ShapeBase. Ok, also from the comments we see that they are calling it on the datablock because it is "standard procedure" (I still don't really know WHY it is standard procedure...). So, if that's the case, then we must have a function defined on the datablock, and there is:function Armor::damage(%this, %obj, %sourceObject, %position, %damage, %damageType)
{
if (%obj.getState() $= "Dead")
return;
%obj.applyDamage(%damage);
%location = "Body";
// Deal with client callbacks here because we don't have this
// information in the onDamage or onDisable methods
%client = %obj.client;
%sourceClient = %sourceObject ? %sourceObject.client : 0;
if (%obj.getState() $= "Dead")
%client.onDeath(%sourceObject, %sourceClient, %damageType, %location);
}You may have noticed it earlier when we were looking higher up in player.cs. So now we know that this function doesn't get called directly on the datablock (since we won't ever have a datablock running around in the world) instead, we call it on the player and it becomes a script callback, similar to the other callbacks, but the c++ isn't telling us to call it. And in this function we see the %obj.applyDamage function which is the one that was actually defined in c++.Ok, now that we know some things about how Players operate, we can now begin to disect the aiPlayer code.
the first thing we see in aiPlayer.cs is:
datablock PlayerData(DemoPlayer : PlayerBody)
{
shootingDelay = 2000;
};We know that an AIPlayer is a Player, and their datablocks are the same. So what is going on here?Well, using the : like that is a form of script inheritance. So this is saying that we are creating another datablock, but this one is called DemoPlayer, its Parent however is PlayerBody. So this is why there is only one variable definition inside. DemoPlayer is the same thing as PlayerBody, so it retains all of the previous setup from the player.cs file where PlayerBody was defined. The shootingDelay varaible was not something that was needed by the main PlayerBody because it was designed with only human players in mind, not AI. Also note that shootingDelay is not a variable that is defined anywhere in c++, it is a dynamic custom variable. Just like we can add any function that we want, whether it is in c++ or not, we can also add any variable that we want. Telling which variables are defined in c++ and which are not is again somewhat difficult to do without looking through the c++ source code. Just like functions that are exposed to script, variables must also be exposed to script, this is done in the InitPersistFields function of the datablock in c++. In addition to creating new variables, and inheriting all the old variables, we can also overwrite any of the old variables. This is why if you don't want your AI units to look the same as the player, you would change the shapeFile here to a different .dts file.
Ok, so next we have some functions that appear to be callbacks on our datablock:
function DemoPlayer::onReachDestination(%this,%obj) function DemoPlayer::onEndOfPath(%this,%obj,%path) function DemoPlayer::onEndSequence(%this,%obj,%slot)That is exactly what they are. Why didn't Armor have these functions if they are callbacks and we know that since Armor is a PlayerData datablock, and DemoPlayer is also a PlayerData datablock they should be the same thing? Well, here is the somewhat tricky part, you could have put these functions on Armor AND they would still work, BUT they would only work for AIPlayers. Why? Because the functions that were added to the AiPlayer c++ class actually make the calls for these callbacks. So, since it's just a function that is being called on the datablock of the object the AIPlayer would say: "ok call this function on my datablock" and since the DemoPlayer = PlayerBody = Armor, it would call them just fine. They are not included at the Armor level however just for good programming practice and code readability, since the Armor class is designed with human players in mind.
Any function that is setup on Armor can also be used by DemoPlayer. So all of the callbacks that are there, and even the custom functions will work just fine. Just like with the variables however, you can choose to override the functions. In this case they have chosen not to, however, you could easily make a DemoPlayer::onCollision function, and put specific functionality for when it happens to a DemoPlayer. It won't affect the Player, because inheritance only goes one way. A DemoPlayer = PlayerBody (Armor) but a PlayerBody != DemoPlayer.
#3
I'd like to however at this point go over the AIManager class. You find this at the bottom of the aiPlayer.cs and there are just a couple of functions: think & spawn. First, in order to understand the AIManager, we have to know what it is, because it isn't an AIPlayer or a Player (we never saw it getting derived from either) so where did it come from? It comes from game.cs in the StartGame function:
Finally, the other function of AIManager is spawn() :
Ok, now that we have all that background information, we should have enough knowledge to go through a real case where we want to create our own AI Agent. Also, on top of that, I'm going to change the AIPlayer and AIManager so that it is much less confusing and very easy to derive new agents from. Then once we understand that, we are going to change the engine so that we have a special AI spawning object that we can add to our missions. This will give us the ability to have different groups of AI agents PER mission. They can spawn in different areas, and they keep track of their agents so that for instance if there is no player near the spawner, it can stop spawning.
08/27/2008 (12:15 am)
Next we see a bunch of AIPlayer:: functions. These are all custom functions that we are defining in script. Again because AIPlayer = Player = ShapeBase all of the previously defined Player:: functions will work just fine on an AIPlayer (like kill). We don't need to re-write them unless we want different functionality than for a Player. There are also c++ methods that have been exposed to script that are specific to AIPlayer that you will see like setAimObject. We dont need to define them in script (just like applyDamage wasn't) because they have been defined in c++ and exposed to script. I'm going to gloss over all the AIPlayer:: functions for now because we should understand the order that things are called now, and I'll come back to it when I discuss creating a "new" AI Unit.I'd like to however at this point go over the AIManager class. You find this at the bottom of the aiPlayer.cs and there are just a couple of functions: think & spawn. First, in order to understand the AIManager, we have to know what it is, because it isn't an AIPlayer or a Player (we never saw it getting derived from either) so where did it come from? It comes from game.cs in the StartGame function:
// Start the AIManager
new ScriptObject(AIManager) {};
MissionCleanup.add(AIManager);
AIManager.think();So it's a ScriptObject. A script object is an object that gets instantiated in script, it doesn't exist in the game world. It doesn't have a datablock. It really just gives you an opportunity to create something in memory and define functions for it. So here they are actually creating an instance of a ScriptObject named AIManager. They add it to the mission cleanup, and then call the think() function on it. So back in aiPlayer.cs we have the following function:function AIManager::think(%this)
{
// We could hook into the player's onDestroyed state instead of
// having to "think", but thinking allows us to consider other
// things...
if (!isObject(%this.player))
%this.player = %this.spawn();
%this.schedule(500,think);
}It can be a little confusing because here we have defined a function for a class that is not defined in c++ and doesn't even technically exist at this point. They are creating the function based on the knowledge that they are going to create the instance of it in the game startup. If the AIManager is not created in the startGame (or really at just some point), then those functions will never be called and never do anything. There are no callbacks, so the function will still never be called unless you call it yourself, this is why after they create the AIManager they call the think function. What the think function is doing first is testing if %this.player is an object. Since all variables can be dynamically created, it will return false simply if it has never been set before. Just below that, they are setting that same variable to the result of the other AIManager function spawn() which returns a player object. Finally, the AIManager schedules itself to call its own think function again in 500ms. The player variable could be confusing, what they could have done is when they created the AIManager, done:new ScriptObject(AIManager) { player = 0; };This would still fail on the initial pass, and therefore cause it to set the variable to a new spawned agent object, but you would know where the variable came from. This seems to be however some good scripting programming practice, or perhaps even just some form of shorthand. The philosophy being that you dont need the variable until you need it. In this case the player variable is a single variable so it only holds one player object instance. When it tests if %this.player is not an object, we get NO on the first pass, and NO if the agent dies and his corpse fades away (fully deleted). The reschduling of think always happens, so no matter how many times you kill Kork, he will respawn.Finally, the other function of AIManager is spawn() :
function AIManager::spawn(%this)
{
%player = AIPlayer::spawnOnPath("Kork","MissionGroup/Paths/Path1");
if (isObject(%player))
{
%player.followPath("MissionGroup/Paths/Path1",-1);
%player.mountImage(CrossbowImage,0);
%player.setInventory(CrossbowAmmo,1000);
return %player;
}
else
return 0;
}I don't really agree with the way that this is laid out, but first I'll just explain what is going on. First, a local variable (local to this function) named %player is set to the result of AIPlayer::spawnOnPath. That function was defined earlier for AIPlayer, and it subsequently calls another spawn function, all of which eventually create a new instance of the AIPlayer c++ class and return a reference to it. It takes that reference, and then performs some actions on it before returning the reference to the AIManager so it can tell whether or not Kork needs to respawn.Ok, now that we have all that background information, we should have enough knowledge to go through a real case where we want to create our own AI Agent. Also, on top of that, I'm going to change the AIPlayer and AIManager so that it is much less confusing and very easy to derive new agents from. Then once we understand that, we are going to change the engine so that we have a special AI spawning object that we can add to our missions. This will give us the ability to have different groups of AI agents PER mission. They can spawn in different areas, and they keep track of their agents so that for instance if there is no player near the spawner, it can stop spawning.
#4
The function of the AIManager at least right now, is purely to handle the spawning of agents. Yet, if we look at the spawn code, it calls a function on AIPlayer. I don't understand why that would be, it makes it so the AIPlayer is capable of not only spawning itself, but other AI agents. While there is no technical reason that doesn't work (and it does work so...) it doesn't make sense to me that an agent should be able to spawn other agents. So, I'm going to move those functions to AIManager. I'm also going to move all of the AIManager code to its own aiManager.cs file. I think that it helps to understand the difference between things when they aren't lumped together.
So I created the new file, and just cut the 2 AIManager functions out, and also cut out the 2 AIPlayer spawn functions and pasted them in. The functions from the AIPlayer will need to be changed:
The AIManager::spawn function called AIPlayer::spawnOnPath , so we need to change that to AIManager::spawnOnPath. It was also listed as a "static" function. I'm not really sure what that means Torquescript wise, but as far as C# or C++(I think) that basically means that the function can be called without having an instance of the class created. I guess that is why it didn't have a %this variable, and it had to be called using AIPlayer::spawnOnPath instead of %this.spawnOnPath. I suppose that would be useful if you are thinking of the AIPlayer as sort of a library instead of as an instance of an agent. So instead, I'm making it a non-static function of the AIManager. (Not sure if there is some sort of technical reason that static functions are good/bad).
Down inside the function itself we see that it called the other static function of AIPlayer, spawn. But we can't keep it that way because the AIManager already has a spawn function. So instead I see that it takes the name of the agent you want to spawn, and a point to spawn at, so I've renamed it spawnAt.
The AIManager::think function doesn't need to change yet, and we just need to change the spawn function so that it uses %this instead of AIPlayer:: since now we have that function in the AIManager.
So at this point here is what I have for aiManager.cs:
Testing this so far, I've separated out aiManager and Kork is still running around like normal. Now the next thing that I want to do is have more than one Kork. So the easiest way to do that right now is just open up the console and try it. I know that when the game starts, an instance of AiManager (scriptObject) is being created. So I just type in aiManager.spawnOnPath("Bob","MissionGroup/Paths/Path1"); and yep, I have another one. I also notice that when I kill Bob he doesn't respawn, but Kork does. This is because the think function is running and only executes the spawn function, which only keeps track of Kork.
Ok, great, now the next thing that I want to do is create a second ai unit that is not a Kork model. I have a wolf, so I want to see those running around, but I dont want to get rid of Kork, so time to get that setup.
08/27/2008 (1:09 am)
When I looked at AIManager and found that it has a think function, and a spawn function. I said that I didnt agree with the way AIManager was laid out, so this is the first thing I want to change to make things easier to understand and build from.The function of the AIManager at least right now, is purely to handle the spawning of agents. Yet, if we look at the spawn code, it calls a function on AIPlayer. I don't understand why that would be, it makes it so the AIPlayer is capable of not only spawning itself, but other AI agents. While there is no technical reason that doesn't work (and it does work so...) it doesn't make sense to me that an agent should be able to spawn other agents. So, I'm going to move those functions to AIManager. I'm also going to move all of the AIManager code to its own aiManager.cs file. I think that it helps to understand the difference between things when they aren't lumped together.
So I created the new file, and just cut the 2 AIManager functions out, and also cut out the 2 AIPlayer spawn functions and pasted them in. The functions from the AIPlayer will need to be changed:
// spawn an agent on the specified path, with the specified name
function AIManager::spawnOnPath(%this, %name, %path)
{
// Spawn a player and place him on the first node of the path
if (!isObject(%path))
return 0;
%node = %path.getObject(0);
%player = %this.spawnAt(%name, %node.getTransform());
return %player;
}The AIManager::spawn function called AIPlayer::spawnOnPath , so we need to change that to AIManager::spawnOnPath. It was also listed as a "static" function. I'm not really sure what that means Torquescript wise, but as far as C# or C++(I think) that basically means that the function can be called without having an instance of the class created. I guess that is why it didn't have a %this variable, and it had to be called using AIPlayer::spawnOnPath instead of %this.spawnOnPath. I suppose that would be useful if you are thinking of the AIPlayer as sort of a library instead of as an instance of an agent. So instead, I'm making it a non-static function of the AIManager. (Not sure if there is some sort of technical reason that static functions are good/bad).
Down inside the function itself we see that it called the other static function of AIPlayer, spawn. But we can't keep it that way because the AIManager already has a spawn function. So instead I see that it takes the name of the agent you want to spawn, and a point to spawn at, so I've renamed it spawnAt.
// spawn an agent at the specified point with the specified name
function AIManager::spawnAt(%this, %name, %spawnPoint)
{
// Create the demo player object
%player = new AiPlayer() {
dataBlock = DemoPlayer;
path = "";
};
MissionCleanup.add(%player);
%player.setShapeName(%name);
%player.setTransform(%spawnPoint);
return %player;
}Again I've decided to make it a non-static function of AIManager and added in the %this so we can have access to the AIManager itself if we need it. The AIManager::think function doesn't need to change yet, and we just need to change the spawn function so that it uses %this instead of AIPlayer:: since now we have that function in the AIManager.
So at this point here is what I have for aiManager.cs:
function AIManager::think(%this)
{
// We could hook into the player's onDestroyed state instead of
// having to "think", but thinking allows us to consider other
// things...
if (!isObject(%this.player))
%this.player = %this.spawn();
%this.schedule(500,think);
}
// this will serve as a default spawn function
function AIManager::spawn(%this)
{
%player = %this.spawnOnPath("Kork","MissionGroup/Paths/Path1");
if (isObject(%player))
{
%player.followPath("MissionGroup/Paths/Path1",-1);
%player.mountImage(CrossbowImage,0);
%player.setInventory(CrossbowAmmo,1000);
return %player;
}
else
return 0;
}
// spawn an agent at the specified point with the specified name
function AIManager::spawnAt(%this, %name, %spawnPoint)
{
// Create the demo player object
%player = new AiPlayer() {
dataBlock = DemoPlayer;
path = "";
};
MissionCleanup.add(%player);
%player.setShapeName(%name);
%player.setTransform(%spawnPoint);
return %player;
}
// spawn an agent on the specified path, with the specified name
function AIManager::spawnOnPath(%this, %name, %path)
{
// Spawn a player and place him on the first node of the path
if (!isObject(%path))
return 0;
%node = %path.getObject(0);
%player = %this.spawnAt(%name, %node.getTransform());
return %player;
}Since I have a separate file, I do need to make sure that it is executed somewhere in the chain. For now, I'm going to put theexec("./aiManager.cs");at the top of aiPlayer.cs since I know that it is already being loaded (in game.cs).Testing this so far, I've separated out aiManager and Kork is still running around like normal. Now the next thing that I want to do is have more than one Kork. So the easiest way to do that right now is just open up the console and try it. I know that when the game starts, an instance of AiManager (scriptObject) is being created. So I just type in aiManager.spawnOnPath("Bob","MissionGroup/Paths/Path1"); and yep, I have another one. I also notice that when I kill Bob he doesn't respawn, but Kork does. This is because the think function is running and only executes the spawn function, which only keeps track of Kork.
Ok, great, now the next thing that I want to do is create a second ai unit that is not a Kork model. I have a wolf, so I want to see those running around, but I dont want to get rid of Kork, so time to get that setup.
Torque Owner Ben Sparks - Warspawn
StarOrdered Games
Ok, so now that we understand somewhat how these things relate to each other in the engine, lets look at how the scripting works. Again, we start with Player.cs because whether we are talking about Players or AI they are the same thing. I'm going to skip all the various audio profiles and particle data profiles because they are blueprints for other things. (Also, I have TGEA + AFX, but I'm going to use TGE152 as the example because it is sort of the "default" implementation).
First we have:
datablock PlayerData(PlayerBody) { renderFirstPerson = false; emap = true; className = Armor; shapeFile = "~/data/shapes/player/player.dts"; ....Here we are defining the blueprint for the PlayerData datablock class. In the parens they've given the blueprint a name of PlayerBody. Also, inside they've set classname to Armor. The rest of the information isn't too important for explanation, it is just defining all of the attributes (default values) that will be used when creating a new player. Now here's where the scripting can get a little confusing because you can reference C++ classes directly in script, and you can also just type whatever you want to create things that only exist in script, you can also sort of rename any c++ class to be something else. I'm not sure why they used the word "Armor" I imagine it's someting from back in Tribes, but this is going to extend the PlayerData class. I believe that if you wanted to, instead of using Armor you could use PlayerData or in this case you could also use PlayerBody.
Now remembering that a Datablock doesn't actually DO anything (it doesn't really have any methods), we start to wonder, why do we have functions like Armor::onAdd(%this, %obj), Armor::onCollision(%this, %obj, %col) or Armor::onDamage(%this, %obj, %delta) ? Well, this is because all of those things happen in the Player class, but inside the functions of the player class, a callback is made to the script. The callback is applied to the datablock however (not 100% sure why callbacks go on the datablock, but they do). So for instance, when the Player instance hits something it says (in addition to whatever it does internally): "ok, we've hit something, now call the onCollision method of the datablock, and send it the object that we've collided with". In this case it actually happens in ShapeBase because that is what Player is derived from but just like how the AIPlayer is still a Player, the Player is still just a ShapeBase. Here is the C++ call:
Ok, so what this is saying is we're going to execute this script function on mDataBlock, which is the PlayerData reference on the player. We're going to execute a function called "onCollision". We're going to send the following parameters: scriptThis() (since we are in the Player at the time, this is a reference to the Player instance we are dealing with), object->scriptThis() (this is the object that we are colliding with), buff1 (this is the x y z of a vector of the collsion), buff2 (this is the length of said vector). If you look at the entire ShapeBase::onCollision call you will see those other variables. So, since our Datablock has got a className of Armor, we would form this in script as:
But wait, we dont have that, we have:
All parameters (besides the first) are optional in Torque Script. We also have the addition of the first parameter, %this, which is a reference to the object that the function is being called on. So in this case it is the Armor class or the Datablock itself. Also, the parameters can be named anything, even the %this it doesn't matter what you name them. The reason that they are named the way they are named is for good programming practice to name your variables something meaninful so that a human can read it. Even the %this can be renamed, %this is used because it resembles how you do the same thing in C++. When you are inside a class, this provides a reference to the instance of the class you are working with.