Hudzilla Coding Academy: Project Six

Code
Hudzilla Coding Academy

 

Continuing with our plan to make every other project a game, in this lesson you're going to learn how to produce a mouse-powered game where clicking quickly (and intelligently!) is the key to winning. Nearly all the important coding theory is behind you now, which technically means there's only a small amount you need to learn to make this game.

However, I don't want to let you get away lightly, so I'm going to use this lull to cram in a few more features that will serve you well in the future - the kinds of features that really add gloss to a game and easy maintenance to your code. Let's crack on!

First steps

The game we're going to create is a simple but fun one. The player starts off seeing a screen full of moving bubbles, with each bubble having a unique number on. The goal is to click the lowest-numbered bubble first, and if the player clicks a higher-numbered bubble by accident the game creates another bubble as a penalty. Over time, more bubbles are created, meaning that the player has to hurry otherwise they will be overwhelmed.

Start by creating a new MonoDevelop console project called BubbleTrouble and give it all the required libraries in the References list - that's System.Drawing, SdlDotNet.dll, and Tao.Sdl.Dll, with those last two copied into your project directory. We looked at how to do this in Project Four, so please follow the instructions there because I won't be repeating them again. Getting this basic, SDL-ready project up and going is something you'll need to do many times in the future!

So, you should be looking at the BubbleTrouble solution in MonoDevelop, with SDL all configured and ready to go. By default the main class will just be called MainClass, and it will have a static Main() method that prints out Console.WriteLine(). Now, here's the first thing I want to introduce you to: what "static" is and why we're about to stop using it...

Goodbye, static!

In previous projects I've shown you how to create your own classes and objects in Mono, and this system is very helpful because it lets you define things in your code just as they would be in real life. For example, we had a Fish class in the Fish Feast project that contained all the information each fish needed to know about itself.

But what do you want to do if you want all objects from a class to share some data between them? One option is to have these sorts of variables in your main class so that they are shared across the entire program, but that gets very messy when you have more than one class. After all, why should one class have access to another class's shared variables?

A better solution is to use so-called static variables, which are defined inside a class and are therefore available inside that class, but are shared between all objects of that class. What's more, these static variables can be made available outside of the class so that they can actually be used even if no objects of that class exist.

An example of a static variable in a class.

An example of a static variable in a class: here we have a web server request logging class that has a Count number as a static variable and everything else being normal. The reason the Count variable is static is because it means it exists in only one place so that when a new web page request comes in we just add one to it regardless of which URL was requested.

The reason I'm telling you all this is because we've been declaring nearly all our variables and methods as "static" so far, but that's about to change. You see, the Main() method that MonoDevelop gives us by default is declared as static, and that's done because it's part of the MainClass class. When Mono runs our app, nothing exists - no objects at all. So to get our program going, it runs the Main() method of our class without even creating an object. That's why the Main() method needs to be static: because no objects exist, it can't be a normal method - it needs to be callable straight from the class definition.

Once the Main() method is running, it can go ahead and create new objects to do the real work. But to make things easy in previous projects, we've been putting all our functionality straight into the MainClass class. Remember: no objects of this class exist; it's all being run statically, straight from the class. As a result, that means we had to declare all our other methods and variables as being static too, because we had no object to reference them from.

If all this doesn't make sense, it will soon enough: create a new class in MonoDevelop and call it BubbleTrouble. Back in Main.cs, replace the Console.WriteLine() call with this:

BubbleTrouble game = new BubbleTrouble();
game.Run();

That's it. That's the MainClass file and Main.cs all done with - we won't be touching that file again. You see, once the Main() method has been run, it creates a new instance of our BubbleTrouble class and hands off control to it. Sure, we haven't defined the Run() method or indeed any sort of code in the BubbleTrouble class at all, but the point is that the Main() method is basically an empty shell that sets up our game then gives control of the program to it. The real work from now on is all done inside BubbleTrouble.cs...

The basic game environment

We've looked at this previously, so I want to go into only the lightest detail here. The first goal is to get a basic SDL environment working so that we can start plugging in parts of our game. To get to the "here's your black SDL screen, now what?" position, you need to do this:

  • Add all the "using" lines at the top - that's SdlDotNet.Core, SdlDotNet.Graphics, SdlDotNet.Input, System, System.Collections.Generic and System.Drawing.
  • Create a surface called sfcGameWindow, and assign it to the return value of Video.SetVideoMode() in the BubbleTrouble constructor method. Use 1024x768 for your resolution.
  • Hook up Events.Tick and Events.Quit to empty methods, then call Events.Run(). Note that I've put these into a special Run() method to keep everything nicely isolated - I suggest you do the same, at least for this project.

