

IntroductionWelcome to the fourth installment of XNAResources.com's tile set engine tutorial. In this part, we are going to focus on using the map engine we have built up to this point to create a simple map editor that we can use to load and save maps instead of keeping all of our map code inlined into the game code.
We will be using the Solution we have been building up to this point as a "fork" for the map editor. Download the Solution form Part 3 if you don't already have it.
Designing the Editor FunctionsOur editor will be fairly simple, but it will involved several new things we haven't done before:
- Using the Mouse - We will be activating the mouse pointer and using it to select our editing modes and paint down tiles.
- Tracking a Program State - We will be toggling between Play Mode and Edit Mode. While in Edit Mode, we will display extra interface pieces to the user and allow them to make changes to the map. We also won't test for Walkability while in Edit Mode.
- Loading and Saving with Streams - We will be saving our map data to disk so that it can be loaded back in between sessions. We'll do this using simple text files and a versioned map file format via the StreamReader and StreamWriter classes.
Here are the basics of how our editor will function:
Pressing the E key will toggle between Play Mode and Edit Mode. While in Play Mode, our game will behave just like it did in Part 3 of this tutorial series.
In Edit mode, however, we will add a box to the lower portion of the screen that displays our current tile for painting, as well as three toggle buttons to allow us to choose the current layer we are drawing on (Base, Trans, or Object). We won't draw the player avatar while in edit mode, and we will paint with the current tile whenever the mouse is clicked in the "playfield". Right clicking on the playfield will make that tile "unwalkable" while Shift-Right Clicking on the playfield will clear the unwalkable flag. Walkability will be indicated by an overlay on the map while in edit mode.
The "W", "A", "S", and "D" keys will be used to scroll through our tileset to select the current drawing tile, and the arrow keys will still be used to move around on the map. Pressing the "L" key will load the map from disk, while pressing the "O" key will save the map.
In this version of the Editor, we will always be loading and saving to the same file (map000.txt), but in a future version we will actually bring up a dialog box to allow us to enter a name for the map.
I should note that there is a LOT of things you could do to spruce up this editor, but I'm not doing most of them here because there are quite a few new concepts to cover and I don't want to get the importance of them lost in the details of implementing editor features.
Enabling the Mouse
Since we no longer have a Designer mode in Beta 2, we can't use the designer interface to make the Mouse Cursor visible now. In order to show the mouse in your game, edit the Initialize method and set the game's IsMouseVisible property to true:
protected override void Initialize()
{
// TODO: Add your initialization logic here
this.graphics.PreferredBackBufferHeight = 480;
this.graphics.PreferredBackBufferWidth = 640;
this.graphics.ApplyChanges();
this.IsMouseVisible = true;
base.Initialize();
}
We'll also need to add a MouseState object so we can tell what is happening with the mouse later on. Right after your declaration for the ksKeyboardState object, add:
MouseState msMouseState;
Altering the Map Declaration
Lets start by adding a few declarations to support a variable sized map. Remove the existing iMapWidth and iMapHeight declarations and replace them with these four:
const int iMapMaxWidth = 100;
const int iMapMaxHeight = 100;
int iMapWidth = 20;
int iMapHeight = 20;
We'll also need to rip out our map declaration (iMap, iMapTrans, iMapObjects, and iMapWalkable) and replace it with a new one:
// Define our Map Layer arrays.
int[,] iMap = new int[iMapMaxHeight, iMapMaxWidth]; // Map Base Layer
int[,] iMapTrans = new int[iMapMaxHeight, iMapMaxWidth]; // Map Transition Layer
int[,] iMapObjects = new int[iMapMaxHeight, iMapMaxWidth]; // Map Object Layer
int[,] iMapWalkable = new int[iMapMaxHeight, iMapMaxWidth]; // Map Walkability Layer
int iMapVersion = 1;
That's alot better than the 90+ lines of code we had before! First, we are going to add a couple of methods to help us handle the map. These can go right after the LoadGraphicsContent method. Here's the first one:
void InitializeMap()
{
for (int x = 0; x < iMapMaxWidth; x++ )
{
for (int y = 0; y < iMapMaxHeight; y++ )
{
iMap[y, x] = 0;
iMapTrans[y, x] = 0;
iMapObjects[y, x] = 0;
iMapWalkable[y, x] = 0;
}
}
}
We'll be calling this method from the Initialize method to make sure that our map array is initialized to a known state before we try to use it for anything.
Next, we need methods to load and save the map. In order to use the StreamReader and StreamWriter classes though, we'll need to add System.IO to our using statements at the top of our project. Do to this, go all the way to the top of your code and add the following below the other using statements:
using System.IO;
In C#, the "using" statements tell the compiler to include references to pre-built assemblies. These could be assemblies you created yourself in C# or another .NET language or assemblies that you only have binary access to. In this case, we are using the Microsoft supplied "System.IO" assembly, which contains functions for reading and writing files (among other things).
Now add these methods to the code below your newly added InitializeMap method:
void WriteMapToFile(string sFileName)
{
StreamWriter swWriter;
swWriter = File.CreateText(sFileName);
swWriter.WriteLine(iMapVersion);
swWriter.WriteLine(iMapHeight);
swWriter.WriteLine(iMapWidth);
for (int y = 0; y < iMapHeight; y++ )
{
for (int x = 0; x < iMapWidth; x++ )
{
swWriter.WriteLine(iMap[y, x]);
swWriter.WriteLine(iMapTrans[y, x]);
swWriter.WriteLine(iMapObjects[y, x]);
swWriter.WriteLine(iMapWalkable[y, x]);
}
}
swWriter.Close();
}
void ReadMapFromFile(string sFileName)
{
StreamReader srReader;
srReader = File.OpenText(sFileName);
int iReadVersion = Convert.ToInt32(srReader.ReadLine());
iMapHeight = Convert.ToInt32(srReader.ReadLine());
iMapWidth = Convert.ToInt32(srReader.ReadLine());
for (int y = 0; y < iMapHeight; y++ )
{
for (int x = 0; x < iMapWidth; x++ )
{
iMap[y,x] = Convert.ToInt32(srReader.ReadLine());
iMapTrans[y, x] = Convert.ToInt32(srReader.ReadLine());
iMapObjects[y, x] = Convert.ToInt32(srReader.ReadLine());
iMapWalkable[y, x] = Convert.ToInt32(srReader.ReadLine());
}
}
srReader.Close();
}
This is a VERY SIMPLISTIC way of saving our map data :) There are certainly better ways to do this, but for example purposes this is nice and easy to understand.
A "stream" is a term referring to a sequence of bytes. Techincally a stream could be a file, bytes in memory, something coming over a communications port, etc. In our case, the StreamReader and StreamWriter classes are used to reading and writing text information in a byte stream.
As I said above, the way we are storing our data for the map is very simple. Each data item is stored on a line in the file. The first line in the file stores the version number for the map format we are using. We are starting here with version 1. This will be important later when we add new features to our map so that we can still read in maps without the new features and update them. The next two lines in the file store the Height and Width of the map.
After that, each tile of the map has 4 lines dedicated to it, one for each layer of the map. The read and write methods loop through the map array and read or write those 4 lines for the 4 map layers.
Finally, we need to update our Initialize method to call InitializeMap:
protected override void Initialize()
{
// TODO: Add your initialization logic here
this.graphics.PreferredBackBufferHeight = 480;
this.graphics.PreferredBackBufferWidth = 640;
this.graphics.ApplyChanges();
this.IsMouseVisible = true;
InitializeMap();
base.Initialize();
}
If you run the program now, you should have a grass-filled field that you can wander around it, but it's a bit boring! I've uploaded the Sample Map from tutorial 3 so that we will have something to load. Download the text file and add it to your project and this time set it to Copy If Newer instead of copy always. That way, if you change the map and save it, it won't be overwritten the next time you compile/run the project, but it will be copied over the first time.
Editing Interface
In order to edit the map, we'll need some interface features to allow us to select the tiles to draw, etc. I've put together a texture page that contains all of the little pieces we will need to make our Editor interface.

