Hudzilla Coding Academy: Project Four

Code
Hudzilla Coding Academy

 

How hard is it to get a quick game working? Not hard at all, as you'll soon find. In this project you're going to learn how to create a quick clone of the now-classic game Feeding Frenzy. The premise is simple: fish can eat other fish that are smaller than they are, but they in turn get eaten by larger fish. Sound simple? It is - but there's lots of good stuff to learn by coding it, so let's get started!

Installing SDL and SDL.NET

In the old days of game programming, coders had to do a lot of work themselves - there were no truly standard ways to output fast video and high-quality audio, nor to read input such as key presses or joystick moves. But since those dark days we have been blessed with something called the Simple DirectMedia Layer, or "SDL" for short. This is a set of libraries that let you build high-quality games (or other multimedia apps) without caring what graphics card is available, whether the user has a working sound card or not, and more. In fact, SDL means you don't even have to care what operating system you're using, because SDL supports Linux, Mac OS X and Windows just fine. To get to the point, SDL has just about everything you need to create high-quality 2D games with very little work.

If all that wasn't good enough already, SDL works just fine in Mono thanks to a little helper layer called SDL.NET. This is basically a huge heap of glue code to turn SDL into something Mono-friendly, and I have to say it's one of the most impressive libraries I've ever come across - and I think you'll like it too.

Because this is your first time working with SDL, I'm going to show you how to setup SDL and SDL.NET from scratch. First, go to System > Administration > Synaptic Package Manager and enter your password when prompted. In the Quick Search box type "libsdl" and hit Enter to filter the available package list. You need to make sure the following are installed:

  • libsdl1.2-dev
  • libsdl-gfx1.2-dev
  • libsdl-image1.2-dev
  • libsdl-mixer1.2-dev
  • libsdl-net1.2-dev
  • libsdl-ttf2.0-dev

If you install those, they should bring with them a raft of other dependencies to make sure you get a wide range of functionality. You probably have a chunk of that already installed if you play games on Linux regularly.

Installing SDL on Ubuntu is very easy thanks to Synaptic.

Installing SDL on Ubuntu is very easy thanks to Synaptic.

Installing SDL.NET is only slightly trickier than installing the basic SDL system. To get started, go to MonoDevelop and create a new command-line project calling FishFeast. Now open up your web browser and go to http://cs-sdl.sourceforge.net and click the Download SDL.NET link - you're looking to download SDL.NET 6.1.0. You should eventually see a list of files like "SdlDotNet-6.1.0.dmg" and "sdldotnet-6.1.0-runtime-setup.exe". From that list, download sdldotnet-6.1.0.tar.gz. If you're using Ubuntu, this should automatically be downloaded to your desktop, so right-click on your new file and choose Extract Here.

What you need to do now is look inside the newly created sdldotnet-6.1.0 folder for a sub-folder called "bin". Inside there you should find two files called SdlDotNet.dll and Tao.Sdl.dll - these are the two files that make up the core of SDL.NET. Leaving that window open, click Places > Home Folder to bring up a new folder view, then browse to your FishFeast folder in there. Now copy the SdlDotNet.dll and Tao.Sdl.dll files from the sdldotnet-6.1.0/bin folder to your FishFeast folder, next to where you see the FishFeast.mds file.

The final stage of this process is to tell MonoDevelop your code references those two DLL files, so go back to MonoDevelop and look in the Solution pane on the left for where it says "References". Right click on that and choose Edit References. You'll be adding references a lot in future projects, so it's essential you get this right!

Adding a reference in MonoDevelop is done by right-clicking on References and choosing Edit References.

Adding a reference in MonoDevelop is done by right-clicking on References and choosing Edit References.

In the window that appears, change the tab at the top to be .Net Assembly and browse to where you put the two DLL files. Double-click on them both in turn, and as you do that they should appear in the list at the bottom of the window. Now return to the Packages tab (the original one you were on when the window appeared) and select the box next to System.Drawing.

Once you have SdlDotNet.dll, Tao.Sdl.dll and System.Drawing all selected, your References window should look like this.

Once you have SdlDotNet.dll, Tao.Sdl.dll and System.Drawing all selected, your References window should look like this.

Click OK when you're done, and you should now see them under the References list in the main MonoDevelop window.

You will need to copy the two SDL.NET files into any SDL project you make in the future, and you will always need to add them to the References list in MonoDevelop. However, SDL itself is installed system-wide and you won't need to follow those instructions again.

First steps

Now that SDL is installed and ready to go in your solution, it's time to do something interesting: we're going to bring up a window and let the user quit the "game" by closing it. So, go the top of your Main.cs file and make sure you have these "using" statements:

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

The first three give us access to the most basic SDL functionality - we'll be adding more in later games. The last one is a very powerful library for drawing all sorts of complicated shapes to the screen, but we'll be using it because SDL uses some basic functionality such as screen positions and rectangles. "System" is of course the default "using" statement to give us all the basic functionality, and we looked at System.Collections.Generic - that's the bit that provides us access to the List array type.

