TGB/BreakoutTutorial4
From TDN
The following links can take you to other sections of the tutorial: The Breakout Tutorial Part 4 - Dying to ScoreA Tetraweb TutorialBreakoutTutorial Part 4 Discussion Thread The Points are the PointWe need to keep score, and eventually save our highest scores so we can proudly show our loved ones how wonderful we are at manipulating ones and zeros on a screen. Before we start adding points for each brick broken, we need to set up a way to write this score to the screen. In TGB, that means GUIs or TextObjects or both. Because this is a tutorial, we are going to cover both ways before we are done. Before we even do that, though, we need a font. Choosing the fonts in your game is as important as any other graphical design choice, and more important than some. Our FontWe need a good font for our score (and all the other text for our game.) We certainly don't want to use Arial or any other common font, it seems amateurish. We are going to use a wonderful, arcade game like font called JuneBug.It's one of many hundreds of freeware fonts you can get from Jakob Fischer [1]. I can't recommend his fonts highly enough. The ones for sale are even better, and the prices are low. Twbt_fnt.zip
The Score GUITo display our score on the the screen, we are going to create a GUI for the score in script. The complexities of the GUI Editor are beyond the scope of this tutorial, so all the GUIs we create will be in script. Create a new file called ScoreboardGui.gui in the folder game/gui and add this to it:
if(!isObject(GuiTextW22Profile)) new GuiControlProfile (GuiTextW22Profile : GuiTextProfile)
{
fontSize = 22;
fontColor = "220 220 220";
fontType = "JuneBug";
};
This creates a new GuiTextProfile which defines a font, size and color for a GUI text object. Immediately after that, add the new GuiControl:
new GuiControl(ScoreboardGui) {
canSaveDynamicFields = "0";
Profile = "GuiModelessDialogProfile";
HorizSizing = "right";
VertSizing = "bottom";
border=0;
Position = "0 0";
Extent = "200 40";
MinExtent = "8 2";
canSave = "1";
Visible = "1";
hovertime = "1000";
modal="false";
new GuiTextCtrl() {
canSaveDynamicFields = "0";
Profile = "GuiTextW22Profile";
HorizSizing = "right";
VertSizing = "bottom";
Position = "20 740";
Extent = "96 20";
MinExtent = "8 2";
canSave = "1";
Visible = "1";
hovertime = "1000";
text = "SCORE:";
maxLength = "1024";
};
new GuiTextCtrl(ScoreboardGuiScore) {
canSaveDynamicFields = "0";
Profile = "GuiTextW22Profile";
HorizSizing = "right";
VertSizing = "bottom";
Position = "116 740";
Extent = "520 20";
MinExtent = "8 2";
canSave = "1";
Visible = "1";
hovertime = "1000";
text = "0";
maxLength = "1024";
};
};
I'm not going to go over every line in the GuiControl above, but the pertinent ones are these: We are creating a new GuiControl and assigning it a profile of type GuiModelessDialogProfile. This is important so the GUI does not intercept mouse movements and clicks. Inside this GuiControl, we are also creating two GuiTextCtrls, one to simply display the word SCORE: and the other right next to it to display the actual score. Both of these GuiTextCtrls are set to the profile we created above, so they use our JuneBug font. We name only the second one, ScoreboardGuiScore, because we will need to reference that one directly in our game when we update the players score.Most of the other fields set in these GUIs relate to position and size. We need to pull the file that we just created in, so add this to your exec.cs file.
exec("~/gui/ScoreboardGui.gui");
And we need to add the GUI to our Canvas. You can have many GUIs of all sizes and functionality on top of your mainScreenGui, which is the only required one. You add additional GUIs by 'pushing' them, and remove them by 'popping' them. Add this to the end of your initializeLevel function in theLevels.cs. Canvas.pushDialog(ScoreboardGui); Save everything, start up TGB and make sure playlevel.t2d is loaded. Now test your game and you should see the Score: 0 on screen. Know the ScoreWe want to use a global variable to keep track of the score per level. We'll set it to zero when we start the level, so add this to the end of your initializeLevel function in theLevels.cs. $levelscore=0; We are going to add to the level score each time we hit a tile or break a tile. While we could hardcode that point addition in our tile hitting and breaking functions, it makes more sense to abstract it so we can control it later by changing variables. Still in your theLevels.cs file, add this to the very top inside your initializeLevel function: $tilebreakpoints = 250; $tilehitpoints =50; This sets our global variables for hitting and breaking. We also may want to temporarily modify these points with a multiplier; for example, say we want to make a powerup that doubles all the points you get for twenty seconds. So we are going to set that as a global variable now, and we'll set it to 1.0 so the points don't get modified at all. Add this to the top of theLevels.cs, with the other globals, outside any functions: $multifactor = 1.0; //point multiplier factor Now we have the variables in place to add the points, so lets add them. We want to add points whenever they hit a tile that doesn't break. In your tiles.cs file, we need to modify our tileClass::hitTile function. We are going to add points to the $levelscore in the if statement that checks if the tile should break. Here is a partial snippet of that function to show the location of our scoring line:
...
if (%tilestrength<=0) {
%this.breakTile(%dstRef);
} else {
$levelscore+=$tilehitpoints*$multifactor;
...
...
So when the tile is still standing and hasn't broken, the else clause above is activated and we add $tilehitpoints times our $multifactor to the current score. Our default would be 50 times 1, so 50 points each time a tile is hit. Now we do the same thing for when the tile actually breaks. In tile.cs, add this line to the top of the tileClass::breakTile function: $levelscore+=$tilebreakpoints*$multifactor; This will add 250 points each time a tile actually breaks. Show the ScoreSo we are racking up the points, and we want the player to see it. The best place to change the score display seems to be at the end of the hitTile function; whether a tile is just hit or is broken, it always goes through this function. So add this at the very end of the tileClass::hitTile function in tile.cs, right before the closing bracket of that function: ScoreboardGuiScore.setValue($levelscore); We named our GuiTxtCtrl ScoreboardGuiScore so now we can refer to it directly and call the setValue method to change its text. Save everything and test it. You should now see your score racking up as you hit and break tiles. Nobody Lives ForeverUntil now a player has had unlimited lives. Lose a ball and another one appears instantly. Now we'll change that, and give a player a limited number of balls and lives. Twbt_pt4.zip Then drag the basegone.eff into your game/data/particles directory and the lostball.ogg and regen.ogg files into your game/data/audio directory. Quit and restart the TGB editor. Don't forget to reopen our playlevel.t2d level. So now we have a new particle effect and two new sound effects to use. We need to prepare the sounds for use, just like in the previous parts. We are going to use the effect in a slightly different way, so we don't need to add it to our initializeEffects routine. Open your gamesScripts/audioDatablocks.cs file and add this to the end of it:
new AudioProfile(regen)
{
filename = "~/data/audio/regen.ogg";
description = "AudioNonLooping";
preload = true;
};
new AudioProfile(lostball)
{
filename = "~/data/audio/lostball.ogg";
description = "AudioNonLooping";
preload = true;
};
This creates two new audio profiles ready to play, just like in part 2 and 3. Life IconsWhat we want to do is have icons onscreen represent how many ships and balls the player has left. To start we are going to give them three ships, each with three balls. When they lose the three balls, a ship goes away. When they lose all three ships, their game is over. We need a backdrop under the icons so it feels more like a hud overlay, so open playlevel.t2d if it is not already, and drag the backdropImageMap onto the scene view.(See fig. 4.2) Now double-click it so we can edit its positioning.In the Scene Object section, set Position X to 82.500 and Y to -67.572. Set the Width to 28.178 and Height to 8.856. Now save your playlevel.t2d. We need a new set of scripts relating to these life icons, so make a new file in your gameScripts/ folder called lifeicons.cs and let's start by adding this function to it:
function createLifeIcon(%lives, %gframe, %position){
%licon = new t2dStaticSprite() {
class = "lifeIconClass";
lives = %lives;
scenegraph = $thescenegraph;
imageMap = "icons1ImageMap";
frame = %gframe;
Position = %position;
size = "6.250 6.250";
};
return %licon;
}
This createLifeIcon function takes three parameters when called, the number of lives (or balls) this icon has, the frame of the imageMap we should start with, and the position of the icon. First we create a new sprite, give it the class "lifeIconClass" so we can add methods to it later, then make our own field called lives and assign it the %lives variable. Next we set the scenegraph so it shows up on our screen, then set the imageMap to our icons1ImageMap and the frame and position to the variables passed to us. The function then returns the newly created object with the return %licon line. We need to call this function three times to set the three icons that we want, so lets create the function that does that.
function initLifeIcons(%i1lives,%i2lives,%i3lives) {
$lifeicons = new SimSet(){};
if (%i1lives > 0) {
%temp = createLifeIcon(%i1lives, 3 - %i1lives, "92.017 -68.559");
$lifeicons.add(%temp);
}
if (%i2lives > 0) {
%temp = createLifeIcon(%i2lives, 3 - %i2lives, "84.017 -68.559");
$lifeicons.add(%temp);
}
if (%i3lives > 0) {
%temp = createLifeIcon(%i3lives, 3 - %i3lives, "76.017 -68.559");
$lifeicons.add(%temp);
}
}
Our initLifeIcons function first creates a new SimSet, like our $theballs SimSet from part two. We take three parameters in this function, the number of balls, or lives, each ship icon should have. Then we step through with three if statements; if the %ilives variable is greater than 0, it means we want to put a life icon up.
We set %temp to the returned value of the createLifeIcon function we just created. We pass the function the three values: the number of lives; the frame, which we get by subtracting the lives number from three. If you look at the frames of the icon1ImageMap you will see how this works. For example, if the lives number is 3 the result of 3 minus 3 will result in frame zero, which is an icon with all three balls showing.(See fig 4.4) The positions we pass are obviously specifically where we want this icon to appear. The next line in each if statement adds the new object to our $lifeicons SimSet. So now we have the icons up on the scenegraph. Now we need some helper methods for our lifeIconClass class. Add these at the end of lifeicons.cs:
function lifeIconClass::isAlive(%this) {
if (%this.lives > 0) {
return true;
} else {
return false;
}
}
function lifeIconClass::loseLife(%this) {
%this.lives--;
}
function lifeIconClass::delayDelete(%this) {
%this.safeDelete();
}
The lifeIconClass::isAlive method simply returns true or false, based on if the particular object it is being called on (%this) has lives left. The lifeIconClass::loseLife method decrements the life count of the lifeicon. And the lifeIconClass::delayDelete method removes the object from the scenegraph completely. Now the last and most important function in the lifeicons.cs file, the function that will actually be called whenever a ball is lost and we want to reduce the lives the player has. Add this at the end of the file:
function reduceLifeIcons() {
%count = $lifeicons.getCount();
%currenticon=$lifeicons.getObject(%count-1);
%currenticon.loseLife();
if (%currenticon.isAlive()) {
%currenticon.frame++;
} else {
$lifeicons.remove(%currenticon);
%currenticon.frame++;
%currenticon.schedule(500,"delayDelete");
$theBase.blowup();
}
return $lifeicons.getCount();
}
Let's go over this. First we use the getCount method of the $lifeicons SimSet to find out how many life icons are in there. The next line sets the local variable %currenticon to the 'topmost' object using the getObject SimSet method. The (%count-1) is there because the SimSet indexes its members starting at zero. So a count of 3 means the objects are referenced by (0), (1) and (2). We call our method loseLife to reduce the number of balls that the icon has, then we call our isAlive method which will return true if our icon still has lives, and false otherwise. If true, we increment the frame of the %currenticon. Remember, the way we set up the frames of the icons1ImageMap means that as we increment the frame, it will show one less ball. If the isAlive method returns false, we need do a few more things. We call $lifeicons.remove(%currenticon) which will pull it out of the SimSet, but not delete the object. Then we increase the frame, which will display the icon image with no balls. The next line schedules a call for the delayDelete method that we created, 500 milliseconds (half a second) in the future. The reason for this is simple; if we immediately deleted the %currenticon object, it would happen too quickly for the player to see the icon with no balls. We then call the blowup() method of our base, which we have yet to write. Finally, we return the $lifeicons count at the end of this function. If the count is zero, it will be returning zero, aka false, to whatever called it, and we will know there are no more ships left and the game is over. Any thing other than zero in the count will be seen as true, we still have life left. We have to pull this new cs file in, so we need to add it to our exec.cs file. At this point, your entire exec.cs file should look like this:
exec("./gameScripts/base.cs");
exec("./gameScripts/theLevels.cs");
exec("./gameScripts/ball.cs");
exec("./gameScripts/firing.cs");
exec("./gameScripts/audioDatablocks.cs");
exec("./gameScripts/tiles.cs");
exec("./gameScripts/lifeicons.cs");
exec("~/gui/ScoreboardGui.gui");
BoomWe're not quite ready to test yet. First, we need to add the blowup method we referenced above. When the last ball of a ship is lost, we want to do something more dramatic than just remove the tiny icon; blowing the ship up seems appropriate. Add this function to the end of your base.cs file.
function baseClass::blowup(%this) {
alxPlay(regen);
%explode = new t2dParticleEffect() { scenegraph = $thescenegraph; };
%explode.loadEffect( "~/data/particles/basegone.eff");
%explode.setEffectLifeMode(kill, 0.5 );
%explode.setSize("15 5");
%explode.setPosition(%this.getPosition());
%explode.playEffect(false);
%this.visible = false;
theArch.visible = false;
}
When the icon has no balls remaining, this method is called on the base. First, we play the destroyed base noise that we created the audioprofile for earlier. Then we create and load a new particle effect on the fly. This is the other way to play an effect during your game; I wouldn't use this method for an effect that may play every few seconds, like our wallhit effect. But it is certainly fine for an effect that we may not use again for several minutes.Note that we are using a local instead of global variable for the effect, and we are setting the EffectLifeMode to 'kill' here. This means that once the effect finishes playing, it will delete itself, which is what we want. We finally set the position to %this.getPosition() so the effect is centered on the base, then we play it. The last thing we do is hide the base during the explosion by setting its visible field to false. We do the same thing with the sheild, which we named theArch in part one. This is another way to refer to objects. Tying it TogetherSo now the pieces are in place, but we still aren't ready to test it because we still have not called our initLifeIcons function to set the icons up, and we haven't been calling the reduceLifeIcons function to take lives away. First, we'll add the initLifeIcons call. Open theLevels.cs and add this to the end of the initializeLevel function there: initLifeIcons(3,3,3); This calls the function and sets the three icons to having three lives each. You may want to look back at that function and see how it handles those three 3s. Now we need to take lives away. The place to do that, of course, is in the lost ball trigger. Open game.cs and find the lostTrig::onEnter function. In that function, find the section:
if ($theballs.getCount()==0) {
$thebase.loadBall();
}
This is what we had been doing; as soon as there were no balls left we would call the loadBall method on our base. Now we are going to change that to this:
if ($theballs.getCount()==0) {
alxPlay(lostball);
if(reduceLifeIcons()) { //still alive
$vmod=0; //stop the base
$thebase.schedule(1000,"unfreeze");
} else {
echo("DEAD");
}
}
We are still checking the getCount of the balls SimSet, and now the fist thing we do is play the lostball sound that we set up earlier. Next we call the reduceLifeIcons function in our if statement. Remember, that function returns true at the end if there are still life icons left, and false it there are not. So if it returns true, then we still have balls left, and we need to load a new one. We don't want to do it right away however, we want to pause for a second. We are going to set $vmod to zero, which has the effect (remember from part 1) of making the base immobile. Any mouse movements will be multiplied by zero, so the base will be frozen. We need to load a ball and unfreeze the base, so we schedule a method called unfreeze to run on $thebase in 1000 milliseconds, or one second. The else of this conditional statement is executed if reduceLifeIcons returned false. Right now, we are just going to echo DEAD to the console so we know we hit this statement; but this is where the game would be over. Finally, we need to create the unfreeze method for our base. Add this to the end of base.cs.
function baseClass::unfreeze(%this) {
%this.visible = true;
theArch.visible = true;
$vmod=$VMODSTANDARD;
%this.schedule(1000,"loadBall");
}
Here we are making the base and shield visible again, then setting the $vmod global back to its normal value, so our base will move freely again. And last we schedule the loadball method to reload the ball in one second. Well, that was a long time between testing, so lets save everything, load up the playlevel.t2d and test it all out. Lose a ball, watch the icons in the upper right to make sure they are working. Lose three balls and watch the base explode. If things don't work as expected, go back over everything we did in this section to make sure something didn't get missed. A TweakOne quick adjustment is needed. When the base blows up we set the visibility of the base and theArch to false. And in every unfreeze call we are setting it back to visible. But I don't care for the way it pops back in. It would be better if it faded back in. We are going to write a simple fade in function, and since we may be able to use such a function for almost any object, instead of writing a method for the baseClass, we are going to write it for the whole t2dSceneObject class. Open game.cs and add this to the end:
function t2dSceneObject::simpleFadein(%this)
{
if (!%this.fadingin) {
//beginning fadein
%this.setBlendColor("1 1 1 0.00");
%this.visible = true;
%this.fadingin = true;
%this.schedule(60,"simpleFadein");
} else {
%currentalpha = getWord(%this.getBlendColor(),3);
%currentalpha+= 0.06;
if (%currentalpha > 0.95) {
//finished fading in
%this.setBlendColor("1 1 1 1");
%this.fadingin = false;
} else {
%this.setBlendColor("1 1 1 "@%currentalpha);
%this.schedule(60,"simpleFadein");
}
}
}
Quickly, this method extends, and so will be able to be used on, almost any object we create. First it checks if a field called fadingin is true. This will be false when this is called the first time on an object, so the beginning fade in code will be executed. We use setBlendColor to set the alpha to 0, which will make it transparent. Then we set visible to true, so the fading in effect will be visible. Next we set our fadingin field to true, since it is now in the process of fading in. Finally we schedule the same method to be called in 60 milliseconds. So 60 milliseconds later this same method will be called on our object, and of course fadingin will be true, so the else section will be executed. Here we get the %currentalpha value with getBlendColor, then add a small amount to it. If the resulting new alpha value is greater than 0.95, we consider it almost faded in, and we use setBlendColor again to set it to fully opaque. Then we set the fadingin field to false. If the alpha is not higher than 0.95 we just set the alpha to the new value and call our method again in 60 milliseconds. We need to start the fade in, so add these two lines to the end of your baseClass::blowup method in base.cs: %this.schedule(2000,"simpleFadeIn"); theArch.schedule(2000,"simpleFadeIn"); This schedules the fading in effect on both the shield and the base for two seconds. Since we are using the simpleFadin method to make these reappear, we can remove the following two lines from the baseClass::unfreeze method: %this.visible = true; theArch.visible = true; Save everything and test it. You should see a smoother fade in of the ship and shield after the base explodes. One More ThingActually two more things. Background images and background music. Download the zipped file (below) and unzip it. It contains 10 background images, back1 through back10. Add only back1.jpg to your TGB editor with the Create New ImageMap button or by dragging it. Drag back2.jpg through back10.jpg to your game/data/images folder without adding them to the editor. The looping music audio profiles require a looping audio description, different than the nonlooping one we use for the sound effects. Open your audioDatablocks.cs file and add this to the very top:
new AudioDescription(AudioLooping)
{
volume = 0.75;
isLooping= true;
is3D = false;
type = $GuiAudioType;
};
Now we have an AudioDescription to use for looping audio, and you can see that we have set the volume to 75%, quieter than the sound effectes audio description. Next we can add the audio profiles for the looping music files we just added to our audio folder. Add this at the bottom of audioDatablocks.cs:
new AudioProfile(levelmusic1)
{
filename = "~/data/audio/lm1.ogg";
description = "AudioLooping";
preload = false;
};
new AudioProfile(levelmusic2)
{
filename = "~/data/audio/lm2.ogg";
description = "AudioLooping";
preload = false;
};
new AudioProfile(levelmusic3)
{
filename = "~/data/audio/lm3.ogg";
description = "AudioLooping";
preload = false;
};
new AudioProfile(levelmusic4)
{
filename = "~/data/audio/lm4.ogg";
description = "AudioLooping";
preload = false;
};
new AudioProfile(levelmusic5)
{
filename = "~/data/audio/lm5.ogg";
description = "AudioLooping";
preload = false;
};
In the next part of this series, as we develop our multilevel game system, each level will have its own background image and music, but for now let's hardcode it into our game. Add this to the end of the initializeLevel function in theLevels.cs:
$backimage = new t2dStaticSprite() {
imageMap = back1ImageMap;
scenegraph = $thescenegraph;
size = "200.000 150.000";
Layer = "20";
CollisionPhysicsSend = "0";
CollisionPhysicsReceive = "0";
};
$backimage.imageMap.imageName = "~/data/images/back1.jpg";
$backimage.imageMap.compile();
$levelmusic = alxPlay(levelmusic1);
Here we set $backimage to a new static sprite, setting the imageMap the one background image we added to the editor with the size set to fill the whole scene. The layer is set to 20 so it is behind everything (layer 0 is the topmost, layer 30 the bottom in the scene.) After we define $backimage, the next two lines are important for memory usage. We assign the back1.jpg as the $backimage.imageMap.imageName. Then we use the imageMap's compile method to update the imageMap itself. So why are we doing this? Well, our background images are quite big, and if we loaded them all into our game using the editor they start to take up quite a bit of memory. Even though the jpg may be only 100k as a file, once it is loaded into memory as a texture it can take up ten times that much RAM. We could uncheck the preload box in the imageMap details in the editor, or set preload to zero in game/managed/datablocks.cs where the editor imageMaps are defined; but this isn't foolproof. Though they won't be loaded into active memory for the running game, they still are loaded in when the TGB Editor runs, and if you run your game from there, you may find memory is overloaded and your framerate drops to a crawl. I also like compiling the imageMap dynamically, whenever we want to. Theoretically we could have hundreds of images used for backgrounds, and we wouldn't have to worry about how much memory it is taking up because we are only swapping in different images to the same imageMap chunk of memory. The last line uses alxPlay to start one of our looping music audio profiles. We assign it to the global variable $levelmusic, because unlike sound effects, this will keep going and we need to reference it with the global variable later to shut it off. Save everything and test, and you should hear and see our new additions. ConclusionWell, now it's getting fun. The next part of this tutorial will expand our game into multiple levels with a main menu system. Thanks for reading, and feel free to post any comments or questions on the BreakoutTutorial Part 4 Discussion Thread. Continue on to:
|
Categories: TGB | GameExample | Tutorial








