Torque 2D/GenreTutorials/PlatformerLevels
From TDN
|
[edit] IntroductionThe purpose of this tutorial is to create a system that allows you to load and unload levels. In a game with any decent number of assets, you're not going to be able to load everything at the start. You'll need to split up your assets and load them and unload them as needed. Of course, the logical way to do this is level by level. This system doesn't really have anything to do with platformers specifically, aside from the fact that they usually have several levels, so you should be able to use it in any game type. The annoying thing is, the actual sample game is going to play identically at the end of this tutorial as it did at the end of the last. Visually, there will have been nothing accomplished, but you should feel much better about the organization and usability of the code. |
|
[edit] The Level FormatThe first thing we need to do is create a format with which we can easily load and unload objects from a single file. The SimGroup was designed specifically for this purpose. The normal way to add objects to a SimGroup, and the way we have used up till now, is like this:
new SimGroup(ExampleGroup);
%object = new T2DSceneObject { scenegraph = t2dscene; };
ExampleGroup.add(%object);
But, there exists another way that simplifies this process when you are planning on adding a bunch of objects to a group. The syntax looks like this:
new SimGroup(ExampleGroup)
{
new T2DSceneObject(ExampleObject)
{
scenegraph = t2dscene;
};
};
This is what we will use. Open the folder in the T2D/data directory called "levels". Inside this folder, create a file called testLevel.cs. I am going to now just give you the contents of the level file that will create a level identical to the one we have been using and then I'll describe the caveats of the format. Open up testLevel.cs and add this to it:
$gravity = 700;
$spawnPoint = "-20 -30";
$enemySpawnPoint0 = "50 0";
$triggerSpawnPoint0 = "20 100";
$triggerSpawnSize0 = "20 5";
new SimGroup(LevelGroup)
{
new SimGroup(ConfigDatablockGroup)
{
new t2dTileMapDatablock(TileMap_Config)
{
tileMapFile = "~/data/tilemaps/platformer.tile";
};
new t2dImageMapDatablock(Enemy_Config)
{
imageMap = enemyImageMap;
constantForce = 0 SPC $gravity;
graviticConstantForce = true;
collisionActiveSend = true;
collisionActiveReceive = true;
collisionPhysicsSend = true;
collisionPhysicsReceive = false;
collisionCallback = true;
collisionResponseMode = CLAMP;
group = $enemyGroup;
layer = $enemyLayer;
collisionGroupMask = BIT($platformGroup);
collisionLayerMask = BIT($platformLayer);
};
new t2dSceneObjectDatablock(Trigger_Config)
{
group = $triggerGroup;
layer = $platformLayer;
collisionGroupMask = BIT($playerGroup);
collisionReceiveMask = BIT($playerLayer);
collisionActiveSend = true;
collisionActiveReceive = false;
};
};
new SimGroup(ObjectGroup)
{
new t2dTileMap(TileMap)
{
config = TileMap_Config;
scenegraph = t2dscene;
};
new SimGroup(EnemyGroup)
{
new t2dStaticSprite()
{
config = Enemy_Config;
scenegraph = t2dscene;
move = 1;
aiState = $aiPatrolState;
runSpeed = 30;
airSpeed = 16;
jumpHeight = 20;
maxRunSurfaceAngle = 35;
chaseDistance = 100;
stopChaseDistance = 150;
patrolMin = -30;
patrolMax = 50;
};
};
new SimGroup(TriggerGroup)
{
new t2dSceneObject()
{
config = Trigger_Config;
scenegraph = t2dscene;
enterAction = "playerFall();";
stayAction = "";
leaveAction = "";
};
};
};
};
We still have two of the same variables. gravity and spawnPoint. By putting them in the level file, we can have different values for different levels. You may not need to change gravity every level, but you probably will want to change the spawn point of the player. The next three variables store the position to spawn the enemy and create and size the trigger.
exec("./level.cs");
We are now going to create another ScriptObject whose purpose is pretty transparent. The only real reason for it is that Torque's package system (which will be described later) works much nicer when functions are a part of a namespace. This ScriptObject will create that namespace:
new ScriptObject(Level) {
running = false;
filename = "";
packageName = "";
};
Now we have a namespace called Level that we can add functions to. First, the three variables: "running" is just a flag specifying whether or not a level is currently running. "filename" is the file the current level was loaded from, and "packageName" I will describe later. Here is the function that is a part of our new namespace that will position all of our objects:
function Level::onLevelLoaded()
{
if (isObject(TileMap))
{
for (%i = 0; %i < TileMap.getTileLayerCount(); %i++)
{
%layer = TileMap.getTileLayer(%i);
%layer.setEnabled(true);
%layer.setCollisionActive(false, true);
%layer.setGraphGroup($platformGroup);
%layer.setLayer($platformLayer);
}
}
if (isObject(EnemyGroup))
{
for (%i = 0; %i < EnemyGroup.getCount(); %i++)
{
%enemy = EnemyGroup.getObject(%i);
%enemy.setCollisionMaxIterations(2);
%enemy.setCollisionPolyPrimitive(8);
%enemy.setPosition($enemySpawnPoint[%i]);
}
}
if (isObject(TriggerGroup))
{
for (%i = 0; %i < TriggerGroup.getCount(); %i++)
{
%trigger = TriggerGroup.getObject(%i);
%trigger.setPosition($triggerSpawnPoint[%i]);
%trigger.setSize($triggerSpawnSize[%i]);
}
}
LevelGroup.add(new SimGroup(TriggerCheckGroup));
}
This function is going to be called every time a level is loaded from our loadLevel function that will be created shortly. The first step is setting up our tile map. Since tile maps can potentially have multiple layers, we are looping through all possible layers when setting properties. The next two sections loop through all of the enemies and all of the triggers respectively. Notice the calls to setPosition and setSize. These are referencing the spawn variables we created at the top of testLevel.cs. If you have two enemies, all you have to do is create another variable called $enemySpawnPoint1 and the second enemy will be positioned accordingly. The last line of the function just creates our TriggerCheckGroup that was described in the last tutorial.
package TestLevel
{
function Level::onLevelLoaded()
{
Parent::onLevelLoaded();
%layer = TileMap.getTileLayer(0);
%layer.setPosition("0 0");
$camera.setLimits(%layer.getArea());
resetPlayer($spawnPoint);
Level.packageName = TestLevel;
}
};
activatePackage(TestLevel);
First we create the package. Any function defined within the first opening curly brace and the last closing curly brace of this code block will be a part of this package. The last line, activatePackage(TestLevel), sets the TestLevel package as the current package. So, anytime after this file is executed, when onLevelLoaded is called, this is the function that is executed, not the one we previously defined in level.cs. So then, what was the point of that one? The first line in this function (Parent::onLevelLoaded) is actually calling that function. Essentially what this allows us to do is perform all of the initialization that occurs for every level in one place, the level.cs version of the function, and still have a place to do level dependent intiialization.
|
|
[edit] Loading and Unloading LevelsThe final part of this tutorial is the actual loading and unloading of the levels. For this we are going to create two new functions in level.cs. loadLevel and endLevel. loadLevel will take as a parameter the filename of the level you want to load. Here it is:
function loadLevel(%level)
{
if (Level.running)
endLevel();
if (isFile(%level))
{
echo("Loading Level" @ %level);
exec(%level);
Level.running = true;
Level.filename = %level;
if (isObject(LevelGroup))
Level.onLevelLoaded();
else
warn("Invalid level file: LevelGroup not found");
}
else
{
warn("Invalid level file: " @ %level @ " not found");
}
}
First, if a level is already running, we end it. Then, we make sure the specified file actually exists. If it does, the file is loaded and we set the running status to true. Finally, we make sure the level file contained a LevelGroup and if so, call the onLevelLoaded function. Remember, this is going to call the onLevelLoaded function defined in the level file, which will then call the generic onLevelLoaded function in level.cs.
function endLevel()
{
if (isObject(LevelGroup))
LevelGroup.delete();
deactivatePackage(Level.packageName);
Level.running = false;
}
This is pretty simple. If we have a LevelGroup, delete it. Then, deactivate the level's package.
if (!Level.running)
return;
The last thing we need to do is delete all of the creation functions and groups. Those would be createLevel in game.cs, the EnemyGroup and createEnemy in enemy.cs, and the TriggerCheckGroup and createTrigger in trigger.cs. All of this stuff is being handled by our level system now, so these functions are useless.
loadLevel("~/data/levels/testLevel.cs");
And, you can create a trigger at the end of a level with enterAction:
loadLevel("~/data/levels/level2.cs");
To load the next level. All you would need is a level2.cs file in the same format as testLevel.cs. If you make use of this and create a bunch of levels, the only thing keeping you from having a complete game is the lack of a menu system. |