Now remove the Console.WriteLine() that MonoDevelop put in your Main() method for you, and replace it with this:

Video.SetVideoMode(1024, 768);
Events.Quit += new EventHandler<QuitEventArgs>(Events_Quit);
Events.Run();

There's all sorts of new stuff in there, but for now ignore it - I want you to add a new method beneath Main():

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

Again there's all sorts of new stuff to marvel at, but keep on ignoring it: press F5 to build and run your code, and you should see a black window appear. If you click the window's close button, the window will go away and you'll be returned to MonoDevelop - it's nothing amazing, but it's the start of your game and it'll do a lot more soon enough.

This black screen is the Hello World of SDL projects - it makes sure you have everything ready to go.

This black screen is the Hello World of SDL projects - it makes sure you have everything ready to go.

Now, the matter of what the new code does. Well, when you work with SDL you have to adopt an entirely different coding system to what you've been using so far. Put simply, before you had complete control over your program - nothing happened without your explicit permission. Well, now SDL is the one that's in control: it will execute all the code it needs to make the game run smoothly, handing control back to you whenever something interesting happens. You need to tell SDL which events you care about, and, once you've done that, SDL will give your code a nudge when one of those events happens.

In our code we have this line:

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

"Events" is an SDL variable that lets us interface with all the events SDL can watch. It has a "Quit" option that we can attach one of our methods to, and that's exactly what that line of code does - it attachs the Events_Quit() method to the Quit event. You'll be using code similar to this whenever you want to attach a method to an event. You can ask SDL to run multiple methods when an event is triggered, but there isn't really much need for that here.

You'll notice the Events_Quit method takes two parameters: "object sender" and "QuitEventArgs e" - you can safely ignore both of these. That just leaves us with the following three new method calls:

  1. Video.SetVideoMode() - this opens the black window with a screen resolution of 1024 x 768.
  2. Events.Run() - this hands control of the program over to SDL. After this, your game will only be called when it has something to do.
  3. Events.QuitApplication() - this tells SDL to finish what it is doing as soon as possible, then hand control back to you, effectively terminating the game.

So right now, all we do is create a window, tell SDL we want it to run the Events_Quit() method when the player wants to quit the game, then hand control over to SDL to do its thing. That last part is important, because SDL needs to be the one doing all the screen drawing, all the input reading, all the sound streaming and such - we just need to fill in the blanks when it comes to deciding what to do with that screen or what to do with that input.

Drawing to the screen

All graphical items are known as surfaces in SDL-speak. That's because you can draw on them using any other surfaces, rather like creating a collage of pictures. You actually already have your first surface, because the call to Video.SetWindowMode() creates a window and returns a surface that you can draw upon. Now, in our code we actually ignore that returned surface, so we can't draw to it easily. However, if we give it a variable then we can use it easily. Put this line just before the start of your Main() method:

static Surface sfcMain;

Now modify your SetVideoMode() call to this:

sfcMain = Video.SetVideoMode(1024, 768);
sfcMain.Fill(Color.DarkBlue);

You should be able to guess that the new line, the second one, fills the screen's surface in a dark blue colour. Try running your program again to make sure everything is working - you'll find MonoDevelop has a full list of colours to choose from if you just type "Color." and let MonoDevelop do the rest.

Let's take this code a step further by introducing one of the most important methods of a surface: Blit(). This copies the contents of one surface to another, essentially letting you draw pictures easily. Add these two new variables next to "static Surface sfcMain":

static Surface sfcBackground;
static Surface sfcLogo;

Now change your SetVideoMode() call to this:

sfcMain = Video.SetVideoMode(1024, 768);
sfcBackground = new Surface("media/background.jpg");
sfcLogo = new Surface("media/logo.png");

The two files background.jpg and logo.png are included in the code for this project. You'll notice I refer to them as "media/", which is because it's smart coding practice to put your assets in a subdirectory like "media" to help keep everything arranged neatly. For now, create your own "media" subdirectory inside your project's directory (probably something like /home/hudzilla/FishFeast/FishFeast), and copy the files background.jpg and logo.png into there.

This bit is important, so read carefully. As you've seen so far, MonoDevelop creates a bin/Debug directory inside your project folder, and that's where it runs your program. The problem is your "media" directory containing your artwork isn't in bin/Debug, it's just in the main project folder. So, you need to tell MonoDevelop that you don't want it to put your compiled program in bin/Debug, but instead you want it in the same directory that contains "media" so that it can find all the pictures correctly. To do that, go to Project > Options and choose Configurations > Debug (Active) > Output and change Output Path to be /home/yourusername/FishFeast/FishFeast rather than /home/yourusername/FishFeast/FishFeast/bin/Debug.

If you were wondering the reason the background is a JPEG and the logo is a PNG, it's because PNG files support smooth transparency - we can layer the logo on top of the background and SDL will ensure the transparent parts are see-through.