Once you've done all that, your Main.cs file should look like this:

using SdlDotNet.Core;
using SdlDotNet.Graphics;
using SdlDotNet.Input;
using System;
using System.Collections.Generic;
using System.Drawing;

namespace BubbleTrouble
{
	public class BubbleTrouble
	{
		Surface sfcGameWindow;		
		
		public BubbleTrouble()
		{
			sfcGameWindow = Video.SetVideoMode(1024, 768);
		}
		
		public void Run() {
			Video.WindowCaption = "Bubble Trouble";
			Events.Tick += new EventHandler<TickEventArgs>(Events_Tick);
			Events.Quit += new EventHandler<QuitEventArgs>(Events_Quit);
			Events.Run();
		}
		
		void Events_Tick(object sender, TickEventArgs e) {

		}
		
		void Events_Quit(object sender, QuitEventArgs e) {
			Events.QuitApplication();
		}
	}
}

If you run your program now, you'll see the black SDL screen - that means you're all set to go. If you want to, take a backup of your solution in this state so that you can use it as a base for your future games.

The Hello World of SDL.

This is it, folks: the starting point for all SDL projects. Get this right, and the rest is easy!

You'll notice I slipped in an assignment to Video.WindowCaption to set the title of the window when the game is running - you can call it whatever you want, but don't do it every tick because it will slow your game down greatly!

Creating and loading bubbles

In the source code download for this project I've provided five bubble pictures of different colours that we'll be using in this game. Each picture is 128x128, which is more than large enough for our needs and makes the bubbles large and colourful enough to be fun for kids.

One of the bubbles we'll be using.

Each of the "bubbles" are a different colour, just because it looks nicer!

