Note: This tutorial has been updated for Game Studio Express Beta 2

Introduction

This tutorial will walk through the steps to create a basic tile-based engine with Game Studio Express Beta 2. This engine is similar to the old style Ultima games and other RPGs back from the "Good Ole Days". With a bit of enhancement (and artistic ability, because my tiles are pretty terrible!) You can put together a decent tile engine in GSE very quickly.

The engine created here is a square-tile based system. I plan on following up with another article on creating an isometric tile based engine in the near future.

Concept

For this tutorial, we will be creating a simple tile-based map engine. In order to do this, we will need:
  • A map grid that represents the tile that will be used on each square.
  • A "Tile Set" of square graphics that can be drawn to represent the player's location on the map

Creating the Project

Start by opening Game Studio Express Beta and create a new project of the type "Windows Game (XNA)". Remember to give your project a name. I would also recommend changing the name of the "Game1.cs" file, though throughout this tutorial I will continue to refer to it as Game1.cs.
The basic "Windows Game (XNA)" template contains a single Graphics object and a class to represent your game in the Game1.cs file. The code in Game1.cs contains five important methods:
  • Initialize - Run as the game is starting up. Here you can register game components and other stuff on the startup of your game.
  • LoadGraphicsContent - Again, called when the game is starting, but also called if the game loses access to the display and needs to reload non-automatically managed content.
  • UnloadGraphicsContent - Called to free content when the game is exiting.
  • Update - This routine is run in a continuous loop and is intended to be the place where your game logic is run. Here you can accept player input, update locations of game objects, etc
  • Draw - Finally, the Draw method is responsible to rendering the current game state to the screen. It is called by the class as rapidly as the system can handle.

Creating our Resources

Before we can create a tile engine, we will need some tiles to draw onto the screen. I've created a few here by simply resizing a few texture images taken from The TextureBin. Someone with artistic talent could do a much better job at creating a tileset. It would also be important to create corner and edge tiles so that grass and beach would blend smoothly into each other, for instance.
That aside, here are the tiles I "created" quickly for this tutorial:
Grass Road Rock Water
You can download and save these tile images our create your own. In this engine example, I have set the tile size at 48x48 pixels. All of the tiles in a tileset (for this engine) must be the same size.

Adding Resource to the Project

In order for these resources to be accessable to the game, we need to add them to the project. Since we aren't using transparency for these sprites, we can leave them as .JPG files. However, the DirectX Texture Tool can be used to create textures with alpha channels. We'll do that later when we add a figure to represent the player to the map.

With the advent of the Beta 2, we will not be using the "content pipeline" to manage our graphics content. The content pipeline will handle getting our resources into a format that is usable on either Windows or the XBox.

Right click on the name of your project in the Solutions Explorer and select "Add" and "New Folder". Call the folder "Content". Right click on the content folder and click "Add" and "New Folder" again. This time call the folder "Textures".

Now, use Windows Explorer to copy your graphical resources to the Textures folder. When you are all done, you should have four .JPG files in the content\textures folder.

Go back to Visual C# and right click on the Textures folder and select "Add" and "Existing Item". In the dialog box that appears, select the tile files (you may need to change the File Type to Images to see them) and click ok. The Content Pipeline will give each of the resources a resource name (visible in the properties window below the Solution Explorer). Previously we had to set our graphics to "Copy Always" so that they were available to our program at run time. This is no longer necessary for any content that the Content Pipeline knows how to handle.

Declaring Variables

