Torque 2D/GenreTutorials/StrategyUnitCreation

From TDN

This page is a Work In Progress.

Contents

Setting up a Unit Creation System and Moving New Units


Introduction



In this tutorial section we will go over how to script a unit creation system and then setting up right click movement of these new units. Throughout this tutorial you may learn a few things you haven't worked in before with Torque Game Builder, or Torque Script in general. We will be heavily using ScriptObjects as well as utlizing their inheritance-like class system. This will enable us to create a robust, efficient, and easily extendable/modifiable unit creation system for our game. Move the units after they have been created is probably the easiest part, since we already have the selection system done from our last tutorial, though we will throw a bit of randomness into this aspect as well (since we aren't covering pathfinding).





We will establish an easy system to "spawn" a "unit" at a specified location



In this step we are going to create the system to spawn a unit at a position. To accomplish this we are going to need to create some makeshift object classes. These basically will be a stream of ScriptObjects that will centrally hold the data for our "classes." These classes will be the units we have in our game, as well as the base building.



Where do we start?


Adding new GUI Profiles

We're going to start by adding a few new GUI profiles. Create a new file (gameScripts/customProfiles.cs). Now add these to the end of it and click save.

if(!isObject(GuiButtonSelectionProfile)) new GuiControlProfile (GuiButtonSelectionProfile : GuiButtonProfile)
{
   fontSize = 13;
};
if(!isObject(GuiLabelCirclesTextProfile)) new GuiControlProfile (GuiLabelCirclesTextProfile : GuiButtonProfile)
{
   justify = "center";
   fontSize = 12;
   fontColor = "255 255 255";
   modal = false;
};
if(!isObject(GuiLabelSquaresTextProfile)) new GuiControlProfile (GuiLabelSquaresTextProfile : GuiButtonProfile)
{
   justify = "center";
   fontSize = 12;
   fontColor = "0 255 0";
   modal = false;
};
if(!isObject(GuiDefaultNoSelectProfile)) new GuiControlProfile (GuiDefaultNoSelectProfile : GuiDefaultProfile)
{
   modal = false;
};
if(!isObject(GuiDefaultNoSelectNoBorderProfile)) new GuiControlProfile (GuiDefaultNoSelectNoBorderProfile : GuiDefaultProfile)
{
   border = 0;
   modal = false;
};



We will use these to reduce our text size of our objects, change their color, as well as preventing the attached labels on our objects to interfere with our selection.

Exec the file in main.cs at the very start of initProject.

New "data" folder and two new script files

To start this out we are going to create a new folder in our "Strategy/gameScripts/scripts" called data. This will hold all our data related script files.

Now create two script files within this folder called "data.cs" and "objectTypes.cs." The objectTypes.cs will hold all of our object types, that way its easy to access and modify these, while the data.cs will hold our functionality.

Exec our two new script files

Our next step will be to exec() our two new script files, that way we don't forget later, so add these lines to your "Strategy/main.cs."

exec("./gameScripts/scripts/data/data.cs");
exec("./gameScripts/scripts/data/objectTypes.cs");


Now we won't have to worry about exec'ing our files later.

Scripting our data.cs and objectTypes.cs


Start creating our object types

Now that we have our files, lets insert some script into them. Lets start out with the objectTypes.cs. This way we can configure some base "classes" to be referenced upon unit creation.

First insert this function frame into the script file.

// --------------------------------------------------------------------
// initObjectTypes()
//
// This function will initialize all the object types
// --------------------------------------------------------------------
function initObjectTypes()
{

}


Now its time to create some object classes, first we will start with a class called "Entity" that will serve as our base class. In this case the other classes will more or less inherit from "Entity" so we can put all the values into Entity that we want in every object (we will use a different base class for our bases). So add this.

   // lets create the base unit object called Entity
   new t2dSceneObjectDatablock(Entity){
      imageMap = tileMapImageMap;
      isSelectable = true; 
      speed = 10;
      creationTime = 2;
      hitPointsMax = 100;
      hitPointBarScaleFactor = 3;
   };

Here we create a new t2dSceneObjectDatablock called "Entity" and attach three values to it, imageMap for a default imageMap, isSelectable so our selection system will work on this entity, name so we have a default name, speed so we have a default speed reference for later, creationTime which will be used in the creation delay time, hitPointsMax which will be referenced for the life of the unit when spawned, and the factor to scale the hit points bar we will attach to each object.

Now lets add two more ScriptObject classes that will more or less inherit from Entity. So add this.

   // now lets create our second level of classes, Worker and Soldier
   new t2dSceneObjectDatablock(Worker : Entity){
      superClass = Entity;
      size = "5 5";
      speed = 12;
      hitPointsMax = 60;
   };
   new t2dSceneObjectDatablock(Soldier : Entity){
      superClass = Entity;
      size = "5 5";
      creationTime = 4;
   };