With that done, put these three lines of code immediately after the three we just looked at (sfcLogo = new Surface etc):

sfcMain.Blit(sfcBackground);
sfcMain.Blit(sfcLogo, new Point(254, 236));
sfcMain.Update();

Those first two lines demonstrate the two most common types of blitting, and the last one just tells the screen to redraw itself. Blitting is, remember, copying the contents of one surface to another - in the first line the background is being drawn to the whole screen, and we don't need to specify where to draw it because the default position is the top-left corner (0, 0). Line two, however, draws the Fish Feast logo to the screen, and so needs to tell SDL where we want to blit the logo to. In this case, we want a specific point on the screen: X 254, Y 236. When you want to specify a point on the screen, you just have to tell Mono you want to create a new Point (note the capital P!) and tell it the X and Y co-ordinates.

By the way, sfcMain.Update() should be called only after all your other drawing has taken place. If you forget to call it every time you've finished making changes to the screen, those changes won't appear!

Go ahead and run your code now. Even though we haven't changed that much, we have introduced external media for the first time so it's important to check whether you've configured MonoDevelop correctly or not. If you've followed these instructions correctly, your game window should appear and look something like the screenshot below.

With the background and logo in place, this is starting to look like a game - at least a little bit!

With the background and logo in place, this is starting to look like a game - at least a little bit!

If you get an error about SDL being unable to find the files background.jpg or logo.png, you probably haven't set up MonoDevelop correctly. If you're really struggling, just copy the "media" directory into your bin/Debug directory - it's not ideal, but it's a quick way to get going.

Putting in the player

In the zip file you downloaded containing the code and files for this project (where you got background.jpg and logo.png from), you should also find the files fish1.png, fish2.png, fish3.png and fish4.png - these are the fish pictures we'll be using in this game. Copy them into your "media" folder, wherever that might be.

What we're going to do is create a list of surfaces that will be used for all our fish pictures. Put this line just before the start of your Main() method:

static List<Surface> sfcFish = new List<Surface>();

Then put this inside your Main() method, immediately after logo.png is loaded:

sfcFish.Add(new Surface("media/fish1.png"));
sfcFish.Add(new Surface("media/fish2.png"));
sfcFish.Add(new Surface("media/fish3.png"));
sfcFish.Add(new Surface("media/fish4.png"));

If you take a look at those fish pictures, you'll notice that fish1.png is the smallest and fish4.png is the largest, so this is probably a good time for me to explain how Feeding Frenzy - the game we're cloning - works. Put simply, you're a fish, and you - for some reason - like eating other fish. We're going to start the player as fish type 1, which is actually the second fish in the list because of the way Mono counts its arrays from 0 rather than 1. The fact that our fish pictures array is ordered by size makes it easy to tell that any fish type lower than the players can be eaten, whereas any fish type above will eat the player!

Our fish pictures are quite simple, but the important thing is that they have a transparent background.

Our fish pictures are quite simple, but the important thing is that they have a transparent background.

At any given time, we need to know exactly what type of fish our player currently is, as well as their position on the screen. This is rather easy, particularly given that we're already using a Point data type that holds an X and Y co-ordinate! Add these two lines of code directly beneath the definition for sfcFish, like this:

static List<Surface> sfcFish = new List<Surface>();

static int PlayerFish = 1;
static Point PlayerPos = new Point(256, 512);

That starts the player near the bottom-left corner of the screen, but doesn't actually draw them there yet - to do that, you need to add this line just before sfcMain.Update():

sfcMain.Blit(sfcFish[PlayerFish], PlayerPos);

The player is now on the screen, but it's all very dull because you can't move! But to make movement requires more than just reading the keyboard and changing PlayerPos, as you'll soon see...

Action!

Think about it: right now, we load all the pictures, draw them to the screen, then call sfcMain.Update() to make sure SDL shows the results of our drawing. After that, nothing happens - the screen is never redrawn. That means even if we were to make the player move, the screen wouldn't be updated to reflect the changes. So, there are two stages to making the player move:

  1. Tell SDL we want to update the game screen regularly.
  2. Read the keyboard and move the player.

That first part can be done in pretty much the same way we tell SDL that we care about the Quit event: create a new method called Events_Tick() like this:

static void Events_Tick(object sender, TickEventArgs e) {
	// we'll put code here soon
}

...then tell SDL we want that method to be run every time we're able to update our game and redraw the screen, like this:

Events.Tick += new EventHandler<TickEventArgs>(Events_Tick);

Put that directly beneath the existing "Events.Quit +=" line. A "tick", if you were wondering, is SDL terminogy meaning one complete execution of your game code. That is, one "tick" is made up of SDL doing everything it needs to do, handing control to you to do everything you need to do, then SDL finishing up - usually it lasts about 1/60th of a second, which means you get the chance to update your game and redraw the screen 60 times a second.