The first thing we need to do is declare some variables that we will use throughout the game to represent our game objects. In this case, it will be the map we are using, the "sprites" we will use to draw the tiles, and some control variables that we will use in the Draw and Update routines to maintain the game state.
Right-Click on Game1.cs in the Solutions Explorer and select View Code. Locate the constructor (Public Game1()) and place the following right after the close brace (These are outside of any method, so they are available to all of the methods in the class):
        // An array of "Texture2D" objects to hold our Tile Set
        Texture2D[] t2dTiles = new Texture2D[4];
        const int iMapWidth = 20;
        const int iMapHeight = 20;
        // Our simple integer-array based map
        int[,] iMap = new int[iMapHeight,iMapWidth] { 
                             { 0, 1, 2, 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, 1, 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, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
                             { 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
                             { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2},
                             { 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
                             { 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
                             { 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
                             { 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
                             { 0, 0, 0, 0, 1, 1, 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, 1, 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, 1, 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, 1, 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, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
        };
        // Variable we will need for Keyboard Input
        KeyboardState ksKeyboardState;
        //Map coordinates for upper left corner
        int iMapX = 0;
        int iMapY = 0;
        // How far from the Upper Left corner of the display do we want our map to start
        int iMapDisplayOffsetX = 30;
        int iMapDisplayOffsetY = 30;
        // How many tiles should we display at a time
        int iMapDisplayWidth = 6;
        int iMapDisplayHeight = 6;
        // The size of an individual tile in pixels
        int iTileWidth = 48;
        int iTileHeight = 48;
        // How rapidly do we want the map to scroll?
        float fKeyPressCheckDelay = 0.25f;
        float fTotalElapsedTime=0;
        //this is the object that will draw the sprites
        SpriteBatch spriteBatch;
There is quite a bit going on here, so I'll explain step by step:
        // An array of "Texture2D" objects to hold our Tile Set
        Texture2D[] t2dTiles = new Texture2D[4];
This declares an array of objects of the type "Texture2D" named t2dTiles. We are defining 4 files in this engine. This array of objects will hold the bitmap data for the sprites we will use for the Tile Set.
        const int iMapWidth = 20;
        const int iMapHeight = 20;
        // Our simple integer-array based map
        int[,] iMap = new int[iMapHeight,iMapWidth] { 
                             { 0, 1, 2, 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, 1, 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, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
                             { 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
                             { 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2},
                             { 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
                             { 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
                             { 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
                             { 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
                             { 0, 0, 0, 0, 1, 1, 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, 1, 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, 1, 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, 1, 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, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
        };
This is a very simplistic representation of a game map. We have pre-defined the size of the map to 20 by 20 tiles (with the iMapWidth and iMapHeight constants) and then simply declare the array that represents the map in the code here. A more realistic system would read the map from an external file or a built-in resource that was built via a map builder. For our purposes in this tutorial, however, this will suffice. The numbers in the array represent the tile number for each sqare on the map.
        // Variable we will need for Keyboard Input
        KeyboardState ksKeyboardState;
We will need these later for allowing the player to "move" by pressing keys on the keyboard.
        //Map coordinates for upper left corner
        int iMapX = 0;
        int iMapY = 0;
        // How far from the Upper Left corner of the display do we want our map to start
        int iMapDisplayOffsetX = 30;
        int iMapDisplayOffsetY = 30;
        // How many tiles should we display at a time
        int iMapDisplayWidth = 6;
        int iMapDisplayHeight = 6;
        // The size of an individual tile in pixels
        int iTileWidth = 48;
        int iTileHeight = 48;
This should all be self explanatory. These variables are used later in the Draw method to determine how to copy the sprites to the display.
        // How rapidly do we want the map to scroll?
        float fKeyPressCheckDelay = 0.25f;
        float fTotalElapsedTime=0;
These variables are used to control how fast the map resonds to user input. Without some kind of movement pacing, the player will press a key and the map will shoot off in that direction as fast as the Update routine is called.
        //this is the object that will draw the sprites
        SpriteBatch spriteBatch;
Finally we need a SpriteBatch object to use in the Draw method. The SpriteBatch is responsible for copying the tiles from our stored sprite variables into the display buffer.

Loading our Resources

The default framework for our project creates the "LoadGraphicsContent" method for us and calls it automatically when the game starts. To load our content, we will use the following code:

        protected override void LoadGraphicsContent(bool loadAllContent)
        {
            if (loadAllContent)
            {
                // TODO: Load any ResourceManagementMode.Automatic content
                t2dTiles[0] = content.Load<Texture2D>(@"content\textures\tilemap_tut_grass");
                t2dTiles[1] = content.Load<Texture2D>(@"content\textures\tilemap_tut_water");
                t2dTiles[2] = content.Load<Texture2D>(@"content\textures\tilemap_tut_road");
                t2dTiles[3] = content.Load<Texture2D>(@"content\textures\tilemap_tut_rock");
                spriteBatch = new SpriteBatch(graphics.GraphicsDevice);
            }
            // TODO: Load any ResourceManagementMode.Manual content
        }

If loadAllContent is true, we should load everything (meaning we are calling the routine for the first time while the game is starting). Otherwise we only need to load resources we are managing manually. We are going to let XNA manage these resources, so we only need to load them once.

Note that we don't supply a file extension to the textures. This is because the Content Pipeline automatically assignes a name to the resources which is a filename without the extension, and we aren't actually working with a file here but rather with it's representation in the Content Pipeline.

Drawing the Map

If you were to run the project at this point, you would still get just the default blue window because we haven't made any changes to the two "Game Loop" functions. Modify your Draw method to look like this:
        protected override void Draw(GameTime gameTime)
        {
            graphics.GraphicsDevice.Clear(Color.CornflowerBlue);

            spriteBatch.Begin(SpriteBlendMode.AlphaBlend);
            // Draw the map
            for (int y = 0; y < iMapDisplayHeight; y++)
            {
                for (int x = 0; x < iMapDisplayWidth; x++)
                {
                    spriteBatch.Draw(t2dTiles[iMap[y + iMapY, x + iMapX]],
                                     new Rectangle((x * iTileWidth) + iMapDisplayOffsetX,
                                     y * iTileHeight + iMapDisplayOffsetY, iTileWidth, iTileHeight),
                                     Color.White);
                }
            }
            spriteBatch.End();
            base.Draw(gameTime);
        }
This is the heart of our map engine, so we'll go through it line by line. All of our drawing will be done between a BeginScene() and EndScene() call.
            spriteBatch.Begin(SpriteBlendMode.AlphaBlend);
Tells the spriteBatch object that we are going to start painting sprites. The use of "SpriteBlendMode.AlphaBlend" ensures that the spriteBatch will take alpha channels (transparency) into account when painting. We we aren't using alpha channels for our initial map display, it will be useful later in overlaying objects (ie, the player) on the map.
            // Draw the map
            for (int y = 0; y < iMapDisplayHeight; y++)
            {
                for (int x = 0; x< iMapDisplayWidth; x++)
                {
                    spriteBatch.Draw(t2dTiles[iMap[y+iMapY,x+iMapX]], 
                                     new Rectangle((x*iTileWidth)+iMapDisplayOffsetX, 
                                     y*iTileHeight + iMapDisplayOffsetY, iTileWidth, iTileHeight),
                                     Color.White);
                }
            }
Here we actually draw the sprites that compose the map. We set up two for loops, one for the Y axis and one for the X axis. the iMapDisplayHeight and iMapDisplayWidth variables determine how many tiles we will draw in each direction.
We call the spriteBatch.Draw method to actually paint a sprite to the display buffer. The first parameter of the spriteBatch.Draw method is the sprite we wish to draw. We find the tile that should be drawn by looking in the iMap array. This will return a number from 0 to 3, corresponding to the tiles in our Tile Set.
The second parameter is a Rectangle object representing where on the display buffer the sprite will be displayed. Using the loop counter values, the width of the tiles, and the DisplayOffset variables we create that rectangle.
Finally, the third parameter represents the tinting that will be used when drawing the sprite. We are drawing the sprite without any tinting, so the Color.White value is used to indicate that.
            spriteBatch.End();
After all of the sprites have been drawn, we wrap up the batch
            base.Draw(gameTime);
        }
    }
The rest of the method is unchanged from the template. It simply calls the underlying class's Draw method. Running the project should produce a map that is set 30 pixels from the top and right of the window and 6 tiles high and wide.

Accepting Player Input

Now that we can draw the map, we need to be able to let the player press arrow keys and move around. In order to do this, we will need to modify the Update method to check for keyboard input.
Replace your Update method with the following:
        protected override void Update(GameTime gameTime)
        {
            // Allows the default game to exit on Xbox 360 and Windows
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();
            float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;
            fTotalElapsedTime += elapsed;
            ksKeyboardState = Keyboard.GetState();
            if (ksKeyboardState.IsKeyDown(Keys.Escape))
            {
                this.Exit();
            }

            // See if enough time has elapsed since we last moved on the map.
            if (fTotalElapsedTime >= fKeyPressCheckDelay)
            {
                if (ksKeyboardState.IsKeyDown(Keys.Up))
                {
                    iMapY--;
                    fTotalElapsedTime = 0.0f;
                }
                if (ksKeyboardState.IsKeyDown(Keys.Down))
                {
                    iMapY++;
                    fTotalElapsedTime = 0.0f;
                }
                if (ksKeyboardState.IsKeyDown(Keys.Left))
                {
                    iMapX--;
                    fTotalElapsedTime = 0.0f;
                }
                if (ksKeyboardState.IsKeyDown(Keys.Right))
                {
                    iMapX++;
                    fTotalElapsedTime = 0.0f;
                }
                if (iMapX < 0) { iMapX = 0; }
                if (iMapX > iMapWidth - iMapDisplayWidth) { iMapX = iMapWidth - iMapDisplayWidth; }
                if (iMapY < 0) { iMapY = 0; }
                if (iMapY > iMapHeight - iMapDisplayHeight) { iMapY = iMapHeight - iMapDisplayHeight; }
            }
            base.Update(gameTime);
        }
Once again, there is alot happening here, so we will go through it step by step:
            // Allows the default game to exit on Xbox 360 and Windows
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();
This is part of the default template. If the player presses the "Back" button on the XBox 360 controller the game will exit.
            float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;
            fTotalElapsedTime += elapsed;
This allows us to accumulate the elapsed time into a float that we can use later to determine how quickly the map should scroll.
                ksKeyboardState = Keyboard.GetState();
Here, we get the current state of the keyboard into a variable we can then use to check for pressed keys.
                if (ksKeyboardState.IsKeyDown(Keys.Escape)) {
                    this.Exit();
                }
For convenience sake, if Escape is pressed, exit the program. Obviously you wouldn't want to do this in a real program. Perhaps you would bring up a menu or other such activity.
                // See if enough time has elapsed since we last moved on the map.
                if (fTotalElapsedTime >= fKeyPressCheckDelay)
                {
Before we check to see if movement keys are pressed, we want to first check to see if the minimum map scroll time has elapsed (I've defaulted it here to 0.25 seconds). If we dont' do something along these lines, the map will scroll REALLY fast.
                   if (ksKeyboardState.IsKeyDown(Keys.Up))
                   {
                       iMapY--;
                       fTotalElapsedTime = 0.0f;
                   }
                   if (ksKeyboardState.IsKeyDown(Keys.Down))
                   {
                       iMapY++;
                       fTotalElapsedTime = 0.0f;
                   }
                   if (ksKeyboardState.IsKeyDown(Keys.Left))
                   {
                       iMapX--;
                       fTotalElapsedTime = 0.0f;
                   }
                   if (ksKeyboardState.IsKeyDown(Keys.Right))
                   {
                       iMapX++;
                       fTotalElapsedTime = 0.0f;
                   }
If any of the arrow keys are pressed, moved the "map" in that direction by updating the X/Y coordinate.
                   if (iMapX < 0) { iMapX = 0; }
                   if (iMapX > iMapWidth-iMapDisplayWidth) { iMapX = iMapWidth-iMapDisplayWidth; }
                   if (iMapY < 0) { iMapY = 0; }
                   if (iMapY > iMapHeight-iMapDisplayHeight) { iMapY = iMapHeight-iMapDisplayHeight; }
Finally we check to see if the X and Y coordinates have gone off the end of the map. If so, bump the coordinate back to the edge.

Wrapping it Up

At this point, you should have a working tile-based map.



There are, however, a number of things that could be done to improve the simple system we have here. I hope to follow this tutorial up with examples of how to implement some of these improvements:
  • Add an "Avatar" for the player. This could be an animated sprited that is drawn in the center of the map with Alpha Blending after the map is drawn
  • Make the map scroll smoothly by keeping track of an offset within the individual tiles. Adjust the drawing position whenever the movement keys are pressed by this fractional tile instead of a full tile. Note that you would want to adjust the fKeyPressCheckDelay to compensate for the fact that you are moving up to 48 times more slowly.
  • Build an interface around the map we have here, perhaps with character stats, etc
  • Alternatively, set iMapDisplayOffsetX and iMapDisplayOffsetY to 0 and set iMapDisplayWidth to 14 and iMapDisplayHeight to 10 so that the map fills the 640x480 window
Of course, you would also want to actually build a game behind the system as well.
 

































 
 
 
Site Contents Copyright © 2006 Full Revolution, Inc. All rights reserved.
This site is in no way affiliated with Microsoft or any other company.
All logos and trademarks are copyright their respective companies.
RSS FEED