Here we create two new t2dSceneObjectDatablocks, though we do something different in the naming area. We put colon Entity at the end of both names. What this does is it will make this new t2dSceneObjectDatablock initially a copy of Entity, it will copy all the values we set to Entity, that way the default values get copied over, though the properties we define while creating these copies will overide the ones copied from Entity.

Another thing we do a bit different is define a "superClass" value. Every script object can have a class and superClass property, what these mean is the object will share namespaces with the objects inserted into that value. This purely refers to functions. So if you attach a function to Entity, Worker and Soldier will both have access to them. The reason I put Entity in "superClass" and not "class" is because we will do even a third layer of objects that will copy all the values of Worker and Soldier. So it will copy over the superClass value, that way I can specify Worker and/or Soldier in the new object's class value resulting in superClass and class being set properly.

The only new value set in these objects is size, this will be used in the object's creation to size it.

Now lets add the new objects in what you could consider the third layer. So add this.

   // lets create our third layer of classes, the team workers and soldiers
   new t2dSceneObjectDatablock(CirclesWorker : Worker){
      class = Worker;
      team = "Circles";
   };
   new t2dSceneObjectDatablock(SquaresWorker : Worker){
      class = Worker;
      team = "Squares";
   };
   new t2dSceneObjectDatablock(CirclesSoldier : Soldier){
      class = Soldier;
      team = "Circles";
   };
   new t2dSceneObjectDatablock(SquaresSoldier : Soldier){
      class = Soldier;
      team = "Squares";
   };

The teams we will used in this tutorial are Circles and Squares. Whether or not we keep that theme in visuals it is an easy to remember comparisson. The first wto are team worker objects, as you can see they copy all the values of Worker so we don't need to specify those, the only new thing we add is class equal Worker that way we can attach functions to Worker or Entity (since the superClass equal Entity is copied over) and then can use them off of these objects. We then create the two team soldier objects in the same manner.

Scripting our initData() function

Now that we have some base classes we can save and close our objectTypes.cs. Now open up your data.cs and we'll add an init function to start it out.

// --------------------------------------------------------------------
// initData()
//
// This funciton will initialize all the base data structures for our game
// --------------------------------------------------------------------
function initData()
{
   $playerTeam = Circles;

   // lets initialize our data structure
   new ScriptObject(gameData);

   // lets initialize our team objects
   gameData.circles = new t2dSceneObject(Circles);
   gameData.squares = new t2dSceneObject(Squares);

   //Layer Sets
   $collision::playerLayer = 5;
   $collision::mapLayer = 30;
   $collision::baseLayer = 29;
   
   //Group Sets
   $collision::playerGroup = 5;
   $collision::mapGroup = 10; // Remember we set this earlier?
   $collision::baseGroup = 15;
}

First we set a global variable called $playerTeam to Circles. Now later when we get a GUI up and when the player can select a team this will be set there, for now we set it here... then we create a new ScriptObject called gameData. Then we create two new ScriptObjects attached to GameData called Circles and Squares.

After we create all of our ScriptObject data structures we then set a few global variables for layers and groups. These will be used when we spawn units. Notice we also define groups and layers for our map, we can go back and change our map scripts to use this value.

Adding setSelectionName() utility function

Before we get back to our data.cs and our spawning function, we are going to add one utility function, open your "Strategy/gameScripts/scripts/game/selection.cs" and add this function after getSelectionName() (should be the second function).

// --------------------------------------------------------------------
// t2dSceneObject::setSelectionName()
//
// This function sets the "selectionName"
// --------------------------------------------------------------------
function t2dSceneObject::setSelectionName(%this, %name)
{
   %this.name = %name;
}

As you can see we are simply adding a setSelectionName() function to match the getSelectionName.

Save and close, now we can get back to our data.cs and spawning function.

Our spawnObject() function

Now that we have initialized our data properly we want to add our functionality for creating objects, to finish this step we add our spawning function, then in our next step we will set up a system to use some of the values we set on the object types to delay the creation based on it.

So lets add this empty function frame.

// --------------------------------------------------------------------
// gameData::spawnObject()
//
// This funciton will spawn our object if we pass the correct values:
// %type = a class object
// %team = a team object
// %pos = a spawning position
// %name = a name to set the object to
// --------------------------------------------------------------------
function gameData::spawnObject(%this, %type, %team, %pos, %name)
{

}


Notice this object is attached to our gameData object. That is another strength of ScriptObjects, they actuallly are quite useful, we can store data on them, inherit functions and data, as well as set functions attached to the object. To call this function we would type gameData.spawnObject() passing the appropriate values.