Now that we have an Events_Tick() method for SDL to execute, you need to take these four lines out of Main() and put them into Events_Tick():

sfcMain.Blit(sfcBackground);
sfcMain.Blit(sfcLogo, new Point(254, 236));
sfcMain.Blit(sfcFish[PlayerFish], PlayerPos);

sfcMain.Update();

Because these are inside Events_Tick() rather than Main(), SDL will run them every time it gives control back to your game to update itself. So if we change PlayerPos now, it means the player will be drawn at the right position because SDL will blank the screen with the background every tick and redraw the player's fish.

So, that's problem one solved. The second problem is to have the player position updated by reading keyboard input. This is a cinch thanks to SDL - it has a special method called Keyboard.IsKeyPressed() that returns true if a specific key on the keyboard is pressed. To check a specific key, just type "Key." and you'll see a list of available keys to check against. For now, we just need to check whether the Up, Down, Left or Right arrow keys are being pressed, and, if they are, move the player in the appropriate direction. Put this code into Events_Tick() before all the Blit() calls:

if (Keyboard.IsKeyPressed(Key.UpArrow)) {
	PlayerPos.Y -= 10;
} else if (Keyboard.IsKeyPressed(Key.DownArrow)) {
	PlayerPos.Y += 10;
}

if (Keyboard.IsKeyPressed(Key.LeftArrow)) {
	PlayerPos.X -= 10;
} else if (Keyboard.IsKeyPressed(Key.RightArrow)) {
	PlayerPos.X += 10;
}

+= and -= are both shortcuts - the first one means "add the value on the right to the variable on the left", and the second one means "subtract the value on the right from the variable on the left." In essence, "foo = foo + 1" can written as "foo += 1" or just "foo++".

I've chosen 10 for the fish's movement speed. We'll be looking at much smarter ways to do this sort of thing in later projects, but for now I want to keep it simple.

At this point, we have a game screen that updates itself every frame, and a player fish that moves around the screen using the keyboard. Now it's time to get to the really interesting stuff: creating and moving other fish.

Adding some AI

To get our player on the screen, we need to know their fish type and their position. To get computer fish onto the screen, we need to tell Mono that we want to track a type and position for each of those fish, then store all those fish inside an array for easy drawing and updating.

The best way to do all that is to create something called a class. This, like "method", is one of those geeky jargon words you just need to remember. Essentially, a class is something that holds its own variables and methods, and we can create as many instances of that class as we want. For example, right now you're using "Surface" from SDL - that's actually a class. When you say "Surface foo = new Surface("myfile.jpg")", Mono creates an instance of that class (known as an "object") and puts it in the variable "foo". If you create other surfaces, they won't affect the variables that belong to "foo".

The neat thing about these classes is that they look and work just like other data types we've been using - "int", "string", "Point", "Surface", etc, so by using them you're basically creating your own custom type of data.

To create a new class, right-click on FishFeast in the Solution pane (not the one that says "Solution FishFeast (1 entry)" - the one below that) and choose Add > New File.

Adding a new class is done by right-clicking on the project, not the solution!

Adding a new class is done by right-clicking on the project, not the solution!

Choose Empty Class from the list that appears, name it "Fish" and click New to create the class. MonoDevelop will put some simple skeleton code into a new file called Fish.cs, but leave it alone for now.

With that class in place, "Fish" is now a variable type just like "int" or "string". To prove that, we're going to create a list array that will store all the computer fish that are in our game. We also need to store the time the last computer fish was created, because we'll be using that to track when more fish should be added. Put these lines of code beneath "static Point PlayerPos" in Main.cs, like this:

static Point PlayerPos = new Point(256, 512);

static List<Fish> AIFish = new List<Fish>();
static int LastFishCreation = 0;

As you can see, Mono knows that "Fish" refers to our new class, which means we can go ahead and give some variables to the class and start creating instances of it. Change to Fish.cs, then make the file look like this:

using System;
using System.Drawing;

namespace FishFeast {
	public class Fish {
		public int Type;
		public Point Pos;

		public Fish(int type, Point pos) {
			Type = type;
			Pos = pos;
		}
	}
}

Notice that we're pulling in System.Drawing again so that we can use Points. We also create two variables to hold the fish type and position, but there's an interesting little twist here. Did you notice that MonoDevelop's skeleton code included a method called "Fish()"? Yes, that's the same name as the class itself, which might be a bit confusing - but it's also a bit special. You see, when you create an instance of a class - ie, you tell Mono you want to create a some fish in your game using the Fish class - Mono can automatically run a method for you to set up any initial data you want.

This method is known as a "constructor", because it's where you get to do any tasks required to make this object ready to go. You can always spot the constructor method of a class because it has the same name as the class itself. In my code above, you'll notice I've modified the Fish() constructor method so that it takes two parameters: "int type" and "Point pos". This means that if someone tries to create a fish object, they must provide those two pieces of data, otherwise Mono will complain.

