-
Notifications
You must be signed in to change notification settings - Fork 137
Getting Started Guide
Torque2D is a game engine for making 2D games. This guide takes you from downloading it to having a game of your own running on your computer — your art, your character, moving under your keys, colliding with things you placed, with a title screen you made in front of it and a HUD you built on top of it.
You do not need to know how to program. You will be writing code before the end of the first hour, but every line of it does something you can see.
This guide covers version 4.0 Early Access 4.
It comes in two halves, and the split is deliberate.
Act 1 — change a game that already works. Torque2D ships with a finished game called PlanetX. You play it, change it, break it on purpose, and then add a feature to it. You never sit in front of an empty file wondering what goes in it. By the end of act 1 you have written about fifteen lines of code and all fifteen did something you can see.
Act 2 — build one of your own. A new project, from nothing to something playable. Every step in act 2 has a matching file in PlanetX doing the grown-up version of the same job, and each step ends by naming it. The demo becomes your worked solution — permanently, long after you have finished the guide.
Work through it in order. Each step ends with something on screen, so if a step does not produce what it says it should, stop and sort that out before moving on. That is much easier than finding out three steps later.
- It does not teach C++, or ask you to compile anything. The download is a finished program. If you want to build the engine from its source code, that is Building, and nothing else here depends on it.
- It does not teach git. You will edit the demo directly. If you wreck it beyond repair, download it again.
- It is not a reference. There is no table here listing every field of anything. When you need one, the guide points at the page that has it.
- It does not cover everything. Networking, joints, ropes, terrain, saving player preferences and a dozen other things are all in the engine and all have their own pages. None of them is needed for a first game.
- A computer running Windows, macOS or Linux. Early Access 4 ships a ready-to-run build for all three — Windows x64, macOS on Apple Silicon, Linux x86_64. On anything else, including an Intel Mac, you build it yourself, which is Building and is not hard.
- A text editor. Game code in Torque2D is plain text. Notepad or TextEdit will do it; something with syntax highlighting, like Visual Studio Code, is nicer.
- About four hours, which you do not have to spend in one sitting. Act 1 is roughly an hour of it.
- Some art, eventually — and you do not have to make it. Download the Coin Collector art pack (4 MB): a character, a walk cycle, a coin, a crate, a ground texture and a title picture. Act 2 assumes you have it. Drawing your own is the better game and the worse way to spend the next four hours — so borrow these, finish the guide, and swap them out afterwards when the code is no longer the thing you are learning.
Act 1 — Change a game that already works
- Get it running, and play PlanetX
- Change one number, and feel it
- Open the console and make the engine talk back
- Give the spaceman a different gun
- Start a project of your own
- A world you can see
- Art on the screen
- Give the hero a name and a file
- Move it
- Coins to collect, blocks to get around
- Walking behind things
- Make it feel alive: an animation, a sound, a burst
- A screen in front of the game
- A HUD on top of the world
- Start the level over without leaking
Go to the Torque2D releases page and download the Early Access 4 build for your computer. There is one for Windows and one for macOS. Unzip it wherever you like — your desktop is fine.
Now open the folder it unzipped into. Inside, among a lot of other things, you will see a file
called main.cs and the program itself: Torque2D.exe on Windows, Torque2D.app
on macOS. Run the program.
Run it from the folder it came in. The engine looks for
main.cs, and for everything else it needs, in whatever folder it was started from. Drag the program out onto your desktop on its own and it will start up and find nothing.
A grid of cards appears. This is the Project Selector, and it is what Torque2D shows instead of starting a game: the engine does not contain a game, it runs one.
There are three cards. PlanetX is a complete little game, and it is the one you want. Toy Box is a shelf of thirty-odd small demonstrations, one engine feature each — worth a rummage later. New Project is how act 2 starts.
Each of those cards is a folder sitting next to the program. Open the Torque2D folder in
your file browser and there is a PlanetX folder and a toybox folder, right there. That is
the whole of the idea for now: a project is a folder, and the Project Selector shows you the
ones it finds.
Click PlanetX, then START 1 PLAYER.
Your rocket came down on the wrong side of the planet, and somewhere out there is the crystal you landed for.
| Control | Does |
|---|---|
| Arrow keys | move |
| Mouse | aim |
| Hold the left mouse button | fire |
| Escape | pause |
Your hull bar is top left and your heat bar is under it. The laser builds heat as you fire — redline it and the gun vents steam and locks until it cools, so shoot in bursts. Touch the crystal to clear the level. Every level is generated fresh, and each one is harder than the last.
Play for five minutes. Die a few times. (If you would rather use WASD, Escape → OPTIONS lets you rebind every key.)
Now the part that actually matters, and the only thing you need to carry out of this step: everything you just played is sitting in that folder as plain text, and you are about to change it.
Would you rather build the engine from its source code? You can — it is C++ and it is on GitHub. Building has the recipe for every platform. You do not need it for this guide, and it will not come up again.
In your file browser, open
PlanetX/PlanetXGame/scripts/player.cs
in your text editor. It is a long file, but you only want the top of it. Around line 16 you will find this:
$PlanetX::PlayerSpeed = 15;
$PlanetX::PlayerMaxHealth = 100;Change the 15 to 40. Save the file.
Now quit the game — Escape → QUIT — and start it again. Play.
The spaceman is much too fast, and you did that. Change it to 4 and he wades. Try a few
numbers until he moves the way you would want him to move.
Same idea, different files. All of these sit near the top of the file that uses them.
| File | Line | What it does |
|---|---|---|
scripts/player.cs |
$PlanetX::PlayerMaxHealth = 100; |
how much punishment the suit takes |
scripts/enemy.cs |
$PlanetX::AlienChaseSpeed = 7; |
how fast an alien comes at you |
scripts/enemy.cs |
$PlanetX::AlienAggroRadius = 20; |
how far away one notices you |
scripts/level.cs |
$PlanetX::MaxAliens = 90; |
how many are allowed on the planet at once |
Set AlienAggroRadius to 200 if you want to know what it feels like when the whole planet
notices you at the same moment.
Game code in Torque2D is text, and the engine reads it when it starts. There is no compiling, no build, no waiting. You change a file, you run the game, you see it. That edit-and-run loop is the loop for the rest of this guide.
Which is why you had to restart. The file had already been read by the time you saved it,
so the running game never saw your change. Quit, run again, and it does. You will not get
caught out by the engine using a stale copy of your file either: main.cs sets
$Scripts::ignoreDSOs = true, which tells it to prefer your text file over anything it
compiled earlier.
And without anyone making a lesson of it, you have now read some TorqueScript:
- A line starting with
//is a comment — a note for humans, ignored by the engine. - A name starting with
$is a global: one value, reachable from anywhere in the game.$PlanetX::PlayerSpeedis one name, not two; the::in the middle is a way of keeping PlanetX's globals grouped together and out of everyone else's way. - Statements end in a semicolon.
- And a value somebody expects to be fiddled with tends to sit at the top of the file that uses it, with a comment saying what it is for. That is a habit rather than a rule, but it is a good one, and you will see it everywhere in this codebase.
You already know how to change this game.
The language has a rulebook — TorqueScript Syntax — for when you want to look something up. You do not want it yet. Everything you need arrives in the step that needs it.
Start the game and get into a level. Now press Ctrl + Tilde (~).
A row of tabs drops over the game — the Console, the Project Manager, the Asset Manager editor and the Gui Editor. It always opens on the Console. Press **Ctrl + ~** again to put it away, and again to bring it back. That is the door to every tool in the engine, and you will be using it for the rest of the guide.
Most of the screen is the log, and PlanetX has been writing to it since you started. Scroll back to where the level began and you will find this:
PlanetX: difficulty for level 1 - hp 3 speed 7 damage 10 threshold 0.699999988
PlanetX: level seed 117650
PlanetX: terrain built in 529 ms
PlanetX: objectives 100.169853 apart
PlanetX: 52 bugs spawned
PlanetX: 3 brutes spawned
PlanetX: level 1 started - 111 scene objects
Your numbers will not be these, and that is the point of the second line. Every level is built from a fresh random seed, so the seed, how long the terrain took, how far apart the rocket and the crystal landed, how many aliens turned up and how many objects that adds up to are different every single time you press START. Only the shape of the block stays put.
None of that is the engine talking. Those lines are in PlanetX's own script files, put there by whoever wrote them so they could see what their game was doing. You will be adding lines exactly like them to your own game within the hour.
At the bottom of the screen is a box you type into. Type this and press Enter:
echo("hello");The console shows you what it ran, prefixed with ==>, and then the result:
==>echo("hello");
hello
echo() prints a value into the log. That is all it does, and it is the most useful
function in the engine. When something is not working, an echo in the middle of it telling
you what the code actually has in its hands is how you find out why.
Note that the console does not print an answer unless you ask for one. Typing
PlanetXScene.getCount(); on its own runs it and shows you nothing. Wrap it in an echo and
you get the number:
echo(PlanetXScene.getCount());That is how many objects are in the world right now — every alien, every rock, every bullet, every tile of ground.
Try this one:
PlanetXGame.level.player.setPosition("0 0");Nothing appears to happen, because the console is covering the game. Press **Ctrl + ~** and look: the spaceman has been teleported to the middle of the map.
Read that line from left to right. PlanetXGame is the game itself, and it has a name, so
you can type it straight in. It is holding the current level. The level is holding the
player. And
a player is an object in the world, so it will answer setPosition. That chain — a name, then
what it owns, then what that owns — is how you get at anything in a running Torque2D game, and
in act 2 you will build the equivalent of every link in it.
Here is one more, which ties back to step 2:
$PlanetX::PlayerSpeed = 40;Press Ctrl + ~ to get back to the game, then move. He is fast — no restart, no file, no save. The game reads that global every time you press a movement key, so changing it changes the next step he takes. Not every value in a game can be changed live like that, but plenty can, and finding out which is a good way to spend ten minutes.
Quit the game. Open scripts/input.cs and go to line 136 — the line that runs when you press
the up arrow. Misspell the last thing on it, so %p.updateVelocity(); becomes
%p.updateVeloctiy();. Save. Run the game, start a level, press the up arrow, and open the
console.
PlanetX/PlanetXGame/scripts/input.cs (136): Unknown command updateVeloctiy.
Object (1543) PlanetXPlayer -> CompositeSprite -> SceneObject -> ...
That is what a mistake looks like: the file, the line number in brackets, and what went wrong. Because you called it on an object, there is a second line naming the object — its id number, and the list of what it is, from the most specific description of it outwards. It is the same shape of message whether you misspelled a name, called something that does not exist, or asked an object for something it has never heard of.
You will have noticed the spaceman no longer walks upwards, either. A broken line does not stop the game; it just stops doing its job, and everything around it carries on as if nothing were wrong. That is why the console matters — the symptom on screen very rarely names the file.
Read the line number. Go to that line. Fix the spelling. Run it again. That is nine tenths of debugging, and you have now done all of it.
There is no other debugger. No breakpoints, no stepping through your code line by line — not any you are likely to use. The console is it, and it is genuinely enough: print what you have, look at the object, read the error. The Console Guide covers the rest of what it can do.
dump(), which prints everything an object currently believes about itself, is the one to read about today.
The blaster the spaceman lands with is a file of its own, and it is nineteen lines long. Open
PlanetX/PlanetXGame/scripts/blaster.cs:
//-----------------------------------------------------------------------------
// PlanetXBlaster: the standard-issue blaster the spaceman lands with. A concrete
// PlanetXWeapon - it runs the base weapon's setup, then dials in its own feel.
// This is the template for adding a new gun: copy the file, give it a class,
// and override the stats. The player never changes.
//-----------------------------------------------------------------------------
function PlanetXBlaster::onAdd(%this)
{
%this.init(); // base weapon: bullet pool, steam vent, heat loop, defaults
// The blaster's tuning.
%this.fireCooldown = 200;
%this.bulletSpeed = 40;
%this.bulletLife = 1200;
%this.heatPerShot = 0.13;
%this.heatDecayPerSecond = 0.32;
%this.heatResumeThreshold = 0.35;
}That is the entire gun. Everything that makes shooting work — the bullets, the steam, the overheating — is somewhere else. This file only says what kind of gun this one is.
So make a different one.
Copy blaster.cs, and call the copy shotgun.cs, in the same scripts folder. Open your
copy and change it to this:
//-----------------------------------------------------------------------------
// PlanetXShotgun: a slow, wide gun. The same weapon machinery as the blaster -
// only the numbers differ.
//-----------------------------------------------------------------------------
function PlanetXShotgun::onAdd(%this)
{
%this.init(); // base weapon: bullet pool, steam vent, heat loop, defaults
%this.fireCooldown = 700;
%this.bulletSpeed = 32;
%this.bulletCount = 5; // five bolts per shot
%this.spreadAngle = 9; // fanned nine degrees apart
%this.heatPerShot = 0.30;
}Two of those the blaster never set at all. bulletCount and spreadAngle were already there,
sitting at 1 and 10 in the shared setup — a fan of one bolt is just a bolt. The shotgun asks
for five.
A new file does nothing until something loads it. Open PlanetXGame/game.cs and find the list
at the top — twenty-seven lines, each one loading a script file. Add yours next to the blaster:
exec("./scripts/weapon.cs");
exec("./scripts/blaster.cs");
exec("./scripts/shotgun.cs"); // <-- your new file
exec("./scripts/bullet.cs");exec() reads a script file. That is the whole mechanism by which any of your code gets
into the game at all, and that list in game.cs is a game saying "here is everything I am made
of".
Open scripts/player.cs and find this, around line 101:
%this.weapon = new ScriptObject()
{
class = "PlanetXBlaster";
superclass = "PlanetXWeapon";
owner = %this;
};Change one word: PlanetXBlaster becomes PlanetXShotgun.
Run the game. Fire.
Five things happened there, and they are worth separating.
function ClassName::method(%this) puts a method on a class. That is what all those double
colons in this codebase have been. PlanetXShotgun::onAdd means "the onAdd belonging to
PlanetXShotgun", and %this is the object it was called on. You will write a great many of
these.
One class per file, and the file named for the class. You just made one by copying one.
shotgun.cs holds PlanetXShotgun and nothing else.
class and superclass are how one kind of thing is built on another. The shotgun's own
settings are in PlanetXShotgun; everything it shares with every other weapon is in
PlanetXWeapon. Which brings us to the first line:
%this.init();Half of that line is the engine and half of it is a choice, and it is worth knowing which is
which. The engine part: when an object has both a class and a superclass, only the most
derived onAdd runs — yours. The superclass's onAdd does not also fire. So if the shared
setup is going to happen, your onAdd has to ask for it. The choice: calling that shared
setup init is a name this codebase picked. It could have been setUp, or common, or
anything else. Do not go looking for init in an engine reference; it is not there.
The gun changed and the player did not. Look back at what you edited: a new file of your
own, one line in a list, and one word in the player. Nothing about shooting was touched. The
player has a weapon, calls startFiring and stopFiring on it, and genuinely does not know or
care which gun it is holding. That is the payoff, and it is worth a pause — you changed the
gun without touching the player.
You have now seen one file holding one class, a general class with a specific one built on top of it, and setup living next to the thing it sets up. It would be easy to read that as the way TorqueScript has to be written. It is not.
The engine does not care. You can put every function in your game in one file, in any order, with no classes at all, and it will run exactly the same. Plenty of games have been written that way. The reason to split things up is not that the engine demands it — it is that in six months you will want to change how the gun works, and you will need to be able to find it. A file called
shotgun.cswith the shotgun in it is easy to find. A two-thousand-line file with the shotgun somewhere in the middle is not.
The honest reason to care today is smaller and more useful than that: the games that ship
with Torque2D are mostly laid out this way. PlanetX is, and so is most of the Toy Box. Once
you can recognize the shape — a file per class, onAdd doing the setup, a general class holding
what is shared — you can open any file in any of them and know where to look. Act 2 builds its
game the same way, for that reason.
Under the hood. When an object is created with a
classand asuperclass, the engine strings the names into a chain — the built-in type, then the superclass, then the class, then the object's own name if it has one — and looks up every method call along that chain, most specific first. That is why youronAddwins over the weapon's, why%this.startFiring()on a shotgun findsPlanetXWeapon::startFiringwithout you doing anything, and why a method you put onPlanetXShotgunquietly replaces the shared one of the same name.
In PlanetX.
scripts/weapon.cs(265 lines) is the class you just built on: the pool of bullets it reuses instead of making new ones, the steam vent that fires when the gun overheats, and the heat loop that ticks the temperature back down. It is worth reading now that you know what calls it — you have met every one of the values it sets.
Here is the send-off out of act 1, and it is optional. The guide carries on either way.
Give the shotgun a magazine instead of heat. Six shells, then a reload it has to sit out before it can fire again.
- Everything you need is machinery you have just watched work. A counter that goes down by one
on each shot. A check that refuses to fire when the counter reaches zero. A
schedule()— the engine's "do this again in 1500 milliseconds" — to put the shells back. The heat loop inweapon.csis those same three ideas wearing a different hat, so read it if you get stuck. - A method you write on
PlanetXShotgunis used instead of the one of the same name onPlanetXWeapon. That is the hook. - There is no right version. Six shells or twelve, a two second reload or half a second, a reload that starts by itself or one that waits for you to press a key — those are game design decisions, and they are yours to make.
- The way to find out which is right is to build one and play it.
This is the first thing in the guide that comes with no answer at the end. That is the point of it.
Act 1 was somebody else's game. This half is yours: a top-down game called CoinCollector, where you walk a character around, collect coins and cannot walk through walls, with a title screen in front of it and a counter on top of it.
Every step here has a matching file in PlanetX doing the grown-up version of the same job, and each one ends by naming it. When your version works and you want to see how far the idea goes, that file is waiting.
Press **Ctrl + ~**, then Torque2D → Close Project. You are back at the Project Selector. Click the New Project card.
Six boxes. Fill them in like this:
| Box | What to type | Why |
|---|---|---|
| Title | Coin Collector |
The name on the card. Written for people, so the space is fine. |
| Directory | CoinCollector |
The folder to make, inside your Torque2D folder. It must be empty or not exist yet. |
| Game Core | Blank Game |
The template to copy. Blank Game gives you a window, a background and some music. |
| Module Name | CoinCollectorGame |
Fills itself in from the Title, minus the space. Leave it. |
| Author | your name | Optional. |
| Description | Collect the coins. |
Shows under the title on the card. |
Press Create, then click your new card. A background appears and music plays. That is the blank game, and it is running.
You typed three slightly different things — Coin Collector, CoinCollector, and the box below
them said CoinCollectorGame. That is not a mistake, and it is worth thirty seconds now because
you will type the last one hundreds of times.
Coin Collector is the title. It is written for people and it is the only one of the three
allowed a space, because the only thing it does is appear on the card.
CoinCollector is the folder. It has to be a legal folder name, which is why the space went.
CoinCollectorGame is the game module inside it, and that name has jobs the other two do not:
it is the folder your game code lives in, it is the front half of every piece of art you refer to
(CoinCollectorGame:hero), and it is the name your game's start-up function hangs off. It has to
survive being used as a script identifier, so it is letters and numbers only — which is exactly
why the dialog stripped the space when it filled the box in for you. PlanetX does the same thing:
the project is PlanetX, the game module is PlanetXGame.
Open the CoinCollector folder in your file browser, next to PlanetX:
CoinCollector/ PlanetX/
├── AppCore/1/ ├── AppCore/1/
├── Audio/1/ ├── Audio/1/
├── themes/ ├── ScreenFade/1/
└── CoinCollectorGame/ ├── themes/
└── PlanetXGame/
PlanetX is your project plus one extra folder and a lot of files. That is the whole difference. Whatever you are looking at in the demo, there is a place for the same thing in yours.
Each of those folders — except themes — is a module.
Two words, now that you own four examples of one of them.
A module is a folder with a module.taml file in it. Open
CoinCollector/CoinCollectorGame/module.taml in your text editor. Six things in it matter:
ModuleId="CoinCollectorGame" the module's name — the front half of every asset id
ScriptFile="game.cs" which file to run when this module loads
CreateFunction="create" which function to call in it: CoinCollectorGame::create
Group="launch" ← see below
Dependencies="Audio=1" load the Audio module first, this one needs it
<DeclaredAssets .../> where this module's art, sound and effects live
That last one gets a step of its own (step 7). The rest is the whole idea: a folder that says what it contains and what to run.
A project is a folder with an AppCore module in it. That is literally the test the Project
Selector applies to every folder next to the program. AppCore starts everything up — it makes
the window, loads the theme your buttons will use, and then loads your game.
-
Group="launch"is what makes AppCore start your module. Nothing else does. A module without it sits on disk being ignored. -
There is no Play button, and you will look for one. The game is already running; Ctrl + ~
only hides the tools. But a change to a
module.tamlis read at start-up, so that one needs Torque2D → Close Project and a fresh pick of your card.
Under the hood. Everything the New Project dialog did, you could have done by hand: it copied three modules out of
library/, copied thethemesfolder in, wrote your title into AppCore'smodule.taml, and then walked your game module rewriting every occurrence ofBlankGametoCoinCollectorGame— in the folder name, inmodule.taml, and in the script. That last part is why renaming a module later is a job for the Project Manager and not for a text editor: the name lives in three places that all have to agree.
In PlanetX.
PlanetXGame/module.tamlis the same file with two more dependencies and six declared-asset entries instead of eight.
Where to go next with this: the Project Manager Guide covers the rest of that tab — adding a module of your own, editing one, dependencies, declaring where assets live, and importing modules from the library.
Open CoinCollector/CoinCollectorGame/game.cs. It is fourteen lines and it is the whole blank game.
Replace it with this:
function CoinCollectorGame::create(%this)
{
exec("./scripts/level.cs");
%this.level = new ScriptObject() { class = "Level"; };
}
function CoinCollectorGame::destroy(%this)
{
if (isObject(%this.level))
%this.level.delete();
}Make a folder called scripts inside CoinCollectorGame, and in it a file called level.cs:
//-----------------------------------------------------------------------------
// Level: builds the world and everything in it, and owns what it builds.
//-----------------------------------------------------------------------------
function Level::onAdd(%this)
{
// The look every control in this level wears. See below.
%theme = $CoinCollector::Theme;
// The world. Top-down, so there is no "down" for things to fall toward.
new Scene(GameScene);
GameScene.setGravity(0, 0);
// The screen. Everything the player sees hangs off this one control.
new GuiControl(GameRoot)
{
Profile = %theme.getProfile("empty");
HorizSizing = "relative";
VertSizing = "relative";
Position = "0 0";
Extent = "1024 768";
};
// The camera: a control that shows you part of the world.
new SceneWindow(GameWindow)
{
Profile = %theme.getProfile("empty");
HorizSizing = "relative";
VertSizing = "relative";
Position = "0 0";
Extent = "1024 768";
};
GameRoot.add(GameWindow);
GameWindow.setScene(GameScene);
GameWindow.setCameraSize(60, 45);
Canvas.setContent(GameRoot);
}Close the project, pick your card again, and you get a black rectangle. That is not much, but it
is a world, a camera and a screen, and they are yours. Make it obvious by giving the canvas a
color — add this at the end of Level::onAdd:
Canvas.BackgroundColor = "MidnightBlue";Then open the console and type:
GameScene.setDebugOn("fps");A frame counter appears in the corner. The world is running. There is nothing in it yet.
This is the arrangement that confuses everyone, so here it is in the only order that works.
Canvas is the window. There is one of it, the engine made it, and it holds exactly one thing
at a time — which is why Canvas.setContent(GameRoot) is the last line. It is measured in
pixels.
SceneWindow is a camera. It is a control like any other — it sits on the screen, it has a position and a size in pixels — but what it draws is a view of a world.
Scene is the world. It is measured in world units, not pixels. setCameraSize(60, 45)
does not mean 60 pixels; it means sixty units of world fit across this window. Make a sprite
6 6 and it is a tenth of the screen wide, whatever resolution the player is running.
Two smaller things went past without comment:
-
Gravity is a vector, and a top-down game sets it to zero.
setGravity(0, 0)says there is no down. Leave it out and everything you place slides off the bottom of the world. -
Naming an object is how you reach it from anywhere.
new Scene(GameScene)gives the scene the nameGameScene, and from that point on any file — and the console — can sayGameScene.something()with no variable to carry around. PlanetX names its scene and window the same way,PlanetXSceneandPlanetXWindow. Step 9 leans on this hard. -
%theme.getProfile("empty")is how a control gets its look. A profile is one control's appearance — fill, border, font. They come in a theme, the matched set copied intoCoinCollector/themesin step 5, and you ask a theme for one by category:"empty"draws nothing at all, which is what you want for a container and for a camera. Others you will meet are"label","panel","button"and"progress".
That $CoinCollector::Theme needs to exist before any of this runs, so put it at the top of
game.cs, above the functions:
/// The look this game wears. AppCore reads every .taml in CoinCollector/themes and
/// each one becomes an object named after itself, so this is the one place the
/// game says which theme it uses.
$CoinCollector::Theme = "Base";Base is the stock theme your project was created with. When you make your own in step 13, this
one line is what changes — not every control in every file.
Why not just write the profile's name? You can:
Profile = "BaseLabelProfile";works today. It stops working the moment you make your own theme, because that name belonged to the old one — and it does not fail loudly. The control quietly falls back to the engine's bare default, which for a label means unstyled text and for a progress bar means an invisible bar. Asking a theme for a category instead survives being rethemed. The GUI Guide covers this in two paragraphs if you want the longer version.
Drag the corner of the window and your world stretches. Everything goes oval. That is not a
bug you have introduced — it follows from what you just wrote. setCameraSize(60, 45) says fit
sixty by forty-five units into this window, and if the window stops being the shape those numbers
assume, sixty by forty-five gets squashed into the new shape to fit.
The fix is to stop treating both numbers as fixed. Keep the height and work the width out
from the window's shape, so resizing shows more or less of the world instead of distorting it. Add
this to the bottom of level.cs:
/// Engine callback: fires on every live window resize.
function GameWindow::onExtentChange(%this)
{
%extent = Canvas.extent;
%aspect = %extent.x / %extent.y;
%camera = %this.getCameraSize();
%camera.x = %camera.y * %aspect;
%this.setCameraSize(%camera);
}Run it and drag the window around. Wide window, more world across; tall window, less. Nothing stretches.
Three things are worth noticing, because you will use all of them again:
-
onExtentChangeis an engine callback, likeonAdd. Nobody calls it; the engine does, every time the control's size changes. You are not polling anything. -
GameWindow::works because you named the window.new SceneWindow(GameWindow)in the code above is what madeGameWindowsomewhere to keep methods. Step 8 is entirely about this and will say it properly — for now, take it that naming a thing lets you write methods on it. -
A vector answers to
.xand.y.Canvas.extentis the string"1024 768", and%extent.xis 1024. Handy, and used everywhere in the demo games.
In PlanetX. The same method, in its own file —
sceneWindow.cs, 21 lines. Yours is four lines shorter because PlanetX also stops the camera showing you the edge of the world.
Early Access 4 has no scene editor and no level editor. There is no window where you drag a sprite into the world and it stays there. Everything in the world is put there by script, which is what the rest of act 2 does.
This is worth saying plainly so you stop looking for one. It is also less of a loss than it sounds: PlanetX generates every level it plays from a random seed, so even with an editor it would place nothing by hand.
Hands off to: the Scene Guide for what else a scene can be told, and the GUI Guide's SceneWindow section for the camera.
In PlanetX.
level.cs::buildScene(35 lines) is this exact code with three more lines: sorting, and a limit that stops the camera showing you the edge of the world.
If you have not already,
download the art pack
and unzip it somewhere. Six PNGs: hero, heroWalk, coin, block, background and title.
The rest of this guide names those files, so following along is easier with them — and every one
of them is meant to be thrown away and replaced with yours later. The end of this guide comes
back to that.
Copy hero.png into:
CoinCollector/CoinCollectorGame/sprites/
That folder already exists, and where you put it matters — more on that in a moment.
Now press **Ctrl + ~** and pick the Asset Manager tab. This is the Asset Manager editor, and it is where the content of your game gets made.
Go to File → New Asset → Image Asset…
Three boxes:
-
Image File — press the button and pick your
hero.png. -
Asset Name — fills itself in as
herothe moment you choose the file. Leave it. -
Target Module — fills itself in as
CoinCollectorGame_1_1, and you cannot type in it. It is worked out from where the file sits. A picture inside your game module's folder belongs to your game module; a picture on your desktop belongs to nothing, and Create stays grayed out.
Press Create. The asset appears in the library on the left.
Now add one line to Level::onAdd, just before Canvas.setContent:
GameScene.add(new Sprite() { Image = "CoinCollectorGame:hero"; Size = "6 6"; });Close the project, open it again. Your drawing is in the middle of your world.
Your hero is standing on nothing. Draw a background — one wide picture, a whole screenful — and
save it into sprites/ as background.png.
Make an image asset from it exactly as you did for the hero: File → New Asset → Image Asset…, choose the file, press Create. The same three boxes, and no surprises this time.
Now put it in the world. Add this line underneath your hero line:
GameScene.add(new Sprite() { Image = "CoinCollectorGame:background"; Size = "60 45"; });Size = "60 45" is not a guess. Step 6 said setCameraSize(60, 45), so a sprite sixty units wide
and forty-five tall is exactly one screenful — it fills the view and stops.
Close the project, open it again. Your hero is gone.
He was not deleted. He is behind the background.
Both sprites went into the world at the same depth, so the engine fell back on the only other thing it had to go on: which one was made last. The background was made last, so the background draws in front — and it is exactly the size of the screen, so it covers him completely.
A scene has thirty-two layers, numbered 0 to 31, and every object sits on one. The engine draws layer 31 first and layer 0 last, which means:
- A bigger number is further back. Layer 31 is the far distance; layer 0 is pressed up against the glass.
- Everything starts on layer 0 unless you say otherwise — which is why your two sprites ended up fighting over the same depth.
Send the background to the back:
GameScene.add(new Sprite()
{
Image = "CoinCollectorGame:background";
Size = "60 45";
SceneLayer = 30; // a big number is a long way back
});Open it again. Hero on top of background, which is what you wanted a minute ago.
The lesson is not "backgrounds need a layer". It is that two objects on the same layer are ordered by which was created last, and that is a miserable thing to depend on. It works right up until something is created in a different order — a coin picked up and replaced, a level built by a loop that runs the other way — and then things draw in front of other things for no reason you can see. Anything whose depth you actually care about belongs on a layer of its own.
Two smaller things while you are here:
- The background cannot be touched. It has no collision shape, so your hero walks over it rather than into it. Nothing in Torque2D collides until you give it a shape by hand — step 8 does that for the hero, step 10 for the coins and blocks.
- Layers are depth, not grouping. Two objects sharing a layer are at the same distance from the camera and nothing more.
In PlanetX. The same idea, with the numbers given names.
scripts/level.csopens with:$PlanetX::TileLayer = 30; $PlanetX::EntityLayer = 20; $PlanetX::BulletLayer = 12; $PlanetX::EffectLayer = 10;Terrain at the back, the spaceman and the aliens in front of it, bullets in front of them, explosions in front of everything. None of those numbers is an engine rule — they are four choices made once and then used everywhere, so that no file has to remember what
30meant. Your game is small enough to write30with a comment beside it. When it stops being small, do what PlanetX did.
Everything you put on the screen from here follows this same three steps: a picture in a declared folder, an asset made from it, and an id your code refers to. Step 13 does it once more for the title screen.
You did not write a file path anywhere. You wrote CoinCollectorGame:hero and
CoinCollectorGame:background, and that is how art is referred to everywhere in Torque2D:
ModuleId:AssetName.
What actually happened is that the dialog wrote a small file called hero.image.taml next to your
PNG. That file is the asset — it says which picture to use and how to treat it. The PNG is
just a picture; the asset is the thing your game can name.
Referring to art by id rather than by path is worth the extra concept. Move the PNG to a different folder and every sprite still works. Swap the picture for a better one and nothing in your code changes.
Two things are called the asset manager, and only one is a program. The Asset Manager editor is this tab. The asset system is the machinery underneath it — the part of the engine that finds asset files, loads them and hands them out by id. Your game uses the second one every time it says
CoinCollectorGame:hero, and never touches the first. This guide always says "the Asset Manager editor" when it means the tab.
Your new asset is registered right now, but it only survives a restart if your module says that folder is somewhere assets live — and says it with a matching filename ending.
That is what the <DeclaredAssets> block in module.taml is. Your game module ships with eight
of them, and one of them reads:
<DeclaredAssets Path="sprites" Extension="image.taml" Recurse="true"/>
Look in sprites, and in folders inside it, for files whose names end in image.taml. Your
hero.image.taml is in sprites and ends in image.taml, so it is found. That is the only
reason step 7 worked.
Now the failure, because it is silent and it will happen to you:
- Put your art in a folder nobody declared, and it is not found.
- Or let a file be called
hero.asset.tamlwhere the declaration saysimage.taml, and it is not found.
There is no error, no warning, nothing in the console. Your sprite comes up as a plain white square, which looks like a problem with the picture and is not. When that happens, check the declared assets first. The Project Manager's declared-assets form is where you fix it.
Hands off to: the Asset Manager Editor Guide for the rest of the tab — finding assets, saving, and the other four kinds. The ImageAsset Guide covers cutting a picture into frames, which is how one file holds a whole sprite sheet. The Asset Manager Guide documents the asset system itself — it is a reference rather than a tutorial, so go there with a question.
Right now the hero is one line inside the level, and everything about it is in that line. Give it a name and a file of its own, so it has somewhere to grow.
The name first. In Level::onAdd, the sprite loses everything it was saying about itself and
gains a word in brackets:
GameScene.add(new Sprite(TheHero) { Position = "0 0"; });new Sprite(TheHero) makes a sprite called TheHero. You have used names like that already —
GameScene and GameWindow got theirs the same way in step 6, and step 3 had you type
PlanetXGame.level.player.setPosition("0 0") into the console.
But a name does a second thing, and it is the point of this step: an object's name is also where
its methods live. Make scripts/hero.cs:
//-----------------------------------------------------------------------------
// TheHero: the character the player moves. There is one of it, so it is reached
// by name.
//-----------------------------------------------------------------------------
function TheHero::onAdd(%this)
{
%this.setSize("6 6");
%this.setImage("CoinCollectorGame:hero");
// A round body, so corners do not catch on things. Smaller than the picture
// on purpose: a character whose shape covers every pixel of their art snags
// on things that looked like a clear gap.
%this.createCircleCollisionShape(2);
// Never spin. Being shoved by physics should not leave the art at an angle.
%this.setFixedAngle(true);
}Nothing joins those two pieces of code except the word TheHero. The sprite is called that, so
TheHero::onAdd is its onAdd.
And — this is the part that will go wrong — game.cs gains a line:
function CoinCollectorGame::create(%this)
{
exec("./scripts/level.cs");
exec("./scripts/hero.cs"); // <-- the new file, or none of it runs
%this.level = new ScriptObject() { class = "Level"; };
}Run it. Nothing on screen changes. That is the point of the step.
Two facts, and they are facts rather than opinions:
A name gives an object somewhere to keep methods. When the engine registers an object it
builds a chain of places to look methods up in: the built-in type it was made from, then a
superclass if it has one, then a class if it has one, and last of all its own name. Most
specific wins. Your sprite has no class and no superclass, so its chain is Sprite with TheHero
on the end. TheHero.setSize(...) finds Sprite's; TheHero.onAdd() finds yours.
onAdd then runs by itself. Nobody calls it. The engine makes the object, links that chain,
looks along it for an onAdd, and runs the one it finds. That is a real engine callback, and it
is the hook everything below hangs off.
Fields set in the new{} block are applied before onAdd runs. So Position = "0 0" is
already true by the time your code starts, and onAdd can read anything you passed in. That is
how you hand values to something as you make it.
A name is unique. There is exactly one object called TheHero, and naming a second one the
same thing does not get you two heroes — the engine refuses the name and says so:
SimObject::assignName() - Attempted to set object to name 'TheHero' but it is already assigned to another object.
For a hero that is exactly right, and it is what lets any file — and the console, and a key
binding — reach it without anything having to pass it around. For a coin it is no use at all,
because you are going to want twelve of those. Step 10 is where the other way in arrives: a
class, which any number of objects can share.
Naming it costs you something, and it is worth knowing what before you build on it. If you later
decide you want two heroes, the name has to go, and a class takes its place. From that moment
there is no name to type, so whatever wants to reach a hero has to be handed one — usually by
whatever made it, the way Level is about to hold on to %this.input.
Whatever makes it decides where it goes. The thing itself decides what it is.
The level says a hero, at the middle. The hero says this is what a hero looks like and how big it is. A hero can now be made from anywhere in one line, and when one looks wrong there is exactly one file to open.
Nothing stops you putting all of it back in the level. This is a way of dividing up the work, not a rule — step 4 already made that argument and this is the same idea applied to a sprite.
This is the first file of your own you have made, and forgetting the exec line is the mistake
almost everybody makes. Learn the symptom before the cause, because the symptom does not look
like the cause at all:
Your sprite comes up as a plain white square, or as the right picture with none of its settings. There is no error. There is no warning. Nothing appears in the console.
What happened is that TheHero::onAdd was never loaded, so the sprite called TheHero ran no
onAdd at all — and an object asked for a method nobody ever defined is not an error in
TorqueScript, it is a no-op.
The rule: every .cs file you add gets a line in your module's create function. You did this
once already, in step 4, when you added exec("./scripts/shotgun.cs"); to PlanetX's list. Go and
look at that list again now — PlanetXGame/game.cs opens with twenty-seven exec lines and
nothing else. That file should suddenly make sense.
Hands off to: the Sprite Guide and, more usefully,
SpriteBase Guide — Image, Frame and Animation actually
live on that one, and it is where you will look for them and not find them otherwise. Underneath
both is the SceneObject Guide, which is everything any object
in the world can be told.
In PlanetX.
game.csmakes one of these and explains itself as it does:// Naming it is all it needs - the name is already the namespace upgrades.cs // writes its methods in, so a class saying the same word again would say nothing. new ScriptObject(PlanetXUpgrades);Every one of the 157 lines of
scripts/upgrades.csis aPlanetXUpgrades::method, and the wordclassappears nowhere in it.PlanetXSceneandPlanetXWindoware named for the same reason.
The hero has a name, and this is the step where that pays for itself.
Start with a controller that owns the keys. Make scripts/input.cs:
//-----------------------------------------------------------------------------
// Input: the level's keyboard controller. Builds a key map in onAdd and takes
// it away again in onRemove.
//-----------------------------------------------------------------------------
function Input::onAdd(%this)
{
%this.map = new ActionMap();
%this.map.bindCmd("keyboard", "w", "TheHero.moveUp(1);", "TheHero.moveUp(0);");
%this.map.bindCmd("keyboard", "s", "TheHero.moveDown(1);", "TheHero.moveDown(0);");
%this.map.bindCmd("keyboard", "a", "TheHero.moveLeft(1);", "TheHero.moveLeft(0);");
%this.map.bindCmd("keyboard", "d", "TheHero.moveRight(1);", "TheHero.moveRight(0);");
%this.map.push();
}
function Input::onRemove(%this)
{
if (isObject(%this.map))
{
%this.map.pop();
%this.map.delete();
}
}The level makes one, at the end of Level::onAdd:
%this.input = new ScriptObject() { class = "Input"; };And the hero learns to move. Add this to hero.cs, and a speed at the top of the file:
$CoinCollector::HeroSpeed = 12;function TheHero::moveUp(%this, %val) { %this.inUp = %val; %this.updateVelocity(); }
function TheHero::moveDown(%this, %val) { %this.inDown = %val; %this.updateVelocity(); }
function TheHero::moveLeft(%this, %val) { %this.inLeft = %val; %this.updateVelocity(); }
function TheHero::moveRight(%this, %val) { %this.inRight = %val; %this.updateVelocity(); }
/// Turn four held keys into one direction, and move at a steady speed along it.
function TheHero::updateVelocity(%this)
{
%x = %this.inRight - %this.inLeft;
%y = %this.inUp - %this.inDown;
if (%x == 0 && %y == 0)
{
%this.setLinearVelocity(0, 0);
return;
}
%length = mSqrt(%x * %x + %y * %y);
%this.setLinearVelocity(%x / %length * $CoinCollector::HeroSpeed,
%y / %length * $CoinCollector::HeroSpeed);
}Add exec("./scripts/input.cs"); to game.cs — you knew that was coming — and run it. WASD
moves your hero.
bindCmd(device, action, makeCmd, breakCmd) takes two strings of script: one run when the key
goes down, one when it comes up. Either may be empty. (The name is bindCmd rather than bind
precisely because two commands are being handed over.)
Those strings are evaluated exactly as if you had typed them into the console. Which is why
naming the hero in step 8 mattered: "TheHero.moveUp(1);" works for the same reason
PlanetXGame.level.player.setPosition("0 0"); worked in step 3. Anything with a name can be
reached from a string.
And notice the name doing both of its jobs in the same four characters. It finds the object,
because a name is how you find anything. It is also where the method lives, because
TheHero::moveUp is written in the namespace that name created. That is the whole return on
having named a one-of-a-kind object: one word, both halves, and nothing to pass around. There is
nothing else special about moveUp — it is an ordinary method that happens to have been called
from a key binding.
The engine binds its own keys this way. Here is editor/EditorCore/EditorCore.cs, line 47:
%this.editorKeyMap.bindCmd( "keyboard", "ctrl tilde", "EditorCore.toggleEditor();", "" );That is Ctrl + ~ — the key you have been pressing since step 3. An empty break command, and a method called on a named object. Your four lines and that one line are the same line.
%val is 1 when the key goes down and 0 when it comes up. That is why the two command strings
differ by a single character, and why one method handles both.
Held flags, not events. updateVelocity does not care which key just changed; it reads all
four flags and works out one direction. That is why holding W and D moves you diagonally at the
same speed as holding W alone, and why letting go of one of two keys does the sensible thing
instead of stopping you dead.
And the first place ownership bites. onAdd pushed the map, so onRemove has to pop it. Leave
a map pushed after its level is gone and the keys go on firing into an object that no longer
exists. Step 15 is about exactly this.
Hands off to: the Input Guide — what every key and device is called, plus the mouse and touch.
In PlanetX.
scripts/input.cs(399 lines) uses the other form:bind, which calls a global function by bare name. It has to, because its keys are rebindable and it rebuilds the whole map whenever they change. Both forms are normal;bindCmdis the shorter road when the binding is fixed.player.cs::updateVelocityis the same six lines you just wrote.And its spaceman has no name. It is
class = "PlanetXPlayer", because PlanetX has a two-player mode — which is step 8's trade-off happening for real. Two spacemen means neither can own the name, so one class makes both and the level holds on to them as%this.playerand%this.player2. That is why its key handlers writePlanetXGame.level.player2where yours writeTheHero: with no name to type, something has to hand the object over. The two are told apart by what the level passes in as it makes them:%player = new CompositeSprite() { class = "PlanetXPlayer"; Position = %position; playerIndex = %index; idleImage = %idleImage; // player 2 wears different colors walkAnim = %walkAnim; aimMode = %aimMode; // and aims a different way };
Two new classes, and the pair is the lesson: they are almost the same file, and the one line that differs is what makes one a pickup and the other a wall.
You will want two more PNGs — a coin and a block. Save them into sprites/ and make an image
asset for each, exactly as in step 7.
scripts/coin.cs:
//-----------------------------------------------------------------------------
// Coin: a pickup. Static, round, and a SENSOR - it reports being touched
// without blocking anything.
//-----------------------------------------------------------------------------
function Coin::onAdd(%this)
{
%this.setSize("1.5 1.5");
%this.setImage("CoinCollectorGame:coin");
%this.setBodyType("static");
%this.createCircleCollisionShape(0.75);
%this.setCollisionShapeIsSensor(0, true); // <-- touch me, do not stop me
%this.setCollisionCallback(true); // <-- or onCollision never fires
%this.pulse();
}
/// Cancel the pulse so no leftover timer survives the coin.
function Coin::onRemove(%this)
{
if (isEventPending(%this.pulseEvent))
cancel(%this.pulseEvent);
}
/// A slow shimmer, so a coin reads as a coin from across the screen.
function Coin::pulse(%this)
{
%this.bright = !%this.bright;
if (%this.bright)
%this.setBlendColor(1, 1, 1);
else
%this.setBlendColor(1, 0.85, 0.4);
%this.pulseEvent = %this.schedule(500, "pulse");
}
function Coin::onCollision(%this, %object, %details)
{
if (%object.getName() $= "TheHero")
{
CoinCollectorGame.coinCollected();
%this.safeDelete();
}
}scripts/block.cs is the same shape of file with the sensor line taken out and a box instead of
a circle. It has no onCollision at all — it does its job by existing:
//-----------------------------------------------------------------------------
// Block: a solid wall. It blocks, and it says nothing.
//-----------------------------------------------------------------------------
function Block::onAdd(%this)
{
%this.setSize("4 4");
%this.setImage("CoinCollectorGame:block");
%this.setBodyType("static");
%this.createPolygonBoxCollisionShape(4, 4);
}Both of those files say class, and neither has a name. That is not a change of style; it is the
only option available.
A coin cannot be named, because a name belongs to exactly one object and you want twelve. So
the twelve share a class instead, and class = "Coin" puts Coin::onAdd on the end of each
one's lookup chain in precisely the place TheHero sits on the hero's. Same mechanism, same
result — onAdd runs, onCollision runs — reached by a word twelve objects share rather than by
a word that belongs to one.
That is the whole choice, and it is decided by counting:
- One of them, forever? Name it. The hero, the scene, the camera window. Anything can reach it by typing its name, including a key binding and the console.
-
Many of them, or a number you will not know until the game is running? Give them a class.
Nothing can reach a particular one by name, so whatever needs one has to be handed it — which is
what
%objectis inCoin::onCollision, and why the coin asks the thing that hit me for its name rather than looking the hero up.
In Level::onAdd, after the hero:
// Blocks first. The coins need to know where they are.
for (%i = 0; %i < 6; %i++)
{
GameScene.add(new Sprite()
{
class = "Block";
Position = getRandom(-25, 25) SPC getRandom(-18, 18);
});
}
%this.coinsTotal = 12;
for (%i = 0; %i < %this.coinsTotal; %i++)
{
GameScene.add(new Sprite()
{
class = "Coin";
Position = %this.findClearSpot();
});
}Both loops throw things at random spots, and sooner or later a coin lands inside a crate. It cannot be reached, the counter never fills, and the game quietly becomes unwinnable — which a player will read as a bug in your game, because it is one.
So ask the world whether a spot is free before using it:
/// A random spot with nothing already in it. Falls back to the last one tried
/// rather than looping forever, because a level can genuinely be too crowded.
function Level::findClearSpot(%this)
{
for (%try = 0; %try < 30; %try++)
{
%spot = getRandom(-25, 25) SPC getRandom(-18, 18);
// "collision" means: only tell me about things with a collision shape.
if (GameScene.pickCircle(%spot, 1.5, "", "", "collision") $= "")
return %spot;
}
return %spot;
}pickCircle asks the scene what is inside a circle and hands back a list of objects — empty
if there is nothing there. It is the same question the engine asks itself constantly, made
available to you, and you will want it again for "what is near the player", "what did the
explosion catch", "can I put a building here".
Three things about that call are worth keeping:
-
The
"collision"at the end is doing real work. Without it the query answers about everything the circle touches, which includes your background sprite — sixty units wide, covering the entire level, and therefore intersecting every spot you will ever test. Nothing would ever be free. With it, only objects that have a collision shape answer, and the background never had one. - The two empty strings are skipped filters — scene groups and scene layers, which you are not using. Empty means "do not care".
- The loop gives up after thirty tries. A guard like that is not pessimism, it is the difference between a crowded level and a frozen game. Make the crates big enough or numerous enough and there genuinely is nowhere left to stand.
And game.cs needs somewhere for the coin to report to, plus the two new exec lines:
function CoinCollectorGame::create(%this)
{
exec("./scripts/level.cs");
exec("./scripts/hero.cs");
exec("./scripts/input.cs");
exec("./scripts/coin.cs");
exec("./scripts/block.cs");
%this.coinsTaken = 0;
%this.level = new ScriptObject() { class = "Level"; };
}
function CoinCollectorGame::coinCollected(%this)
{
%this.coinsTaken++;
echo("coins:" SPC %this.coinsTaken);
}Run it, and walk around. The coins vanish when you touch them. The blocks do not, and you cannot walk through them. That is a game.
This is Box2D — the physics engine inside Torque2D — at exactly the depth a top-down game needs.
A sensor reports a touch without blocking it. A solid shape blocks and says nothing. One line of difference, two behaviors, both on your screen at once.
Body type. static means never moves, and nothing can shove it. That is right for both a
coin and a wall. The other one worth knowing is dynamic, which is everything the physics engine
is allowed to push around — your hero is one, which is why blocks can stop it.
setCollisionCallback(true) is what makes onCollision fire at all. Leave it out and nothing
happens, silently, for as long as you care to stare at it. This costs people hours. The block is
the useful opposite: it needs no callback, because it never wants to be told anything.
A collision shape is not the picture. The two are separate, and nothing keeps them in step. Turn this on in the console:
GameScene.setDebugOn("collision");Now you can see the shapes, and right now they are blunt: a circle around the whole hero, a box
over the whole crate. A coin whose circle is twice the size of its art picks itself up from a
stride away, and that looks exactly like a bug in your collision code. It is not; it is a number.
Step 11 is where these get shaped properly, and it is worth leaving this turned on until then. setDebugOn also takes aabb, wireframe and fps, singly or several at once, and it
is the best debugging tool in the engine.
Two more things you used without being told:
-
getRandom(min, max)gives you a whole number between the two, inclusive. Hand it one argument and you get 0 to that; hand it none and you get a fraction between 0 and 1. -
safeDelete(), notdelete(). The coin is removing itself from inside a collision — that is, from inside the physics engine's own work.safeDeletesays when this is over, take me out.deletesays now, and now is the wrong moment.
And a free reinforcement of step 9: CoinCollectorGame.coinCollected() works because the module
object is reachable by its own name, exactly like TheHero. PlanetX does the identical thing —
crystal.cs calls PlanetXGame.onWin() from inside its onCollision.
Worth knowing and safe to skip. Every object can be put in a numbered scene group, and an object can be told which groups it is willing to collide with. It is how you say "bullets do not hit other bullets" without checking in script. You do not need it yet. PlanetX's five groups are the worked example when you do.
Now go and place your coins somewhere interesting instead of at random. That is your game.
Hands off to: the Physics Guide for body types — it is a long page, so
go for a section rather than the front door — and the
Scene Guide's debugging section for the rest of
setDebugOn.
In PlanetX.
crystal.cs(a sensor, 51 lines) besiderock.cs(a solid static prop, 19 lines) is the pair you just built, classed for the same reason yours are. Readcrystal.csnow — it is the shortest complete class in the demo, and the first PlanetX file you will understand every line of.barrier.cs(15 lines) is four invisible solid walls around the edge of the world, which is worth a look since your hero can currently walk off into nothing.
Walk your hero up to a crate and past it. He slides over the front of it like a sticker, at every height, and the world goes flat. Two things are wrong, and they are the same thing twice: what is in front of what, and where an object actually is on the ground.
Your hero, your coins and your crates are all on scene layer 0. You never set that; it is the default, and it is the right answer — they all stand on the same ground. The background is far behind them on layer 30, and that part already works.
Within one layer, though, the engine has been drawing in the order things were made. That is why the hero is always in front: he was created before the crates. It has nothing to do with where anybody is standing.
For a top-down game the rule you actually want is whatever is lower on the screen is nearer the
camera, and draws in front. One line, at the top of Level::onAdd:
GameScene.setLayerSortMode(0, "-Y");"-Y" means sort by Y, largest first — so things high up the screen are drawn early and end up
behind. (Plain "Y" is the same idea upside down, which is what you want for a game seen from the
side.)
Run it and walk around a crate. He goes behind it going up, in front of it coming down.
Try walking so you are level with a crate and it still looks wrong — he pops in front too early, or too late. That is because sorting compares object positions, and an object's position is its middle. Your hero's middle is his chest. His chest is not what is touching the floor.
Every object can nominate the point that gets compared:
%this.setSortPoint(0, -1.9);That says sort me as though I were 1.9 units below my middle — at his feet. Add it to
TheHero::onAdd. The crate gets one too, at its base, and so does the coin:
| In | Line | Why |
|---|---|---|
TheHero::onAdd |
%this.setSortPoint(0, -1.9); |
His feet, not his hat |
Block::onAdd |
%this.setSortPoint(0, -2); |
The bottom of the crate |
Coin::onAdd |
%this.setSortPoint(0, -0.5); |
Where the coin meets the ground |
Now the comparison is between the points where things touch the floor, which is what "in front" has meant all along.
They are not taste. Your hero is drawn 6 6 — six world units for a 128-pixel picture — so:
6 units / 128 pixels = 0.047 units per pixel
Everything else is arithmetic on that. The picture's bottom edge is 3 units below his middle
(6 / 2). His feet sit about 9 pixels up from that edge, which is 9 x 0.047 = 0.42 units. So
the ground under him is at -3 + 0.42, near enough -2.6, and a sort point a little above that
— -1.9 — sits in the middle of his shadow, which is the part of him actually on the floor.
This is the conversion you will do all day in a 2D engine, so it is worth doing slowly once:
pixels are what your drawing program measures, world units are what the game measures, and
size / pixels is the exchange rate between them.
The hero still bumps into crates from a stride away, because his collision circle is a metre wide and centered on his chest. A character in a top-down game should collide with his feet — the part of him on the same floor as the crate — and with as little of himself as you can get away with, because every extra inch is a gap he cannot walk through.
The shadow under him is about 52 by 28 pixels, centered, sitting 8 pixels up from the bottom of
the picture. In units, that is a flat oval roughly 2.4 x 1.3 about 1.9 below his middle:
// A small circle at his feet, not a big one around his body.
%this.createCircleCollisionShape(0.8, 0, -1.9);0.8 is a judgement rather than a measurement, and it is worth knowing which way to lean. Half the
shadow's short side — about 0.65 — is the smallest that could be called honest, and it plays
slippery: the hero squeezes through gaps that look too narrow for him, which reads as him being
insubstantial. Much bigger and he stops at gaps that look passable, which is worse, because a
player will try again and blame you. 0.8 still sits well inside the shadow's width. Play it and
adjust it; this is a number you tune by walking around, not by measuring, and the debug view
exists so you can see what you are tuning.
The two extra numbers are a local offset — where in the picture to put the shape. Every
create...CollisionShape takes them, and almost every one you write in a top-down game will use
them.
The crate gets the same treatment. It is drawn 4 4, and only its bottom half is on the ground —
the rest is the front face you should be able to walk behind:
%this.createPolygonBoxCollisionShape(4, 2, 0, -1);The coin does not get one, and that is worth a sentence. Its sensor stays exactly where it started, at the middle of its own picture:
%this.createCircleCollisionShape(0.75);The hero and the crate both needed moving because both are drawn standing up — a figure seen from above still has a body above the feet that carry it, and a crate has a front face you should be able to walk behind. A coin has neither. It lies flat on the ground, so the whole picture is already the part on the floor and there is nothing to walk behind. Its middle is its footprint.
It still gets a sort point, though — that is a different question, and one it does have an answer to.
Turn the shapes on with GameScene.setDebugOn("collision") and walk around.
The hero's little circle slips between crates that used to stop him, and he passes behind their top halves. Every shape is now down where its object meets the ground, and none of the coins is sitting inside a crate.
This is the step that makes a top-down game look like one, and it is three ideas: sort by the ground, name the point that touches the ground, and collide with the part on the ground. Every top-down game you have played does all three.
In PlanetX. Exactly this, everywhere.
level.csline 245 isPlanetXScene.setLayerSortMode($PlanetX::EntityLayer, "-Y"), and every entity sets a sort point at its feet —setSortPoint(0, -0.9)on the spaceman and the bugs,-1.6on the brute,-5on the rocket, which is tall. The bugs go further and put their collision circle at their feet as well, with the comment "feet-centric collision, feet-centric Y-sort key".
Three small wins, all three through the Asset Manager editor. This is the step where the editors earn their keep, and the reason is the same every time:
If you would have to guess the numbers, use the editor.
You cannot pick an animation's timing out of the air. You cannot type a particle effect. What you can do is watch one and change it until it looks right, and that is what the editor is for.
You need a picture with more than one frame in it — a strip of your hero mid-stride. Save it as
heroWalk.png in sprites/ and make an image asset from it as before, then select that asset in
the library and tell the inspector how it is cut up: how many cells across, how many down. The
editor draws the grid over your picture and numbers the cells, so you can see immediately whether
you have it right.
Then File → New Asset → Animation Asset…:
- Image Asset — the one you just made.
-
Target Folder — leave it. It offers the folder your image is in, which is
sprites, which your module already declares foranimation.taml. -
Asset Name —
heroWalkAnim. NotheroWalk— see the trap below. - Target Module — works itself out from the folder, exactly as it did for the image, and you cannot type in it.
A module has one namespace for every kind of asset, and this is where it bites. You already have an image asset called
heroWalk. Call the animationheroWalkas well and the two are not two things with the same name in different families — they are one id with two claimants, andCoinCollectorGame:heroWalkanswers with the image.playAnimationthen fails with a line in the console about failing to acquire the asset "or the correct asset type", which is a long way from the word name, and you will not connect the two.The fix is a naming habit rather than a rule: the picture is
heroWalk, the animation made from it isheroWalkAnim. PlanetX does exactly this —spacemanWalkis the strip,spacemanWalkAnimis the animation — and now you know what those four letters are for.
The preview splits into three: a stage in the middle that plays, a palette on the right holding every frame the image has, and a timeline along the bottom. Drag frames from the palette into the timeline, set the animation time, and watch it. Nothing in a text file does this for you.
Then Ctrl + S — see the trap below.
In hero.cs, play it while moving and go back to the still picture when stopped.
updateVelocity already knows which of those is true, so it only needs to say so — and it needs
to say so only when the state changes, or it will restart the animation on every key event and
never get past the first frame. That is what the moving flag is for:
if (%x == 0 && %y == 0)
{
%this.setLinearVelocity(0, 0);
if (%this.moving)
{
%this.setImage("CoinCollectorGame:hero");
%this.moving = false;
}
return;
}
%length = mSqrt(%x * %x + %y * %y);
%this.setLinearVelocity(%x / %length * $CoinCollector::HeroSpeed,
%y / %length * $CoinCollector::HeroSpeed);
if (!%this.moving)
{
%this.playAnimation("CoinCollectorGame:heroWalkAnim");
%this.moving = true;
}
// You drew one direction. This is the other one.
if (%x < 0)
%this.setFlipX(true);
else if (%x > 0)
%this.setFlipX(false);playAnimation, not setAnimation. The method that starts an animation on a sprite is called
playAnimation; setImage is how you go back to a still frame. (There is also an Animation
field you can set in a new{} block, which is the same thing said at creation time.)
You only have to draw one direction. Your strip walks to the right; setFlipX(true) mirrors
the sprite and it walks to the left, using the same frames and costing nothing. This is why almost
no 2D game ships a mirrored copy of its walk cycle.
Note what the else if is for. Testing %x < 0 and %x > 0 separately, rather than
setFlipX(%x < 0), means that walking straight up or down leaves the facing alone — with %x
at zero neither branch runs. Write it the short way and your character snaps to face right every
time they walk vertically, which looks like a bug because it is one.
In PlanetX. The spaceman does the same thing, one layer up: he is a
CompositeSprite, so his body sprite is picked out by name first —selectSpriteName("body")and thensetSpriteFlipX(...)— and he faces the mouse rather than his direction of travel, because he can walk one way while shooting another.
Drop a .wav or .ogg into CoinCollectorGame/sound/, then File → New Asset → Audio Asset….
Two boxes that matter: the file, and the Asset Name — call it pickup. The Target Module fills
itself in the same way it did for the image.
One line in the coin's onCollision:
Audio.PlaySound("CoinCollectorGame:pickup");Audio is the sound module, which was copied into your project in step 5 and which your
module.taml already names under Dependencies. That is what a dependency buys you: the module is
loaded and its name is there to be called.
File → New Asset → Particle Asset… — this one wants an Image Asset or an Animation Asset to start from, because particles are made of pictures. A small bright blob will do.
The one trap in this dialog. The Target Folder offers the folder your image lives in, which is
sprites— andspritesis declared forimage.tamlandanimation.taml, not forparticle.taml. Change it toparticles. Get this wrong and the effect works perfectly today and is gone tomorrow, with no error. This is the same trap as step 7 and it is the one that catches people twice.
Most of a particle emitter is not numbers but curves: size over a particle's life, color over its life, how many come out over time. You drag points around and the effect replays as you go. This is genuinely not authorable by hand, and the editor exists because of it.
In level.cs, build one and park it:
%this.burst = new ParticlePlayer() { Particle = "CoinCollectorGame:burst"; };
GameScene.add(%this.burst);
%this.burst.stop();and fire it where a coin was, from the coin's onCollision:
CoinCollectorGame.level.burst.setPosition(%this.getPosition());
CoinCollectorGame.level.burst.play(true);Creating an asset writes its file. Editing one does not. Change a field on an asset and the change is in memory only, like an unsaved document — the title shows it is dirty and Ctrl + S writes it. (Ctrl + Z undoes, fifty steps back, per asset.) Older tutorials never mention saving because older versions wrote the file on every keystroke; that was changed on purpose, so that a mistake does not reach your disk before you have noticed it.
A ParticlePlayer starts playing the moment it joins a scene. Not when you call play. So the
pattern above is: make it, add it, stop() it immediately, and play(true) it when you actually
want it. Build it once at the start and replay it forever — making a new one for every coin means
loading an asset in the middle of your game, which is exactly when you cannot afford it.
Hands off to: the Asset Manager Editor Guide for the editor itself, and then AnimationAsset, AudioAsset and ParticlePlayer for what each one can be told. The Audio Guide is five lines long and covers everything a first game needs from sound.
In PlanetX.
weapon.cs::buildSteamVentandlevel.cs::createDeathFxPoolare the same build-it-once, park-it, replay-it pattern — one effect, and a pool of ten. It is why firing into a swarm never stutters.
Your game starts the instant it loads. Put a title screen in front of it.
Press **Ctrl + ~** and pick the Gui Editor tab.
Every control you are about to place will already look right, and you will not have done anything
to make it. That is your project's theme — the themes folder copied in at step 5, a matched
set of fills, borders and fonts so that a button you drop in belongs with everything else.
%theme.getProfile("empty") back in step 6 was reaching into the same place.
It is also the first thing that will stop looking like your game. The theme you were handed is the editor's own and is deliberately neutral, which beside your art reads as a stock dialog sitting on a painting. Fix that now, before you place anything, because everything you place afterwards inherits it.
In the Gui Editor's Tools window, the second button — the one with the document and pencil — opens the Gui Profile Editor. Click the theme at the very top of the tree.
Everything below in that tree — every button, panel, label and scroll bar — is built out of the handful of fields on this one screen. Six colors, three fonts, a size, a border width. Change one here and the whole project follows.
These are Coin Collector's, if you would like to start from something that already goes with the art rather than from gray:
| Field | Value | Doing what |
|---|---|---|
| Background | 56 41 26 |
The darkest tone — panels, the HUD backing |
| Surface | 175 124 71 |
Raised things sitting on the background |
| Foreground | 212 178 126 |
Text and anything that has to be read |
| Accent | 113 127 85 |
The buttons, and anything that wants picking out |
| Highlight | 249 212 20 |
Attention: selection, focus, a coin's gold |
| Warning | 196 54 71 |
The one red in the set. Deletions and mistakes |
| Fonts | Rockwell / Raleway Medium / roboto | Title, body, code |
| Border size | 2 |
How heavy every edge in the game looks |
Rename it while you are here: it is called Base because that is what the project template
shipped, and it is not the name of anything you are making. With the theme still selected, the
second button along the Profile Editor's own toolbar — Rename Theme — asks for a new one. Call it
after your game. Then tell your game which one to wear, in game.cs:
$CoinCollector::Theme = "CollectorGold";That is the line step 6 promised you would change exactly once.
Now change something. In the tree on the left, under Profiles, click button. That is the look every button in your game wears, and its Font Size is the theme's default — which is sized for editor panels, not for the two big buttons on a title screen. Set it to 30 and watch the preview on the right.
That is the whole loop: pick a thing, change a number, look. No code, no restart. Do it again with a color, or with the border size, or with the accent. Twenty minutes here does more for how finished the game looks than anything else in this step, and unlike most of what you have done today you can judge the result by looking at it. Press Save when you like it.
One thing does not follow the theme: a control you have already placed in the Gui Editor, because the editor wrote a specific profile into the file when you dropped it. The Set Theme button re-skins a whole screen at once and exists for exactly that. Doing the theme first, as you just did, avoids the problem entirely.
Hands off to: the Gui Editor Guide — it covers the Profile Editor properly, what a profile is made of, and how a theme is applied to a whole Gui at once.
You need a title picture. There is one in the art pack — title.png — and it already has the
game's name painted into it. Use it, or draw your own: this is the one screen where the game says
what it is called, and a drawing program will beat any text the engine can put on screen for a
logo. Either way, save it into sprites/ and make an image asset from it exactly as in step 7,
called Title.
If you are drawing your own, make it bigger than the screen and wider than it needs to be. The reason is in the next paragraph.
Now drag a sprite control onto the canvas and set its Image to CoinCollectorGame:Title.
Set its sizing to fill both ways, so the control is always exactly the size of the window
whatever the player does to it.
Then set four flags in the properties pane's Fit section. These decide how the picture is laid into the control, and they only make sense together, so here is what each one does:
| Flag | Set it | What it does |
|---|---|---|
| Full Size | on | Scale the picture to use the control's space, rather than drawing it at a fixed size |
| Keep Proportions | on | Scale width and height together, so nothing is stretched or squashed |
| Clamp Image | off | Keep the whole picture inside the control's edges, shrinking it if it would overflow. Off, it is allowed to overflow and is cut off at the edges |
| Tile Image | off | Repeat the picture over and over instead of scaling it. That is for wallpaper, not for this |
The first two are the ones doing the work: use the space and do not distort. The other two are off because you want neither of the things they do.
With that combination a picture with room to spare is scaled up until it covers, and trimmed at whichever edges do not fit — which is why the art wants to be bigger than the screen. A picture sized to exactly one window has nothing spare to trim and will show bars on any other.
These four interact more than a table can fairly show, and the
GUI Guide's GuiSpriteCtrl section describes each one properly, along
with ImageSize and PositionOffset, which matter once Full Size is off. If a background is ever
doing something you did not ask for, that page is where the answer is.
Then drop two buttons on top and caption them START and QUIT. Put them where your picture has room — that is a composition decision and it is yours.
Give them sizing flags while you are there, because a title screen is the most likely thing in your game to be looked at in a window nobody planned for. In the example the buttons are set to center horizontally and anchored to the bottom vertically, so they stay in the middle of a wider window and keep the same distance from the bottom edge of a taller one. The alternative — leaving them anchored to the top left, which is the default — means they drift up and off to one side the moment somebody drags the corner. The GUI Guide covers the rest of the sizing flags; these two are the ones a title screen actually needs.
Now give each one a Command, the field in the properties pane that says what the button does. For START:
CoinCollectorGame.startGame();
and for QUIT:
quit();
A command is just script. CoinCollectorGame.startGame() is a method on your own game object;
quit() is a function the engine has always had. The button neither knows nor cares which — it
runs the text, exactly as if you had typed it into the console back in step 3. Anything you can
type there, a button can do.
Give the root control a name while you are there — the name field is at the top of the properties
pane — and save with Ctrl + S as TAML, into CoinCollectorGame/gui/titleGui.gui.taml.
If you have a smaller screen, your background may look letterboxed while you are editing it, and that is not a mistake. The Gui Editor's canvas is the shape of the editor's own window area, not the shape of your game's window, and on a smaller display that area is a tall narrow box nothing like the picture. Judge a full-bleed background by running the game, not by looking at the editor.
That is as much of the Gui Editor as this guide is going to teach you, because the Gui Editor Guide is current, thorough and written for you. This step's job is to get you far enough in to want it.
Make scripts/titleScreen.cs:
//-----------------------------------------------------------------------------
// TitleScreen: owns the title GUI. Reads it in onAdd, frees it in onRemove.
//-----------------------------------------------------------------------------
function TitleScreen::onAdd(%this)
{
%this.gui = TamlRead(expandPath("^CoinCollectorGame/gui/titleGui.gui.taml"));
}
function TitleScreen::onRemove(%this)
{
if (isObject(%this.gui))
%this.gui.delete();
}
function TitleScreen::open(%this)
{
Canvas.setContent(%this.gui);
}And game.cs stops building a level at start-up and shows the title instead:
function CoinCollectorGame::create(%this)
{
exec("./scripts/level.cs");
exec("./scripts/hero.cs");
exec("./scripts/input.cs");
exec("./scripts/coin.cs");
exec("./scripts/block.cs");
exec("./scripts/titleScreen.cs");
%this.titleScreen = new ScriptObject() { class = "TitleScreen"; };
%this.titleScreen.open();
}
function CoinCollectorGame::startGame(%this)
{
%this.coinsTaken = 0;
%this.level = new ScriptObject() { class = "Level"; };
}
function CoinCollectorGame::destroy(%this)
{
if (isObject(%this.level))
%this.level.delete();
if (isObject(%this.titleScreen))
%this.titleScreen.delete();
}Level::onAdd already ends with Canvas.setContent(GameRoot), so starting the game swaps the
canvas from your title to your world.
Run it. Your title screen. Click Start and the game begins.
The GUI is a separate world from the scene. Pixels, not world units. Controls, not sprites. The two only meet where a SceneWindow — itself a control — draws one inside the other.
A saved .gui.taml is data you own. Open it in a text editor and it is readable. A screen
written by hand opens in the editor. Nothing about it is a black box, and the editor is a
convenience rather than a gate.
Command is a string of script that the button runs — and that is the same mechanism as
bindCmd in step 9. A string, evaluated as if typed into the console, calling a method on
something with a name. Two parts of the engine that look unrelated turn out to work identically,
and noticing that is most of what makes an engine feel learnable rather than memorizable.
In PlanetX.
gui/titleScreen.csis 37 lines — very nearly the file you just wrote — andgui/titleGui.gui.tamlis what its editor wrote. Read the two together; it is the clearest example in the demo of an editor's output and the script half that drives it.
Your game counts coins into the console. Put the count on the screen.
Make scripts/hud.cs:
//-----------------------------------------------------------------------------
// Hud: the coin readout. One panel holds every widget, so one delete frees the
// lot. The level owns it.
//-----------------------------------------------------------------------------
function Hud::onAdd(%this)
{
%theme = $CoinCollector::Theme;
// Full-screen, ON TOP of the scene window - so it must not eat the mouse.
%this.panel = new GuiControl()
{
Profile = %theme.getProfile("empty");
HorizSizing = "relative";
VertSizing = "relative";
Position = "0 0";
Extent = "1024 768";
useInput = false;
};
GameRoot.add(%this.panel);
// Something for the readout to sit on.
%this.backing = new GuiControl()
{
Profile = %theme.getProfile("panel");
HorizSizing = "right";
VertSizing = "bottom";
Position = "16 16";
Extent = "272 84";
};
%this.panel.add(%this.backing);
%this.label = new GuiControl()
{
Profile = %theme.getProfile("label");
HorizSizing = "right";
VertSizing = "bottom";
Position = "16 10";
Extent = "220 30";
Text = "COINS 0 / 0";
};
%this.backing.add(%this.label);
%this.bar = new GuiProgressCtrl()
{
Profile = %theme.getProfile("progress");
HorizSizing = "right";
VertSizing = "bottom";
Position = "16 46";
Extent = "240 20";
};
%this.backing.add(%this.bar);
%this.bar.setProgress(0);
}
/// One delete frees the panel and everything inside it.
function Hud::onRemove(%this)
{
if (isObject(%this.panel))
%this.panel.delete();
}
function Hud::setCoins(%this, %taken, %total)
{
%this.label.setText("COINS" SPC %taken SPC "/" SPC %total);
%this.bar.setProgress(%taken / %total);
}The level makes one, after the scene window exists:
%this.hud = new ScriptObject() { class = "Hud"; };
%this.hud.setCoins(0, %this.coinsTotal);And the method the coin has been calling since step 10 now has something to tell:
function CoinCollectorGame::coinCollected(%this)
{
%this.coinsTaken++;
%this.level.hud.setCoins(%this.coinsTaken, %this.level.coinsTotal);
}Add the exec line. Run it. The counter moves as you play.
GUI can sit on top of a scene window because both are controls and the scene window is nothing
special. Add it to GameRoot after the window and it is drawn over it.
The backing panel is not decoration. A theme's text colors were chosen to sit on that theme's own surfaces — its panels and windows — not on top of whatever your game happens to be drawing underneath. Put a label straight onto the world and it is legible over your dark areas and gone over your bright ones, changing as the player walks around. Give the readout something to sit on and it is readable everywhere. This is why almost every HUD you have ever seen has a panel, a gradient or an outline behind its text.
useInput = false is the line that matters, and leaving it out is the trap. A full-screen
panel over your game swallows every click meant for the game — and every child of it is skipped
along with it, which is the good news: you set it once, on the panel, and the whole HUD becomes
mouse-transparent. Your game has no mouse yet, so leave it out and you will not notice; add
mouse aiming later and your aim will die for reasons that look nothing like a HUD.
Sizing flags, in one sentence. HorizSizing = "right" means hold still relative to the left
edge and let the right side land where it lands. They are named from the wrong end, and the only
way to remember them is to be told once that they are.
One panel, one delete. The HUD manager makes one control and puts everything inside it, so
onRemove is a single line. That is a manager owning a subtree, and step 15 is about to lean on
it.
A HUD is pushed to, not polled. Nothing checks the coin count every frame. The one place that changes it says so, once, and then everything is quiet again. It is the difference between a HUD that costs nothing and a HUD that costs you a frame.
Hands off to: the GUI Guide — it is long, so go to the section you want: HorizSizing and VertSizing, UseInput, Command.
In PlanetX.
scripts/hud.cs(204 lines) is mostly this, plus a hull bar, a heat bar with an overheat warning, and a mirrored copy of the whole thing for player two.
Make winning mean something. The HUD already knows when the last coin is gone, so that is your trigger: tear the level down and build a fresh one.
In game.cs:
function CoinCollectorGame::coinCollected(%this)
{
%this.coinsTaken++;
%this.level.hud.setCoins(%this.coinsTaken, %this.level.coinsTotal);
if (%this.coinsTaken >= %this.level.coinsTotal)
%this.schedule(1000, "nextLevel");
}
function CoinCollectorGame::nextLevel(%this)
{
%this.coinsTaken = 0;
%this.level.delete();
%this.level = new ScriptObject() { class = "Level"; };
}Note the schedule, and it is not for drama. coinCollected is called from inside the coin's
onCollision, which the physics engine is in the middle of running. Tearing the world down from
in there is the same mistake safeDelete saves you from. schedule(1000, "nextLevel") says in a
second, when all this is over — and it gives the player a beat to see the last coin go.
Run it and clear a level.
The next level comes up, and something is wrong. Your keys do nothing, or they do things twice. There may be two of you. Open the console and type this after each level:
echo(GameScene.getCount());The number climbs. Every level adds another twelve coins and six blocks and a hero to a world that never threw the last lot away.
The cause is that Level::onAdd built five things and Level::onRemove does not exist, so
deleting the level deleted the level object and nothing else. The scene, the screen, the keys and
the HUD are all still there — with a fresh set laid on top of them.
Give Level an onRemove that frees what onAdd made:
/// Free what onAdd created. One delete of the level takes the whole world with it.
function Level::onRemove(%this)
{
if (isObject(%this.input))
%this.input.delete();
if (isObject(%this.hud))
%this.hud.delete();
if (isObject(GameRoot))
GameRoot.delete();
if (isObject(GameScene))
GameScene.delete();
}Now clear three levels in a row and check the number again. It should hold steady. That is the whole lesson, and you can measure it.
Three things are true regardless of how you like to write code.
onRemove fires on every object as it goes. That is what makes one delete cascade: deleting
the level runs Level::onRemove, which deletes the HUD, which runs Hud::onRemove, which deletes
the panel, which takes its label and its bar with it. You wrote four lines and a whole tree came
down.
Adding something to a container hands over its lifetime. Once you have said
GameScene.add(%coin), the scene frees that coin when the scene goes. You do not delete it
yourself, and you do not keep a list of what you added. Deleting GameScene above disposes of
every coin, every block and the hero, and not one of them is mentioned in that function.
Deleting an object cancels the timers posted on it. The coin's pulse reschedules itself
forever, and when the coin is deleted the engine drops the pending event with it. So Coin::onRemove
cancelling pulseEvent is belt and braces rather than the thing standing between you and a crash.
That last one has an important edge, and it is where timers really do bite: a timer posted on an
object that is not being deleted will still fire. %this.schedule(1000, "nextLevel") above sits
on CoinCollectorGame, which never dies. If something else tore the level down in that second, it
would fire into a world that no longer exists. PlanetX guards exactly this case, and its comment
says why:
// A death-hold timer may still be pending (player died, then quit before the
// dialog) - drop it so it can't fire suspend() into a torn-down level.
if (isEventPending(%this.gameOverEvent))
cancel(%this.gameOverEvent);Free it where you made it. If a class made something in onAdd, delete it in onRemove. Then
there is no separate cleanup list to keep in step with the setup, and no line to forget when you
add a sixth thing to the level six weeks from now. Where a delete reaches across an ownership
boundary — the HUD's panel lives inside GameRoot, which somebody else might have freed first —
guard it with isObject().
Why it is worth the trouble, once, and then we will stop: a game restarts its level hundreds of times, and a small leak becomes a stutter. You have the measurement in front of you. You do not need to be sold it twice.
In PlanetX.
PlanetXLevel::onRemoveandPlanetXGame::teardownLevelare this, done for real. Runecho(PlanetXScene.getCount())in PlanetX after each level and watch it stay put — that is why the demo can play thirty levels in a row without slowing down.
You have a game. Here is where the rest of it is.
Everything on your screen is borrowed. That was the right trade while you were learning where a sprite comes from and what a sort point is for — art is a lovely way to spend four hours and a terrible way to spend the four hours you meant to spend learning an engine. But the borrowed version is not your game, and replacing it is the fastest your game will ever improve.
You do not have to be good at drawing to do this. You need six files, and you already know exactly what each one has to be, because you have watched them work:
| File | What it has to be |
|---|---|
hero.png |
A character seen from above. 128 square is plenty. Leave a little space under the feet |
heroWalk.png |
The same character, several times, mid-stride. Any grid — you tell the editor how it is cut |
coin.png |
Whatever you are collecting. Small; ours is 32 square and looks fine |
block.png |
The thing in the way. Its bottom half is what you bump into |
background.png |
The ground. Big enough to cover the camera |
title.png |
A picture with your game's name in it, bigger than the screen |
Swapping one is: put the new PNG in sprites/, and either point the existing asset's Image
File at it or make a new asset and change the id in your script. Nothing else moves. That is
what asset ids bought you back in step 7 — the sprite says CoinCollectorGame:hero and does not
care which picture that is this week.
Two things to re-measure when you do, both from step 11: the sort point, because your character's feet will not be where mine are, and the collision circle, because a shape drawn for my figure will be wrong for yours. Both are one number each, and the debug view shows you whether you got them right.
Shortest complete class first, state machine last. Open them from
PlanetX/PlanetXGame/scripts/.
| File | Lines | What it shows you |
|---|---|---|
crystal.cs |
51 | A complete class, start to finish. You already understand every line. |
rock.cs |
19 | The solid-prop version of the same thing, sized by whatever made it. |
bug.cs |
19 | A subclass that sets six numbers and inherits everything else — your shotgun. |
enemy.cs |
198 | The base class those numbers belong to: chasing, damage, death. |
player.cs |
457 | Two sprites acting as one object, held-key movement, aiming, and a weapon it does not know the type of. |
level.cs |
652 | Everything step 6 and step 10 did, generated from a seed. |
game.cs |
459 | The state machine: title, playing, won, lost, and back. |
A collect-a-thon becomes a game the moment something is trying to stop you, and this is the most common next question. You already have every piece of it.
An enemy is a coin that moves. Look at what Coin already is: a class, an onAdd that sets
a size, an image and a collision shape, and an onCollision that does something when the hero
arrives. An enemy is the same file with a different onCollision — and one thing more.
The thing more is a tick. Nothing in your game currently changes on its own; everything waits for a key or a collision. PlanetX gives each enemy a repeating schedule and lets it decide where to go each time:
%this.chaseEvent = %this.schedule($PlanetX::ChaseTickMs, "updateChase");updateChase ends by scheduling itself again — that is the whole trick, and it is the same
schedule your coin already uses to pulse. enemy.cs cancels the event in onRemove, for exactly
the reason step 9 gave about the key map: an orphaned reschedule outlives the thing it belonged to.
Then read these three together, in this order — they are 198, 19 and 25 lines:
| File | What it is |
|---|---|
enemy.cs |
The base class. Chasing, wandering, taking damage, dying. All of the behavior |
bug.cs |
A whole enemy in 19 lines: a size, an animation, a collision shape. Nothing else |
brute.cs |
A second whole enemy in 25 lines, differing by a sprite and some numbers |
That ratio is the lesson. One base class carries the behavior and each new monster costs twenty
lines, which is the same class/superclass split you used on the shotgun in step 4 — and it is
why PlanetX can add a new alien in an afternoon.
Not covered here. Each is one file, and each is a pointer rather than a lesson.
-
upgrades.cs— a catalog of things that modify a run, applied to the player each level. -
settings.cs— saving player preferences to disk and loading them back. -
input.cs— rebindable keys, plusoptionsScreen.csandkeyCapture.cs, the screens for them. -
camera.cs— a camera that keeps two players in frame. -
tileMap.cs— a whole terrain built from noise. -
bullet.cs— reusing a fixed pool of objects instead of making new ones.
The Toy Box card in the Project Selector. Thirty-odd modules, one idea each.
- SpriteToy — what a sprite can be told to do, in isolation.
- CollisionToy — collision callbacks, on their own, with nothing else in the way.
- ScrollerToy — a background that scrolls forever.
- AudioToy — music and sound effects.
- DeathBallToy — a small complete game, closer to yours than to PlanetX.
The tools
- Project Manager Guide — projects, modules, dependencies, declared assets.
- Asset Manager Editor Guide — making and editing every kind of asset.
- Gui Editor Guide — building screens.
-
Console Guide —
dump(),trace(), the log file.
The language
- TorqueScript Syntax — the rulebook.
- Math Guide — read the first section before you do any trigonometry. Angles are in degrees, and vectors are strings.
- Callback Guide — the hooks the engine calls on your objects.
The world
- Scene Guide · SceneObject Guide · Sprite Guide · SpriteBase Guide
- Physics Guide — bodies, shapes, and what a collision really is.
- TextSprite Guide — text drawn in the world rather than on the GUI.
Screens, assets and files
- GUI Guide — the control reference.
- ImageAsset · AnimationAsset · ParticlePlayer · AudioAsset · FontAsset
-
Taml Guide — the file format behind
module.taml, every asset, and every saved screen.
One more pattern, for when your game gets bigger
- Object-to-Object Events — letting objects announce things instead of calling each other directly. Useful the first time three unrelated parts of your game all want to know that the player scored.