We pass in a lot of values to this function (I explain them individually in the comments). So lets flesh out this function by adding.

   // lets piece together our call to create a new t2dStaticSprite
   %eval = "%obj = new t2dStaticSprite(" @ %name @ ") { config = " @ %type @ "; sceneGraph = $strategyScene; };";
   eval(%eval);

In this line we peice together a string to be run in eval() as a call. The eval() function allows us to put together a function in a string and process it. So if %name was "BoB" and %type was "CirclesSoldier" this function would execute like this.

//DO NOT ADD THIS CODE, JUST AN EXAMPLE
%obj = new t2dStaticSprite(BoB : CirclesSoldier) { sceneGraph = $strategyScene; };

So as you can see this eval() simply executes a t2dStaticSprite being created.

Now add this large chunk of code.

   // set the position of the object
   %obj.setPosition(%pos);

   // set the team of the object, using getId() to get the team object's
   // internal ID
   %obj.team = %team.getId();

   // set the objects type
   %obj.type = %type;

   // set the objects name
   %obj.name = %name;

   // set the objects name
   %obj.setSelectionName(%type.getName());

In this long line of script we do numerous things, mose of this is what we've done in the past though. We set the imageMap equal to the imageMap value stored on the %type (remember the objectTypes.cs and how we attached all these values).
We then set the size, position, team Id, type, name, and hitPoints to the stored value on the object type.

Now add this.

//
// Entity::onLevelLoaded, this function is like an init function, but for the entity class. (%obj is the object before the dot.)
// E.G. %obj = $dave where $dave is an Entity and you do $dave.onAdd();
//
function Entity::onLevelLoaded(%obj, %scene)
{
   // set the object hit points to the max hit points
   %obj.hitPoints = %obj.hitPointsMax;

   // create a label name using the name plus the string "Label"
   %labelName = %obj.name @ "Label";
   // create the label object name
   %labelObjName = %labelName @ "Obj";
   // create the label object
   %labelObj = new t2dSceneObject(%labelObjName) { sceneGraph = %scene; };
   // mount the object just above the sprite
   %labelObj.mount(%obj, "0 -1");
}

Here we peice together a %labelName and %labelObjName then the label object itself and we finish by mounting the label to the object we're creating offset to just above it. Basically we just create an invisible label object that the GUIs will be attached to and then mount the label object to our player so the label follows the player.

Now add this.

// create the whole label name
   %wholeLabelName = %labelname @ "Whole";

We simply peice together a name we will use to create our GUIs.

Then lets add some of the GUI stuff we will attach to our player for the Label text, HP bar, and HP border. So add this.

   // lets create the gui objects that will serve as the label, the hit points bar,
   // and the border for the hit points bar
   new GuiControl(%wholeLabelName) {
      profile = "GuiDefaultNoSelectNoBorderProfile";
      horizSizing = "right";
      vertSizing = "bottom";
      fitParentWidth = "0";
      fitParentHeight = "0";
      position = "-1 -1";
      extent = "50 25";
      minExtent = "8 2";
      visible = "1";
   };

Here we generate the main GUI that will hold all of our labels that will hover over our player.

Lets move on and add this.

   new GuiTextCtrl(%labelName) {
      profile = "GuiLabel" @ %obj.team.getName() @ "TextProfile";
      horizSizing = "right";
      vertSizing = "bottom";
      fitParentWidth = "0";
      fitParentHeight = "0";
      position = "0 -2";
      extent = "50 12";
      minExtent = "8 2";
      visible = "1";
      text = %obj.name;
      maxLength = "255";
   };

This is the text gui that will hold the units name.

Now add this.

   // grab the scale factor
   %scaleFactor = %obj.hitPointBarScaleFactor;
   // create the border bar name
   %borderBar = %obj.name @ "HitPointsBorderBar";

we grab the scaleFactor from our objectType so we can use them in the next two GUIs. We also peice together the borderBar name for the borderBar GUI.

Continue by adding this.

   new GuiBitmapCtrl(%borderBar) {
      profile = "GuiDefaultNoSelectProfile";
      horizSizing = "right";
      vertSizing = "bottom";
      fitParentWidth = "0";
      fitParentHeight = "0";
      position = "0 8";
      extent =  mFloor(%obj.hitPointsMax/%scaleFactor) SPC "4";
      minExtent = "0 2";
      visible = "1";
      wrap = "0";
   };
   
   %bar = %obj.name @ "HitPointsBar";
   new GuiBitmapCtrl(%bar) {
      profile = "GuiDefaultNoSelectProfile";
      horizSizing = "right";
      vertSizing = "bottom";
      fitParentWidth = "0";
      fitParentHeight = "0";
      position = "0 8";
      extent = mFloor(%obj.hitPointsMax/%scaleFactor) SPC "4";
      minExtent = "0 2";
      visible = "1";
      bitmap = "Strategy/data/images/Bar";
      wrap = "0";
   }; 

