-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Fill Battle Tutorial
While it is always tempting to dive head first into Torque 2D and start working on your "dream" game, it is especially helpful for beginner users to get an idea of the T2D "workflow" by following tutorials that make or remake existing games.
The following tutorial is based on a version originally made for Torque Game Builder by William Lee Sims and published on the Torque Developer Network. It has been given a new coat of paint and made to work now with Torque 2D MIT.
So what’s the game? You “own” the upper-left square (which is yellow in the above picture). You select a color, and the tile you “own” becomes that color. In addition, all adjacent colors that match become yours. So if you select “pink”, you’d own two squares. Instead, if you selected “light blue”, you’d own 4 squares. The goal is to “own” the whole board (also making it all the same color) by making the fewest color changes.
- 1. In the modules folder of Torque 2D, create a new folder and call it "FillBattle".
- 2. In the FillBattle folder, create a subfolder called "1".
- 3. Inside the "1" folder, create another folder called "assets".
- 4. Now it is time to create the module.taml file in the "1" folder.
- In a text editor of your choice, add the following in XML:
<ModuleDefinition
ModuleId = "FillBattle"
VersionId = "1"
Dependencies = "AppCore=1"
ScriptFile = "module.cs"
CreateFunction = "create"
DestroyFunction = "destroy">
<DeclaredAssets
Path = "assets"
Extension = "asset.taml"
Recurse = "true"/>
</ModuleDefinition>
- 5. Next step is to create a module.cs file, also located in the "1" folder. This file will have two empty functions:
function FillBattle::create(%this)
{
}
//-----------------------------------------------------------------------------
function FillBattle::destroy(%this)
{
}
Remember to save your files after each step.
Step 4: module.taml
This is the module definition file. The fields should be fairly self-explanatory - we are naming our module "FillBattle" and giving it a version number of 1. We want this module to be dependant on AppCore. In many other modules, like all the sandbox toys, the main entry script is called main.cs. This can be confusing for new users since there is also a main.cs file in the root directory where the T2D executable or app exists. To show that we are not restricted to certain file names, our entry script is called module.cs. The next lines define the names of the create and destroy functions. Afterwards, the module definition file tells T2D to look for assets in the assets folder with the file extension __.asset.taml. Recurse being true also allows the engine to search through any subfolders inside the assets folder that might be created in the future for additional assets.
Step 5: module.cs
As mentioned in the module.taml section, this is the entry script for this module. Here, we add two methods the engine will always look for - create and destroy. Create will be called when the module is first loaded, and destroy when the module is unloaded.
Module File Review
- 1. Right click and download the following 2 PNG files and save them in the assets folder.
The SquareBorders image is actually not used for this part of the tutorial. We include it now for completeness.
- 2. For the SixColors.png file, create the following XML file:
<ImageAsset
AssetName = "SixColors"
ImageFile = "SixColors.png"
CellCountX = "4"
CellCountY = "2"
CellWidth = "64"
CellHeight = "64"
FilterMode = "NEAREST" />
Save the file as SixColors.asset.taml
- 3. For the SquareBorders.png file, create the following XML file:
<ImageAsset
AssetName = "SquareBorders"
ImageFile = "SquareBorders.png"
CellCountX = "4"
CellCountY = "4"
CellWidth = "64"
CellHeight = "64"
FilterMode = "NEAREST" />
Save the file as SquareBorders.asset.taml
Step 2, 3: ImageAsset Files
In order for T2D to recgonize images that are to be used in game, an asset definition file needs to be created. We simply assign a name and point to the image file name within the TAML format. Since both PNG files contain multiple frames, it is also necessary to define the number of frames in X and Y and their respective width/height. Changing the filter mode is also necessary because the default bilinear filtering will pull in neighboring frame colors along the border - nearest filter mode fixes this.
Module File Review
- 1. In the "1" folder, create a subfolder called "objects".
- 2. Again with the text editor of your choice, create a file called SceneWindow.taml. The file should look like this:
<SceneWindow
Name="GameWindow"
Profile="SceneWindowProfile"
UseObjectInputEvents="1" />
Save this file as SceneWindow.taml in the newly created "objects" folder.
- 3. Now we have to go back to our module.cs file
- In the create method, add a call to a method called loadGame:
function FillBattle::create(%this)
{
// Load the game
%this.loadGame();
}
- 4. This function does not exist yet, so let's create it now. Below the destroy function in module.cs, add this:
function FillBattle::loadGame(%this)
{
%window = TamlRead("./objects/SceneWindow.taml");
Canvas.setContent(%window);
}
- 5. There is one more thing needed for a SceneWindow to function properly and not generate a crash - a valid GuiProfile. Below the loadGame method, add the following code:
if (!isObject(SceneWindowProfile)) new GuiControlProfile(SceneWindowProfile)
{
modal = true;
};
- 6. We're closing in on wanting to run the binary (Torque2D.exe or Torque2D.app). In order to do that though, we need to change the module that AppCore loads as defined in its main.cs file. In a default installation, it loads the group "gameBase" which is the Sandbox and all of its toys. If you module folder is filled with toys (or other projects) - remember to comment out this:
ModuleDatabase.loadGroup("gameBase");
and replace it with:
ModuleDatabase.loadExplicit("FillBattle");
- 7. If you run the game now, all you would see is a default "CornflowerBlue" screen. To provide a better contrast to our eventual colored game board - change the background Canvas color to black. In the create method of module.cs:
function FillBattle::create(%this)
{
Canvas.BackgroundColor = "black";
// Load the game
%this.loadGame();
}
Step 2: SceneWindow
The SceneWindow is part of the GUI system, its parent being GuiControl. As its name implies, it is a window that displays the contents of a Scene. The TAML XML file we created gives us a basic window with the name "GameWindow", which will allow us to reference the SceneWindow from anywhere in a script file if needed. The Profile is a very important field - it defines the GuiProfile used. Without this, the engine would crash when the Canvas tries to load and display the SceneWindow. The SceneWindow normally captures all input events from the mouse or touch and provides us with a callback so we can script a reaction to that event. The last field, UseObjectInputEvents, we set to true and this allows the SceneWindow to pass input events to scene objects. Specific examples will be covered in a later chapter.
Step 5: GuiProfiles
GuiProfiles (their proper name being GuiControlProfile) describe the look and formatting of GuiControls. Since the SceneWindow is a derivative of GuiControl, we need to assign it a valid profile. The main role of the SceneWindow is to display the contents of our Scene though, so any properties from GuiControlProfile are, in this case, not important. This is why we can get away with just creating a nearly blank profile - the modal property is set to true to allow mouse/touch input events to be handled properly.
Module File Review
- 1. Let's give the SceneWindow an empty Scene to display. It won't stay empty long though.
- Create a file called Scene.taml:
<Scene
Name="GameScene" >
</Scene>
Save Scene.taml in the "objects" folder.
- 2. Back in the module.cs file and in the loadGame method, read the TAML file in and assign it to the SceneWindow:
function FillBattle::loadGame(%this)
{
%window = TamlRead("./objects/SceneWindow.taml");
Canvas.setContent(%window);
%scene = TamlRead("./objects/Scene.taml");
%window.setScene(%scene);
}
- 3. Let's finally add a game object to display when we run Torque 2D. While the following could be added to the Scene file we just created, it can be argued that during prototyping it is easier to have separate files for most objects. Create a new file called TileBoard.taml:
<CompositeSprite
Name = "TileBoard"
DefaultSpriteSize = "5 5"
DefaultSpriteStride = "5 5"
BatchLayout = "rect" >
<CompositeSprite.Behaviors>
<RandomFillBehavior />
</CompositeSprite.Behaviors>
</CompositeSprite>
Again, remember to save this file to the "objects" folder.
- 4. Notice we gave the composite sprite a behavior? Time to create it. First, in the "1" folder, create a new folder called "scripts". Then create a script file called RandomFillBehavior.cs and save it to the scripts folder:
if (!isObject(RandomFillBehavior))
{
%template = new BehaviorTemplate(RandomFillBehavior);
%template.addBehaviorField(GridSize, "The size of the board", int, 15);
}
//-----------------------------------------------------------------------------
function RandomFillBehavior::onBehaviorAdd(%this)
{
// Iterate over all the sprites
for (%x = 0; %x < %this.GridSize; %x++)
{
for (%y = 0; %y < %this.GridSize; %y++)
{
// Pick a random number from 0 to 5
// This will equal the frame number from the ImageAsset
%randomFrame = getRandom(0,5);
// Add a sprite (aka "tile")
%this.owner.addSprite(%x SPC %y);
// Assign it an ImageAsset and a random frame
%this.owner.setSpriteImage("FillBattle:SixColors", %randomFrame);
}
}
}
- 5. There's 2 additional changes needed in module.cs so the engine can recognize the behavior script file and load the TileBoard composite sprite.
-
exec("./scripts/RandomFillBehavior.cs");
is needed in the create function -
%board = TamlRead("./objects/TileBoard.taml");
and%scene.add(%board);
are needed in the loadGame function. - Here is the complete file:
function FillBattle::create(%this)
{
// Load all behavior scripts
exec("./scripts/RandomFillBehavior.cs");
Canvas.BackgroundColor = "black";
// Load the game
%this.loadGame();
}
//-----------------------------------------------------------------------------
function FillBattle::destroy(%this)
{
}
//-----------------------------------------------------------------------------
function FillBattle::loadGame(%this)
{
%window = TamlRead("./objects/SceneWindow.taml");
Canvas.setContent(%window);
%scene = TamlRead("./objects/Scene.taml");
%window.setScene(%scene);
%board = TamlRead("./objects/TileBoard.taml");
%scene.add(%board);
}
//-----------------------------------------------------------------------------
if (!isObject(SceneWindowProfile)) new GuiControlProfile(SceneWindowProfile)
{
modal = true;
};
- 6. Make sure you've saved all your files and start T2D. You should now see a colored tile board:
Step 1: Scene
The Scene can be thought of as a container which holds all of our scene objects. Right now we are just creating a blank scene and will be manually adding an object to it.
Step 3: CompositeSprite
We need a game board that resembles in many ways a tilemap. Tilemaps are implemented in T2D MIT through the CompositeSprite class. In addition to giving it a name, we define the size of each tile, the distance from one tile to another, and we give it a rectilinear layout. Assigning the object a behavior is also quite easy inside the TAML XML framework.
Step 4: RandomFillBehavior
The CompositeSprite created in step 3 is empty. For a tilemap, it was beyond empty (no tiles), we didn't even specify how big of a grid we should have. What we are doing now is creating a behavior which defines the grid size and then randomly assigns each logical position in the grid with a frame from the SixColors image asset. The default grid size is 15 x 15 - this is defined in the behavior field of the template. We will not be changing the default in this tutorial. The onBehaviorAdd callback is performed when a behavior is added to an object. Since we already assigned this behavior to the object in XML, the callback is carried out when the object is read in.
Module File Review
- 1. Looking at the tile board we created, all of the logical tile positions are in the positive X and Y axis. This makes it easy for us when doing math with the tile positions but right now part of the board is off the screen. Let's center the board on the screen.
- Create a new script file called CenterPositionBehavior.cs. Remember to save it to your scripts folder:
if (!isObject(CenterPositionBehavior))
{
%template = new BehaviorTemplate(CenterPositionBehavior);
}
//-----------------------------------------------------------------------------
function CenterPositionBehavior::onAddToScene(%this, %scene)
{
// Get the number of sprites in the composite
%count = %this.owner.getSpriteCount();
%length = mSqrt(%count);
%spriteSize = %this.owner.getDefaultSpriteSize();
%boardSizeX = %length * %spriteSize.x;
%boardSizeY = %length * %spriteSize.y;
%x = (%boardSizeX / 2) - (%spriteSize.x / 2);
%y = (%boardSizeY / 2) - (%spriteSize.y / 2);
%this.owner.setPosition(-%x, -%y);
}
- 2. Open up TileBoard.taml and add the CenterPositionBehavior to the object:
<CompositeSprite
Name = "TileBoard"
DefaultSpriteSize = "5 5"
DefaultSpriteStride = "5 5"
BatchLayout = "rect" >
<CompositeSprite.Behaviors>
<RandomFillBehavior />
<CenterPositionBehavior />
</CompositeSprite.Behaviors>
</CompositeSprite>
- **3.**Since we have a new behavior script file, we need to exec it otherwise T2D will not know it exists. In module.cs add the line just below the other behavior in the create function:
function FillBattle::create(%this)
{
// Load all behavior scripts
exec("./scripts/RandomFillBehavior.cs");
exec("./scripts/CenterPositionBehavior.cs");
- 4. After saving, start up T2D again and see the difference!
Step 1: CenterPositionBehavior
Similar to the RandomFillBehavior, in the same script file we create both the behavior template plus the methods that apply to that behavior. As you can guess from the name, the onAddToScene callback is run when the object is added to the Scene. In terms of event order, this always happens after onBehaviorAdd, so we don't have to worry about the callback in the RandomFillBehavior script not being finished before this callback is performed.
Why did we bother with this behavior though? Certainly, we could have just changed the position of the TileBoard to have it be in the center of the screen for the purposes of this tutorial. Combined with the RandomFillBehavior though, you do have the option to change the size of the tile board and this CenterPositionBehavior will always position it in the middle of the screen.
Module File Review
- 1. Update Scene.taml with entries for each button:
<Scene
Name="GameScene" >
<Sprite
Image = "FillBattle:SixColors"
Frame = "0"
Size = "10 10"
Position = "44 30"
UseInputEvents = "1" >
<Sprite.Behaviors>
<ButtonBehavior />
</Sprite.Behaviors>
</Sprite>
<Sprite
Image = "FillBattle:SixColors"
Frame = "1"
Size = "10 10"
Position = "44 19"
UseInputEvents = "1" >
<Sprite.Behaviors>
<ButtonBehavior />
</Sprite.Behaviors>
</Sprite>
<Sprite
Image = "FillBattle:SixColors"
Frame = "2"
Size = "10 10"
Position = "44 8"
UseInputEvents = "1" >
<Sprite.Behaviors>
<ButtonBehavior />
</Sprite.Behaviors>
</Sprite>
<Sprite
Image = "FillBattle:SixColors"
Frame = "3"
Size = "10 10"
Position = "44 -3"
UseInputEvents = "1" >
<Sprite.Behaviors>
<ButtonBehavior />
</Sprite.Behaviors>
</Sprite>
<Sprite
Image = "FillBattle:SixColors"
Frame = "4"
Size = "10 10"
Position = "44 -14"
UseInputEvents = "1" >
<Sprite.Behaviors>
<ButtonBehavior />
</Sprite.Behaviors>
</Sprite>
<Sprite
Image = "FillBattle:SixColors"
Frame = "5"
Size = "10 10"
Position = "44 -25"
UseInputEvents = "1" >
<Sprite.Behaviors>
<ButtonBehavior />
</Sprite.Behaviors>
</Sprite>
</Scene>
- 2. Our intention is to have Sprites that act like buttons, so to make them work like that we introduce a button behavior:
if (!isObject(ButtonBehavior))
{
%template = new BehaviorTemplate(ButtonBehavior);
}
//-----------------------------------------------------------------------------
function ButtonBehavior::onTouchUp(%this, %touchID, %worldPosition)
{
%newColor = %this.owner.getImageFrame();
TileBoard.selectSprite("0 14");
%oldColor = TileBoard.getSpriteImageFrame();
%stackObject = TamlRead("./../objects/Stack.taml");
%locStack = %stackObject.getBehavior(StackBehavior);
%locStack.push("0 14");
%counter = 0;
while (%stackObject.len > 0)
{
%topCard = %locStack.pop();
TileBoard.selectSprite(%topCard);
TileBoard.setSpriteImageFrame(%newColor);
TileBoard.deselectSprite();
if (%topCard.x > 0)
{
TileBoard.selectSprite((%topCard.x - 1) SPC %topCard.y);
%leftColor = TileBoard.getSpriteImageFrame();
TileBoard.deselectSprite();
if (%leftColor == %oldColor)
{
%locStack.push((%topCard.x - 1) SPC %topCard.y);
}
}
if (%topCard.x < 14)
{
TileBoard.selectSprite((%topCard.x + 1) SPC %topCard.y);
%rightColor = TileBoard.getSpriteImageFrame();
TileBoard.deselectSprite();
if (%rightColor == %oldColor)
{
%locStack.push((%topCard.x + 1) SPC %topCard.y);
}
}
if (%topCard.y > 0)
{
TileBoard.selectSprite(%topCard.x SPC (%topCard.y - 1));
%upColor = TileBoard.getSpriteImageFrame();
TileBoard.deselectSprite();
if (%upColor == %oldColor)
{
%locStack.push(%topCard.x SPC (%topCard.y - 1));
}
}
if (%topCard.y < 14)
{
TileBoard.selectSprite(%topCard.x SPC (%topCard.y + 1));
%downColor = TileBoard.getSpriteImageFrame();
TileBoard.deselectSprite();
if (%downColor == %oldColor)
{
%locStack.push(%topCard.x SPC (%topCard.y + 1));
}
}
%counter++;
}
%locStack.delete();
}
Save this file as ButtonBehavior.cs in your scripts folder.
- 3. The behavior script:
if (!isObject(StackBehavior))
{
%template = new BehaviorTemplate(StackBehavior);
}
//-----------------------------------------------------------------------------
function StackBehavior::onBehaviorAdd(%this)
{
%this.owner.len = 0;
}
//-----------------------------------------------------------------------------
function StackBehavior::push(%this, %val)
{
%this.owner.v[%this.owner.len] = %val;
%this.owner.len++;
}
//-----------------------------------------------------------------------------
function StackBehavior::pop(%this)
{
if (%this.owner.len == 0)
{
error("Stack Underflow");
return;
}
%this.owner.len--;
return %this.owner.v[%this.owner.len];
}
Save this file as StackBehavior.cs in your scripts folder.
- We also need a TAML XML file for our ScriptObject:
<ScriptObject>
<ScriptObject.Behaviors>
<StackBehavior />
</ScriptObject.Behaviors>
</ScriptObject>
Save this as Stack.taml in your "objects" folder.
- 4. As a final step, both the button behavior script and the stack script need to be exectued for them to work. Back in module.cs change the create function to look like this:
function FillBattle::create(%this)
{
// Load all behavior scripts
exec("./scripts/RandomFillBehavior.cs");
exec("./scripts/CenterPositionBehavior.cs");
exec("./scripts/ButtonBehavior.cs");
exec("./scripts/StackBehavior.cs");
Canvas.BackgroundColor = "black";
// Load the game
%this.loadGame();
}
- 5. After everything is saved, time to see the results in game.
Step 1: Adding objects to the Scene
We are creating buttons here using Sprites. Since the buttons are static objects within the context of this tutorial, it is easier to add each object directly within the Scene's TAML file. After creating the first Sprite template, we are just changing the frame number and position within the Scene for the rest of the buttons. For Sprites to work properly as buttons, they need to capture input events (i.e. mouse clicks or touches) within their bounds. To do this, we need to make sure UseInputEvents is true on each button object as well as UseObjectInputEvents being true for the SceneWindow (we set this to true as part of the SceneWindow setup in chapter 3).
Step 2: Button behavior
It is within the heart of the button code that the acutal gameplay of this tutorial occurs. When the user selects a color, we need to change the upper left-hand corner to that color and all other tiles adjacent to it that also match the color. This logic is a little tricky. What we are going to use here is a stack, which is covered in step 3.
For now, here’s the general flow we’re trying to create:
- The user clicks on a color which we call “newColor”.
- We remember the original upper left-hand corner’s color and call it “oldColor”.
- We create a string, “0 14” to represent the upper left-hand tile. The first word of the string is the x-coordinate; the second, the y-coordinate.
- We push this string onto the stack.
- While our stack has any size...
- Remove and read the top-most card.
- Get the first word on that card and call it “xCoord”.
- Get the second word on that card and call it “yCoord”.
- Change <xCoord,yCoord> to the “newColor”.
- NOTE: It should equal the oldColor, but that check is done in the next step.
- Check all of the tiles adjacent to <xCoord,yCoord>
- If any are equal to the “oldColor”, push it onto the stack.
- Our stack should be empty now, so we are done!
Step 3: ScriptObject and Stacks
ScriptObjects are objects that exist outside the scope of a Scene. This means that you can delete the current Scene and load a new Scene and the ScriptObject still exists unchanged. In T2D 3.0 ScriptObjects have the ability to use behaviors - for those still using version 2.0 all functions need to be assigned to the ScriptObject through its name, class, or superClass.
So what is a Stack? A stack is a data structure where the last thing put on it is the first thing that comes off. As always, Wikipedia has more detail if you wish to learn more. Imagine it as a stack of note cards. We can write on a note card and put it on the stack. We can also take a note card off the stack and read it.
We’re using some TorqueScript magic here by dynamically creating variables. “len” is the size of the stack. “v” is an array that has the cards. So “v[0]” is the bottom card, “v[1]” is on top of that, “v[2]” is on top of “v[1]”, and so on.
When we “push” a card onto the stack, we put it at “v[0]” and then increase the length/height of the stack by 1. The next push will then put the card at “v[1]” and then increase the length/height by 1.
“pop” does the opposite. It reduces the length/height by 1 and returns the top-most card. So if our height was 1 and we called “pop”, we’d reduce the length/height to 0 and return “v[0]”.
We do a little error checking in “pop”. If our length/height is zero, we return a blank string and print out an error message into the console.
Module File Review