The reason I'm forcing people to provide a type and position for each fish they create is simply because a fish without a type or position is useless and shouldn't exist! Using this system, people must tell Mono what type the fish should be and where it should be on the screen, and Mono stores those two variables in the object itself for later reference. You've probably noticed that I'm taking advantage of Mono's case sensitivity here, eg "Type = type" works because "Type" is one variable and "type" is another. This is fairly common - another technique is to use underscores to differentiate different versions of a variable. Do what works best for you.

Now that the Fish class is ready for action, we need to do three more things:

  1. Code a CreateFish() method that creates a fish at a random screen position, adding it to the AIFish list.
  2. Use a foreach loop to go over every fish and draw it to the screen.
  3. Check the LastFishCreation time to see whether it's time to create a new fish.

Let's tackle them in order - create a new method in Main.cs called CreateFish(), and make it look like this:

static void CreateFish() {
	// we need to generate some random values, so this is required!
	Random rand = new Random();

	// we're always going to create small fish right now
	int fishtype = 0;
	
	// generate a random position on the screen
	int fishx = rand.Next(1024);
	int fishy = rand.Next(700);

	// create a new fish, giving it the random position we just generated
	Fish newfish = new Fish(fishtype, new Point(fishx, fishy));
	
	// add it to the list of fish
	AIFish.Add(newfish);

	//  update the timer so we wait until creating more fish
	LastFishCreation = Timer.TicksElapsed;
}

I've scattered comments through that so you should be able to see how it works. Notice that creating an object of the Fish class is done simply by asking Mono to create a new Fish - easy, huh? The only really new thing in there is Timer.TicksElapsed, which is a value that SDL provides to us that stores the amount of time (in milliseconds) since your game was started.

The second thing we need to do is loop over all the fish in AIFish, drawing them to the screen at their current positions. Remember, each time we create an instance of a class, it has its own copy of the class's variables, so we can just reference Type and Pos directly in each Fish object to see where and how it should be drawn.

Put this in Events_Tick(), just before the call to sfcMain.Update():

foreach (Fish computerfish in AIFish) {
	sfcMain.Blit(sfcFish[computerfish.Type], computerfish.Pos);
}

That loop draws each fish at its own position, which should make them appear all over the screen.

The final change is to have the game call CreateFish() every second so that there are always new fish coming along. This can be done by comparing the value of LastFishCreation against the current system time - if we add 1000 milliseconds to LastFishCreation and it's less than the system time, we know at least a second has passed so we should create a fish. Put this code in Events_Tick(), just before the first Keyboard.IsKeyPressed() line:

if (LastFishCreation + 1000 < Timer.TicksElapsed) {
	CreateFish();
}

Remember: the last line of CreateFish() automatically updates LastFishCreation to be the current time, which means the above code will call CreateFish() once a second.

Press F5 to build and run your code, and all being well you should now see AI fish popping up on the screen once every second or so. They aren't moving yet, so there's still more to do!

Swim with the fishes

In the real Feeding Frenzy game, fish come from both sides of the screen so that players need to keep moving all the time. In this simplified version of the game, our fish are only going to come from the right, which means they are always moving left. This isn't hard to do, but it does make the game a little dull if all the fish are moving at the same speed! So, go into Fish.cs and add this line directly beneath "public Point Pos;":

public int Speed;

Back in Main.cs, we need to amend the CreateFish() method so that fish are always created off the right side of the screen and also have their new Speed variable set to something a little bit random. Change your CreateFish() method to this:

static void CreateFish() {
	Random rand = new Random();

	int fishtype = 0;
	int fishx = 1024;
	int fishy = rand.Next(700);

	Fish newfish = new Fish(fishtype, new Point(fishx, fishy));
	
	// now set the speed to 5, with a little bit of randomness!
	newfish.Speed = 5 + rand.Next(-3, 3);
	AIFish.Add(newfish);

	LastFishCreation = Timer.TicksElapsed;
}

That rand.Next() call will return a number between -3 (inclusive) and +3 (exclusive), meaning that the fish speed will vary between 2 and 7.

Finally, we just need to update the Events_Tick() method so that the fish are moved before being drawn - change the foreach loop to this:

foreach (Fish computerfish in AIFish) {
	computerfish.Pos.X -= computerfish.Speed;
	sfcMain.Blit(sfcFish[computerfish.Type], computerfish.Pos);
}

And now run your game again: the fish should be created off the right edge of the screen, and will move smoothly towards the left at different speeds.

The game so far: one type of fish is created in random positions on the screen.

The game so far: one type of fish is created in random positions on the screen.

Fast collision detection