The first GUI is the border bar, as you may notice its a "GuiBitmapCtrl" so its basically an image. In this case we don't set the image so its just a border.

The next GUI object is the actual HP bar. Its another "GuiBitmapCtrl" though this one we set the image to the "bar" image... you can set this to anything, in my case I simply used this basic small bar.png. Here is my image in case you want to use this.

Image:Bar.png

We're nearly finished with this function, a couple more sections to go. Add this.

   // now we want to add our GUIs to the main GUI
   %wholeLabelName.add(%labelName);
   %wholeLabelName.add(%borderBar);
   %wholeLabelName.add(%bar);

Here we simply add all the sub GUI's to our container GUI stored in %wholeLabelName.

Now add this.

   // then we store all the important Ids and values on our object
   %obj.labelName = %labelName.getId();
   %obj.hitPointsBorderBar = %borderBar.getId();
   %obj.hitPointsBar = %bar.getId();
   %obj.labelObj = %labelObj;
   %obj.labelGui = %wholeLabelName.getId();

Here we simply store the ID's and names of the object we created on the actual sprite object... that way if we need to reference them on a per object basis, we can.

To finish off this function add this.

   // attach our GUI to our label object
   %labelObj.attachGui(%wholeLabelName, SceneWindow2D);
   
   return %obj;

First we take the %labelObj, if you remember this object is a sceneObject mounted to our sprite with an offset just above it, and attach the %wholeLabelName GUI to it. Previously we added all of our GUIs to this %wholeLabelGUI so basically this is the final step that attaches the GUI objects to the label object which is mounted to our sprite.

Then we return the %obj for further use when we call this function.

So here we have our spawning function. We do a few nifty things here, mainly setting up our new GUIs and attaching them to our object as a label. We also pass around a lot of values, initially we load most of our values from %type, this %type we will pass in is our classes from objectTypes.cs... thats why we set all of them up already.

There is one other thing we should add to make sure that the labels are deleted:

//
// Entity::delete. This deletes the labels for the entity.
//
function Entity::onLevelEnded(%obj, %scene)
{
   %labelName = %obj.name @ "Label";
   %labelObjName = %labelName @ "Obj";
   %wholeLabelName = %labelname @ "Whole";
   %borderBar = %obj.name @ "HitPointsBorderBar";   
   %bar = %obj.name @ "HitPointsBar";
   
   %labelName.delete();
   %labelObjName.delete();
   %wholeLabelName.delete();
   %borderBar.delete();
   %bar.delete();
}
Placing our init calls

We need to do one thing before we test this, we need to call our new init functions. So open your main.cs and add these lines before "// Remove the following four lines if you would like to start the game without running the".

initData();
initObjectTypes();


Save it and close the file and we're good to go!

Time to test


Fire up TGB

Now lets test this. Fire up TGB.

You should see something like this.

Image:CrateWithLabelAndLifeTag.jpg




Add a variable delay to make it a creation process and create a "base" that can be used to create such units



In this section we will basically extend our spawning system to a full creation system. To properly set this up for the addition of a creation progress/update bar we must have it call a function at east minute step of creation. Fortunately Torque makes this easier than it may seem.

Where do we start?


Adding the proper base classes to objectTypes.cs

Add these lines to the end of your initObjectTypes() function in your objectTypes.cs after all the unit object types.

   // our root Base class for our team fortresses
   new t2dSceneObjectDatablock(Base){
      imageMap = CityImageMap;
      size = "12 12";
      hitPointsMax = 1000;
      hitPointBarScaleFactor = 22;
      isSelectable = true;
      soldierCount = 0;
      workerCount = 0;
   };
   // our team bases derived from Base
   new t2dSceneObjectDatablock(CirclesBase : Base){
      superClass = Base;
      name = "Circle Base";
   };
   new t2dSceneObjectDatablock(SquaresBase : Base){
      superClass = Base;
      name = "Square Base";
   };

Here we do basically what we did with Entity and each of its sub-classes.

Spawning our bases


In the level editor drag on two image maps. Set their layers to 29 and their groups to 15. These will be the bases.

Set their config-block to CirclesBase and SquaresBase.

Now we can move on and add our functions to initiate our creation sequence.

Adding the creation delay system


Add funcitons to data.cs

We will start by adding a couple functions to our data.cs. First we must create our initial function to be called, the function that will start this chain of unit creation. Add this function frame.