Inside your BubbleTrouble project directory (that's probably something like /home/yourusername/BubbleTrouble/BubbleTrouble), create a new subdirectory called "media" and copy my five bubble pictures into there. Now, beneath the "Surface sfcGameWindow" line, add this:

List<Surface> BubbleTypes = new List<Surface>();

Then put these lines into the BubbleTrouble() constructor method, underneath the call to Video.SetVideoMode():

BubbleTypes.Add(new Surface("media/bubble_blue.png"));
BubbleTypes.Add(new Surface("media/bubble_green.png"));
BubbleTypes.Add(new Surface("media/bubble_purple.png"));
BubbleTypes.Add(new Surface("media/bubble_red.png"));
BubbleTypes.Add(new Surface("media/bubble_yellow.png"));

Notice that those lines load the image from the "media" directory, but by default MonoDevelop outputs its binaries into the bin/Debug directory. To change that, go to Project > Options and look under the Configurations > Debug (Active) > Output category. In there you should find the "Output path" option: change it from "/home/yourusername/BubbleTrouble/BubbleTrouble/bin/Debug" to "/home/yourusername/BubbleTrouble/BubbleTrouble". If you don't do this, your program won't be able to find any of the artwork, and it will crash spectacularly!

The BubbleTypes list is - somewhat predictably - going to hold all the different bubble types. There isn't really any difference between the different bubble types, they just have different colours. To create real bubbles on the screen, we need to create a class that defines a bubble on the screen then put all the bubbles we create into another List array.

So, create a new class called Bubble and give it this code:

using SdlDotNet.Graphics;
using System;
using System.Drawing;

namespace BubbleTrouble
{
	class Bubble {
		public float X;
		public float Y;
		public int Direction;
		public int Speed;
		public int Type;
		public int Number;
	}
}

Most of that should be self-explanatory: each bubble has its own X and Y position, the direction and speed of its movement, and its bubble type (which is an index into the BubbleTypes list). What's interesting in there is the Number variable, which is where the whole point of this game comes in: each bubble displays a number, and they need to be clicked in the ascending order. Our program will check this number to make sure the correct number is clicked.

However, even more interesting in that code block is a new data type: "float". This stores what's known as a floating-point number, which is geek speak for any number that isn't whole, such as 3.1, 3.14 or 3.141592654. Think of it like an integer, just with some numbers after the decimal point.

Back in BubbleTrouble.cs, add this directly underneath where the BubbleTypes list is defined:

List<Bubble> Bubbles = new List<Bubble>();
int LastCreatedTime;
int MaxNumber;
Random Rand = new Random();

The first line stores all the bubbles that are on the screen right now so that we can update and draw them appropriately. The second line will be used to track when the last bubble was created, so we know when to create another one automatically. The third line stores the current highest number of any bubble, so we know where we left off. And of course the last line creates a new random-number generator - we'll be needing that a lot soon!

The next step is to tell the program to update and draw itself every tick. Right now we have the empty method Events_Tick(), and, rather than dump lots of code in there, we're going to separate updating and drawing into two small methods, so change Events_Tick to this:

void Events_Tick(object sender, TickEventArgs e) {
	Update(e.SecondsElapsed);
	Draw();
}

void Update(float elapsed) {
	
}

void Draw() {
	
}

So, Events_Tick() now calls two other methods, which are in turn empty. Note how I'm passing e.SecondsElapsed into the Update() method - this will be needed later.

For now, we need to put some code into the Update() method to create a new bubble every second. Change the Update() method to this:

if (LastCreatedTime + 1000 < Environment.TickCount) {
	CreateBubble();
}

Clearly that creates bubbles far too fast for the game to be fun, but we're just testing right now so it's fine. The CreateBubble() method needs to do the following things:

  • Add a random number to MaxNumber, generating gaps between bubbles. That is, if we add 1 to MaxNumber each time, the next bubble is obvious, whereas if we add a number between 1 and 4, then it's harder - the player needs to be sure they have found the next bubble.
  • Create a new bubble somewhere on the screen with a random speed and direction.
  • Add the new bubble to the list and change LastCreatedTime so the game knows another bubble was made.

All that - minus one part, which I'll explain in a moment - can be done with very few lines of code. Here's how CreateBubble() should look:

void CreateBubble() {
	MaxNumber += Rand.Next(1, 4);
	Bubble bubble = new Bubble();

	bubble.X = Rand.Next(1024);
	bubble.Y = Rand.Next(768);
	bubble.Speed = Rand.Next(70, 120);
	bubble.Type = Rand.Next(0, BubbleTypes.Count);
	bubble.Number = MaxNumber;

	Bubbles.Add(bubble);

	LastCreatedTime = Environment.TickCount;
}

The observant among you will have noticed that there's nothing in there that assigns a direction to the bubble. That's because giving bubbles direction requires some very specific code, so we're going to be covering it separately. For now, we just need to make sure the bubbles are being created at random places, so edit your Draw() method to look like this:

void Draw() {
	foreach(Bubble bubble in Bubbles) {
		// the next line has been broken across two lines
		sfcGameWindow.Blit(BubbleTypes[bubble.Type],
		new Point(Convert.ToInt32(bubble.X), Convert.ToInt32(bubble.Y)));
	}
	
	sfcGameWindow.Update();
}

You should be familiar with blitting and Update() from the Fish Feast project, but what's new here is that we have to pass each bubble's X and Y position through a method called Convert.ToInt32() before drawing the bubble. The reason for this is that we're using floats (floating-point numbers) for the bubble's X and Y position, but we have to draw in exact pixels on the screen. Seeing as there's no such thing as a 0.3 of a pixel (or indeed any fraction of a pixel), we have to convert the float to an integer (known as int32, because it uses 32-bits of memory to store the number) before drawing.

Of course, the follow-on question to that is, "why use a float for the position, if you're just going to convert it to an integer?" We'll deal with that soon enough. For now, all you need to know is that bubble position is stored as two floats, but converted to integers before being drawn.

If you run the game now, you should see bubbles appearing on the screen every second. They won't move, they won't have numbers on, and you can't click them, but that's all coming soon - honest!

The game so far.

The game so far: bubbles are created, but don't move, can't be clicked on, and have no numbers. Have patience!

Choosing a direction

Support for giving bubbles a direction was left out of the CreateBubble() method because it's not as easy as you might at first think. You see, there are two distinct problems: first, a bubble might be created half-way off the screen, and have a 0-degree direction, making it move directly upwards. If that happens, its number will never be fully visible, which makes the game impossible. Second, choosing purely random directions may result in lots of bubbles having very similar directions, which makes the game look dull.

The solution to both of these problems is to create a list of all acceptable directions, randomise it, then use the next direction from that list each time a bubble is created. To do that, we need a new list array, and a number that remembers the last position used. Put these two under the "Random Rand" line you added a few minutes ago:

List<int> Directions = new List<int>();
int DirectionPos;

Now scroll down to your constructor for BubbleTrouble(), and put these lines beneath all the calls to BubbleTypes.Add():

for (int i = 0; i < 360; ++i) {
	if (i < 10) continue;
	if (i >= 350) continue;
	if (i > 80 && i < 100) continue;
	if (i > 170 && i < 190) continue;
	if (i > 260 && i < 280) continue;

	Directions.Add(i);
}

That loops over all the angles between 0 and 359, adding them to the Directions array only if the angle isn't almost vertical or almost horizontal. Once that runs, Directions will be filled with the 300 or so directions that are at interesting angles that make the game playable.

Because all those angles get added in order, it looks odd when the bubbles are created because their movement isn't random. So, what we need to do is to randomise the contents of that list to ensure the directions don't all come in sequential order.

To perform this task, I want to give to you one of the methods I use nearly all the time in my own games: ShuffleList(). This is a tiny little method I wrote myself to randomise the order of a list, and uses a particularly advanced coding technique that allows a list that holds any data type to be randomised. Here's the code - paste this method somewhere into Main.cs:

void ShuffleList<T>(List<T> list) {
	for (int i = 0; i < list.Count; i++) {
		T tmp = list[i];
		list.RemoveAt(i);
		list.Insert(Rand.Next(0, list.Count), tmp);
	}
}

All that <T> stuff is there to tell Mono we don't know what kind of data to expect - ignore it, you don't need to know how it works. The rest of the method simply loops over each item in the list, removing it, then re-inserting it at a random place in the list, effectively shuffling the whole list.

This method is just magic: give it a List<int>, a List<string> or even a List<Bubble> and it will randomise its contents - you'll find yourself using it a lot.

So, with ShuffleList() in place, put in the BubbleTrouble constructor, just after the "for" loop for building the Directions list:

ShuffleList(Directions);

The next step is to assign bubbles a direction when they are created. Because of the need to choose the next direction from the Directions list and increment the DirectionPos number each time a direction is read, this is best done in a method. So, put this new method into your code somewhere:

int ChooseBubbleDirection() {
	++DirectionPos;
	if (DirectionPos == Directions.Count) DirectionPos = 0;			
	return Directions[DirectionPos];
}

You should be able to read that and see that it returns an integer (the direction for the bubble) and increments DirectionPos by one each time it is called (going back to 0 if we've run out of directions).

To make use of ChooseBubbleDirection, just put this line into CreateBubble(), just before Bubbles.Add():

bubble.Direction = ChooseBubbleDirection();

If you run the code now, it will look like nothing has changed. That's because even though we've created the Directions array, randomised it and given each bubble a direction, we haven't actually told them to move. This is where it gets a wee bit trickier...

Frame-independent movement

We're about to cover one of the most important lessons in this project, and that is frame-independent movement. You see, so far we've been telling game objects to move a certain amount whenever a tick happens - whenever the game updates. This is good for very simple games, but for anything serious you need to be smarter.

You see, if you have a lot happening on the screen, slower computers will take longer than faster computers to update and draw your scene. So if you move an object 10 pixels to the left every tick, it will move faster on the faster computers than it will on the slower computers because ticks happen more frequently. Whereas a slow computer may struggle along at 5 ticks per second (moving 50 pixels to the left every second), a fast computer might perform 60 ticks a second (moving 600 pixels to the left every second).

The solution is to change the amount something is moved depending on how much time has elapsed since the last update. So, let's say you want to move something to the left 100 pixels per second. If 1/10th of a second has passed since the last update, you just need to multiply the object's movement (100) by 1/10th of a second (0.1 seconds) to get 10, meaning that the object should be moved to the left 10 pixels. If 1/100th of a second has passed since the last update, you multiply 100 by 0.01, to get 1, meaning that the object should be moved to the left 1 pixel.

Now, what happens if 1/400th of a second passed since the last update? Continuing our formula, we'd need to multiply 100 by 0.0025, which comes to 0.25, meaning that the object should be moved 0.25 pixels to the left. Er... wait a minute - we already said there was no such thing as a fraction of a pixel, so how can we possibly move something 0.25 pixels to the left?

The answer is that we can't. At least not if we use plain old "int"s to store our positions. Using integers, something that moved 0.51 pixels to the left would have to be rounded up to move 1 pixel, and something that moved 0.49 pixels or left would have to be rounded down to 0 pixels - causing it not to move at all.

The solution to this problem is of course to use floating-point numbers to store positions, which is of course what we're doing. That means internally a bubble might know they it is at X 34.291 and Y 291.889102, but it will be rounded when drawn to X 34 and Y 292, allowing it to glide smoothly across the screen at the correct speed.

Our Update() method already receives a float value called "elapsed", which contains the number of seconds that have passed since the last tick. To make any movement frame-independent (ie, to make it run the same speed across all machines), just multiply it by "elapsed".

If you're thinking, "gosh, that was painful to learn," then I'm afraid I've got something even scarier: to turn a direction into actual movement, we need a bit of mathematics. You see, to get the X movement from a direction, you need to convert it to radians (like degrees, except based on Pi rather than the magic number 360) then calculate the cosine of that angle. To get the Y movement, you do the same except use the sine of that angle.

Once you've calculated the sine or cosine of the angle, you need to multiply that by the speed of the bubble, then multiply the result by the "elapsed" variable to make it frame-indepedent to get the finished movement. The one small hiccup here is that calculating sines and cosines is done in an all-new data type in Mono called "double", which is just like a "float" except it has much higher accuracy.

To Mono, a "float" is just a simplified "double" - in fact, the name "double" is used because it is stored using 64 bits of memory, whereas a "float" is stored using just 32 bits. As a result, you'll sometimes see the "float" data type referred to as "single", because it needs half as much data storage space as a double.

So, let's bring all this hard stuff together into just four lines of code: calculating the X movement, calculating the Y movement, converting the X movement to a single and adding it to the bubble's X position, and converting the Y movement to a single and adding it to the bubble's Y position. Once you top and tail it with a foreach loop so that every bubble is moved, you get this:

foreach(Bubble bubble in Bubbles) {
	double xmove = (Math.Cos(bubble.Direction * Math.PI / 180) * bubble.Speed) * elapsed;
	double ymove = (Math.Sin(bubble.Direction * Math.PI / 180) * bubble.Speed) * elapsed;

	// these next two lines convert the doubles to floats
	bubble.X += Convert.ToSingle(xmove);
	bubble.Y += Convert.ToSingle(ymove);				
}

Yes, that's incredibly ugly. Yes, it's an algorithm that "just works" and so is best left alone. And yes, there are better ways to do it, namely calculating the X and Y movements just once, storing them in each bubble, then moving by that amount each tick. But the above code is the easiest, and, like I said, it just works - put it in and forget about it.

Now what we have are bubbles that move every tick, and, thanks to all that frame-independent movement stuff, they move the same amount regardless of whether they are running on a 486 or a Core i7.

Background and wrapping

As the bubbles move across the screen, you'll notice they leave a trail behind them against the black background. That's because we're not actually clearing the background every tick, which means we're drawing on top of whatever was there previously. The solution is simple: load a background, and draw it before everything else each tick.

In the download for this project I've included background.png - copy that into your "media" directory alongside the bubble pictures. Now put this line near the top of BubbleTrouble.cs, just beneath "Surface sfcGameWindow":

Surface sfcBackground = new Surface("media/background.png");

Down in the Draw() method, put this above everything else in the method:

sfcGameWindow.Blit(sfcBackground);

That eliminates any trails left from the previous tick by filling the whole screen with the background image.

Another problem you might have noticed is that bubbles move off the screen and never come back, which makes the game impossible to play because the bubble you need to click might be 5000 pixels off the screen by the time you get to it!

The solution here is to make the bubbles wrap around when they hit the edge of the screen. That is, if a bubble goes off the bottom of the screen, it should re-appear on the top of the screen. This is easy to do, so to make things a little more interesting we're going to make use of constants as seen in Project Three.

You see, our code already uses the numbers 1024 and 768 in two places: creating the window and creating bubbles. What happens if we change the screen resolution to 800x600 or 1920x1200? The answer is that we'd need to go through all the code, changing 1024 to 1920 and 768 to 1200 - a very painful process indeed. A smarter option is to create two new constants, GameWidth and GameHeight, that store the screen resolution. So, add these two lines after "int DirectionPos" near the top of BubbleTrouble.cs:

const int GameWidth = 1024;
const int GameHeight = 768;

Now you can change the two references to 1024 and 768 to GameWidth and GameHeight.

With these two new constants in place, it's much easier (and cleaner) to write the code to make bubbles wrap at the edge of the screen:

  • If the bubble's X position is greater than GameWidth, move it to off the left-hand edge of the screen.
  • Otherwise, if the bubble's X position is less than 0 minus its width, move it to off the right-hand edge of the screen.
  • Repeat for Y and height.

The "0 minus its width" part is important, because it means our 128-pixel-wide bubble will be moved to -128, placing it exactly off the left-hand edge of the screen, ready to come on.

Here's how that code looks in C# - put this just after the "bubble.Y += Convert.ToSingle(ymove)" line in your Update() method:

if (bubble.X > GameWidth) {
	bubble.X = -BubbleTypes[0].Width;
} else if (bubble.X < -BubbleTypes[0].Width) {
	bubble.X = GameWidth;
}

if (bubble.Y > GameHeight) {
	bubble.Y = -BubbleTypes[0].Height;
} else if (bubble.Y < -BubbleTypes[0].Height) {
	bubble.Y = GameHeight;
}

If you run the game now, you'll see the bubbles wrap neatly when they hit the screen edges, meaning that they are always available for play.

Wrapping bubbles across the edges of the screen

Wrapping bubbles across the edges of the screen: this picture shows the play area (the bubble background) and the space immediately off-screen (the light grey area). The bubble starts at position A, moves to B, then to C. When it hits C, it's fully off the right-hand edge of the screen, so our code moves it immediately to position D, where it continues to move to E and F.

Eliminating "pop"

It looks a bit silly for bubbles to appear randomly on the screen - a much better system is to have the game start with 15 or so bubbles that are created automatically and on the screen somewhere, but have additional bubbles made off the screen so that they don't just appear.

The way to do this is to make CreateBubble() accept a boolean parameter: if it's true, the bubble should be created off screen; otherwise, create it anywhere. So, change the start of your CreateBubble() method to this:

void CreateBubble(bool offscreen) {
	MaxNumber += Rand.Next(1, 4);
	Bubble bubble = new Bubble();

	if (offscreen) {
		switch (Rand.Next(2)) {
			case 0:
				// enter from the top or bottom
				bubble.X = Rand.Next(GameWidth);
				bubble.Y = -BubbleTypes[0].Height;
				break;

			case 1:
				// enter from the left or right
				bubble.X = -BubbleTypes[0].Width;
				bubble.Y = Rand.Next(GameHeight);
				break;
		}
	} else {
		bubble.X = Rand.Next(GameWidth);
		bubble.Y = Rand.Next(GameHeight);
	}

In that code, if "offscreen" is set to be true then we create a bubble either off the top edge or off the left edge of the screen. If the bubble is created off to the left and is told to move left, it will automatically wrap around to appear on the right edge of the screen, whereas if it's moving to the right it will appear from the left.

Now, go up to the top of your Update() method and change the "LastCreatedTime + 1000" conditional statement to read this:

if (LastCreatedTime + 8000 < Environment.TickCount) {
	CreateBubble(true);
}

With that change, new bubbles are created only once every eight seconds, and they are also created offscreen because we're passing "true" to CreateBubble().

Now that bubbles are created at a more stately pace, we can create a whole load of them at the beginning to get the game going. As these are created before anything is shown, we can create them onscreen (passing "false" to CreateBubble), by putting this code just after the call to ShuffleList(Directions) in the BubbleTrouble constructor:

for (int i = 0; i < 15; ++i) {
	CreateBubble(false);
}

If you've made it this far, we now have a game where bubbles fly around the screen smoothly, wrapping at the edges, with a new bubble being created every eight seconds. Now it's time to make the real game - we need to add numbers to the bubbles and make them clickable!

SDL and fonts

You've already seen how easy SDL makes it to load pictures and draw to the screen. Well, it turns out that fonts are just easy to work with, and I could probably explain to you how to use them in just a few lines of code. Sadly, my goal isn't to turn you into a good programmer - my goal is to turn you into a great programmer. So instead, I'm going to show you the smartest way to work with fonts in this game.

To recap, our bubbles already have a number assigned to them, but it's just not visible. To make that visible, we have to ask SDL to load a font, then render that font to a surface with some text on - specifically, the number for that balloon. The problem is that rendering some text to a surface is quite slow and wasteful with memory, so a better solution is to have generate the text once then store it inside the bubble object.

The smartest way to do this is to use something called properties, which are a curious cross between methods and variables. You see, Mono lets us control what happens when a variable is read or set. Rather than just output the value or change it, we can write special code that does something more interesting. For example, when we assign a number to a bubble, wouldn't it be nice if it automatically used SDL to render the text to a surface just that once? Sure it would - and properties let us do just that.

First up, we need to load the SDL font. In the files for this project I've included a file called freesans.ttf - this is taken from the GNU Project's FreeFont collection, which is freely redistributable under the GPL. Copy that into your media directory so we can use it inside the game.

In BubbleTrouble.cs, put this line of code just beneath "const int GameHeight":

public static SdlDotNet.Graphics.Font fntMain =
   new SdlDotNet.Graphics.Font("media/freesans.ttf", 36);

I've had to split that into two lines because it's so long. And it's so long because if we just used "Font" as the data type, it could mean either the Font class provided inside System.Drawing or the Font class provided inside SdlDotNet.Graphics. To tell Mono exactly which Font class you mean, you just need to be specific: SdlDotNet.Graphics.Font.

Yes, the "static" is back - you'll see why in a minute.

To create a new font, you need to tell SDL which filename to load and what size you want to work with. If you want to make the font stand out a little more clearly, you could add this to your constructor method just after the final BubbleTypes.Add() call:

fntMain.Bold = true;

Now comes the magic: whenever the Number variable of a bubble is set, we want Mono to automatically generate a surface containing the bubble's number. The best way to explain this is by showing you the code, interspersed with lots of comments. Here goes:

// this will contain the SDL surface of the rendered text
public Surface Text;

// this will contain the actual number
public int _Number;

// this next bit is our property - it's not a real variable!
public int Number {
	get {
		// this bit will be called whenever the variable is read:
		// we just want to return whatever is in _Number.

		return _Number;
	}

	set {
		// this bit will be called whenever the variable is changed
		// we want to set _Number to be the value that is being written
		_Number = value;

		// and then we need to re-render the surface with the new number
		Text = BubbleTrouble.fntMain.Render(Convert.ToString(_Number), Color.Black);
	}
}

The whole "public int Number" part is new - notice how it doesn't end with the usual semi-colon (";"), but instead has its own block of code all to itself. The "get { }" part is executed whenever the variable is read, and means you can do all sorts of fancy shenanigans before returning the value. To pass the value back to Mono, all you need to do is use "return", just like normal methods. For writing, Mono has a special variable called "value" that contains the value the code is trying to assign to your property.

In the code above, the important stuff lies in the "set" section: "value" is stored in "_Number", then the text surface is recreated using fntMain. But of course fntMain isn't available inside Bubble.cs because it belongs to the BubbleTrouble class. Fortunately, we declared it as "public static", which means that any code can read its value simply by referencing the class name - and that's why we use BubbleTrouble.fntMain to read the font.

The Render() method takes two parameters we're interested in: the string to draw and the colour to draw it. You can provide more if you want to experiment, but two is all we need right now.

Before we move on, a warning: if in your "set" section you assign value to the property name by accident - eg by using "Number = value" rather than "_Number = value", your code will enter an infinite loop and lock up pretty quickly. What happens is that Mono tries to set Number... which in turn tries to set Number... which in turn tries to set Number... and so on. If you want to use properties, make sure you use them correctly!

If you were wondering, using a property without specifying a "set" section makes it read-only. Easy, huh?

Drawing the numbers

Now comes the interesting bit: drawing the numbers on the bubbles so the player can see exactly which bubble needs to be clicked next. Believe it or not, this takes just one line of code, although it's a particularly long one so I've split it across four lines. Put this inside the foreach loop inside the Draw() method:

sfcGameWindow.Blit(bubble.Text, new Point(
(Convert.ToInt32(bubble.X) + 64) - (bubble.Text.Width / 2),
(Convert.ToInt32(bubble.Y) + 64) - (bubble.Text.Height / 2)
));

That code is actually really easy to understand, despite what you probably think at first. You see, what we need to do is draw the number directly in the centre of the bubble. The dead centre of the bubble is easy to calculate: our bubbles are 128x128 each, so we just need to add 64 to the bubble's X and Y positions to get the middle. However, if we draw the text at the dead centre of the bubble, it will actually be offset to the bottom-right because it will be drawn from the text's top left.

The solution is to take the centre of the bubble, then subtract half the width and height of the text that will be drawn. That way, the centre of the text will be over the centre of the bubble, which is what we want. The above line of code does just that: finds the centre of the bubble, subtracts half the size of the text, and draws the surface just there.

With the numbers being drawn on the bubbles, the game is almost complete.

With the numbers being drawn on the bubbles, the game is almost complete.

Handling mouse clicks

We are, finally, coming to the most important part of this game: handling mouse clicks so that players can select the lowest-numbered bubbles to clear the playing field. This requires us to ask SDL to monitor mouse clicks, then to check whether the bubble that was clicked was the correct one.

To tell SDL you want to monitor mouse clicks, put this line up in the Run() method, just beneath the assignment to Video.WindowCaption:

Events.MouseButtonUp += new EventHandler<MouseButtonEventArgs>(Events_MouseButtonUp);

To make that work, you need to create a new method called Events_MouseButtonUp, like this one:

void Events_MouseButtonUp(object sender, MouseButtonEventArgs e) {

}

The question is, what should go inside there? Well, what it needs to do is loop over each of the bubbles that exist, and check whether the mouse click took place over each bubble. If it did, then that bubble was clicked and we need to check whether it was correct or not.

Forget about the checking whether the correct bubble was clicked or not for now. The first problem we have to solve is how to check whether the mouse click took place over a bubble or not. The easiest way to do this is to use Pythagoras's theorem to calculate the hypotenuse of a triangle - with the other two sides being the X and Y offset of the mouse click from the bubble's centre.

If your schoolboy maths are a little weak, let me recap:

  • We know where each bubble is. If we add 64 to its X and Y position, we have its centre.
  • We know each bubble is 128x128 pixels, which means its radius (the length from its centre to its edge) is 64 pixels.
  • We can check where the mouse click took place by reading SDL's Mouse.MousePosition variable.
  • If we subtract the circle's X and Y from the mouse click's X and Y positions, we know how far away the mouse click was from the centre in terms of X and Y co-ordinates.
  • We can then use Pythagoras'<s theorem to check how long the line is between 0 and the point of the X and Y distance.

Pythagoras's theorem states that A2 + B2 equals C2, meaning that if our X distance squared added to our Y distance squared is less than our radius squared, the click was over the bubble.

Using Pythagoras's theorem to calculate whether a point is inside a circle.

Using Pythagoras's theorem to calculate whether a point is inside a circle. In this example, the large green circle is the bubble, and the small red circle is the mouse click. We can calculate the length of A and B by simply subtracting the circle's centre from the mouse click's position, but we need to calculate the length of C by squaring A and B then adding them together.

Here's what the Events_MouseButtonUp method should contain:

for (int i = 0; i < Bubbles.Count; ++i) {

	Bubble bubble = Bubbles[i];
			
	// calculate the bubble's centre
	float bubble_cx = bubble.X + 64;
	float bubble_cy = bubble.Y + 64;
	
	// now see how far away our click was
	float dist_x = bubble_cx - Mouse.MousePosition.X;
	float dist_y = bubble_cy - Mouse.MousePosition.Y;
	
	// calculate the hypotenuse squared
	float dist = dist_x * dist_x + dist_y * dist_y;
	
	// if the distance is less than our radius squared, it was clicked!
	// NB: 4096 is 64 * 64
	if (dist < 4096) {
		ClickBubble(bubble);
		return;
	}
}

If you want to make your code a little clearer at the expense of a huge amount of processing time (really, it's very slow), you can pass "dist" through Math.Sqrt to calculate its square root and compare that against 64 rather than 4096. But trust me: it's hideously slow.

That code relies on an outside method, ClickBubble(), to be run when a bubble is clicked. Let's look at that now - add this new method somewhere into BubbleTrouble.cs:

void ClickBubble(Bubble clicked) {
	// loop over all the bubbles
	foreach(Bubble bubble in Bubbles) {
		// if we find a bubble with a lower number
		// than the one we clicked - that's bad!

		if (bubble.Number < clicked.Number) {
			// if we're here, they clicked the wrong bubble

			if (Bubbles.Count > 20) {
				// we already have lots of bubbles - don't create any more!
				return;
			}

			// penalise the player by creating a new bubble
			CreateBubble(true);

			// now bail out
			return;
		}
	}

	// if we're still here, this bubble was the correct one!
	Bubbles.Remove(clicked);
}

And that's it: you can now run the game and play it properly - enjoy!

Let's wrap up

This is, ultimately, a very simple game. But along the way you have learned what static variables are, what floats and doubles are, how to do frame-independent performance, how to shuffle a list array, how to work with SDL fonts and the mouse, and let's not forget properties - I think they are something you'll come to love with time.

So even though it was a very simple game, I think you have learned a huge amount along the way, including several techniques that you'll use for years to come. Well done!

Homework

If you're following this with a tutor, you will be required to complete the following homework before continuing. If you're working by yourself, I strongly recommend you find someone who can help check your work and provide feedback.

The homework for this project is made up of three coding problems; all are required.

  • In the source code download for this project I have included menu.png. Change the code so that the menu screen is shown when the game starts, but when the player clicks the mouse the normal game begins.
  • In this project I have hard-coded the bubbles to be 128x128 (giving a 64-pixel radius). Change that into one or more constants for easier maintenance. Don't forget to change any other values that depend on it - I'll leave it up to you to spot them all!
  • Change the code so that bubbles are created faster over time, making it harder if the player is slow. How quickly you reduce the creation time is down to how hard you want it to be!

If you have problems, try to solve them yourself - you might not succeed, but you'll learn a lot by trying! If you're still having problems, drop your tutor an email and ask for help.

The small print

This work was produced for TuxRadar.com as part of the Hudzilla Coding Academy series. All source code for this project is licensed under the GNU General Public License v3 or later. All other rights are reserved.

You should follow us on Identi.ca or Twitter


Your comments

Typo

Events.Quit += new EventHandler>QuitEventArgs>(Events_Quit);

Should be:

Events.Quit += new EventHandler<QuitEventArgs>(Events_Quit);

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

Post new comment

CAPTCHA
We can't accept links (unless you obfuscate them). You also need to negotiate the following CAPTCHA...

Username:   Password:
Create Account | About TuxRadar