As with all of our graphical resources, you'll need to add this to your project so that Content Manager can access it. We will need to declare a Texture2D object for this image, so add this along with the other Texture2D declarations:
Texture2D t2dEditorImages;
We'll also need to update our LoadGraphicsContent function to load them:
protected override void LoadGraphicsContent(bool loadAllContent)
{
if (loadAllContent)
{
// TODO: Load any ResourceManagementMode.Automatic content
t2dTileSet = content.Load<Texture2D>(@"content\textures\fulltileset_alpha");
t2dGameScreen = content.Load<Texture2D>
(@"content\textures\gamescreen");
t2dPlayerAvatar = content.Load<Texture2D>(@"content\textures\playeravatars");
t2dEditorImages = content.Load<Texture2D>(@"content\textures\editorimages");
spriteBatch = new SpriteBatch(graphics.GraphicsDevice);
}
// TODO: Load any ResourceManagementMode.Manual content
}
There are a several pieces of this image we won't be using in this version of the editor, so let me explain what we will and won't be using:
The large box in the upper left of the image is intended to be a dialog box. In a future version of the editor, we will be using this box to display a menu of options, like the Save, Load, etc images next to it. The dialog box and those buttons won't be used yet. Nor will the small arrow buttons or the long input window.
However, we will be using the layer selector buttons (Trans, Object, and Base), the "current tile" window (on the right hand side right above the small arrows), and the red X and red square images. The Red X will be used to indicate that a tile is not walkable, while the square will be used to hilight the square under our mouse cursor when we are in editor mode.
You might also see that it includes a small character set that we will use for displaying text. There are other ways to do this, perhaps the best one out there for XNA right now being the BMFontClass. I would have used that, but going thru and creating the bitmap font and adding the supporting classes to the program would take away from our focus, and doing the text output this way is both simple enough for us to use now, and shows a bit of the fun we can have with ASCII and math! All we will use the font for in this version of the editor is to display what mode we are in down in the corner of the display screen, but the implementation should be interesting on it's own.
Declarations for the EditorAfter we have our editor component surface added to the project, we need to add some declarations to allow us to locate various things inside it. Add the following to your declarations section:
// Editor Declarations
int iProgramMode = 0; // 0=Play Mode, 1=Edit Mode
int iEditorCurrentTile = 0; // The Tile Number we are currently drawing with
int iEditorLayerMode = 1; // 1=Base, 2=Trans, 3=Object
// Locations of various resources in the Editor image.
Rectangle rectEditorBaseButtonSRC = new Rectangle(540, 225, 80, 30);
Rectangle rectEditorBaseButton = new Rectangle(90, 430, 80, 30);
Rectangle rectEditorTransButtonSRC = new Rectangle(540, 45, 80, 30);
Rectangle rectEditorTransButton = new Rectangle(90, 400, 80, 30);
Rectangle rectEditorObjectButtonSRC = new Rectangle(540, 135, 80, 30);
Rectangle rectEditorObjectButton = new Rectangle(90, 370, 80, 30);
Rectangle rectEditorXOverlay = new Rectangle(405, 270, 48, 48);
Rectangle rectEditorBoxOverlay = new Rectangle(405, 360, 48, 48);
// How far down from the unpressed button the pressed version is
int iEditorModeButtonPressedOffset = 45;
// Where to draw the current working tile on the screen.
int iEditorCurrentTileX = 28;
int iEditorCurrentTileY = 377;
There are a few important things here. First, we are defining a variable to track what "mode" we are in (Play/Edit). This will be used to control both what we draw in the Draw method and what input we respond to in the Update method.
Next are the tile we are currently drawing with (defaulting to 0, or grass in our tile set) and what layer we are working with in the editor (defaulting to the Base layer).
Next we define a bunch of Rectangles. The items ending with SRC are the location of the buttons within our editor image, while the rectangles with the same base name but without the SRC ending are the locations they will be drawn to on the screen. We'll need these both for drawing and for detecting mouse clicks on these buttons.
Next we have iEditorModeButtonPressedOffset. (I know, the variable names are getting long! But I want them that way for clarity). If you look at our editor image, you will see that for the layer mode buttons, there is a pressed and and unpressed version. The pressed version is always exactly 45 pixels below the pressed version (top to top) in the image. We'll use that to our advantage (it was set up that way intentionally) to help us draw the right version of the button.
Finally, we have the location on the screen where the editor's current tile will be drawn.
Cleaning Up Some CodeWe are going to be doing a LOT of mucking about with our Update and Draw methods. Since they are already getting pretty large, we are going to pull out some of the code and create a few functions to help keep things managable. To this end, replace your current Update routine 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;
fAnimationTime += elapsed;
// Read and save the current Keyboard State
ksKeyboardState = Keyboard.GetState();
// Read and save the current Mouse State;
msMouseState = Mouse.GetState();
// Check to see if the Escape key has been pressed. Exit the program if so.
if (ksKeyboardState.IsKeyDown(Keys.Escape))
{
this.Exit();
}
// If we AREN'T in the process of completing a smooth-scroll move...
if (iMoveCount <= 0)
{
if (fTotalElapsedTime >= fKeyPressCheckDelay)
{
if (CheckMapMovementKeys(ksKeyboardState) == 1)
{
fTotalElapsedTime = 0;
}
if (iProgramMode == 1)
{
if (CheckEditorKeys(ksKeyboardState) == 1)
{
fTotalElapsedTime = 0;
}
}
}
// Check for Editor Mode Mouse Clicks
if (iProgramMode == 1)
{
CheckEditorModeMouseClicks(msMouseState, ksKeyboardState);
}
}
else
{
// If we ARE in the middle of a smooth-scroll move, update the
// Offsets and decrement the move count.
if (iMoveDirection == 0)
{
iMapYOffset -= iMapYScrollRate;
iMoveCount -= iMapYScrollRate;
}
if (iMoveDirection == 1)
{
iMapYOffset += iMapYScrollRate;
iMoveCount -= iMapYScrollRate;
}
if (iMoveDirection == 2)
{
iMapXOffset -= iMapXScrollRate;
iMoveCount -= iMapXScrollRate;
}
if (iMoveDirection == 3)
{
iMapXOffset += iMapXScrollRate;
iMoveCount -= iMapXScrollRate;
}
// If we move off of a tile, change our map location to the next tile
if (iMapXOffset < 0) { iMapXOffset = iTileWidth; iMapX--; }
if (iMapXOffset > iTileWidth) { iMapXOffset = 0; iMapX++; }
if (iMapYOffset < 0) { iMapYOffset = iTileHeight; iMapY--; }
if (iMapYOffset > iTileWidth) { iMapYOffset = 0; iMapY++; }
// If we move off of the side of the map, "snap" back (player won't see a move at all)
if (iMapX < 0) { iMapX = 0; iMapXOffset = 0; iMoveCount = 0; }
if (iMapX > iMapWidth - iMapDisplayWidth)
{
iMapX = iMapWidth - iMapDisplayWidth;
iMapXOffset = 0; iMoveCount = 0;
}
if (iMapY < 0) { iMapY = 0; iMapYOffset = 0; iMoveCount = 0; }
if (iMapY > iMapHeight - iMapDisplayHeight)
{
iMapY = iMapHeight - iMapDisplayHeight;
iMapYOffset = 0; iMoveCount = 0;
}
}
if (fAnimationTime >= fAnimationDelay)
{
iTileAnimationFrame++;
if (iTileAnimationFrame > iTileAnimationFrameCount)
{
iTileAnimationFrame = 0;
}
fAnimationTime = 0.0f;
}
base.Update(gameTime);
}
At the moment, your program won't run because we are missing a few functions. The most important area to look at in the revised Update routine above is:
.
.
.
// If we AREN'T in the process of completing a smooth-scroll move...
if (iMoveCount <= 0)
{
if (fTotalElapsedTime >= fKeyPressCheckDelay)
{
if (CheckMapMovementKeys(ksKeyboardState) == 1)
{
fTotalElapsedTime = 0;
}
if (iProgramMode == 1)
{
if (CheckEditorKeys(ksKeyboardState) == 1)
{
fTotalElapsedTime = 0;
}
}
}
// Check for Editor Mode Mouse Clicks
if (iProgramMode == 1)
{
CheckEditorModeMouseClicks(msMouseState, ksKeyboardState);
}
.
.
.
This is where we used to check for key presses and respond to them. You'll notice that our input delay checking (at least 0.25 seconds between responses to key presses) is back because we don't want to save 40 times when someone presses the "O" key.
We call CheckMapMovementKeys, passing it our Keyboard State, no matter what mode we are in (because you can still move around on the map in Edit mode). If we are in Edit Mode (iProgramMode == 1) we also call CheckEditorKeys, again passing it our Keyboard State to see if an editor-specific key press was detected.
Finally, we call CheckEditorModeMouseClicks if we are in Edit Mode.
Adding our FunctionsFirst, lets add the ability to move back in. All of these functions can be added after the functions to load and save the map. Create the following new function:
int CheckMapMovementKeys(KeyboardState ksKeyboardState)
{
// Check to see if an arrow key is pressed. If so, set the
// iMoveDirection to indicate the direction we will be moving in,
// and the iMoveCount to how many times we need to execute.
if (ksKeyboardState.IsKeyDown(Keys.Up))
{
if ((iMapWalkable[(iMapY + iPlayerAvaterYOffset) - 1, (iMapX + iPlayerAvatarXOffset)] == 0) ||
(iProgramMode==1))
{
if (iMapY > 0)
{
iMoveDirection = 0;
iMoveCount = iTileHeight + iMapYScrollRate;
return 1;
}
}
}
if (ksKeyboardState.IsKeyDown(Keys.Down))
{
if ((iMapWalkable[(iMapY + iPlayerAvaterYOffset) + 1, (iMapX + iPlayerAvatarXOffset)] == 0) ||
(iProgramMode==1))
{
if (iMapY < (iMapHeight - iMapDisplayHeight))
{
iMoveDirection = 1;
iMoveCount = iTileHeight + iMapYScrollRate;
return 1;
}
}
}
if (ksKeyboardState.IsKeyDown(Keys.Left))
{
if ((iMapWalkable[(iMapY + iPlayerAvaterYOffset), (iMapX + iPlayerAvatarXOffset) - 1] == 0) ||
(iProgramMode==1))
{
if (iMapX > 0)
{
iMoveDirection = 2;
iMoveCount = iTileHeight + iMapXScrollRate;
return 1;
}
}
}
if (ksKeyboardState.IsKeyDown(Keys.Right))
{
if ((iMapWalkable[(iMapY + iPlayerAvaterYOffset), (iMapX + iPlayerAvatarXOffset) + 1] == 0) ||
(iProgramMode==1))
{
if (iMapX < (iMapWidth - iMapDisplayWidth))
{
iMoveDirection = 3;
iMoveCount = iTileHeight + iMapXScrollRate;
return 1;
}
}
}
if (ksKeyboardState.IsKeyDown(Keys.E)) {
if (iProgramMode==0) {
iProgramMode=1;
} else {
iProgramMode=0;
}
return 1;
}
return 0;
}
You will notice that this is MOSTLY the code we pulled out of the Update routine to check to see if the user is trying to scroll the map by pressing the arrow keys. There are a couple of important changes, though:
First, we modify our iMapWalkable check to ignore walkability if we are in Edit mode. We do this by wrapping another set of parenthesis around it and useing " || (iProgramMode==1)". In C#, the double-vertical-bar operator means "or", so the check is saying "if the tile is walkable OR we are in Edit mode allow the move". Essentially, we are removing the restrictions on where you can "walk" to if you are in edit mode.
Next, you'll notice we have added another key press check for the "E" key. Pressing E will toggle Edit mode on and off. The check is here because we call this routine no matter what mode we are in, so we can switch between modes by simply changing the iProgramMode value. Because the rest of our drawing and editing routines will check this variable and act appropriately we don't need to do anything else here.
The next function we need is CheckEditorKeys, which will only be called while we are in Edit mode:
int CheckEditorKeys(KeyboardState ksKeyboardState)
{
if (ksKeyboardState.IsKeyDown(Keys.O))
{
WriteMapToFile("map000.txt");
return 1;
}
if (ksKeyboardState.IsKeyDown(Keys.L))
{
ReadMapFromFile("map000.txt");
return 1;
}
if (ksKeyboardState.IsKeyDown(Keys.W))
{
iEditorCurrentTile -= 12;
if (iEditorCurrentTile < 0) { iEditorCurrentTile += 12; }
return 1;
}
if (ksKeyboardState.IsKeyDown(Keys.A))
{
iEditorCurrentTile--;
if (iEditorCurrentTile < 0) { iEditorCurrentTile++; }
return 1;
}
if (ksKeyboardState.IsKeyDown(Keys.S))
{
iEditorCurrentTile += 12;
if (iEditorCurrentTile > 120) { iEditorCurrentTile -= 12; }
return 1;
}
if (ksKeyboardState.IsKeyDown(Keys.D))
{
iEditorCurrentTile++;
if (iEditorCurrentTile > 120) { iEditorCurrentTile--; }
return 1;
}
return 0;
}
Pretty straightforward stuff for the most part. We check to see if the user presses "L" (to load) or "O" (to save) the map. If so, we call the load/save routines.
If the user pressed "W", "A", "S", or "D", we move up, left, down, or right (respectively) on the tile sheet. It would be much nicer to pick the tile from a dialog box, but this will work for now. We add a few checks to make sure we don't end up off the end of the tile sheet as well.
In both of these functions, you will notice that if we do something, we "return 1", and if we don't do anything we "return 0". This return value is used in the update routine to reset the key press check delay timer if we did something and not reset it if we didn't.
The MouseThe other function we need to make our new Update method work is CheckEditorModeMouseClicks. This is really the heart of the editor, so I'll paste in the function here and then break it down:
void CheckEditorModeMouseClicks(MouseState msMouseState, KeyboardState ksKeyboardState)
{
Rectangle rectPlayField = new Rectangle(iMapDisplayOffsetX, iMapDisplayOffsetY,
(iMapDisplayWidth - 1) * iTileWidth,
(iMapDisplayHeight - 1) * iTileHeight);
// Check for mouse clicks
if (msMouseState.LeftButton == ButtonState.Pressed)
{
// First, lets check to see if we click on one of the "mode" buttons. If we do,
// update the iEditorLayerMode variable as appropritate.
if ((msMouseState.X >= rectEditorObjectButton.Left) &
(msMouseState.X <= rectEditorObjectButton.Right) &
(msMouseState.Y >= rectEditorObjectButton.Top) &
(msMouseState.Y <= rectEditorObjectButton.Bottom))
{
iEditorLayerMode = 3;
}
if ((msMouseState.X >= rectEditorTransButton.Left) &
(msMouseState.X <= rectEditorTransButton.Right) &
(msMouseState.Y >= rectEditorTransButton.Top) &
(msMouseState.Y <= rectEditorTransButton.Bottom))
{
iEditorLayerMode = 2;
}
if ((msMouseState.X >= rectEditorBaseButton.Left) &
(msMouseState.X <= rectEditorBaseButton.Right) &
(msMouseState.Y >= rectEditorBaseButton.Top) &
(msMouseState.Y <= rectEditorBaseButton.Bottom))
{
iEditorLayerMode = 1;
}
// Finally, lets check to see if we are clicking inside the map area. If so, we
// will update the appropriate layer of the clicked tile with the currrently selected
// drawing tile.
if ((msMouseState.X >= rectPlayField.Left) &
(msMouseState.X <= rectPlayField.Right) &
(msMouseState.Y >= rectPlayField.Top) &
(msMouseState.Y <= rectPlayField.Bottom))
{
// Determine the X and Y tile location of where we clicked
int iClickedX = ((msMouseState.X - iMapDisplayOffsetX) / iTileWidth) + iMapX;
int iClickedY = ((msMouseState.Y - iMapDisplayOffsetY) / iTileHeight) + iMapY;
// If we are in "Base" mode:
if (iEditorLayerMode == 1)
{
iMap[iClickedY, iClickedX] = iEditorCurrentTile;
}
// If we are in "Trans" mode:
if (iEditorLayerMode == 2)
{
iMapTrans[iClickedY, iClickedX] = iEditorCurrentTile;
}
// If we are in "Object" mode:
if (iEditorLayerMode == 3)
{
iMapObjects[iClickedY, iClickedX] = iEditorCurrentTile;
}
}
}
// we will use the right mouse button to toggle walkable and non-walkable squares.
if (msMouseState.RightButton == ButtonState.Pressed)
{
// If we right-clicked in the map area...
if ((msMouseState.X >= rectPlayField.Left) &
(msMouseState.X <= rectPlayField.Right) &
(msMouseState.Y >= rectPlayField.Top) &
(msMouseState.Y <= rectPlayField.Bottom))
{
// Determine the X and Y tile location of where we clicked
int iClickedX = ((msMouseState.X - iMapDisplayOffsetX) / iTileWidth) + iMapX;
int iClickedY = ((msMouseState.Y - iMapDisplayOffsetY) / iTileHeight) + iMapY;
if (ksKeyboardState.IsKeyDown(Keys.RightShift) || ksKeyboardState.IsKeyDown(Keys.LeftShift))
{
// Shift-Right Clicking clears the walkable flag
iMapWalkable[iClickedY, iClickedX] = 0;
}
else
{
// Normal Right-Clicking sets the walkable flag, preventing walking
iMapWalkable[iClickedY, iClickedX] = 1;
}
}
}
}
The Mouse State class works very similar to the Keyboard State class. We capture the mouse state in Update to the msMouseState object and then use it to find out what the mouse is doing.
If one of the mouse buttons is pressed, the msMouseState's LeftButton or RightButton value will be equal to "ButtonState.Pressed". The first thing we do in our routine is check to see if the left mouse button is pressed with:
if (msMouseState.LeftButton == ButtonState.Pressed)
If it is, then we need to figure out what the user is trying to do based on where the mouse is. The first three things we check for in the routine above are the little mode selection buttons. Remember that we defined rectangles for these earlier, so we can just check to see if the mouse's X and Y location is inside the bounds of the rectangle.
A note here about the XNA framework. I tried to find a way to test the rectangle as a single object (like a IsInRectangle(X,Y,rectTesting), but I couldn't find anything along those lines. The Help files for XNA/GSE are mostly empty right now, so maybe when the full version is released I'll be able to find a better way to do this test than just checking the X and Y against the Left, Right, Top, and Bottom of the rectangle.
Anyway, if we find that we are clicking within one of those rectangles, we simply set the iEditorLayerMode to whatever button is being clicked on. The actual changing of the button will be handled in the Draw routine.
If we didn't click on one of the buttons, the other place we might have clicked is within the "playfield". We check against the play field rectangle we created at the beginning of the function to see if we are clicking inside it. If so, we determine the tile that was clicked on and update the appropriate layer (based on iEditorLayerMode) with the current tile (iEditorCurrentTile).
Updating the map is just that simple!
But we aren't done yet. We are also checking for right-click and shift-right-click to toggle walkability. We are doing everything the same way we did for the left mouse button, except we check the ksKeyboardState to see if shift is pressed when setting the iMapWalkable layer. If shift isn't pressed, we set walkable to 1 (not walkable). If it is, we set it to 0 (walkable).
One last thing I should note about all three of these functions is that I passed in the msMouseState and ksKeyboardState variables when I probably didn't really need to. They are, after all, global variables. Passing them in creates a local copy of the variables that is used inside the function only. The function could just as easily access the global variables, but making a function complete in and of itself and not relying on globals is usually a good idea. I further clouded the issue by giving the global and local variables the same name, which isn't a great idea but it isn't a problem for C# to figure out which one we are dealing with.
Drawing the InterfaceIf you run the program now, you should end up with an empty grass field. If you hit the "E" key and the hit "L" it should load the sample map. Currently though, there is no indication that you are in editor mode at all, and no indication that you exit editor mode. Now we need to update our Draw routine and it's associated functions.
Replace your Draw method with this one. This is a big chunk of code, but most of it is unchanged from Part 3. I'll explain the parts that are changed below:
protected override void Draw(GameTime gameTime)
{
graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
int iTileToDraw;
spriteBatch.Begin(SpriteBlendMode.None);
// Draw the map
for (int y = 0; y < iMapDisplayHeight; y++)
{
for (int x = 0; x < iMapDisplayWidth; x++)
{
iTileToDraw = iMap[y + iMapY, x + iMapX];
if (iTileToDraw == iTileAnimationStartFrame)
{
iTileToDraw += iTileAnimationFrame;
}
Rectangle recSource = new Rectangle((iTileToDraw % iTileSetXCount) * iTileWidth,
(iTileToDraw / iTileSetXCount) * iTileHeight,
iTileWidth, iTileHeight);
spriteBatch.Draw(t2dTileSet,
new Rectangle(((x * iTileWidth) + iMapDisplayOffsetX) - iMapXOffset,
((y * iTileHeight) + iMapDisplayOffsetY) - iMapYOffset,
iTileWidth, iTileHeight),
recSource,
Color.White);
}
}
spriteBatch.End();
spriteBatch.Begin(SpriteBlendMode.AlphaBlend);
// Draw transitions and Objects layers of the map
for (int y = 0; y < iMapDisplayHeight; y++)
{
for (int x = 0; x < iMapDisplayWidth; x++)
{
Rectangle recDest = new Rectangle(((x * iTileWidth) + iMapDisplayOffsetX) - iMapXOffset,
((y * iTileHeight) + iMapDisplayOffsetY) - iMapYOffset,
iTileWidth, iTileHeight);
iTileToDraw = iMapTrans[y + iMapY, x + iMapX];
if (iTileToDraw > 11)
{
Rectangle recSource = new Rectangle((iTileToDraw % iTileSetXCount) * iTileWidth,
(iTileToDraw / iTileSetXCount) * iTileHeight,
iTileWidth, iTileHeight);
spriteBatch.Draw(t2dTileSet, recDest, recSource, Color.White);
}
iTileToDraw = iMapObjects[y + iMapY, x + iMapX];
if (iTileToDraw > 11)
{
Rectangle recSource = new Rectangle((iTileToDraw % iTileSetXCount) * iTileWidth,
(iTileToDraw / iTileSetXCount) * iTileHeight,
iTileWidth, iTileHeight);
spriteBatch.Draw(t2dTileSet, recDest, recSource, Color.White);
}
// If in EDITOR MODE, draw the Red X Overlay over tiles that are not walkable
if (iProgramMode == 1)
{
if (iMapWalkable[y + iMapY, x + iMapX] == 1)
{
spriteBatch.Draw(t2dEditorImages, recDest, rectEditorXOverlay, Color.White);
}
}
}
}
// Draw the Player Avatar if we aren't in Editing Mode
if (iProgramMode == 0)
{
spriteBatch.Draw(t2dPlayerAvatar, new Rectangle(((iPlayerAvatarXOffset * iTileWidth) + iMapDisplayOffsetX),
((iPlayerAvaterYOffset * iTileHeight) + iMapDisplayOffsetY),
iTileWidth, iTileHeight), new Rectangle(0, 0, 48, 48), Color.White);
}
spriteBatch.Draw(t2dGameScreen, new Rectangle(0, 0, 640, 480), Color.White);
if (iProgramMode == 1)
{
DrawEditorInterface();
}
spriteBatch.End();
base.Draw(gameTime);
}
All this does is check to see if we are in edit mode. If we are, and the tile we are drawing is not walkable (ie, it's iMapWalkable entry is 1) we draw the Big Red X at this tile location.
Then next change is that we wrap a "if (iProgramMode==0)" check around the command to draw the avatar. This way, we will draw the avatar in Play mode, but not in Edit mode.
Finally, whe check to see if we are in Edit Mode and if so, call the DrawEditorInterface function, which means we've once again broken our ability to run the program until we implement that function, so here it is:
void DrawEditorInterface()
{
// Draw our Editor interface components. The spriteBatch object should be in a Begin mode with alpha
// blending BEFORE calling this routine.
// Draw the square that holds the current tile
Rectangle rectBase = rectEditorBaseButtonSRC;
Rectangle rectTrans = rectEditorTransButtonSRC;
Rectangle rectObject = rectEditorObjectButtonSRC;
if (iEditorLayerMode == 1) { rectBase.Offset(0,iEditorModeButtonPressedOffset); }
if (iEditorLayerMode == 2) { rectTrans.Offset(0,iEditorModeButtonPressedOffset); }
if (iEditorLayerMode == 3) { rectObject.Offset(0,iEditorModeButtonPressedOffset); }
spriteBatch.Draw(t2dEditorImages, new Rectangle(10, 370, 75, 75), new Rectangle(540, 315, 75, 75), Color.White);
spriteBatch.Draw(t2dEditorImages, rectEditorObjectButton, rectObject, Color.White);
spriteBatch.Draw(t2dEditorImages, rectEditorTransButton, rectTrans, Color.White);
spriteBatch.Draw(t2dEditorImages, rectEditorBaseButton, rectBase, Color.White);
int iTileToDraw = iEditorCurrentTile;
if (iTileToDraw == iTileAnimationStartFrame)
{
iTileToDraw += iTileAnimationFrame;
}
Rectangle recSource = new Rectangle((iTileToDraw % iTileSetXCount) * iTileWidth,
(iTileToDraw / iTileSetXCount) * iTileHeight,
iTileWidth, iTileHeight);
spriteBatch.Draw(t2dTileSet,
new Rectangle(iEditorCurrentTileX, iEditorCurrentTileY,iTileWidth, iTileHeight),
recSource,
Color.White);
Rectangle rectPlayField = new Rectangle(iMapDisplayOffsetX, iMapDisplayOffsetY,
(iMapDisplayWidth - 1) * 48, (iMapDisplayHeight - 1) * 48);
// if the mouse is currently in the "playfield", draw it's location
if ((msMouseState.X >= rectPlayField.Left) &
(msMouseState.X <= rectPlayField.Right) &
(msMouseState.Y >= rectPlayField.Top) &
(msMouseState.Y <= rectPlayField.Bottom))
{
int iOverlayX, iOverlayY;
iOverlayX = (((msMouseState.X - iMapDisplayOffsetX) / iTileWidth) * iTileWidth) + iMapDisplayOffsetX;
iOverlayY = (((msMouseState.Y - iMapDisplayOffsetY) / iTileHeight) * iTileHeight) + iMapDisplayOffsetY;
spriteBatch.Draw(t2dEditorImages,
new Rectangle(iOverlayX, iOverlayY, iTileWidth, iTileHeight),
rectEditorBoxOverlay,
Color.White);
}
}
We start out by creating copies of our rectangles for the source images for our three layer mode buttons. We do this because one of these will need to be modified by being pushed down 45 pixels to use the "pressed" version of the button. Since we don't want to modify our original rectangles, we'll use copies in this routine.
We check the iEditorLayerMode variable and push the appropriate rectangle down using the Rectangle class' "Offset" method by the iEditorModeButtonPressedOffset (45 pixels in our case).
After we have all three of our buttons ready (one pushed down to be selected), we draw them out with our spriteBatch object. Next, we get ready to draw the current tile in the lower left corner of the screen, taking into account our simple tile animation. This is all standard stuff we have done before, except that we are just drawing one tile instead of looping thru the map's display area.
The last thing we do here is check to see if the mouse is in the "play field". If it is, we draw the red box overlay on the tile the mouse is inside. This is helpful because the tiles can all seem to blend together after a while, and seeing exactly where you are going to draw makes things a bit easier.
Adding Some TextWhile not strictly required, this is a fun little side exercise that will demonstrate how to use a (very simple) bitmap font to draw some text. In the editor image, I added a string of characters in Courier New (a fixed width font) to the bottom. The order of these characters is VERY IMPORTANT to what we are doing. It starts with a space (ASCII character 32) and runs through all of the ASCII characters up to Z. I didn't do both upper and lower case because I didn't want to deal with wrapping around the edge of the image in this case, but it wouldn't be hard to set up. Even better would be to make a seperate surface for your font and make it wide enough that you wouldn't have to wrap it.
This is a lot easier than it might seem. Add the following function to your project:
void WriteText(string sTextOut, int x, int y, Color colorTint)
{
// Use our simple font rendering to write out a line of text. The spriteBatch object should be
// in a Begin mode with alpha blending BEFORE calling the routine.
// Information about our "Font" :)
int iFontX = 0;
int iFontY = 450;
int iFontHeight = 12;
int iFontWidth = 9;
int iFontAsciiStart = 32;
int iFontAsciiEnd = 90;
int iOutChar;
for (int i = 0; i < sTextOut.Length; i++)
{
iOutChar = (int)sTextOut[i];
if ((iOutChar >= iFontAsciiStart) & (iOutChar <= iFontAsciiEnd))
{
spriteBatch.Draw(t2dEditorImages,
new Rectangle(x + (iFontWidth * i), y, iFontWidth, iFontHeight),
new Rectangle(iFontX + ((iOutChar - iFontAsciiStart)*iFontWidth),
iFontY, iFontWidth, iFontHeight),
colorTint);
}
}
}
Here we define the location with our surface of the font and how big each character is. Then, we loop through the string that was passed to the routine and draw out each individual character. We do this by taking the character out of the string and converting it to it's ASCII code. The ASCII code for a Space character is 32, while capital A is 65. In our bitmap font, we have every character from 32 to 90 in our bitmap.
Then it is just a matter of pulling out the character just like we do a tile in the tileset. If we know the width, height, and character number (ASCII code mius 32) we can create a source rectangle, and knowing how many character's we've drawn already tells us how far over to create our destination rectangle.
In the Draw method, find the call to DrawEditorInterface() and update it to look like this:
if (iProgramMode == 1)
{
DrawEditorInterface();
WriteText("EDIT MODE", 550, 460, Color.White);
}
else
{
WriteText("PLAY MODE", 550, 460, Color.White);
}
This should add either "PLAY MODE" or "EDIT MODE" to the lower right corner of the screen. Since the text in our font is all white pixels, we can alter the color using the spriteBatch.Draw's tint parameter (the last one, which up until now we have always set to Color.White). Try changing Color.White in the calls above to some other color, and the text displayed on the screen should show in the selected color.
I should note that there are more advanced ways to draw text already available for XNA/GSE. The best one I've seen so far is the BMFontGen utility and BitmapFont class from the folks over at XNA Diaries. This utility/class can handle non-monospaced fonts, which our quick implementation here can't do.
Our quick implementation, however, shows the basics of what is going on with drawing text to the screen, and is pretty easy to set up, needing only a few lines of code to draw the text.


Until Next Time...Part 5 of the tutorial is going to take a bit of time to put together, so expect about two weeks before it is ready, but rest assured, it's on it's way!
|