// --------------------------------------------------------------------
// gameData::createObject()
//
// This is the initial function in our unit creation function chain,
// this function will initalize the process and then call 
// gameData::creatingObject() which will repeat until the creation process
// is finished
// --------------------------------------------------------------------
function gameData::createObject(%this, %type, %team)
{

}


Now lets flesh our function out, add this.

   // check if this is the player's team, that way we can handle
   // things such as the progress bar gui, etc
   if(%team $= $playerTeam)
   {
      %isPlayer = true;
   } else
   {
      %isPlayer = false;      
   }

We start by checking whether or not if this object to be created is the player's object. If so we store it so later we can add a progress bar easily.

Now add this.

   // then lets check if we're already creating something
   // we want to make sure we can only create one thing at a time
   if(%team.isCreating)
   {
      // lets return out so we don't do anything    
      return;      
   }

We double check to see if we are already creating, if we are already creating something then we won't create anything else and exit this. Later if you add your own queueing system in you can put some plugs here.

Lets move on by adding this.

   // now we set that we have started creating something
   %team.isCreating = true;
   
   // initiate the creating count value that we will use during the
   // creation process
   %team.creatingCount = 0;

Since we have alreayd checked if we are creating already, we should already know we aren't. In that case we set .isCreating to true to prevent us from creating two units at the same time. We also set a value called creatingCount to 0. This value will help us in our next function.

Finish off the function by adding this.

   // now we call the repeatable function to create the object
   // we pass %isPlayer so we don't have to do this check again
   // for the progress bar updates during creation
   %this.creatingObject(%type, %team, %isPlayer);

Here we simply call our repeatable function that will persist until the object is created.

Add a global variable ($creationCount)

Before we move on to our next function we are going to add a global variable to our initData() function... add this line after $playerTeam.

   $creationCount = 100;


Create creatingObject() function

Now we can add our next function. Add this function frame.

// --------------------------------------------------------------------
// gameData::creatingObject()
//
// This is a re peatable function that will call itself until creation
// is done, this allows us to make unit creation a process that can have
// a linked update and pogress bar
// --------------------------------------------------------------------
function gameData::creatingObject(%this, %type, %team, %isPlayer)
{   

}


Now lets add our first lines into this function, so add this.

   // increment the team's creating count to represent progress
   // this is based off of a value in the object's type...
   // 1 will go through at a normal speed, 2 will go through at double
   // the speed, etc
   %team.creatingCount = %team.creatingCount + %type.creationTime;

Here we use the creationTime value stored on the object type in our objectTypes.cs. We take the present creatingCount and add the creationTime to it. In the first call the creatingTime will be 0 and if we use a worker our creationTime will be 2...

Then add this.

   // if we  haven't reached the creation time then we want to continue 
   // creating the object with a schedule passing all viable information
   if(%team.creatingCount <= $creationCount)
   {
      %this.schedule(100, "creatingObject", %type, %team, %isPlayer);
   } else
   {
      // this means we have finished the creation time of the object
      // so we need to increment the team's object type count
      %eval = "%team.base." @ %type.class.getName() @ "Count++;";
      eval(%eval);

      // now lets spawn the actual object
      %this.spawnUnit(%type, %team, %type.getName());

      // set the isCreating to false so we can continue on with something else
      %team.isCreating = false;   
   }

If our creatingCount (now at a 2 in the first pass of a Worker) is less than or equal to our $creationCount (which is set at 100 in initData()) then we call a schedule. What this does is tell Torque to perform the specified function at a specified time from now. In our case we just call the same function at 100 milliseconds. This results in this function repeating if we haven't reached 100, which will in turn keep adding to the value until it hits 100.

If the value is equal to or great than 100 then we do a couple things.
First we increment the team's unit type count. Say we are creating a CirclesWorker... well the CirclesWorker.class is "Worker"... so it increments the Circles.base.WorkerCount value.

Then we call a function spawnUnit() that we need to create, all we do is pass it the %type and %team, it will then appropriately call our spawning function. We finish by toggling isCreating to false.

Creating the spawnUnit() function

Ok now we need to create a function, the spawnUnit() function. Now this really is just a small function that ends up calling our spawnObject() function with some specified parameters.

Add this function to the end of your data.cs.

// --------------------------------------------------------------------
// gameData::spawnUnit()
//
// This is a small function that in turn calls our gameData::spawnObject()
// function
// --------------------------------------------------------------------
function gameData::spawnUnit(%this, %type, %team, %name)
{
   // lets grab the bases' position and divide it up into X and Y   
   %pos = %team.base.getPosition();
   %posX = getWord(%pos, 0);
   %posY = getWord(%pos, 1);
   
   // now lets add 10 to the Y position so it will spawn our units
   // just below the base
   %posY = %posY + 10;
 
   // lets peice together our positions again
   %pos = %posX SPC %posY;
   
   // then we call spawnObject() passing it some of the same values along
   // with our new values
   %this.spawnObject(%type, %team, %pos, %name);
}

