Saturday 28 February 2009

Tutorial 1: Levels

Ok, so the first part of our tower defence that we are going to make is a simple terrain class. Please note that most of this code is going to be from Nick Gravelyn’s Tile Engine Series. I’m assuming you know how to create a new project so I’ll skip that bit.

Ok, so to start off with create a new class called “Level.cs”. This is going to be what holds all the information about our level (which textures go where basically).

First add the following using statements to the top of “Level.cs”

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;


Now, go ahead and add a new 2-Dimensional array to the top of your “Level.cs” class.

int[,] map = new int[,]
{
    {0,0,1,0,0,0,0,0,},
    {0,0,1,1,0,0,0,0,},
    {0,0,0,1,1,0,0,0,},
    {0,0,0,0,1,0,0,0,},
    {0,0,0,1,1,0,0,0,},
    {0,0,1,1,0,0,0,0,},
    {0,0,1,0,0,0,0,0,},
    {0,0,1,1,1,1,1,1,},
};

This array is going to be used to map out our level. Imagine that each one of these numbers is a texture. In this data a zero means draw a grass texture whereas a one means draw a path. So the above array would translate into something like this:



So, how do we turn this array of numbers into the above image? Well that’s simple. First off add a new list underneath the array.

private List<Texture2D> tileTextures = new List<Texture2D>();


This list is going to contain our two textures: grass and path. Now because our level class does not have access to the ContentManager we will pass in the textures using a new method called “AddTexture” which looks like the following:

public void AddTexture(Texture2D texture)
{
    tileTextures.Add(texture);
}

All this method does is add a texture to our texture list. So, now we have a list of textures, but how do we make them be drawn and how do we make them be drawn so they correspond to our array? Well this is all done in our draw method. But before we move on to the draw method we need to add to more variables to the top of our class:

public int Width
{
    get { return map.GetLength(1); }
}
public int Height
{
    get { return map.GetLength(0); }
}

What these two variables do is get the width and height of our level by retreiving our array’s row length(how many numbers are in a row) and column length(how many numbers are there in a column).

Now add a new method called “Draw” at the end of the class:

public void Draw(SpriteBatch batch)
{
    for (int x = 0; x < Width; x++)
    {
        for (int y = 0; y < Height; y++)
        {
            int textureIndex = map[y, x];


            if (textureIndex == -1)
                continue;

            
            Texture2D texture = tileTextures[textureIndex];


            batch.Draw(texture, new Rectangle(
                x * 32, y * 32, 32, 32), Color.White);
        }
    }
}

This is where the magic happens.

First of all we itterate through each of the numbers in our array. Then we find out if it’s one or zero and store this value in an integer named textureIndex.

Next we find the texture that needs to be drawn. We do this by getting the texture from tileTextures which is at the texture index. Then all we do is draw the texture. All the textures we are going to be using are going to be 32 by 32 pixels.

So, how do we know where to draw the texture? Well we know the x and y coordinates of the texture in the array. So all we need to do is transform the array coordiantes into screen coordinates by multiplying the x and y coordinates by the texture width or height which is 32.

So, we now have a basic level class which we can expand upon later. So let’s incorporate this into our “Game1.cs” class.

First off save these two images into your projects Content folder and then add them into your project:

Then at the top of “Game1.cs” add

Level level = new Level();


Next add these lines of code to your "Game1()" constructor to make sure you game window is an appropriate size:

graphics.PreferredBackBufferWidth = level.Width * 32; 
graphics.PreferredBackBufferHeight = level.Height * 32;
graphics.ApplyChanges();

IsMouseVisible = true;


Remember the level’s width and height needs to be multiplied by 32 to bring the value out of array space and into screen space.

Then in the “LoadContent()” method add the following code :

Texture2D grass = Content.Load<Texture2D>("grass");
Texture2D path = Content.Load<Texture2D>("path"); 

level.AddTexture(grass);
level.AddTexture(path);


Note: the order you add your textures is important. The order you add your textures in corresponds to the numbers in our level array.

Now, in the “Draw” method add

spriteBatch.Begin();
level.Draw(spriteBatch);
spriteBatch.End();


And there we have it. Next time we will be creating a level editor to create new levels and edit existing ones. Thanks for reading. Feel free to comment or ask any questions.

