TGB/BreakoutTutorial3
From TDN
The following links can take you to other sections of the tutorial: The Breakout Tutorial Part 3 - Miles of TilesA Tetraweb TutorialBreakoutTutorial Part 3 Discussion Thread First, a FixBefore we start this part of the tutorial, we need to make a quick fix. You may have noticed, if you bounced the ball around enough in the previous tutorial, that you can occasionally get the ball stuck in the paddle if you move it fast enough, and sometimes it seems to kind of double bounce. What's happening is this: you can get the paddle moving faster than the ball, so the ball bounces off the paddle and starts moving...but the paddle is already in front of it again by now, and so the ball bounces off it again. And sometimes again and again and again. (See Fig 3.a)So what do we do? Well, we could change the way the paddle is controlled, have it move at a speed always slower than the ball; but that feels too limiting and it then takes so long to move the paddle across the screen that you often miss the ball. So we are going to do a hack, a mild one as hacks go. When the paddle hits the ball, we are going to immediately turn off the collision for the paddle for a fraction of a second; plenty of time for the ball to get out of the area. Then we'll turn it back on. We need to do that in the ballClass::onCollision function in ball.cs. Add the following to lines to that function, right after the if paddleClass statement: %dstObject.schedule(500,"makeHittable"); %dstObject.setCollisionActive(0,0); The first line schedules a function called makeHittable to be called in 500 milliseconds, or half a second, in the %dstObject namespace, and we know that it is paddleClass. The second line turns off collision for the paddle. Now we need to write our makeHittable function, and we put that in the base.cs file where the paddleClass functions also reside. So add this to that file, right after paddleClass::onLevelLoaded:
function paddleClass::makeHittable(%this){
%this.setCollisionActive(0,1);
}
This function, called a half second after the paddle hits the ball, turns collision receiving back on for the paddle. Save everything and test. Move the paddle fast and hit the ball; now the ball catching in the paddle should be greatly reduced. This hack fix illustrates an important point about designing a game. Actually it illustrates a point about finishing a game. You will often, whatever kind of game you are building, run into issues like the one we had here. Some much more troublesome. Your best bet to finish a playable game is to just blast through the problems. Don't let them stop you, do whatever hacky fix works to bring it back to a playable game. Adding the TilemapNow back to our tutorial. The tilemap feature of TGB has many uses, and is most often thought of as a way to build very large, multi-screen platformers using a set of repeating and connecting graphical tiles.We are going to use it in a slightly different way, but perfectly suited to our application. What we need is a way to easily paint a variety of layouts of different bricks, or tiles, that differ from level to level, and which interact with our bouncing ball.
Next open the Scene Object section. Set the following: Position X: 0.000, Position Y: -12.500, Width: 182.000, Height: 110.000, Layer: 4. Then, in the Scripting section, set the name to tlayer and the class to tileClass; and in the Collision section, have only Receive Collision checked.
We can paint these layers full of different tiles, with different layouts. This is a great way for us to create a large variety of tile levels. We do this painting using tile brushes. Each tile layer we create with the brushes, we are going to save as a level.lyr file. But first, let's save an empty one, a layer file with no tiles in it; we may need this later. Click the Edit Tile Layer button in the Tile Map section. If the Tile Editing section is not expanded, use the triangle to expand it. Then click the Save Layer button (See Fig. 3.3). It should default to your data/tilemaps folder. Save it as empty.lyr.A Brush ShortcutNow a shortcut. I could, at this point, detail the step by step process of creating brushes to use for painting tiles. But instead, I'm going to skip that and jump ahead to the brushes already created, for a couple reasons. First, it would take quite a bit of time to detail what each brush should be like, and also the process itself is kind of slow. You can learn about creating brushes yourself from other tutorials, if you have questions; but I have no doubt that it is fairly self-evident anyway. Twbt_brsh.zip Now find your game/managed/brushes.cs file. This file should have been automatically created when you made the Breakout project. You need to replace it with the brushes.cs file contained in the zip file you downloaded above. If the brushes don't appear in your drop down menu, try replacing the brushes.cs while the editor is closed. Now save everything, quit the TGB editor and restart it, and lets make a simple tile layer. Painting with BrushesLoad playLevel.t2d and select the tilemap. Edit it by clicking the last icon that hovers when you mouseover it. This will expand the Tile Editing section of the tile map.In the Brush dropdown, you will see the list of brushes contained in the brushes.cs file we just added. Choose bluebrush from the dropdown (See fig. 3.4) then click the paintbrush icon from the row right above and paint four tiles onto the tilemap so it looks something like figure 3.5. So the brushes are just a shortcut for setting the tile options in this section. The imagemap and frame and the collision checkbox are set just by choosing the brush from the dropdown. We could do it manually each time, but you can imagine that painting a full level with different types of tiles would get very tedious. The other field that gets set by the brush is Custom Data. (We are not going to use the Tile Script field for our purposes.) The Custom Data field is going to turn out to be very useful for us. Each tile in our map has this field, and when our ball hits a tile we can poll this field to find out some info about it and see just what we should do. Click the Save Layer button and save it as test.lyr. The click the Selection tool from the TGB toolbar at the top to quit editing the layer. Now save your playLevel.t2d scene. We are going to eventually have many functions associated with the tileset, so we'll make a new file called tiles.cs in your /gamescripts/ folder and add this function to it:
function tileClass::onLevelLoaded(%this, %scenegraph)
{
$thetileset=%this;
$thetileset.setCollisionResponse(CLAMP);
$thetileset.setMass(4.1);
$thetileset.setImmovable(true);
$thetileset.setCollisionPhysics(0,0);
}
This sets a few things on the tileset when the level is first loaded, most of which we could have set in the editor; but it's good to know how to do it in script as well. One important thing we do here is assign the global variable $thetileset to this tileset object, so we can access it from anywhere in our script. We have to pull this new cs file in, so add this to the end of your exec.cs file:
exec("./gameScripts/tiles.cs");
You could save everything and test it now, but not much would happen except the ball will bounce off the tiles as well as the walls. We want something different to happen, so we need to test in our ball collision function if it is a tile that we hit. So open your ball.cs file and find the ballClass::onCollision function. After the else if test for "wallClass" add this next else if:
} else if (%dstObject == $thetileset) {
$wallbang.setPosition(%points);
$wallbang.playEffect(false);
$thetileset.clearTile(%dstRef);
}
Now, we could have compared %dstObject.class to "tileClass" and it would have worked the same, but just for variety we are comparing the %dstObject to our actual global variable $thetileset to see if our ball hit one of the tiles. If so, we set our $wallbang effect to the point of collision, the we use the built in clearTile method of the tileset to clear the tile. The %dstRef local variable inside the onCollision function will always contain the logical tile x/y coords if the collision recipient is a tileset. Save everything and test, you should see the wallbang effect when the ball hits a tile, then it should disappear. Tiles That Break and Tiles That Don'tWe want to do more complicated things than just clear the tile though, so we need to replace the simple clearTile call with our own function. We may clear the tile, but we may also add points, weaken the tile, explode it, etc. We need to replace the line $thetileset.clearTile(%dstRef); with the line $thetileset.hitTile(%dstRef,1.0); Your entire ballClass::onCollision function should now look like this:
function ballClass::onCollision(%srcObject, %dstObject, %srcRef, %dstRef, %time, %normal, %contacts, %points)
{
if (%dstObject.class $= "paddleClass") {
%dstObject.schedule(500,"makeHittable");
%dstObject.setCollisionActive(0,0);
%speed = getword($thebase.getlinearVelocitypolar(),1);
if (%speed > 150) {
%force = t2dVectorScale(%normal,-(%speed*2));
%srcObject.setImpulseForce(%force, false);
}
%r=getRandom(1,2);
%hitsound="shieldhit"@%r;
alxPlay(%hitsound);
$wallbang.setPosition(%points);
$wallbang.playEffect(false);
} else if (%dstObject.class$="wallClass") {
alxPlay(wallhit);
$wallbang.setPosition(%points);
$wallbang.playEffect(false);
} else if (%dstObject == $thetileset) {
$wallbang.setPosition(%points);
$wallbang.playEffect(false);
$thetileset.hitTile(%dstRef,1.0);
}
}
We are passing the %dstRef logical coords of the hit tile and the value 1.0 to our hitTile function. We'll discuss that 1.0 value when we create the hitTile function. Before we create that function, though, we have a few supporting elements we need to get ready. Participating ParticlesTwbt_pe3.zip So now we have two new particle effects (one used an already existing imagemap that we created earlier) and two new sound effects to use. We want to have a new effect and sound when when the ball hits a tile, but does not break it. And we want to have a different effect and sound when the tile actually breaks. You'll see how it can hit a tile but not break it in a moment. First, we need to prepare the sounds and effects for our use. Open your gamesScripts/audioDatablocks.cs file and add this to the end of it:
new AudioProfile(tilebreak)
{
filename = "~/data/audio/tilebreak.ogg";
description = "AudioNonLooping";
preload = true;
};
new AudioProfile(tilehit)
{
filename = "~/data/audio/tilehit.ogg";
description = "AudioNonLooping";
preload = true;
};
This creates two new audio profiles ready to play, just like in part 2. Now open gameScripts/game.cs and find the initializeEffects function that we created in part 2. Inside that function, right after we initialize the $wallbang effect, add these two new effects:
$tilepop = new t2dParticleEffect() { scenegraph = $thescenegraph; };
$tilepop.loadEffect( "~/data/particles/popper.eff");
$tilepop.setEffectLifeMode(stop, 0.6 );
$tilepop.setSize("15 5");
$tilehum = new t2dParticleEffect() { scenegraph = $thescenegraph; };
$tilehum.loadEffect( "~/data/particles/tilehum2.eff");
$tilehum.setEffectLifeMode(stop, 1.0 );
$tilehum.setSize("15 5");
And we are going to clear these as well, just like $wallbang. So add this to the endGame function in game.cs, right after the $wallbang line: if ($tilehum) $tilehum.safeDelete(); if ($tilepop) $tilepop.safeDelete(); To Break or Not To BreakWe are finally ready to put these pieces to use. Let's create that hitTile function now at the end of the tiles.cs file.
function tileClass::hitTile(%this,%dstRef,%force) {
%customdata=%this.getTileCustomData(%dstRef);
%tiletype=getWord(%customdata,0);
%tilestrength=getWord(%customdata,1);
if (%tiletype$="live") {
%tilestrength -= %force;
if (%tilestrength<=0) {
%this.breakTile(%dstRef);
} else {
%tposition = %this.getTilePosition(%dstRef);
$tilehum.setPosition(%tposition);
$tilehum.playEffect(false);
alxPlay(tilehit);
%imap=getWord(%this.getTileType(%dstRef),1);
%frame= mFloor(4-%tilestrength);
%this.setStaticTile(%dstRef,%imap,%frame);
%this.setTileCustomData(%dstRef,%tiletype SPC %tilestrength);
}
} else { //not a breakable tile
alxPlay(wallhit);
}
}
So you can now see that the 1.0 we are passing to the hitTile function is %force, how hard we are hitting the tile. Right now we are only passing 1.0 when the ball hits a tile, but you can imagine there may be times when we want to pass less force (during a weakening power up, for example) or more force. Let's go over this function piece by piece. The first line uses the %dstRef variable and pulls the custom data for that tile into a local variable, %customdata. If you looked closely at the brushes you saw that the custom data was in the format of "live 1" or "live 2" or "dead 0" etc. We are using the word live to denote tiles that can be broken, and the number after it to denote its strength. So "live 3" would have to be hit three times at 1.0 to break. So the next two lines, for convenience, break the %customdata into two local variables, %tiletype and %tilestrength. The next line checks %tiletype to see if it is "live." If not, the corresponding else just plays the wallhit sound. If it is "live", the next line subtracts %force from the %tilestrength. Then we see if the %tilestrength is equal or less than zero, and if so we call a breakTile() function, which we will get to in a moment. If the %tilestrength after being hit is still above zero, then instead of calling breakTile we do a couple other things. First, we want to get the real world x/y position of the center of the tile, because we want to play our $tilehum effect centered on the tile.While TGB provides a pickTile method to get the tilemap x/y from real world coords, it does not provide the reverse, getting realworld coords from a tilemap x/y. We are going to use a function Ben Amos wrote called getTilePosition to do that, which we will get to in a moment. We assign the real world x/y center of the tile to the local %tposition, then we put our $tilehum effect there and play it. Then we play the tilehit sound. Next we use the getTileType method, which returns as its second word the name of the ImageMap for the tile. We store that in %imap. The next line takes advantage of how we set up the image map for our bricks. If you look at it you will see that frame 0 is a complete tile, frame 1 is slightly broken, frame 2 is more broken, and frame 3 even more. So the next line uses mFloor(4-%tilestrength) to round down from 4 minus the %tilestrength. If the remaining tilestrength is 1, for example, we would get 3 for our %frame local variable. So the next line uses the setStaticTile method to set the imagemap and frame for the %dstRef tile that we hit. And then the final line uses the setTileCustomData method to assign our new remaining tilestrength back into the custom data for that tile. Supporting FunctionsWe still have to write the two functions we referenced above. The getTilePosition() function is very useful, and you may at some point want to put it in a common.cs file, but for now let's add it to the end of our tiles.cs file:
function tileClass::getTilePosition(%this, %tile )
{
// ************************************************************************
// Gets world position from logical tile position
// Stripped down version of original getTilePosition function by Ben Amos, Jan 26, 2006
// Note: mine is not a good general function for tile position; no wrapping, rotation, etc.
// ************************************************************************
%offset = t2dVectorSub( %this.getTileCount(), "1 1" );
%offset = t2dVectorScale( %offset, 0.5 );
%tilePos = t2dVectorSub( %tile,%offset);
%tileX = getWord( %tilePos, 0 );
%tileY = getWord( %tilePos, 1 );
%tileSizeX = getWord( %this.getTileSize(), 0 );
%tileSizeY = getWord( %this.getTileSize(), 1 );
%tilePos = (%tileX * %tileSizeX) SPC (%tileY * %tileSizeY);
%tilePos = t2dVectorAdd( %tilePos, %this.getPosition() );
return %tilePos;
}
I'm not going to detail this function; if you are curious you can search the TGB private forums for the thread that produced it. Relevant for our purpose is that you pass it a tilelayer and a tile position in the form of "x y" and it returns the center of the tile in world point coords. The other function we need is the breakTile function. Here it is:
function tileClass::breakTile(%this,%destination){
%tposition = %this.getTilePosition(%destination);
%this.clearTile(%destination);
$tilepop.setPosition(%tposition);
$tilepop.playEffect(false);
alxPlay(tilebreak);
}
Our breakTile function takes the %destination (%dstRef) of the tile to break; it gets the world position of the center of the tile, places the $tilepop effect there and plays it, then plays the tilebreak sound effect. You may be wondering why we are going to all this trouble to get the center position of the hit tile, when the onCollision function already has the %points variable set which is the position of the actual collision. It's because we want the particle effect centered on the tile itself. It looks strange if it is centered on wherever the collision happened to be. A Few More Test TilesAlmost ready to save everything and test. But we need some tiles with a strength greater than 1 if we want to see our full hitTile function in action. So really quickly, lets add a few to our test layer. Make sure your playlevel.t2d scene is open, select the tilemap and click the Edit Tile Layer button. From the Brush dropdown, choose bluebrush3 and draw a few on the tilemap. Then choose deadbrush from the dropdown and draw a few of those on the tile map. Then click the Save Layer button and save it over the test.lyr file. Choose the TGB Selection tool to get out of editing the tile layer. Now save everything you can and test. You should see the tiles with a strength of one, the bluebrush tiles, break when hit. And the tiles with a strength of three should take three hits before the break, each time giving off the tilehum effect. The deadbrush tiles have "dead 0" in their custom data, so they never break. One More ThingSince our final game is going to have multiple levels, we need to be able to keep track of when a player completes a level by clearing all live tiles. So we need to do two things, when the level starts we need to count up all the live tiles and store that, and each time a tile is broken we need to decrement that stored counter. When we get to zero we can take the appropriate action. We are adding the countTiles function as a method of the tileClass class, so add this at the end of your tiles.cs file:
function tileClass::countTiles(%this,%counter){
%tcount=0;
%widthx=%this.getTileCountX();
%widthy=%this.getTileCountY();
for (%x=0;%x<=%widthx;%x++){
for (%y=0;%y<=%widthy;%y++){
%tiletype=getWord(%this.getTileCustomData(%x SPC %y),0);
if (%tiletype$=%counter) %tcount++;
}
}
return %tcount;
}
We are passing the method the %counter variable, which is what we want to count. We could just hard code "live" in the method, but we are keeping it more flexible this way; we can count whatever kind of tiles we may need to later. We simply get the x/y size of the tile layer and step through every column of tiles, checking the first word of the tile's custom data, If it matches &counter, we increment %tcount and finally return that value. Now we need to call this method, so add this line to the end of your initializeLevel function in theLevels.cs, right after the initializeEffects() line:
$liveTileCount=$thetileset.countTiles("LIVE");
This will store the total live tiles in the global $liveTilecount. Add this line right after the one you just added:
echo("The live tile count is " @ $liveTileCount);
Now save everything and test, and when you show the console (~) you should see the number of live tiles you have (with dead tiles not counted.) Now comment that echo statement out. The last thing is to decrement the count as we break tiles. Find your breakTile function in tiles.cs and add this to it, right after alxPlay(tilebreak):
$liveTileCount--;
if ($liveTileCount <= 0) {
//tiles are gone on this level so lets go to the next level
echo("Level cleared!");
//levelAdvance(%tposition);
}
We decrement the $liveTileCount, see if it is zero, and if so we are going to advance to the next level. For now, of course, that action is commented out and we simply echo that we cleared it. Save everything and test. When you clear the final tile, check your console and you should see the Level Cleared statement. ConclusionWell, now we have the core of the game in place, really; though we are far from finished. The next part of this tutorial will focus on keeping score and limiting lives. Thanks for reading, and feel free to post any comments or questions on the BreakoutTutorial Part 3 Discussion Thread. Continue on to:
|
Categories: TGB | GameExample | Tutorial