Here we do a few minor things, first we get the position of the team's base... we then divide this position up into X and Y (you should be more than familiar with this by now :). The next step is to add 10 to the position, this will spawn our unit just below our base. In our last two actions we peice the position together then call our spawnObject() command (much like we tested in the first section of this tutorial step).

Creating the Base:onLevelLoaded function

To make sure that our bases can be found we set them to Team.base. We can get them assigned at the start of the level by using this:

//
// Base::onLevelLoaded
//
function Base::onLevelLoaded(%this, %scene)
{
   %this.team.base = %this;
}



Create the City imageMap

Ok, before we can test we need to set up one thing... we need to set up the CityImageMap I had you reference in the Base objectType. So set up an image map with a city image that will do, feel free to use the one I did.

Image:City.png

After you have put the image in your "Strategy/data/images" folder, add it as a datablock.

Time to test


Ok now we can test this!

Fire up your TGB

Fire up your TGB... now we need to test two things. First thing is that the Bases loaded correctly, second that the new unit creation system works.

Testing to see if the bases loaded correctly

Well the positions I gave you to spawn the bases are off your central screen (they make much more sense than putting bases right in the center :) So we will manually move are camera to that position by typing this command in the console and hitting enter.

SceneWindow2D.setCurrentCameraPosition("-75 -75");

You should see something like this.

Image:BaseProperlyLoaded.jpg

Test our unit creation

Now we can test our unit creation... type this command in your console and press enter.

gameData.createObject(SquaresSoldier, Squares);


It should take a moment (if the delay works)... and then you should see this.

Image:SquaresSoldierSpawnedWithDelay.jpg

You may notice the name is clipped, you can modify that in either what name you pass (In the above code it is %type.getName()) or you can make the name text GUI bigger (be sure to make the main GUI control bigger too). For our purposes this is a great starting point... it worked!



Implement a basic click and move system





Object specific right click actions


Add values to the object types

The most organized way to add right click actions is to add it to the objectTypes and then reference the object type upon right clicking. This way we can have multiple right click responses very easily (especially when we add combat support).

Start by opening your objectTypes.cs. We're going to add an additional property to our Entity class... Add this line to your Entity class.

isMoveable = true;


To make the Entity class look like this.

   // lets create the base unit object called Entity
   new t2dSceneObjectDatablock(Entity){
      isSelectable = true; 
      name = "Entity";
      speed = 10;
      creationTime = 2;
      hitPointsMax = 100;
      hitPointBarScaleFactor = 3;
      team = "Circles";
      isMoveable = true;
   };


This means all of our unit objects will get this isMoveable value since it all inherits from Entity.

Creating the action function attached to t2dSceneObject

Ok now we are going to add a new function to the end of our objectTypes.cs. This will be the start of our action system, so add this function.

// --------------------------------------------------------------------
// t2dSceneObject::action()
//
// This function is attached to every t2dSceneObject (which includes
// static and animated sprites), you can call this function passing it
// an action and an %obj, though the  %val can hold any value the action
// may need to reference, it then passes this action on to the object's
// type, making the makeshift class system work with actions
// --------------------------------------------------------------------
function t2dSceneObject::action(%this, %action, %val)
{
   // peice together our action to be called with passing the correct value
   %eval = %this @ ".type." @ %action @ "(" @ %this @ ", \"" @ %val @ "\");";
   eval(%eval);   
}

This function is attached to all of our objects, we can pass it an action and a value to be used by the action. The action is then passed on to the object type. In this case if we were to pass it ("moveToLoc", "50 50") to a CirclesWorker it would call CirclesWorker.moveToLoc("50 50")... Also remember we set the class to CirlcesWorker as "Worker" so it could also call Worker.moveToLoc("50 50") or even it's superclass Entity.moveToLoc("50 50")... this system will help us attach all actions to our objectTypes instead of each individual object...

Add functions to the object types

Continue by adding this function after our action() function.

// --------------------------------------------------------------------
// Entity::OnRightClick()
//
// This is the main class onRightClick function, it just tell the object
// to move to the location passed using the speed stored on the object
// --------------------------------------------------------------------
function Entity::OnRightClick(%this, %worldPos, %mode)
{

   if(%this.isMoveable) 
      %this.action("moveToLoc", %worldPos, %this.speed); 
}