The source code for this tutorial can be downloaded here.

46 comments:

  1. XNA? WOW play the TD on the XBOX will be fantasy !
    waiting for the continue post !!!

    ReplyDelete
  2. WOOH! THANK YOU

    Please carry on

    ReplyDelete
  3. kitty, kitty. On the wall, where are you now?

    ReplyDelete
  4. Error 1 'TowerDefense.Level' does not contain a constructor that takes '0' arguments

    Is there any reason why I would get this error, I followed the tutorial step by step but I can't seem to fix this error.

    ReplyDelete
  5. The level class should either contain no constructor or at least have one like this :

    public Level()
    {

    }

    Hope that helps!

    ReplyDelete
  6. I keep getting an error saying "Cannot find file grass" or something like that. Don't understand why because the png is in the project

    ReplyDelete
  7. "batch.Draw(Texture" "new Rectangle"
    are giving me errors.

    in Game1.cs --
    "Level.Width" "Level.Height"
    are giving me errors.

    ReplyDelete
  8. Hi, i need some help. When i take out these lines:

    graphics.PreferredBackBufferWidth = level.Width * 32;
    graphics.PreferredBackBufferHeight = level.Height * 32;
    graphics.ApplyChanges();

    it works fine, but the actual tiles only take up about 1/4 of my screen. Maybe i am putting those lines of code in the wrong spot or something? can you help me please?

    ReplyDelete
  9. Hi,

    What happens when when you don't take out those lines?

    Those lines should be in the Game1 constructor:

    public Game1()
    {
    graphics = new GraphicsDeviceManager(this);
    Content.RootDirectory = "Content";

    // The width of the level in pixels
    graphics.PreferredBackBufferWidth = level.Width * 32;
    // The height of the toolbar + the height of the level in pixels
    graphics.PreferredBackBufferHeight = 32 + level.Height * 32;

    graphics.ApplyChanges();

    IsMouseVisible = true;
    }

    Are you using the images from the tutorial? If not you will need to replace 32 with the size of your textures.

    Hope that helps!

    ReplyDelete
  10. Thanks Firefly08,

    useful stuff.

    ReplyDelete
  11. This comment has been removed by the author.

    ReplyDelete
  12. Nevermind, I found it! Great tutorial.

    ReplyDelete
  13. awesome tutorial

    ReplyDelete
  14. How can I make it so there are more then just 1 map available? Ive got it so that I can change an int in Visual Studio, then when it fires the constructor for Level(), it picks a diffrent map and set of waypoints for it, but that wouldnt work in game, is there a way for it to happen at like a title screen sort of menue? Id think the whole logic of the game constuction would have to be modded though

    ReplyDelete
  15. Nevermind, I figured it out, It was alot simpler then I thought it would be, All you need to do is set level.map to something new, and clean out the waypoints and add in new ones. This can also happen on the fly as I figured out, For example whenever my game is running, you can press N and it fires this method in level

    public void ChangeMap()
    {

    map = new int[,]
    {

    {1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
    {0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
    {0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0},
    {0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0},
    {0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0},
    {0,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,3,0,0,0,0},
    {0,0,0,0,4,0,0,0,0,2,0,0,0,0,0,0,0,3,0,0,0,1,0,0,0,0,0,0,0,0,0,0},
    {0,0,3,3,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0},
    {0,3,3,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0},
    {0,0,0,0,0,0,0,0,0,0,4,0,0,0,0,0,1,1,1,1,1,1,0,0,0,4,0,0,0,0,0,0},
    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
    {0,0,0,0,0,4,0,0,0,0,0,0,0,4,0,0,1,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0},
    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
    {0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0},
    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
    {0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,0,1,0,0,0,0,0,0,0,4,0,0,0,0,0,0,0},
    {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},


    };

    waypoints = new Queue();
    waypoints.Enqueue(new Vector2(0, 19) * 32);
    waypoints.Enqueue(new Vector2(3, 0) * 32);
    waypoints.Enqueue(new Vector2(3, 3) * 32);
    waypoints.Enqueue(new Vector2(21, 3) * 32);
    waypoints.Enqueue(new Vector2(21, 9) * 32);
    waypoints.Enqueue(new Vector2(16, 9) * 32);
    waypoints.Enqueue(new Vector2(16, 19) * 32);
    }



    Now if you wanted to take that a step further, having 3, or 4, or really as many maps as you want, just modify the method to accept a paramater, such as

    Press N : Fires
    ChangeMap(2)

    back in level

    public void ChangeMap(int battlefield)
    {
    if (battlefield == 1)

    code for map 1

    if (battlefield == 2)

    code for map 2

    ReplyDelete
    Replies
    1. Something even more flexible and more 'best practice' I guess is to actually have it read the maps from say an XML or even a CSV file. That way you won't have to even touch code or recompile in order to change the maps.

      Delete
  16. hey i'd like help with this. i'm using xna 4.0 and i keep getting weird errors like missing ) or level does not exist in this current context. i've double and triple checked the code and its exact. what am i doing wrong?

    ReplyDelete
  17. graphics.PreferredBackBufferWidth = level.Width * 32;
    graphics.PreferredBackBufferHeight = level.Height * 32;
    graphics.ApplyChanges();


    Error is showing near 32.....and its telling NullHandlingReference,
    Wat to do???

    ReplyDelete
  18. hi firefly,
    im interested in TD games, but not so much in the programing as in the "strategy" and level aspect - how do you coordinate the difficulty levels of the game? keeping the game playable? not to easy, not to hard? finding that equilibrium between the defense option s and the enemy? is the flw of enemies random?
    whats the mathematical system behind the vary levels?

    do you know where i can read about that? thanks, nadav (nadi.bar@gmail.com)

    ReplyDelete
  19. Level level = new Level(); gives the folowing error to me:


    Error 1 The type or namespace name 'Level' could not be found (are you missing a using directive or an assembly reference?) \WindowsGame1\WindowsGame1\WindowsGame1\Game1.cs

    ReplyDelete
  20. I doubt Firefly is still checking this - but I'll give it a shot in case anyone else is. Currently the levels are "small" (my playing window is only about 4-5in height/width - if I wanted to make this "larger" and add more blocks to the level, how would I do that? I assume it would be something to do with the *32, but that part kind of loses me....

    ReplyDelete
  21. Hey, Don't worry all comments get sent to my email so I still know if people ask questions!

    If you want to make the actual block textures bigger, then you would need to change 32 to the new size of the image you're using (At the moment the tutorial uses images that are 32x32).

    If you want to add more blocks to the level then you need to change the level array at the top i.e either add more columns or more rows!

    I hope that helped! :)

    ReplyDelete
  22. Ok - For some reason I guess I just wasn't understanding that the texture of the image was 32 pixels - if I created a larger one, lets say a 50x50 pixel texture, I could just * by 50 instead of 32 then? That would work out the same?

    I may have a few more questions on the other tutorials since I know you see this, I ran them all up to the point of shooting (I am on the waves tutorial) but wanted to go back and really try to expand on each one to learn a bit more, so thank you again!

    ReplyDelete
  23. I have deleted some previous comments because I found and corrected my typo.

    The problem I am having now is that when the application launches it does not draw the tiles (grass and path) I just see the background color.

    ReplyDelete
  24. The Draw() method says "outside the bounds of the array"

    ReplyDelete
  25. I'm getting the same problem as Brock Laster, any help would be greatly appreciated.

    ReplyDelete
  26. Hmm that's odd, which line is causing the error?

    If it is the line:

    Texture2D texture = tileTextures[textureIndex];

    Then you need to make sure that you are calling the AddTexture methods in LoadContent.

    If it is:

    int textureIndex = map[y, x];

    Then make sure that Width and Height look like the code I posted above.

    If neither of those suggestions fix your problem, I just uploaded some source code for this tutorial that you can compare your code with!

    Let me know if this helps!

    ReplyDelete
  27. @Firefly

    Thanks for replying! In an act of frustration I copy/pasted your game1.cs source code over my own code and the solution will not build.

    protected override void Draw(GameTime gameTime) causes error: Error 1 Expected class, delegate, enum, interface, or struct

    ReplyDelete
  28. I found my error thanks to your source code, I place brackets containing code for an if statement by habit, it was locking out access to the class. Thanks for the help! =)

    ReplyDelete
  29. @Richard I'm glad you got it sorted!

    @Brock Hmm when you copied the code over a } must have been missed somewhere. I would suggest making sure every { has a corresponding }. If that is the case then there must me an extra } somewhere, my guess would be just above the draw method.

    ReplyDelete
  30. This comment has been removed by the author.

    ReplyDelete
  31. This comment has been removed by the author.

    ReplyDelete
  32. This comment has been removed by the author.

    ReplyDelete
  33. Hmm, are you sure that the Width and Height properties are exactly the same as mine? With Width returning GetLength(1) and Height returning GetLength(0).

    Also have you checked that in your for loop you are doing y < Height and x < Width?

    ReplyDelete
  34. Ah yeah I see your problem was using <= instead of <. I guess you have seen that yourself now? :)

    ReplyDelete
  35. Sorry for blowing up your blog :/ feel like a real nag. I'm still not sure exactly what the problem was but apparently there was an issue with my Draw() method. I copy/pasted just the Draw() from your source code and now the tiles are drawn when the application is compiled.

    Thanks so much for your help! Its really appreciated. I only started trying to learn programming about a month ago and I'm "sort of" starting to get the hang of it. Your tutorials really are a godsend!

    Thanks you!

    ReplyDelete
  36. It's no problem man, we all had to learn at some point! I think the problem was that in the draw method your for loop looked like:

    for (int x = 0; x <= Width; x++)

    when it should have looked like

    for (int x = 0; x < Width; x++)

    Not the difference between < and <=, < means that x will never be equal to or be greater than Width, whereas <= means that x could be equal to but will never be greater than Width!

    Hopefully that makes some sense.

    ReplyDelete
  37. I'm not sure if u read these anymore but im making a TD game with my friend. He has made me some textures but the forest around the grid is going to be hard to tile he said and won't look as good. I was wondering if it was possible for me to have the border be an image and the center be textures? Thanks a lot!!!

    ReplyDelete
  38. First of all, thank you for those great tutorials!

    I encountered one error though :
    In the first picture of this tutorial you showed how the map will look like with this map-array.
    But if i start the game, my level looks like i would have filled the array like this :
    00000000
    00000000
    11000111
    01101101
    00111001
    00000001
    00000001
    00000001

    it solved this error by changing the x and y in the Draw-Method :

    public void Draw(SpriteBatch batch)
    {
    for (int x = 0; x < Width; x++)
    {
    for (int y = 0; y < Height; y++)
    {
    //Some Code here//

    batch.Draw(texture, new Rectangle(
    y * 32, x * 32, 32, 32), Color.White);
    }
    }
    }

    Is this the right way or am i missing something?

    ReplyDelete
    Replies
    1. I missed something :-)
      I accidentally swapped the x and y here :
      int textureIndex = map[y, x]; (tutorial version)
      int textureIndex = map[x, y]; (my version)

      Delete
  39. i got an error on
    Level level = new Level();
    Level could not be found

    ReplyDelete
  40. Hi,

    First, thanks, it's a very useful tutorial.

    I have a problem, SpriteBatch can't be resolved, even if I added the two references needed. Can you help me with this? It seems that microsft.xna.framework.graphics does not contains that class.

    Thnaks !

    ReplyDelete
  41. Hmm that is strange, it definitely should be contained in Microsoft.Xna.Framework.Graphics... If you download the source at the end of the tutorial does that run ok?

    ReplyDelete
  42. Thanks for your answer! I took the wrong dll to add in my project. I needed those in Windows/Micxrosoft .Net/XNA. Works like a charm now.

    ReplyDelete
  43. Hi,
    First of all i want to really thank you for taking your time to make this tutorial, i actually have a project due for in a couple of months, and i seem to be getting the same error all the time which is: "The type or namespace 'Level' could not be found", I tried searching for mistypes but haven't found anything. What do you suggest i do?

    Awaiting your swift reply,
    Eran Bodokh.

    ReplyDelete
  44. Hey Firefly, can you give some tips on handling multiple levels?
    One of the above comments does suggest an easy method, like using different numbers in the same map to denote different levels, but is there an easier approach>

    ReplyDelete