What we have now is - almost - a working game. Everything moves correctly, but it's missing the crucial thing: challenge. That is, there's no way to win or lose right now, which means there's no fun! That's easily fixed, though: we need to check every enemy fish to see whether they overlap the player or not. And, if they do, they either get eaten by the player (if they are a lesser fish type), eat the player (if they are a greater fish type) or do nothing (if they are the same fish type.

The fastest way to perform collision detection is using something bounding boxes, which basically means we draw an invisible box around each fish and check whether that overlaps the player's invisible box. Checking whether two bounding boxes overlap is ludicrously fast in Mono, both to code and perform during game play, which makes it perfect for checking for collisions. Of course, the downside is that our fish aren't exactly square-shaped, which means sometimes a collision will happen even though technically one fish didn't touch another fish.

We'll be solving that problem in a later project by using something called per-pixel collision detection, but for now bounding box collision is just fine. That task can be broken up into three smaller tasks:

  1. Calculate the player's bounding box. This needs to be done once per game update, to take into account the player's current position.
  2. Calculate the bounding box for each fish. This also needs to be done once per game update, but more specifically once per fish per game update.
  3. Compare the two, and remove an enemy fish if it collides with the player.

Let's start with the first problem: we need to create a rectangle that contains the player's X and Y position, plus the width and height of their current fish surface. The C# code for this is actually almost identical to what I just said - put this just before the AIFish foreach loop in Events_Tick():

// this long line has been broken in two
Rectangle player_rect = new Rectangle(PlayerPos.X, PlayerPos.Y,
		sfcFish[PlayerFish].Width, sfcFish[PlayerFish].Height);

That creates a "player_rect" variable containing a bounding box for the player. We create it here, before the foreach loop, because it doesn't change inside the loop so there's no point recreating it.

With that done, you should be able to figure out how to create a bounding box for each of the enemy fish. Pretty much the same code is used, except this time it needs to go inside the AIFish foreach loop and use the enemy fish's position and size. Change the foreach loop to this:

foreach (Fish computerfish in AIFish) {
	computerfish.Pos.X -= computerfish.Speed;

	// this bit is new - split into three lines for easy reading
	Rectangle enemy_rect = new Rectangle(computerfish.Pos.X,
		computerfish.Pos.Y, sfcFish[computerfish.Type].Width,
		sfcFish[computerfish.Type].Height);

	// marker - ignore for now!

	sfcMain.Blit(sfcFish[computerfish.Type], computerfish.Pos);
}

The important new line in that code - well, three lines, because I've had to split it up to fit on the screen neatly - is the one that creates a rectangle from each fish's position on the screen.

Finally, we need to compare the player's rectangle with each fish's rectangle to see if there's any collision. This is actually really easy, because Mono already has functionality in place for checking whether one rectangle intersects (overlaps) another - all you have to do is use the IntersectsWith() method on a rectangle, passing in another rectangle as a parameter, and Mono will return true if they overlap each other. Put this code where the "// marker - ignore for now" text is:

if (enemy_rect.IntersectsWith(player_rect)) {
	if (computerfish.Type < PlayerFish) {
		// eat!
	} else if (computerfish.Type > PlayerFish) {
		// die!
	}
}

That shows off the new call to IntersectsWith() to perform collision detection between two rectangles, and you'll notice I've also slipped in another conditional statement inside that checks whether the player should eat the fish or be eaten by it. Remember, our fish types are ordered by size, so if the enemy fish type is a lower number than the player's, it gets eaten; if it's a higher number, it eats the player; if it's neither of them, then nothing happens.

And now, for the tricky part: in that space marked "// eat!", we need to destroy the enemy fish because it has been eaten by the player. This is, by itself, not hard to do, but will cause a problem - try it and see:

AIFish.Remove(computerfish);

Put that where the "// eat!" line is and run the game. Now try eating a fish. What happens? Why do you think it happens?

This is a common problem, and something you'll encounter a lot. Just for the benefit of people skim-reading this, let me repeat what I just said.

This is a common problem, and something you'll encounter a lot.

So, how do you fix it? Well, first you need to know why it happens. Put simply, when you loop over an array in any way, you can't change the array if it affects what comes later. Think about this loop:

for (int i = 0; i < somearray.Count; i++) {
	// process each item here somehow
}

At 0 you do nothing. At 1 you do nothing. But at 2 you remove item 2. When you remove item 2, item 3 and everything afterwards move down one place to fill the gap, so item 3 becomes the new item 2, item 4 becomes the new item 3, etc. But this leaves some uncertainy: should Mono ignore the i++ so you can work on item 2 again, seeing as it's actually the old item 3 and hasn't be processed yet? Or should it carry on, leaving one item untouched?

This is a nasty place to be, so the solution is simple: if you must remove items from an array while counting over the array, count over it backwards. That way, when all the other items move down one place, it doesn't matter because you've already processed them.

To do this, we just need to use a for loop that counts downwards rather than upwards, like this:

for (int i = somearray.Count - 1; i >= 0; i--) {
	// process each item here somehow
}

Note how "somearray.Count - 1" is used for the starting number, we count down until i is greater or equal to 0 - both of these are there because of Mono's practice of counting arrays from 0.

Now you know the safe way to remove items from an array, we have to rewrite the AIFish loop so that it works correctly. Change this line to:

foreach (Fish computerfish in AIFish) {

...to this:

for (int i = AIFish.Count - 1; i >= 0; --i) {
	Fish computerfish = AIFish[i];

So, that counts backwards through the entire list, each time assigning the current item to the "computerfish" variable for the rest of the code to use. If you run your game now, you should be able to go up to the other fish and have them disappear when you touch them.

There is one other small change we're going to make here before moving on, and that's to change this line:

AIFish.Remove(computerfish);

...to this one:

AIFish.RemoveAt(i);

The original line searched through the AIFish list to remove a specific item, whereas the second takes advantage of the fact that we already know where the item is (because we're counting the position in "i"), so we can remove the item at a specific position rather than searching for it. It makes no difference here because the program is so simple, but it's good practice to write the best code you can because it makes a difference in larger programs!

A common hack used with bounding box collision detection is to pull the box in by a few pixels on all sides to make it a bit less noticeable.

A common hack used with bounding box collision detection is to pull the box in by a few pixels on all sides to make it a bit less noticeable.

Death of a player

We need to make it possible for the player to die when they go too near other fish that are bigger than they are. That means we need to show a message on the screen along the lines of "you died!", which in turn means we need to hide the Fish Feast logo as soon as the game starts. It also means we need to actually create some of the larger fish types, because right now the fish are always the same size as us!

Let's put that into the most sensible order:

  1. Make the game generate all sizes of fish, but preferably smaller ones.
  2. Add a new variable that tracks whether the game has started or not. If it hasn't, show the Fish Feast logo and do nothing else. If it has, hide the Fish Feast logo and play normally.
  3. Add a new variable to track whether the player is alive or dead. If they are alive, play the game as usual. If they aren't, don't show the player and show a "you're dead!" message instead.

Starting at the top, go into your CreateFish() method and put this code directly beneath "int fishtype = 0;":

switch (rand.Next(20)) {
	case 14:
	case 15:
	case 16:
		fishtype = 1;
		break;
	case 17:
	case 18:
		fishtype = 2;
		break;
	case 19:
		fishtype = 3;
		break;
}

You should be able to figure out what nearly all that does, with the exception of the empty "case" statements. Previously every case statement had its own code to execute and ended with a "break" statement, but this time we have several case statements all together and without any "break"s. What happens here is that Mono "falls through" to the next case statement. For example, if rand.Next() returns 14, Mono will look for "case 14:" and find it, but it contains no code so Mono will fall through to the next case, case 15. That in turn has no code, so Mono will fall through to the next case statement, which is case 16 and does have some code, so Mono will execute it. In essence, cases 14-16 will all execute the same code.

rand.Next(20) will return a value between 1 and 19 inclusive, but we only deal with cases 14 to 19 - the rest will use fishtype's default value of 0, which is where our small-fish bias comes from.

Onto the second problem: we need a variable to track whether the game has started or not. In a later project I'll be showing you a smarter way to track game state, but for now a simple bool variable (they store only true or false, remember) is just fine. Put this just beneath "static Surface sfcLogo;" near the top of your Main.cs file:

static bool GameStarted;

That will default to "false", so what we need to do is modify Events_Tick() so that it checks whether the variable is set to true or and decides what to update and draw based on that. What you want to do is go into Events_Tick() and find these two lines:

sfcMain.Blit(sfcBackground);
sfcMain.Blit(sfcLogo, new Point(254, 236));

Select them and copy to them to your clipboard by pressing Ctrl+C. Now delete the second of them, leaving just sfcMain.Blit(sfcBackground). Go up to the top of the method and, before the "if (LastCreationTime ...)" line, put this:

if (GameStarted) {

Scroll down to the end of the method and put this just before sfcMain.Update():

} else {
	sfcMain.Blit(sfcBackground);
	sfcMain.Blit(sfcLogo, new Point(254, 236));
}

You should have those middle two code lines in your clipboard, so just press Ctrl+V to paste them into MonoDevelop. So, now this method reads "if GameStarted is true, go ahead and do all the normal stuff we've been doing so far. If not, just draw the background and logo." If you run the game now, GameStarted will of course be false so you won't see much. That's easily solved, but we can't do it using the Keyboard.IsKeyPressed() method we've been using so far because that check will be run every time the game updates, so if the player holds down the spacebar for a mere tenth of a second it will run our game starting code about six times!

An easier solution is to tell SDL we want to be informed when a key has been pressed. Go up to your Main() method to where you have "Events.Quit +=" and other lines like it, and add this somewhere before Events.Run():

Events.KeyboardUp += new EventHandler<KeyboardEventArgs>(Events_KeyboardUp);

Now add this new method to check what key was pressed and, if it was spacebar and the game hasn't already been started, start the game:

static void Events_KeyboardUp(object sender, KeyboardEventArgs e) {
	if (e.Key == Key.Space) {
		if (!GameStarted) {
			GameStarted = true;
		} else {
			// game already started!
		}
	}
}

Previously you've seen that "==" means "is equal to" and "!=" means "is not equal to". Well, now you can see plain old "!" by itself, which means just "not". For example, if GameStarted is set to be true, "if (GameStarted)" would evaluate as true. But if you throw a ! in there, it makes the opposite value, so "if (!GameStarted)" would evaluate as being true only if GameStarted were false. In the code above, we set GameStarted to be true only if it is current false; it doesn't really make much difference here, but in a real game you'd probably call some sort of StartGame() method rather than just a set a single variable!

The final change that needs to be made is to make another bool variable to track whether the player is alive or not, then to update Events_Tick() so that the player is only moved, drawn and collided with when they are alive. But I think that's something best left for homework...

The finished product: all sorts of fish fly out, and you can eat them by colliding with them.

The finished product: all sorts of fish fly out, and you can eat them by colliding with them.

Let's wrap up

At the end of the fourth project, you've now been introduced to classes and objects, constructor methods, events and of course everything that SDL brings with it. All the non-SDL learning here is really valuable stuff that you'll find yourself using in all your programming projects - you should particularly focus on being comfortable creating a new class for your own work. And as for SDL, it's something we'll be revisiting many times in future projects, so take the time to make sure it works properly!

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.

  • Make the fish come from both sides. Use bool GoingLeft to track their movement, decide whether Speed be added or subtracted, and choose their direction. I've provided alternate fish pictures for you to use - the simplest way to use these is to creat another List<Surface> to store fish pictures going the opposite way.
  • Make the player turn the opposite way depending on their last move. The best way to do this is to set a variable in the IsKeyPressed() methods.
  • Kill the player when they touch a bigger fish. I've included a simple "Oops!" picture in the code download that you might want to show when the player is dead. Let the player press Space to restart, but make sure you reset the player's position to the same point each time.

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

Flicker Problems

Great set of tutorials BTW.

On this one though I am having some problems with flickering fish. I know this is slightly beyond the scope of the tutorial but any hints for improving the situation?

I've looked at the library documentation and while there are several performance tips on there I can't seem to make any difference. I have tried speeding up and slowing down the frame rate, and converting all pixel depths to the same value but haven't made much progress.

I'm using an Asus EEE 901 - I imagine the GPU isn't that powerful - but surely should be able to render a few moving fish! Or perhaps not.

Is anyone else experiencing similar problems?

Very very nice,thanks for

Very very nice,thanks for this article,good job.

Thanks

Nice one for beginners,
Hope others will come with a more Object Oriented aspect.
Good job.

@ steve
Are you using Software or Hardware rendering? Try to switch between them and see what's going.

So long... and thanks for

So long... and thanks for all the fish!

Distributing Tao and SDL.Net without licence files.

Tao is under the MIT licence and SDL.Net is under the LGPL.

Both should be supplied with their respective licences.

FishFeast.mds

Hi
Excellent tutorial.

I can't find the file FishFeast.mds and there are two folders with the name FishFeast under Projects. Iḿ not sure witch one a should put the dll:s in?

Best regards
Pelle

PS. I installed the CD on a new computer just to be sure itś clean...

Couple of problems with Homework

on the third item of homework I can display the Ooops but I can't think of how to stop the game. It just displays the Oops while the two fish 'touch' each other and then continues on merrily.

The other is a little more serious is that I get ArgumentOutOfRangeException's. It start to occur after I did the first peice of homework. Basically, it's got to do with the for loop that you go backwards through... I suspect that the hack to add a new Surface for the Left facing fish is causing it. Does anyone else have the same problem?
It's not a biggy. well, it is really but I will move onto the next lesson anyway.

here's the third fix

just set GameStarted to false. I used the timer to wait 5 seconds (or whatever that is) to allow the player to restart.
I call clear screen, remove all fish from the array, and draw the original screen.

else if (computerfish.Type > PlayerFish)
{
sfcMain.Blit(sfcOops, new Point(256, 512));
sfcMain.Update();
Video.WindowCaption = "Your final score is: " + score.ToString();

GameStarted = false;
Timer.DelayTicks(1500);

//Environment.Exit(1);

}

Out of memory?

I've got a different sort of question...
Is there any risk of a running out of memory?
What I mean is can a Fish object be located off sfcMain (e.g. where the X position is -100 for instance)? Do they somehow get destroyed or do they continue to exist.

Would it not be a good idea to remove the Fish objects if the X pos is off the sfcMain (e.g.computerfish.Pos.X < -200 || computerfish.Pos.X > 1300)

Cant get past black draw screen

Been doing the other tuts and I love them!

However, I'm stuck at the main black screen. I finally fixed the linking to the mdeia/ folder, and got the output path to create the .exe in the FishFeast/FishFeast folder. I don't get an error, but the screen still shows black.

I'm running Monodevelop 3.0.3.2 and have downloaded and applied all of the libs mentioned at the start of the tut..

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