This function is fairly simply... we pass it multiple values. Remember the first value is always the object itself, usually the first value is %this... We then pass the position right clicked and an option value of %mode. Right now we won't use %mode, but this will help us to later set up modes like attacking and following.

Now lets create the subclass onRightClick functions that will call back to the Entity class, add these functions after our previous one.

// --------------------------------------------------------------------
// Worker::OnRightClick()
//
// This is the Worker onRightClick() callback, it basically just forwards
// the action to the Entity class. (Its not really necessary, but good to get used to!)
// --------------------------------------------------------------------
function Worker::OnRightClick(%this, %worldPos, %mode, %objList)
{
   Entity::OnRightClick(%this, %worldPos); 
}

// --------------------------------------------------------------------
// Soldier::OnRightClick()
//
// This is the Soldier onRightClick() callback, it basically just forwards
// the action to the Entity class. (Its not really necessary, but good to get used to!)
// --------------------------------------------------------------------
function Soldier::OnRightClick(%this, %worldPos, %mode, %objList)
{
   Entity::OnRightClick(%this, %worldPos); 
}

As you can see we simply call the Entity function passing the object. In these function's case we not only pass in %mode, but %objList... this will be for later use.

Add a class function moveToLoc()

Now that we have a right click function that calls a moveToLoc function we need to create the moveToLoc function.

Add this function after our onRightClick functions.

// --------------------------------------------------------------------
// Entity::moveToLoc()
//
// moveToLoc function attached to the class which will be called by the
// action function attached to the object
// --------------------------------------------------------------------
function Entity::moveToLoc(%this, %worldPos)
{
   // lets grab the object's speed
   %speed = %this.speed;

   // tell our object to move to the specified position at the specified
   // speed, and the true signals it to stop there
   %this.moveTo(%worldPos, %speed, true); 
}

This function is rather simple, we call the engines moveTo function and tell it to go to the clicked position at the objects speed and then to stop there.

Insterting the right click function calls


Our last couple steps are to create a function that calls the proper onRightClick functions for the selected objects, then to plug a call to that function into our onRightMouseDown callback.

Go to our mouse.cs and add a function

To do this, first we will go to our "Strategy/gameScripts/scripts/mouse/mouse.cs" script file. We are going to add an additional callback for onRightMouseDown... we will add it after our present onMouse callbacks, so instert this function right after our onMouseDragged() function...

// --------------------------------------------------------------------
// SceneWindow2D::onRightMouseDown
//
// This function is triggered from the engine when the right mouse is "down"
// --------------------------------------------------------------------
function SceneWindow2D::onRightMouseDown(%this, %modifier, %worldPos, %mouseClicks)
{  
   // when we right click we want to move objects if needed
   // the moveObjectsTo will basically trigger each selected
   // objects right click action
   processAction(%worldPos);
}

Ok, nothing special here, we simply call the processAction() function passing it the position the user right clicked. Now lets create the processAction() function. We will do this in a different file though (for organization purposes).

Create a new script file, game.cs

We're going to create a new script file in our "Strategy/gameScripts/scripts/game" folder called game.cs. This will be a good place to hold our processAction() function (later you can organize this however you want, presently its a good idea to keep consistent with this tutorial).

Exec() it

So after you create "Strategy/gameScripts/scripts/game/game.cs" you might have already guessed what we need to do, exec() it. So open your main.cs and add this line.

exec("./gameScripts/scripts/game/game.cs");


Adding the initGame() function

First lets start our game.cs with a proper init function... so add this first function to game.cs.

// --------------------------------------------------------------------
// initGame()
//
// This is a simple init function that
// --------------------------------------------------------------------
function initGame()
{
   // these are values we will use to offset our moving of units
   $posOffset[0] = "0 0";
   $posOffset[1] = "0 -5";
   $posOffset[2] = "0 5";
   $posOffset[3] = "0 -10";
   $posOffset[4] = "0 10";
   $posOffset[5] = "0 -15";
   $posOffset[6] = "0 15";
   $posOffset[7] = "0 -20";
   $posOffset[8] = "0 20";
}

This is a global array we are storing ahead of time to offset our moving positions. This will be explained later.

Call initGame() in our startGame()

Now we need to call initGame() in our startGame... so tag it on to the end of your startGame() function in game.cs... like this.

function startGame(%level)
{
   // Set The GUI.
   Canvas.setContent(mainScreenGui);
   Canvas.setCursor(DefaultCursor);
   
   moveMap.push();
   if( isFile( %level ) )
      sceneWindow2D.loadLevel(%level);
      
   $strategyScene = sceneWindow2D.getSceneGraph();
      
   initMouse();
   
   initGame();
}


Create our processAction() function

Now lets create our processAction() function in our game.cs, like this.

// --------------------------------------------------------------------
// processAction()
//
// This will basically process the right click action of a single set
// or group of objects with the option to pass a "mode,"  this mode is
// used when you click the move action button and then click on a space 
// in the world.  This will prevent you from attacking while in that "mode,"
// though it seemed redundant to set up a whole seperate set of of functions
// when the actions are the right click actions, so this just forces the right
// click action down a path
// --------------------------------------------------------------------
function processAction(%worldPos, %mode)
{

}


Now lets start out this function by adding this.

   // get the count of objects selected
   %count = Selections.getCount();
   
   // if we have more than one object selected then we store that we have
   // multiple selections
   if(%count > 1)
   {   
      %multiple = true;
   } else
   {
      %multiple = false;
   }

Here we get a count of how many Selections we have. If the count is over one we set %multiple to true, if there are only 1 objects selected then we set it to false, that way we can know if we need to loop through each object and process their right click actions or not.

Now we add a meaty loop, add this.

   // here we loop through all of our selections
   for(%i=0;%i<%count;%i++)
   {
      // if we have a multiple selection we want to generate some offset points
      // so when we move a group of units they won't go to the same spot...
      // formation integration would go in here, but right now a simple
      // offset works for testing purposes
      if(%multiple)
      {
         // grab and divide the world positions as well as the offsets so we
         // can generate some -very- simple position offsets
         %worldX = getWord(%worldPos, 0);
         %worldY = getWord(%worldPos, 1);
         %offsetX = getWord($posOffset[%i], 0) + getRandom(0,2);
         %offsetY = getWord($posOffset[%i], 1) + getRandom(0,2);
         
         // lets peice together the new position
         %pos = (%worldX += %offsetX) SPC (%worldY += %offsetY);
      } else
      {
         // if this is a single selection we want the object to go
         // where we clicked
         %pos = %worldPos;
      }
      
      // lets grab a list of all objects at the area clicked so we can pass this
      // to the object for its own use
      %objList = $strategyScene.pickPoint(%worldPos);
      
      // we store the object we're currently looping through and pass it
      // information the object might need
      %obj = Selections.getObject(%i);
      %obj.OnRightClick(%pos, %mode, %objList);
   }

This is a big loop so I will explain it in sections...

      // if we have a multiple selection we want to generate some offset points
      // so when we move a group of units they won't go to the same spot...
      // formation integration would go in here, but right now a simple
      // offset works for testing purposes
      if(%multiple)
      {
         // grab and divide the world positions as well as the offsets so we
         // can generate some -very- simple position offsets
         %worldX = getWord(%worldPos, 0);
         %worldY = getWord(%worldPos, 1);
         %offsetX = getWord($posOffset[%i], 0) + getRandom(0,2);
         %offsetY = getWord($posOffset[%i], 1) + getRandom(0,2);
         
         // lets peice together the new position
         %pos = (%worldX += %offsetX) SPC (%worldY += %offsetY); 
      }

There are a few things going on here, first we check to see if there are multiple selections, if so we do a few calculations, if you slowly look through the calculations you may notice all we are doing is grabbing the offset array we set up in initGame() and applying it to our locations, this is only applied if we have multiple selections and is merely to prevent our objects from moving to the exact same position. Later you can sub this out with pathfinding code.

Our next section covers the else of the if multiple statement, so this is what happens if we have a single object selected...

       else
      {
         // if this is a single selection we want the object to go
         // where we clicked
         %pos = %worldPos;
      }

As you can see we simply store the position clicked in the position to be set, no offseting on a single selection.

Our final section...

      // lets grab a list of all objects at the area clicked so we can pass this
      // to the object for its own use
      %objList = $strategyScene.pickPoint(%worldPos);
      
      // we store the object we're currently looping through and pass it
      // information the object might need
      %obj = Selections.getObject(%i);
      %obj.OnRightClick(%pos, %mode, %objList);

First we grab a list of objects at the location right clicked, we then grab the object we are currently itterating through... finally we call the .onRightClick function (what we just created in objectTypes.cs) and pass it the important values. We also pass it the object list, this will later be used for combat (right click attacking) and other actions.

Ok, now we should be good to test this...

Time to test it


Fire up TGB and spawn BoB

Fire up TGB...

You should see our friendly little BoB unit again, drag and select him, then right click somehwere nearby, he should move to it!


Multiple BoBs!

If you make sure there are 3 BoBs, just use copy and paste, you can try to move them together! Like this:

Image:ThreeObjectsMovedwithRightClick.jpg

It works!

Here are my files for reference:

main.cs game.cs game/game.cs data/data.cs objectTypes.cs