XNA (or Monogame) Tutorial: Tile Maps

GOAL: To provide an example of how to manage a tile map (and editor)

Step 1) Start a new windows game project, and give it a name (I just called it "TileGame"). And set up some variables to keep track of kepresses, backGround images, and scrolling values. Also we need to add a couple of background layers to the game. One is the far background which should be totally opaque and the second one is a layer with transparent pixels. Right click on Content and add the existing background (.png or .dds) images. We will load these in the Load method. Example:

Code:
namespace TileGame
{    
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        const int SCREENWIDTH = 800, SCREENHEIGHT = 600;
        const bool FULLSCREEN = false;
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        KeyboardState kb, old_kb; bool ignore_escape = false;
        Texture2D background1, background2;
        Vector2 scroll; 

        //---------------------------
        #region C O N S T R U C T O R
        //---------------------------
        public Game1()
        {
            graphics = new GraphicsDeviceManager(this)
            {
                PreferredBackBufferWidth = SCREENWIDTH, PreferredBackBufferHeight = SCREENHEIGHT, IsFullScreen = FULLSCREEN,                
            };                 
            Content.RootDirectory = "Content";
        }
        #endregion

        
        //-------------
        #region I N I T
        //-------------
        protected override void Initialize()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);
            base.Initialize();
        }
        #endregion


        //---------------------
        #region L O A D 
        //---------------------
        protected override void LoadContent()
        {
            background1 = Content.Load<Texture2D>("far_background1");
            background2 = Content.Load<Texture2D>("mid_background1");
        }
        #endregion        
        protected override void UnloadContent() {   }


        //-----------------
        #region U P D A T E 
        //-----------------
        protected override void Update(GameTime gameTime)
        {
            kb = Keyboard.GetState();      
            if ((!ignore_escape) && (KeyPressed(Keys.Escape))) this.Exit(); //ignore escape prevents 
            if (kb.IsKeyUp(Keys.Escape)) ignore_escape = false; // (so if user pressed escape to exit game to menu, it won't automatically quit the game due to "carry-over")         

            if (KeyDown(Keys.Right)) scroll.X++;
            if (KeyDown(Keys.Left)) scroll.X--;
            if (KeyDown(Keys.Down)) scroll.Y++;
            if (KeyDown(Keys.Up)) scroll.Y--;            

            base.Update(gameTime);
        }
        bool KeyPressed(Keys key) { return kb.IsKeyDown(key) && old_kb.IsKeyUp(key); }
        bool KeyDown(Keys key) { return kb.IsKeyDown(key); }
        #endregion


        //-------------
        #region D R A W 
        //-------------
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // the backgrounds are scrolled by setting LinearWrap (so the UV coordinates wrap) and then we change the start coordinates for the rectangle source            
            spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.LinearWrap, null, null);
            spriteBatch.Draw(background1, Vector2.Zero, new Rectangle((int)scroll.X, (int)scroll.Y, background1.Width, background1.Height), Color.White);
            spriteBatch.Draw(background2, Vector2.Zero, new Rectangle((int)scroll.X*2, (int)scroll.Y*2, background2.Width, background2.Height), Color.White);
            spriteBatch.End();


            base.Draw(gameTime);
        }
        #endregion
    }
}

Made a couple helper functions to help with keypressed events. Later scrolling will need to be more precise so using Vector2.
The way this works is we tell the hardware (in Begin(...SamplerState.LinearWrap...)) that we want to wrap UV coordinates when the source rectangle sampling goes out of bounds of the texture image. That way we can set the start position of the rectangle window somewhere on the image that will sample out of bounds causing it to wrap and thus we can infinitely scroll in any direction.

If you run it you should have 2 layers scrolling nicely at different speeds when you press the arrow keys.
backgrounds
Step 2) Now we add our tile map image to the Content. Note that I use grid line guides set to 128 or 64 pixel divisions and Info window (which shows x,y coordinates under mouse) as a guide for figuring out the source Rectangles for each tile. In the Load method we will also load the tileSheet.
tile sheet
Note: We want to avoid texture switching as much as possible so try to use one tile sheet for tiles (otherwise it slows it down).
Note: Make sure to keep a pure original copy of the tileSheet to make changes to. Changing the dds or png version can result in gradual degradation of the quality of the art(and cause transparency artifacts). Once you are done with the original draft, you can export that to the png or dds in your Content.

Step 3) We need a font for the editor. Right click on Content. Add a new item - a spritefont.. Now add SpriteFont font; to the variables in game1.cs and also add font = Content.Load<SpriteFont>("SpriteFont1"); to Initialize, and now we add another class and call it Text.cs -- this will make drawing fonts a bit easier. Also we add a clickable font which returns true if clicked. Another thing we need is an Input method to get filenames(or any text) as input from the user. Here is the text.cs I made.

Code:
class Text
    {
        SpriteFont font;
        SpriteBatch spriteBatch;
        public Color color;
        public bool is_over_text=true; 
        int screenWidth, screenHeight;
        Vector2 center; //screen center (for centering text)
        int last_letter_x, last_letter_y; //last letter position

        // C O N S T R U C T O R
        public Text(SpriteFont _font, SpriteBatch _batch, int _screenWidth, int _screenHeight)
        {
            font = _font;
            spriteBatch = _batch;
            color = Color.DarkGreen;
            screenWidth = _screenWidth; screenHeight = _screenHeight;
            center = new Vector2(screenWidth / 2, screenHeight / 2);
        }

        // D R A W 
        public void Draw(int x, int y, string str) {
            spriteBatch.DrawString(font, str, new Vector2(x, y), color);
        }
        // D R A W  C E N T E R E D 
        public void DrawCenteredText(int x, int y, String s)
        {
            Vector2 cent = center;
            Vector2 offset = Vector2.Zero;
            offset = font.MeasureString(s);
            last_letter_x = (int)(center.X + offset.X / 2 + x - 6); last_letter_y = (int)(center.Y + offset.Y / 2 + y - 14); //used if needed for cursor (elsewhere)
            cent.X = center.X - offset.X / 2 + x; cent.Y = center.Y - offset.Y / 2 + y;
            spriteBatch.DrawString(font, s, cent, color);            
        }

        // D R A W  C L I C K A B L E
        // Detects mouse hover (and becomes brighter) and returns true if the text is clicked on...
        public bool DrawClickable(int x, int y, string str, bool mouseClick)
        {            
            Vector2 size = font.MeasureString(str); //<--determine size of string as image
            if ((Mouse.GetState().X > x) && (Mouse.GetState().X < x + size.X) && (Mouse.GetState().Y > y) && (Mouse.GetState().Y < y + size.Y))
            {
                is_over_text = true; //<--useful for canceling unwanted text clicks elsewhere
                spriteBatch.DrawString(font, str, new Vector2(x, y), Color.LightGreen); //make it brighter
                if (mouseClick) return true;
            }
            else { spriteBatch.DrawString(font, str, new Vector2(x, y), color); } //no hover - draw as regular color
            return false;
        }



        //-------------------------------------------
        // I N P U T  (polls for string input and returns true when the user is done entering input)
        private float timer; //cursor timing   
        private KeyboardState currentKeyboardState, lastKeyboardState;
        //used for keyboard input thing: 
        private Keys[] keysToCheck = new Keys[] { Keys.A, Keys.B, Keys.C, Keys.D, Keys.E, Keys.F, Keys.G, Keys.H, Keys.I, Keys.J, Keys.K, Keys.L, Keys.M, Keys.N, Keys.O, Keys.P, Keys.Q, Keys.R, Keys.S, Keys.T, Keys.U, Keys.V, Keys.W, Keys.X, Keys.Y, Keys.Z, Keys.D0, Keys.D1, Keys.D2, Keys.D3, Keys.D4, Keys.D5, Keys.D6, Keys.D7, Keys.D8, Keys.D9, Keys.Space, Keys.Back, Keys.OemPeriod, Keys.Decimal, Keys.OemMinus, Keys.Divide, Keys.OemQuestion };
        private int[] keyTime = new int[] { 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
        // position, current input, size restrictions(like for file saving)
        public bool Input(int x, int y, ref String input_str, int min_length, int max_length, GameTime gameTime, bool first_letter_restrictions, bool center_input)
        {
            bool shift = false;
            timer += (float)gameTime.ElapsedGameTime.TotalMilliseconds;    if (timer > 200f) timer = 0;
            currentKeyboardState = Keyboard.GetState();                    if (currentKeyboardState.IsKeyDown(Keys.Escape)) return false;
            if (currentKeyboardState.IsKeyDown(Keys.Enter) && lastKeyboardState.IsKeyUp(Keys.Enter)) if (input_str.Length >= min_length) return true;
            if (currentKeyboardState.IsKeyDown(Keys.LeftShift) || currentKeyboardState.IsKeyDown(Keys.RightShift)) shift = true;
            if (input_str.Length > 0)
                if (center_input) { DrawCenteredText(x, y, input_str); if (timer < 100f) Draw(last_letter_x, last_letter_y, "_"); }  else Draw(x, y, input_str);
            else { DrawCenteredText(x, y, " "); if (timer < 100f) Draw(last_letter_x, last_letter_y, "_"); }   
            int a = -1;
            foreach (Keys key in keysToCheck) {
                a++;
                if (lastKeyboardState.IsKeyDown(key) && currentKeyboardState.IsKeyDown(key)) {
                    keyTime[a]++; //for held key repeats and delays
                }
                else keyTime[a] = 0;
                bool do_it = false;
                if (keyTime[a] > 15) { do_it = true; keyTime[a] = 7; }
                if (lastKeyboardState.IsKeyUp(key) && currentKeyboardState.IsKeyDown(key)) do_it = true;
                if (do_it)
                {
                    String newChar = "";
                    if (input_str.Length >= max_length && key != Keys.Back) continue; //don't do anything if we reached size limit (unless is Back key)                    
                    switch (key)
                    {
                        case Keys.A: newChar += "a"; break; case Keys.B: newChar += "b"; break; case Keys.C: newChar += "c"; break; case Keys.D: newChar += "d"; break;
                        case Keys.E: newChar += "e"; break; case Keys.F: newChar += "f"; break; case Keys.G: newChar += "g"; break; case Keys.H: newChar += "h"; break;
                        case Keys.I: newChar += "i"; break; case Keys.J: newChar += "j"; break; case Keys.K: newChar += "k"; break; case Keys.L: newChar += "l"; break;
                        case Keys.M: newChar += "m"; break; case Keys.N: newChar += "n"; break; case Keys.O: newChar += "o"; break; case Keys.P: newChar += "p"; break;
                        case Keys.Q: newChar += "q"; break; case Keys.R: newChar += "r"; break; case Keys.S: newChar += "s"; break; case Keys.T: newChar += "t"; break;
                        case Keys.U: newChar += "u"; break; case Keys.V: newChar += "v"; break; case Keys.W: newChar += "w"; break; case Keys.X: newChar += "x"; break;
                        case Keys.Y: newChar += "y"; break; case Keys.Z: newChar += "z"; break; case Keys.D0: newChar += "0"; break; case Keys.D1: newChar += "1"; break;
                        case Keys.D2: newChar += "2"; break; case Keys.D3: newChar += "3"; break; case Keys.D4: newChar += "4"; break; case Keys.D5: newChar += "5"; break;
                        case Keys.D6: newChar += "6"; break; case Keys.D7: newChar += "7"; break; case Keys.D8: newChar += "8"; break; case Keys.D9: newChar += "9"; break;
                        case Keys.Space: if (!((first_letter_restrictions) && (input_str.Length < 1))) newChar += " "; break;
                        case Keys.OemPeriod: if (!((first_letter_restrictions) && (input_str.Length < 1))) newChar += "."; break;
                        case Keys.Decimal: if (!((first_letter_restrictions) && (input_str.Length < 1))) newChar += "."; break;
                        case Keys.OemMinus: if (!((first_letter_restrictions) && (input_str.Length < 1))) { if (!shift) newChar += "-"; else newChar += "_"; } break;
                        case Keys.Divide: if (!((first_letter_restrictions) && (input_str.Length < 1))) newChar += "/"; break;
                        case Keys.OemQuestion: if (!((first_letter_restrictions) && (input_str.Length < 1))) { if (!shift) newChar += "/"; else newChar += "?"; } break;
                        case Keys.Back: if (input_str.Length != 0) input_str = input_str.Remove(input_str.Length - 1, 1); continue;
                    }
                    if (shift) newChar = newChar.ToUpper();
                    input_str += newChar;
                    break;
                }
            }
            lastKeyboardState = currentKeyboardState;
            return false;
        }//Input        
    }
    // G E T  L A S T  (helpful for seeing if there's a file extension already typed in by the user 
    // (checks a substring of whatever length at the end of the string)
    public static class StringExtension {
        public static string GetLast(this String s, int tail_length) {
            if (tail_length >= s.Length) return s;
            return s.Substring(s.Length - tail_length);
        }
    }

Note: StringExtension, GetLast, was added so I can easily check to see if the last few characters in a string are something (ie: ".txt" or ".lev" or whatever)
Input was kind of hard to figure out but you will see how to use it later.

Step 4) Back in Game1.cs we need to add the variable Text text; //text class and also create it in Initialize:
text = new Text(font, spriteBatch,SCREENWIDTH,SCREENHEIGHT);
It is now possible to draw fonts (either with spritebatch.DrawString or text(x,y,"bla bla bla")...)

Step 5) Lets start building a map system. Rightclick on your project's name(ie: TileGame), and add a folder called "Map Stuff" and then right click on the folder and add a class to it called TileSheetType.cs
This holds all the info about each image on the tile sheet. Here are some useful enums and fields for this class:

Code:
    enum CollideFlag { empty, solid, platform, slope } // different types of collision response
    enum TileFlag    { none, liquid, damage } //could add stuff like ladder, movable, switch, etc... 
    
    class TileSheetType
    {
        public Rectangle rect;    //source rectangle
        public Vector2   origin;  //rotation point
        public CollideFlag collideFlag; //default type of collision detection
        public TileFlag tileFlag; //tile has special properties? 
        public Vector2 offset;    //offset of the image from any tile its associated with
        public int tiles_wide, tiles_high;      //how many tiles does this "tile" occupy? 
        public int slope_left_v, slope_right_v; //(for slopes: left and right vertical positions of the slope)
        public int frameCount; // >0 if is an animated strip (source rect moves right a width each frame for frameCount times then resets)
        public int timePerFrame = 160; //set timing speed of animated tiles
        public int frame = 0;         //current frame
        private int timeEllapsed = 0; //tracks time (so we know if we should update the frame)

Note: class is for holding sheet data - doesn't represent tiles of the map... so it holds info that is the same for any tile (including looped animations).

Step 6) We'll add a constructor and an Update(for animated tiles).

Code:
// C O N S T R U C T O R
public TileSheetType(Rectangle _rect, int tiles_w, int tiles_h, CollideFlag _collideFlag, Vector2 _offset, TileFlag _tileFlag, Vector2 _origin, int l_slope_v, int r_slope_v, int num_frames)
{
    rect = _rect;
    collideFlag = _collideFlag;
    offset = _offset;
    tileFlag = _tileFlag;            
    origin = _origin;
    tiles_wide = tiles_w; tiles_high = tiles_h;
    slope_left_v = l_slope_v; slope_right_v = r_slope_v;
    frameCount = num_frames;
}

// U P D A T E  ( animation stuff )
public void Update(GameTime gameTime) 
{
    if (frameCount <= 0) return; //no animation... return
    timeEllapsed += gameTime.ElapsedGameTime.Milliseconds; 
    if (timeEllapsed > timePerFrame) //if enough time has passed, update the animation frame:
    {
        timeEllapsed -= timePerFrame;
        rect.X += rect.Width;        //update the source Rectangle in the animation strip
        frame++;
        if (frame >= frameCount) { frame = 0; rect.X -= (rect.Width * frameCount); } //reset animation at last frame
    }
}

Note: Update returns if no frames to animate. It would probably be better to check this from outside Update before calling it.
The way this works is if it is animated, it updates the position of the source rectangle on the tile sheet and resets at last frame.

Step 7) We'll set up the tile sheet in a map class based on the level (map ID) passed to it. Right click on the Map Stuff folder and add a Map.cs class. We'll start by setting some public constants (may be needed by editor), the Texture2D sheetTex, and an array of sheet objects (TileSheetTypes) which we will fill with data about each image.
Code:
class Map
{
    public const int MAP_WIDTH=200, MAP_HEIGHT=100; //<--NOTE: if this changes, you won't be able to load a binary copy of the file without conversion (.txt file yes) 
    public const float xoff = Game1.SCREENWIDTH / 2 - 32, yoff = Game1.SCREENHEIGHT / 2 - 32; //offset to middle of screen (-half_tile)
    GraphicsDevice gpu;     
    ContentManager Content;
    SpriteBatch spriteBatch;

    public Texture2D sheetTex;     // sprite sheet of tiles (of some level)
    public TileSheetType[] sheet;  // sheet data (information about each image on the sprite sheet)
    public int num_sheet_defs;     // number of sheet definitions
      
    public int tile_x, tile_y;    //this keeps track of the screen's tile coordinates (approximate center tile)        
    public Vector2 tile_offset;   //keeps track of the offset of all visible tiles (ie: range of 0-64 for 64x64 tiles) [for scrolling animation]
    public Color tile_color = Color.White; //default tile color
                

We set constants for the map size. It is important that you choose a size that isn't too big or too small for future purposes because it may be hard to change later. I find 200x100 to be very generous (and you can always load new areas if you want smaller).
We need access to Content (to load sheetTex from content. ie: "TileSheet1")
sheet - holds all the tile sheet informations
tile_x, tile_y - keep track of the center tile on the screen (everything will be drawn around that tile [ie: -8 to +8 tiles]
tile_offset - is used to scroll the map up to 64 pixels(then resets and starts at a new tile index)

Step 8) We need to pass a few things from the main game1. We'll allocate 400 possible tile images for a sheet using the TileSheetType we made (should be way more than enought)..

Code:
//---------------------------
        #region C O N S T R U C T O R
        //---------------------------
        public Map(GraphicsDevice graphicsDevice, ContentManager content, SpriteBatch batch)
        {            
            gpu = graphicsDevice; //we'll need this stuff... 
            Content = content;
            spriteBatch = batch;
            sheet   = new TileSheetType[400]; //this should be enough tile types
            
            tile_offset = Vector2.Zero; //tile offset animation for scrolling
            tile_x = 100; tile_y = 50;  //just starting somewhere near the middle of the map...(level load should change this)
        }
        #endregion

Step 9) We need a method to add sheet data for each item on the sheet. We'll give it the top-left pixel and bottom-right pixel of an image on the sheet and how many 64x64 tiles wide or high it should occupy (like for collision detection), and where the image that top-left collision cornder is(should be aligned to the grid if you made the sheet right). Also we need to specify what type of collider it is (if at all). We'll also add some optional parameters such as slope and any other flag of interest. The default offset of the image is relative to the collision corner and calculated here..

Code:
//-----------------------
#region S H E E T  A D D
//-----------------------
// Prepares sheet data for the sheet's constructor. 
// Calculates rectangle from top-left_to_bottom-right pixel coords on sheet -- also calculates image offset relative to top-left collision corner pixel coordinate on sheet
public void SheetAdd(int x1, int y1, int x2, int y2, int w, int h, CollideFlag collideType, int top_left_corner_x, int top_left_corner_y, TileFlag? flag, Vector2? _origin,
    int l_slope_v=0, int r_slope_v=0)
{
    TileFlag f = TileFlag.none; if (flag.HasValue) f = flag.Value; 
    Rectangle r = new Rectangle(x1, y1, (x2 - x1 + 1), (y2 - y1 + 1));      //example: (0,0,15,15) ...width is actually 16 pixels
    Vector2 offs = new Vector2(x1-top_left_corner_x, y1-top_left_corner_y); //offset of tile image relative to tile it occupies
    Vector2 origin; if (_origin.HasValue) origin = _origin.Value; else origin = new Vector2(r.Width / 2, r.Height / 2); //rotation pivot
    sheet[num_sheet_defs] = new TileSheetType(r, w, h, collideType, offs, f, origin, l_slope_v, r_slope_v,0); num_sheet_defs++; //increment num sheets after construct
}        
#endregion

Step 10) Now we load the texture map of the TileSheet1 (based on level) and set the sheet information using the SheetAdd method we just made:

Code:
//-------------------------
#region L O A D 
//-------------------------
// Loads sheet image and prepares data for the sheet.
// Used Info to get top-left pixel and bottom-right pixel of each image in graphics editor
// Note: top_left_corner is where you'd expect the solid(collision) part of the tile to be
public void Load(int level)
{
    num_sheet_defs=0;
    switch (level)
    {
        case 1:                    
            sheetTex = Content.Load<Texture2D>("TileSheet1");
            // assumes you are providing the top-left pixel of image and bottom-right pixel of image
            SheetAdd(0,  11,  255, 110, 4,1, CollideFlag.solid,   0, 32, null, null); //Grass1  0
            SheetAdd(256, 11, 511, 110, 4,1, CollideFlag.solid, 256, 32, null, null); //Grass2  1
            SheetAdd(512, 11, 767, 110, 4,1, CollideFlag.solid, 512, 32, null, null); //Grass3  2
            SheetAdd(768, 11,1023, 110, 4,1, CollideFlag.solid, 768, 32, null, null); //Grass4  3
            SheetAdd(0,  127, 127, 255, 2,2, CollideFlag.solid,  0, 128, null, null); //Wall1   4
            SheetAdd(128,128, 255, 255, 2,2, CollideFlag.solid, 128,128, null, null); //Wall2   5
            SheetAdd(272,128, 367, 255, 1,1, CollideFlag.empty, 288, 192,null, null); //Door    6
            SheetAdd(384,128, 511, 255, 2,1, CollideFlag.solid, 384, 192,TileFlag.damage, null); //Spikes 7
            SheetAdd(544,160, 607, 223, 1,1, CollideFlag.empty, 544, 160,null, null); //PinkFlower 8
            SheetAdd(644,131, 760, 249, 1,1, CollideFlag.platform, 672, 152,null, null); //Balloon 9
            SheetAdd(768,128, 895, 255, 1,1, CollideFlag.empty, 800, 176,null, null); //Bush    10
            SheetAdd( 16,256, 111, 383, 1,2, CollideFlag.empty,  32, 256,null, null); //Ladder  11
            SheetAdd(128,256, 255, 319, 1,1, CollideFlag.empty, 128, 256,null, null); //Linear Shade 12
            SheetAdd(128,320, 191, 383, 1,1, CollideFlag.empty, 128, 320,null, null); //Radial Shade 13 
            SheetAdd(192,320, 255, 383, 1,1, CollideFlag.empty, 192, 320,null, null); //Radial Lit 14
            SheetAdd(272,272, 367, 367, 1,1, CollideFlag.empty, 288, 288,null, null); //Window  15
            SheetAdd(896,256,1023, 383, 2,1, CollideFlag.platform,896,256,null, null); //Roof   16
            SheetAdd( 0 ,384,  63, 447, 1,1, CollideFlag.solid,   0, 384,null, null); //Dirt1   17 
            SheetAdd(64 ,384, 127, 447, 1,1, CollideFlag.solid,  64, 384,null, null); //Dirt2   18
            SheetAdd(128,384, 191, 447, 1,1, CollideFlag.solid, 128, 384,null, null); //Dirt3   19 
            SheetAdd(192,384, 255, 447, 1,1, CollideFlag.solid, 192, 384,null, null); //DirtGrass1 20
            SheetAdd(256,384, 319, 447, 1,1, CollideFlag.solid, 256, 384,null, null); //DirtGrass2 21
            SheetAdd(320,384, 383, 447, 1,1, CollideFlag.solid, 320, 384,null, null); //CliffGrassLeft 22
            SheetAdd(384,384, 447, 447, 1,1, CollideFlag.solid, 384, 384,null, null); //CliffGrassRight 23
            SheetAdd(448,384, 511, 447, 1,1, CollideFlag.solid, 448, 384,null, null); //CornerTrimLeft 24 
            SheetAdd(512,384, 575, 447, 1,1, CollideFlag.solid, 512, 384,null, null); //CornerTrimRight 25
            SheetAdd(576,384, 639, 447, 1,1, CollideFlag.solid, 576, 384,null, null); //CliffLeft 26
            SheetAdd(640,384, 703, 447, 1,1, CollideFlag.solid, 640, 384,null, null); //CliffRight 27
            SheetAdd(704,384, 767, 447, 1,1, CollideFlag.solid, 704, 384,null, null); //GreenRock 28
            SheetAdd(0  ,448, 63 , 511, 1,1, CollideFlag.solid, 0  , 448,null, null); //MossCliffLeft  29
            SheetAdd(64 ,448, 127, 511, 1,1, CollideFlag.solid, 64 , 448,null, null); //MossCliffRight 30
            SheetAdd(64 ,448, 127, 511, 1,1, CollideFlag.solid, 64 , 448,null, null); //MossCliffRight 31
            SheetAdd(128,448, 191, 511, 1,1, CollideFlag.empty, 128, 448,null, null); //UnderL   32
            SheetAdd(192,448, 255, 511, 1,1, CollideFlag.empty, 192, 448,null, null); //UnderR   33
            SheetAdd(256,448, 319, 511, 1,1, CollideFlag.solid, 256, 448,null, null); //Under1   34
            SheetAdd(320,448, 383, 511, 1,1, CollideFlag.solid, 320, 448,null, null); //Under2   35
            SheetAdd(384,448, 447, 511, 1,1, CollideFlag.slope, 384, 448,null, null,64,32); //Slope_30_R_1 (64 down the left, 32 down the right) 36
            SheetAdd(448,448, 511, 511, 1,1, CollideFlag.slope, 448, 448,null, null,64,32); //Slope_30_L_1 (32 down the left, 64 down the right) 37
            SheetAdd(512,448, 575, 511, 1,1, CollideFlag.slope, 512, 448,null, null,32, 0); //Slope_30_R_2 (32 down the left, 0  down the right) 38
            SheetAdd(576,448, 639, 511, 1,1, CollideFlag.slope, 576, 448,null, null, 0,32); //Slope_30_L_2 (0  down the left, 32 down the right) 39
            SheetAdd(640,448, 703, 511, 1,1, CollideFlag.slope, 640, 448,null, null,64, 0); //Slope_45_R_1 (64 down the left, 0  down the right) 40
            SheetAdd(704,448, 767, 511, 1,1, CollideFlag.slope, 704, 448,null, null, 0,64); //Slope_45_L_1 (0  down the left, 64 down the right) 41
            SheetAdd(0  ,512,  63, 575, 1,1, CollideFlag.slope,   0, 512,null, null,64, 0); //Slope_45_R_2 (64 down the left, 0  down the right) with flowers 42
            SheetAdd(64 ,512, 127, 575, 1,1, CollideFlag.slope,  64, 512,null, null, 0,64); //Slope_45_L_2 (0  down the left, 64 down the right) with flowers 43
            SheetAdd(128,544, 191, 575, 1,1, CollideFlag.empty, 128, 512,null, null); //RedFlowers  44
            SheetAdd(192,544, 255, 575, 1,1, CollideFlag.empty, 192, 512,null, null); //PinkFlowers 45
            SheetAdd(256,513, 319, 559, 1,1, CollideFlag.empty, 256, 512,null, null); //LeftMoss1   46
            SheetAdd(320,513, 556, 559, 1,1, CollideFlag.empty, 320, 512,null, null); //RightMoss1  48
            SheetAdd(384,512, 447, 559, 1,1, CollideFlag.empty, 384, 512,null, null); //Moss2R      49
            SheetAdd(448,512, 511, 559, 1,1, CollideFlag.empty, 448, 512,null, null); //Moss2L      50
            SheetAdd(2,  578, 254, 895, 1,2, CollideFlag.empty,  96, 768,null, null); //Tree1       51
            SheetAdd(362,615, 529, 788, 2,2, CollideFlag.solid, 384, 640,null, null); //CrystalBlock 52
            SheetAdd(256,832, 319, 894, 1,1, CollideFlag.empty,256,832, TileFlag.liquid, null); sheet[num_sheet_defs-1].frameCount=8; //animated strip: Water1  53
            SheetAdd(256,896, 319, 959, 1,1, CollideFlag.empty,256,896, TileFlag.liquid, null); sheet[num_sheet_defs-1].frameCount=8; //animated strip: WaterSparkle1 54                                
            break;
    }
}
#endregion

Note: This can take a long time... but for new areas you can just place new graphics in the same locations on the sheet (and just set it to modify some of the sheet elements that act different in the new area)

Step 11) Right click Map Stuff and add a class called TileType.cs -- this is that actual information of any tile on the game map -- it tracks an index to the sheet so we can fetch more information about it (instead of storing tons of information in every single tile on the map). I will add a second index for adding decals on top of stuff(if want) and seperate offsets for customizing positions of images. Also scale, rotation and color tint might be something we will edit later. Just under the TileType class, let's add a CollisionTileType so we can customize the collision aspect of any tile.

Code:
[Serializable] //make it so we can save this type of stuff to a binary file later
class TileType
{
    //Common tile attributes should be kept track of by the tile sheet (use index to find)
    public int index;  //tile sheet index                
    public int index2; //decal
        
    //information specific to an individual tile.
    public Vector2 offset, offset2; //(ofsset 2 is for decal)
    public float scale;
    public float rot;
    public Color tint;
}
[Serializable] 
class CollisionTileType
{
    public int index;               //in case we need to know something more specific about this collision
    public CollideFlag collideFlag; //over-ride of default (so we can edit the collision info on the map)                
}
[Serializable]
class StartData
{
    public int x, y;  //starting tile position of player (will use later)
}

Step 12) Now we need to add some fields to the map class. We'll use a 2-dimensional array of tiles, and same for collision tiles. Also we'll keep track of starting data for a level (like player position). Add these to variables:

Code:
public TileType[,] tiles;      // the actual tiles (for some level)
public CollisionTileType[,] CDtiles; //collision detection tile data   
public StartData startData;   //starting position of player (will use later)
And also we need to create them in the constructor like so:
Code:
//---------------------------
#region C O N S T R U C T O R
//---------------------------
public Map(
Code:
//---------------------------
#region C O N S T R U C T O R
//---------------------------
public Map(GraphicsDevice graphicsDevice, ContentManager content, SpriteBatch batch)
{            
    gpu = graphicsDevice; //we'll need this stuff... 
    Content = content;
    spriteBatch = batch;
    sheet   = new TileSheetType[400]; //this should be enough tile types
    tiles   = new TileType[MAP_WIDTH, MAP_HEIGHT];
    CDtiles = new CollisionTileType[MAP_WIDTH, MAP_HEIGHT];
            
    tile_offset = Vector2.Zero; //tile offset animation for scrolling
    tile_x = 100; tile_y = 50;  //just starting somewhere near the middle of the map...(level load should change this)
    startData = new StartData(); startData.x = tile_x; startData.y = tile_y; // Load will later correct this for positioning starting point of player. 
}
#endregion

Step 13) We need a series of methods for editing tiles. Add, Delete, etc... In each one we must be sure the tile is not null and delete a tile(or tiles) if we are replacing one using add. To avoid redraw, the main tile will only occupy the top-left tile of its placement. The collision tiles however need to fill in their information(tiles_wide, tiles_high). The default offset is extracted from the sheet.
Note we will set index2 (for decal) to -1 initially, to indicate to the map system that it us empty at this time. Deleting tiles will be a matter of setting them to null (except also need to clear out any collision stuff not being used by tile now too). Here are some useful methods:
Code:
#region E D I T   T I L E S (I would use these with Editor - for play_mode you may want to modify some fields directly)
//-------------------------
public void AddTile(int x, int y, int index) {
    if (tiles[x, y] != null) DeleteTile(x, y); //make sure its empty first
    tiles[x, y] = new TileType();              
    tiles[x, y].index = index; tiles[x, y].index2 = -1; // set the sheet index (which tile is it?) [index2 to -1 cuz no decal right now]
    tiles[x, y].offset = sheet[index].offset;           // default offset (aligns image properly around the tile it belongs to)
    tiles[x, y].rot = 0; tiles[x, y].scale = 1f; ; tiles[x, y].tint = Color.White; 
    for (int y1 = y; y1 < y+sheet[index].tiles_high; y1++) {
        for (int x1 = x; x1 < x+sheet[index].tiles_wide; x1++) {
            AddCollisionTile(x1, y1, index);            // add all the appropriate collision stuff to the CDtiles map (collision map)
        }
    }
}

public void AddDecal(int x, int y, int index)  // add a decal image over-top of an existing tile (must use top-left tile. Move with offset2)
{
    if (tiles[x, y] == null) return;
    tiles[x, y].index2 = index;
    tiles[x, y].offset2 = sheet[index].offset; // can set later to position it (Right-Shift + arrows)
}

public void DeleteDecal(int x, int y)         
{
    if (tiles[x, y] == null) return;
    tiles[x, y].index2 = -1; tiles[x, y].offset2 = Vector2.Zero; // set to -1 to indicate its not in use (reset offset2 in case using again later)
}
                
public void DeleteTile(int x, int y) {         // delete a tile from map
    if (tiles[x, y] == null) return;
    for (int y1 = y; y1 < y+sheet[tiles[x,y].index].tiles_high; y1++) { 
        for (int x1 = x; x1 < x+sheet[tiles[x,y].index].tiles_wide; x1++) {
            DeleteCollisionTile(x1, y1);       // remove all the collision tile stuff that belongs to this tile
        }
    }
    tiles[x, y] = null;
}

public void AddCollisionTile(int x, int y, int index) {
    if (CDtiles[x,y]==null) CDtiles[x, y] = new CollisionTileType(); 
    CDtiles[x, y].index = index;  // knowing the index could be handy for deciding on a kind of response to the collision 
    CDtiles[x, y].collideFlag = sheet[index].collideFlag; // set to default collision type of selected sheet item
}

public void DeleteCollisionTile(int x, int y) {            
    CDtiles[x, y] = null;
}

public void Scale(int x, int y, float scale) {
    if (tiles[x, y] != null) tiles[x, y].scale = scale;
}

public void Tint(int x, int y, Color color) {
    if (tiles[x, y] != null) tiles[x, y].tint = color;
}

public void Rotate(int x, int y, float rotate) {
    if (tiles[x, y] != null) tiles[x, y].rot = rotate;
}

public void Offset(int x, int y, int offx, int offy) { //set offset position of image relative to its tile (Left-Shift + arrows)
    if (tiles[x, y] != null) tiles[x, y].offset += new Vector2(offx, offy);
}
public void Offset2(int x, int y, int offx, int offy) {//set offset position of decal relative to its tile (Right-Shift + arrows)            
    if (tiles[x, y] != null) tiles[x, y].offset2 += new Vector2(offx, offy);
}

public void Reset(int x, int y) { //not using yet, but could come in handy... 
    if (tiles[x, y] == null) return;
    int index = tiles[x,y].index;   tiles[x, y].offset = sheet[index].offset; 
    tiles[x, y].rot = 0;   tiles[x, y].scale = 1f;   tiles[x, y].tint = Color.White;
}

//creates a safety barrier of solid blocks around the perimeter or the map (helps prevent crash if player tries to fall out of map bounds)
public void CreateSafetyBarrier(int i) {
    int n = 0;
    do {  AddTile(0, n, i);   AddTile(MAP_WIDTH-1, n, i);   n++;  } while (n < MAP_HEIGHT);
    n = 1;
    do {  AddTile(n, 0, i);   AddTile(n, MAP_HEIGHT-1, i);  n++;  } while (n < MAP_WIDTH-2);
}
#endregion
Step 14) Let's use the barrier making method by adding it into the Load section for level 1 like so:
Code:
    SheetAdd(256,896, 319, 959, 1,1, CollideFlag.empty,256,896, TileFlag.liquid, null); sheet[num_sheet_defs-1].frameCount=8; //animated strip: WaterSparkle1 54                    
    CreateSafetyBarrier(17); //<<---added: creates a barrier to help prevent illegal memory access if player falls off map            
    break;

Step 15) We need a way to keep track of scrolling the tile map. THIS is key to making the map move while only processing tiles within a certain range. So the tile map offset slowly moves to the left until it has moved 64 pixels... then it jumps back to where it started but changes the index so it looks like it is just smoothly scrolling by. Tiles too far off the screen will never be drawn.

Code:
//-----------------
#region U P D A T E
//-----------------
public void Update(GameTime gameTime, ref Vector2 scroll_speed)
{
    tile_offset -= scroll_speed*4; //tiles are closer so they move 4 times faster
    if (tile_offset.X < 0)   { tile_offset.X = 63.0f; tile_x++; } //animate the scrolling of the tiles by moving them 64 pixels(then reset and change the index)
    if (tile_offset.X >= 64) { tile_offset.X = 0.0f;  tile_x--; }
    if (tile_offset.Y < 0)   { tile_offset.Y = 63.0f; tile_y++; }
    if (tile_offset.Y >= 64) { tile_offset.Y = 0.0f;  tile_y--; }
    int i = 0; do { sheet[i].Update(gameTime); i++; } while (i < num_sheet_defs); // call to update the animated sheet stuff (returns if not animated) - could optimize to avoid call
}
#endregion

Note: You may have noticed that this is also where we call to update the sheet animations (like bubbling water or something)

Step 16) So how do we draw the visible part of the map?
We are saying tile_x,tile_y represent the map's[x,y] coordinate (which is at the center of the screen) [in map coordinates - not screen coordinates]
First we need to calculate the ideal position of a 64x64 tile in the center the screen... we actually did this already with a constant at the top of our map class:
public const float xoff = Game1.SCREENWIDTH / 2 - 32, yoff = Game1.SCREENHEIGHT / 2 - 32;

So at the beginning of the Draw, we'll memorize the center + tile_offset (used to offset-scroll 64 pixels) as tile_xoff, tile_yoff
Then we'll loop throught the tiles starting at (-8 to +8)-vertically and (-10 to +10)-horizontally. We add the index offsets using tile_x, tile_y to get the index of the tile being scanned (tx,ty)..
Now we can get look up the index of the tile (so we know what to draw) and we can the screen position of the tile like so:

i=tiles[tx,ty].index; //what is the sheet index of the current tile?
pos = new Vector2(a * 64.0f+tile_xoff, b * 64.0f+tile_yoff);

Also when we draw we'll add the tile's own custom offset to this screen position:

spriteBatch.Draw(sheetTex, pos+tiles[tx,ty].offset, sheet[i].rect, tile_color);

So here is what the draw method should look like then:

Code:
//-------------
#region D R A W
//-------------
public void Draw()
{            
    int tx, ty;  //tile indices
    float tile_xoff = tile_offset.X+xoff, tile_yoff = tile_offset.Y+yoff; //remember the tile offset (relative to screen center)
    Vector2 pos;           //used in finding screen position of a tile
    int a, b = - 8, i, i2; //(starting 8 tiles above screen center to 8 tiles below)
    do
    {
        ty = tile_y + b;   //get ty index of tile (tile_y is center)
        if ((ty < 0) || (ty >= MAP_HEIGHT)) { b++; continue; } //don't draw if illegal access... 
        a = -10; //(starting 10 tiles left of screen center to 10 tiles right)
        do
        {
            tx = tile_x + a;  //get tx index of tile (tile_x is center)
            if ((tx < 0) || (tx >= MAP_WIDTH)) { a++; continue; } //avoid illegal memory access...
            if (tiles[tx, ty] == null) { a++; continue; } //..." " ...
                    
            i=tiles[tx,ty].index; //what is the sheet index of the current tile? 
            pos = new Vector2(a * 64.0f+tile_xoff, b * 64.0f+tile_yoff); //calculate the screen position of this tile
            spriteBatch.Draw(sheetTex, pos+tiles[tx,ty].offset, sheet[i].rect, tile_color); //(note: adding the image offset from the tile position)
                    
            i2 = tiles[tx, ty].index2; // (decal index)
            if (i2 > -1) {             // if active... 
                pos = new Vector2(a * 64.0f + tile_xoff, b * 64.0f + tile_yoff); 
                spriteBatch.Draw(sheetTex, pos + tiles[tx, ty].offset2, sheet[i2].rect, tile_color);
            }
            a++;
        } while (a < 10);
        b++;
    } while (b < 8);
}
#endregion

Note: we also skip empty tiles to avoid unnecessary calculations.
Step 17) OK. So next we will need to create an editor but before we do that we may need some conversion methods in the map class for converting between screen position and world tile and vice-versa. Potentially later you could also add stuff for getting true world position and such (for characters)

Code:
//-----------------------------------------------------------------
#region C O N V E R S I O N S ( ie: map coords to screen position )
//-----------------------------------------------------------------
// I figured out these conversions on paper. These could probably be optimized... 
// ie: what is tiles[a,b] as a position on the screen right now? 
public Vector2 WorldTileToScreenPos(int a, int b)
{
    int x1 = (a - tile_x) * 64, y1 = (b - tile_y) * 64; //distance in tiles from center of screen tile (then * 64 pixels)
    Vector2 pos = new Vector2(tile_offset.X + xoff, tile_offset.Y + yoff); //offset of center tile from center of screen (is really the position of tile_x, tile_y[middle tile])
    pos.X += x1; pos.Y += y1; //add the relative tile position to the center screen offset
    return pos;
}
// What is the a,b for tiles[a,b] if we have a position on the screen: 
public void ScreenPosToWorldTile(Vector2 pos, ref int a, ref int b) 
{
    pos.X -= (tile_offset.X + xoff); pos.Y -= (tile_offset.Y + yoff);
    a = (int)(pos.X * 0.015625f + tile_x); b = (int)(pos.Y * 0.015625f + tile_y); // instead of /64 I used *1/64 = 0.015625

}
#endregion

Step 18) We'll add some editing helper images to the project - right click Content and add something like editor_stuff.png IE:

edit_example (doesn't have to be this ugly)

Step 19) Right click on your project(ie: TileGame) and add a class called Editor.cs

We'll need variables for mouse data, string polling (like getting input of a filename using text.Input), and selected to represent the sheet part we want to paint onto the tile map (-1 will mean no selection or -2 will mean set player position -- all other values 0,1,2,3.... will be an index to the sheet)

Code:
class Editor
{
    Map map;  
    SpriteBatch spriteBatch;
    Text text;
    Texture2D editor_images;  // helper images for editor
    MouseState mos, old_mos;  
    Vector2 mosV;             // Vector2 version of mouse coords
    int selected = -1;        // current sheet index selected (what type of tile)
    int mtx, mty;             //mouse coordinates in tile space
    bool mouseClick = false, click=false; //helpful to prevent clicks from effecting inputs that shouldn't happen
    String inputStr="";       // for input of filename to save (or load)
    bool ignore_escape = true;
                

    //-----------------------------------------
    #region C O N S T R U C T O R  and  L O A D
    //-----------------------------------------
    public Editor(Map _map, SpriteBatch _spriteBatch, Text _text)
    {
        map = _map; 
        spriteBatch = _spriteBatch;
        text = _text;            
    }
    public void Load(ContentManager Content)
    {
        editor_images = Content.Load<Texture2D>("editor_stuff"); // images just for editor
    }
    #endregion
Step 20) Let's add a method to take care of scrolling using the editor input:
Code:
void GetScrollInput(ref Vector2 scroll_speed) // user input to scroll the map... 
{
    if (Game1.KeyDown(Keys.Right)) scroll_speed.X = 1f; 
    if (Game1.KeyDown(Keys.Left))  scroll_speed.X = -1f;
    if (Game1.KeyDown(Keys.Down))  scroll_speed.Y = 1f;
    if (Game1.KeyDown(Keys.Up))    scroll_speed.Y = -1f;
    if (Game1.KeyDown(Keys.A)) scroll_speed.X = -10f;
    if (Game1.KeyDown(Keys.D)) scroll_speed.X = 10f;
    if (Game1.KeyDown(Keys.W)) scroll_speed.Y = -10f;
    if (Game1.KeyDown(Keys.S)) scroll_speed.Y = 10f;     
}        


Step 21) Now we add the Update function which will return a bool to Game1.cs to decide whether the escape button should actually cause the program to exit or not. (not graceful I know)
In here we'll use the map conversion method we made to look up the tile coordinates of the mouse which will be mtx, mty

Also we'll use input_action (enum) to determin what kind of input we're doing at the moment. We'll need edit_map (shift keys will modify how it works - ie: for offsets or decals), edit_collisions, etc.. Input types of pick_image, save, and load will be processed in the draw function (for drawing convenience purposes -- this is just the map editor so no big deal).

Code:
        //--------------------------
        #region U P D A T E
        //--------------------------                
        // we need to be able to modify scrolling in this update (thus is ref)
        public bool Update(ref Vector2 scroll_speed)
        {
            if (Keyboard.GetState().IsKeyUp(Keys.Escape)) ignore_escape = false;

            // G E T  M O U S E  I N F O --------------------------------------
            mos = Mouse.GetState();  mosV = new Vector2(mos.X, mos.Y); 
            map.ScreenPosToWorldTile(mosV, ref mtx, ref mty); //what tile is the mouse over? 
            mtx = MathUtil.Clamp(mtx, 0, Map.MAP_WIDTH - 1); mty = MathUtil.Clamp(mty, 0, Map.MAP_HEIGHT); //don't let the mouse try to edit outside the map array (illegal)

            //get fresh mouse clicks... one is for text and one is for other stuff... seperated to prevent unwanted click behavior problems
            if ((mos.LeftButton == ButtonState.Pressed) && (old_mos.LeftButton == ButtonState.Released)) { mouseClick = true; click = true; }  else { mouseClick = false; click = false; }
            if (text.is_over_text) mouseClick = false; //prevent unwanted clicks which are ment only for text clicks
            
            bool move_off1 = true;  //if LeftShift is held (with arrow keys), we edit offset1(regular tiles) else offset2 (decals)
            scroll_speed = Vector2.Zero; //start as zero - is set by input of arrow keys

            switch(input_action) {
                case InType.edit_map: // INPUT:  E D I T  M A P --------
                    // RIGHT-CLICK - Delete Tile
                    if ((mos.RightButton == ButtonState.Pressed) && (old_mos.RightButton == ButtonState.Released) && (!text.is_over_text)) map.DeleteTile(mtx, mty);                   

                    if (Game1.KeyDown(Keys.LeftShift) || Game1.KeyDown(Keys.RightShift)) // Adjust OFFSETS... 
                    {
                        if (Game1.KeyDown(Keys.RightShift)) move_off1 = false; //gonna move offset2 (decal instead cuz pressing LeftShift)
                        if (Game1.KeyDown(Keys.Right)) { if (move_off1) map.Offset(mtx, mty, 1, 0);  else map.Offset2(mtx, mty, 1, 0); }
                        if (Game1.KeyDown(Keys.Left))  { if (move_off1) map.Offset(mtx, mty, -1, 0); else map.Offset2(mtx, mty, -1, 0); }
                        if (Game1.KeyDown(Keys.Down))  { if (move_off1) map.Offset(mtx, mty, 0, 1);  else map.Offset2(mtx, mty, 0, 1); }
                        if (Game1.KeyDown(Keys.Up))    { if (move_off1) map.Offset(mtx, mty, 0, -1); else map.Offset2(mtx, mty, 0, -1); }
                        if ((Game1.KeyPressed(Keys.Delete)) || (Game1.KeyPressed(Keys.Back))) map.DeleteDecal(map.tile_x, map.tile_y);  // SHIFT+BACKSPACE - Delete Decal
                        if (selected >= 0)
                            if (mouseClick) map.AddDecal(mtx, mty, selected); // SHIFT+click - add decal (over tile corner only right now)
                    }
                    else // R E G U L A R   E D I T I N G   I N P U T  (scrolling, clicks to add tiles)--------------
                    {
                        GetScrollInput(ref scroll_speed); // Arrows control scrolling in editor
                        if ((Game1.KeyPressed(Keys.Delete)) || (Game1.KeyPressed(Keys.Back))) map.DeleteTile(map.tile_x, map.tile_y); //Keypress to delete tile also
                        if (selected >= 0) { if (mouseClick) map.AddTile(mtx, mty, selected); } // if a tile is selected and we have a left mouse click, add the selected tile
                        else if (selected == -2) { if (mouseClick) { map.startData.x = mtx; map.startData.y = mty; selected = -1; } }
                    }                               
                    break;
                case InType.edit_collisions: // INPUT:  E D I T  C O L L I S I O N S -------
                    // right click to remove a collision tile... 
                    if ((mos.RightButton == ButtonState.Pressed) && (old_mos.RightButton == ButtonState.Released) && (!text.is_over_text)) map.DeleteCollisionTile(mtx, mty);
                    // left click to add a collision tile... 
                    if (mouseClick) map.AddCollisionTile(mtx, mty, selected);
                    break;
                case InType.pick_image: if (Game1.KeyPressed(Keys.Escape)) {input_action=InType.edit_map; } ignore_escape = true; break;
                case InType.save: ignore_escape = true; break; // this is done where fonts are drawn in Draw 
                case InType.load: ignore_escape = true; break; // " " 
            }
            return ignore_escape;
        }

Right Click = Add the selected tile at mouse position
Left Click = Delete tile under mouse
Arrows = scroll map, A,S,D,W = scroll fast
LeftShift + Arrows = move tile offset
Shift + click = add decal (only works on top left tile occupation)
RightShift + Arrows = move decal offset

Step 22) You may have noticed the use of MathUtil.Clamp. I forgot to mention to add a class called MathUtil.cs. And add the following (restricts range of integer) - we're using it to prevent the mouse's tile from going off the map.

Code:
class MathUtil
{
    // restricts integer into some range: 
    public static int Clamp(int value, int min, int max) { return (value < min) ? min : (value > max) ? max : value; }
}


Step 23) We will need to be able to draw rectangles to show which image we are selecting in the image picker, so we'll add this:

Code:
void DrawRect(Rectangle r, float scale) // draws a line rectangle.... 
{
    int x=(int)(r.X*scale), y=(int)(r.Y*scale), rw=(int)(r.Width*scale), rh=(int)(r.Height*scale);
    Rectangle src = new Rectangle(0, 0, 1, 1);
    spriteBatch.Draw(editor_images, new Rectangle(x, y, rw, 1), src, Color.White);
    spriteBatch.Draw(editor_images, new Rectangle(x, y, 1, rh), src, Color.White);
    spriteBatch.Draw(editor_images, new Rectangle(x, y+rh, rw, 1), src, Color.White);
    spriteBatch.Draw(editor_images, new Rectangle(x+rw, y, 1, rh), src, Color.White);
}
Note: we probably won't need to scale it... I just added in case...
Step 24) We need input_action types. We'll add these:
Code:
enum InType { edit_map, pick_image, edit_collisions, save, load } 
InType input_action = InType.edit_map; //default


Step 25) We need to make a Draw method which shows collision information on the map as well as clickable texts for editor options. First we need to reset the test: text.is_over_text to false (we use to make sure clicks to text don't cause unwanted editing)

The we draw like in map.draw but this to draw editor helper images to show what type of collision each tile has associated with it.

Code:
//-------------
#region D R A W
//-------------
public void Draw(GameTime gameTime)
{
    text.is_over_text = false;  //reset this at the top of draw (NOT in Text class[won't work])
    //----- D R A W  C O L L I S I O N  I N F O R M A T I O N -------------
    Rectangle solid_rect = new Rectangle(0, 0, 64, 64); // source image of X square thing... 
    CollideFlag cdflag;
    int tx, ty; // tile indices
    float tile_xoff = map.tile_offset.X + Map.xoff, tile_yoff = map.tile_offset.Y + Map.yoff; //remember the offset from screen center
    Vector2 pos;
    int a, b = -8;
    do //scan through tiles (-8 to +8 vertical) and (-10 to +10 horizontal)
    {
        ty = map.tile_y + b;
        if ((ty < 0) || (ty >= Map.MAP_HEIGHT)) { b++; continue; } //not legal - skip
        a = -10;
        do
        {
            tx = map.tile_x + a;
            if ((tx < 0) || (tx >= Map.MAP_WIDTH)) { a++; continue; } //not legal - skip
            if (map.CDtiles[tx, ty] == null) { a++; continue; }       // " " 
            cdflag = map.CDtiles[tx, ty].collideFlag;                 //what is the collision flag? 
            pos = new Vector2(a * 64.0f + tile_xoff, b * 64.0f + tile_yoff); //what is the screen position of the tile? 
            switch(cdflag) {  // based on collision flag, draw an indicator from editor images       
                case CollideFlag.solid: 
                    spriteBatch.Draw(editor_images, pos, solid_rect, new Color(255,255,255,100)); 
                    break;
                case CollideFlag.platform:
                    spriteBatch.Draw(editor_images, new Rectangle((int)pos.X, (int)pos.Y, 64,32), solid_rect, new Color(255, 255, 255, 150));
                    break;
            }
            a++;
        } while (a < 10);
        b++;
    } while (b < 8);//------------------------------------


Step 26) We need a response section to pick_image if that is the current input action.
It will first draw the sheet texture (scaled to fit on screen reasonably well)

Then it will test to find the closest tile to the mouse and see if the mouse is actually inside it (which will cause selected to be set to that tile). It does this by remembering the last closest distance calculated between the mouse and a rect's center (since the sheet image is scaled, the rectangles and such need scaling too).

If the clickable text: "Start Pos" is selected, selected becomes -2 which Update knows that if a click occurs now, startData(ie: player start position) must be set to the mouse tile:(mtx,mty) and -1 means select nothing...

Note map tile color is modified for some actions to make the background map partly transparent during edit (ie: collisions).

Code:
// ------------------- P I C K  I M A G E ------------------
    if (input_action == InType.pick_image)
    {
        Rectangle r; Vector2 center, vec; float dist, closest_dist=60000; int closest_item=-1; //used in finding closest tile to select
        float scale = 0.58f; //58% the size (to fit on screen - CHANGE THIS IF YOUR TILESHEET IS BIGGER)
        // draw the entire sheet image :
        spriteBatch.Draw(map.sheetTex, Vector2.Zero, null, Color.White, 0f, Vector2.Zero, scale, SpriteEffects.None, 0f); 
                
        //find closest tile to mouse and draw a rectangle around it:                
        int i = 0; do
        {
            r = new Rectangle((int)(map.sheet[i].rect.X * scale), (int)(map.sheet[i].rect.Y * scale), (int)(map.sheet[i].rect.Width * scale), (int)(map.sheet[i].rect.Height * scale));
            r = map.sheet[i].rect;    center = new Vector2(r.X + r.Width / 2, r.Y + r.Height / 2); //looking for closest rect center to mouse position
            vec = mosV - center*scale;  //everything must be scaled because of the image scale... 
            dist = vec.LengthSquared(); //approximate the distance from the mouse to the rect-center.. 
            if (dist < closest_dist) { closest_dist = dist; closest_item = i; } // record if closer than last test in loop
            i++;
        } while (i < map.num_sheet_defs);
        if (closest_dist < 60000) { //if anything selected: 
            i = closest_item;       //get the sheet index and figure out the rectangle (scale it)
            r = new Rectangle((int)(map.sheet[i].rect.X * scale), (int)(map.sheet[i].rect.Y * scale), (int)(map.sheet[i].rect.Width * scale), (int)(map.sheet[i].rect.Height * scale));
            if (r.Contains(new Point((int)mosV.X, (int)mosV.Y))) //is the mouse actually in the rectangle? 
            {
                selected = closest_item; // set selected - this is tile you will paint to the map with 
                DrawRect(r, 1);          // draw the surrounding rectangle (we already calculated a scaled version so set as 1)
            } 
            else selected = -1; //nothing was selected
        }
        if (text.DrawClickable(690, 20, "Start Pos", click)) { selected=-2; map.tile_color = Color.White; } // Pick Hero Starting Position
        if ((mos.LeftButton == ButtonState.Pressed) && (old_mos.LeftButton == ButtonState.Released))
            { input_action = InType.edit_map; map.tile_color = Color.White; }// exit apon any click (even if nothing selected)
    }

Step 27) Continue by adding clickable section to return from collision input mode to normal edit mode:

Code:
    else if (input_action == InType.edit_collisions) // --EDIT COLLISIONS--
    {
        if (text.DrawClickable(320, 40, "Edit Map", click)) { input_action = InType.edit_map; map.tile_color = Color.White; }  // can exit PickImage with this also... 
    }

Step 28) Continue by adding a section for polling for user input of a file name to save or load as (if in this input_action mode)

Code:
    else if ((input_action == InType.save)||(input_action==InType.load)) // S A V E  --- A N D --- L O A D: 
    {
        bool done = false;
        if (input_action == InType.save) text.DrawCenteredText(0, -12, "Save file as:"); else text.DrawCenteredText(0, -12, "Load file as:"); 
        done = text.Input(-10, 18, ref inputStr, 1, 20, gameTime, true, true);
        if (done) {
            if (inputStr.GetLast(4) != ".txt") inputStr += ".txt";
            //save the actual file                                        
            string fileString = @"Content\\" + inputStr;
            if (File.Exists(fileString)) {  //To do: (if save) Prompt "Overwrite file?"
            }
            //if (input_action == InType.save) { map.SaveMap_TXT(fileString); }  else { map.LoadMap_TXT(fileString); }
            inputStr = ""; input_action = InType.edit_map;
        }
        if (Keyboard.GetState().IsKeyDown(Keys.Escape)) { inputStr = ""; input_action = InType.edit_map; }
    }

Note the text position of the input is offset from the screen center(-10,18) and inputStr is constantly being updated each loop waiting for more input until the user presses enter (or escape to cancel). For now - the call to load or save is commented out because we still need to add those methods to the map.
Step 29) Continue by adding a section to deal with regular edit_tiles mode in which we need to draw the currently selected item under the mouse (slightly transparent) and also show the clickable texts for edit options (which set input_actions) [lastly keep track of old mouse state]

Code:
else
    { // ----------------E D I T  T I L E S ---------------------
        //Draw Editor:
        pos = map.WorldTileToScreenPos(mtx, mty); // get mouse tile position as screen coord
        //draw a slightly transparent version of the selected item: 
        if (selected >= 0) spriteBatch.Draw(map.sheetTex, pos + map.sheet[selected].offset, map.sheet[selected].rect, new Color(255, 255, 255, 150));
        else if (selected==-2) text.Draw(mos.X, mos.Y, "Start Position"); 
        // draw that weird X square indicator to show what tile we're over: 
        spriteBatch.Draw(editor_images, pos, new Rectangle(0, 0, 64, 64), Color.White);
        if (text.DrawClickable(320, 550, "Pick Image", click)) { input_action = InType.pick_image; map.tile_color = new Color(155, 155, 155, 150); }
        if (text.DrawClickable(320, 40, "Edit Collisions", click)) { input_action = InType.edit_collisions; map.tile_color = new Color(255, 255, 255, 150); }
        if (text.DrawClickable(20, 550, "Save", click)) { input_action = InType.save; }
        if (text.DrawClickable(700, 550, "Load", click)) { input_action = InType.load; }
    }            
    old_mos = mos;
}//end of Draw

Step 30) Let's uncomment this line from the Load and Save section of draw:
if (input_action == InType.save) { map.SaveMap_TXT(fileString); } else { map.LoadMap_TXT(fileString); }

Now we can go ahead and add SaveMap_TXT and LoadMap_TXT to Map.cs

The reason I'm using txt files with ID's associated with each bit of information, is because if we later decide to change the TileTypes to add more stuff or maybe modify something, or maybe even modify the map size -- it will still be able to load old map data. Later you may want to use a binary serlializer or xml serializer or intermediate serializer (fast loads and saves) If the map will never change then intermediate might be a good idea. If the user can change the map contents (sandbox world) you may want to use binary serializer. Its actually EASIER and quicker to do these than the method I'm showing you.

In Map.cs we'll add these:

Note: For each new item you provide an ID, plus just simple ints, bytes, or floats of the data, then use WriteLine at the end of the line to make sure the ID is always parts[0] when loading the file

Code:
//----------------------------
#region S A V E  M A P   T X T 
//----------------------------
// I use ID/Name each line written so that when its read there's no mistake about what data it is 
// (useful if you decide to change TileType and still need to load old file data)
public void SaveMap_TXT(String fileName) //(its actually easier to write the binary serialized - will do later [or content version])
{            
    using (StreamWriter writer = new StreamWriter(fileName))
    {
        int width = MAP_WIDTH, height = MAP_HEIGHT, temp;
        writer.Write(width.ToString() + ","); writer.Write(height.ToString() + ","); writer.WriteLine(); //write map size
        writer.Write(startData.x.ToString() + ","); writer.Write(startData.y.ToString() + ","); writer.WriteLine(); //write start data
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                if (tiles[x, y] != null) {
                    writer.Write("XY,"); writer.Write(x.ToString() + ","); writer.Write(y.ToString() + ","); writer.WriteLine();
                    writer.Write("INDEX,"); writer.Write(tiles[x, y].index.ToString() + ","); writer.WriteLine();
                    writer.Write("INDEX2,"); writer.Write(tiles[x, y].index2.ToString() + ","); writer.WriteLine();
                    writer.Write("OFFSET,"); writer.Write(tiles[x, y].offset.X.ToString() + ","); writer.Write(tiles[x, y].offset.Y.ToString() + ","); writer.WriteLine();
                    writer.Write("OFFSET2,");writer.Write(tiles[x, y].offset2.X.ToString() + ","); writer.Write(tiles[x, y].offset2.Y.ToString() + ","); writer.WriteLine();
                    writer.Write("ROT,"); writer.Write(tiles[x, y].rot.ToString() + ","); writer.WriteLine(); 
                    writer.Write("SCALE,"); writer.Write(tiles[x, y].scale.ToString() + ","); writer.WriteLine();
                    writer.Write("TINT,"); writer.Write(tiles[x, y].tint.R.ToString() + ",");  writer.Write(tiles[x, y].tint.G.ToString() + ",");
                                            writer.Write(tiles[x, y].tint.B.ToString() + ",");  writer.Write(tiles[x, y].tint.A.ToString() + ","); writer.WriteLine();
                }
                if (CDtiles[x, y] != null) {
                    writer.Write("CDXY,"); writer.Write(x.ToString() + ","); writer.Write(y.ToString() + ","); writer.WriteLine();
                    temp = (int)CDtiles[x, y].collideFlag;
                    writer.Write("COLLIDEFLAG,"); writer.Write(temp.ToString() + ","); writer.WriteLine();
                    writer.Write("CDINDEX,"); writer.Write(CDtiles[x, y].index.ToString() + ","); writer.WriteLine();
                }
            }                    
        }
    }                        
}
#endregion


//----------------------------
#region L O A D  M A P   T X T 
//----------------------------
// I use ID/Name each line written so that when its read there's no mistake about what data it is 
// (useful if you decide to change TileType and still need to load old file data - use this only during development)
public void LoadMap_TXT(String fileName) //(its actually easier to write the binary serialized - will do later [or content version])
{
    int width = MAP_WIDTH; int height = MAP_HEIGHT;            
    using (StreamReader reader = new StreamReader(fileName))
    {
        string line = reader.ReadLine(); string[] parts = line.Split(',');
        width = Convert.ToInt32(parts[0]); height = Convert.ToInt32(parts[1]);
        line = reader.ReadLine(); parts = line.Split(',');
        startData.x = Convert.ToInt32(parts[0]); startData.y = Convert.ToInt32(parts[1]); tile_x = startData.x; tile_y = startData.y; //position the map at start position
        int x=0, y=0, cx=0, cy=0, temp;
        while (reader.EndOfStream != true)
        {
            line = reader.ReadLine(); parts = line.Split(',');
            switch(parts[0]) {
                case "XY":  x = Convert.ToInt32(parts[1]); y = Convert.ToInt32(parts[2]);
                            if (tiles[x, y] == null) tiles[x, y] = new TileType(); break;
                case "INDEX":   tiles[x, y].index = Convert.ToInt32(parts[1]); break;
                case "INDEX2":  tiles[x, y].index2 = Convert.ToInt32(parts[1]); break;
                case "OFFSET":  tiles[x, y].offset.X = Convert.ToInt32(parts[1]); tiles[x, y].offset.Y = Convert.ToInt32(parts[2]); break;
                case "OFFSET2": tiles[x, y].offset2.X = Convert.ToInt32(parts[1]); tiles[x, y].offset2.Y = Convert.ToInt32(parts[2]); break;
                case "ROT":     tiles[x, y].rot = Convert.ToSingle(parts[1]); break;
                case "SCALE":   tiles[x, y].scale = Convert.ToSingle(parts[1]); break;
                case "TINT":    tiles[x, y].tint.R= Convert.ToByte(parts[1]); tiles[x, y].tint.G= Convert.ToByte(parts[2]);
                                tiles[x, y].tint.B= Convert.ToByte(parts[3]); tiles[x, y].tint.A= Convert.ToByte(parts[4]); break;
                case "CDXY":    cx = Convert.ToInt32(parts[1]); cy = Convert.ToInt32(parts[2]);
                                if (CDtiles[cx, cy] == null) CDtiles[cx, cy] = new CollisionTileType(); break;
                case "COLLIDEFLAG": temp = Convert.ToInt32(parts[1]); CDtiles[cx,cy].collideFlag = (CollideFlag)temp; break;
                case "CDINDEX":     CDtiles[cx, cy].index = Convert.ToInt32(parts[1]); break; 
            }
        }                
    }            
}
#endregion
Note: Even if new data were added to the TileType or it was assembled in a different order, this should still work (just make sure the xy comes first in here) Step 31) Lets go back to game1.cs and set it up so we can have an edit mode and load and call then necessary methods to make them for us:
Code:
public class Game1 : Microsoft.Xna.Framework.Game
{
    const bool       EDIT_MODE = true; 
    public const int SCREENWIDTH = 800, SCREENHEIGHT = 600;
    const bool       FULLSCREEN = true;
    GraphicsDeviceManager graphics;
    SpriteBatch spriteBatch;
    SpriteFont  font;         //for text
    Text text;                //text class
    static KeyboardState kb, old_kb; 
    public bool ignore_escape = false; //ignore escape if returning to main menu
    Texture2D background1, background2;
    Vector2 scroll;           //scrolls background via UV coordinate scroll
    Map     map;              //tile map   
    Editor  editor;           //map editor
    int     level=1;          //which map to load
    Vector2 scroll_speed;     //how fast we scroll

    //---------------------------
    #region C O N S T R U C T O R
    //---------------------------
    public Game1()
    {
        graphics = new GraphicsDeviceManager(this)
        {
            PreferredBackBufferWidth = SCREENWIDTH, PreferredBackBufferHeight = SCREENHEIGHT, IsFullScreen = FULLSCREEN,                
        };                 
        Content.RootDirectory = "Content";
    }
    #endregion

        
    //-------------
    #region I N I T
    //-------------
    protected override void Initialize()
    {
        spriteBatch = new SpriteBatch(GraphicsDevice);
        font = Content.Load<SpriteFont>("SpriteFont1");
        text = new Text(font, spriteBatch,SCREENWIDTH,SCREENHEIGHT);
        map = new Map(GraphicsDevice, Content, spriteBatch);
        if (EDIT_MODE) { editor = new Editor(map, spriteBatch, text); IsMouseVisible = true; } //only make if in EDIT MODE
        base.Initialize();
    }
    #endregion


    //---------------------
    #region L O A D 
    //---------------------
    protected override void LoadContent()
    {            
        // L O A D   I M A G E S --- 
        if (EDIT_MODE) editor.Load(Content);            
        switch(level) {
            case 1:
                background1 = Content.Load<Texture2D>("far_background1");
                background2 = Content.Load<Texture2D>("mid_background1");                    
                break;
        }
        // L O A D  L E V E L (or area) stuff and set rectangle coordinates on spritesheet:
        map.Load(level);
    }
    #endregion        
    protected override void UnloadContent() {   }


    //-----------------
    #region U P D A T E 
    //-----------------
    protected override void Update(GameTime gameTime)
    {
        kb = Keyboard.GetState();      
        if ((!ignore_escape) && (KeyPressed(Keys.Escape))) this.Exit(); //ignore escape prevents 
        if (kb.IsKeyUp(Keys.Escape)) ignore_escape = false; // (so if user pressed escape to exit game to menu, it won't automatically quit the game due to "carry-over")         

        if (EDIT_MODE) { ignore_escape = editor.Update(ref scroll_speed); }
        else
        { // ...game play updates will be called here...
        }
        map.Update(gameTime, ref scroll_speed); // update map scrolling and map's sheet animation stuff
        scroll += scroll_speed;

        base.Update(gameTime);
    }
    public static bool KeyPressed(Keys key) { return kb.IsKeyDown(key) && old_kb.IsKeyUp(key); }
    public static bool KeyDown(Keys key) { return kb.IsKeyDown(key); }
    #endregion


    //-------------
    #region D R A W 
    //-------------
    protected override void Draw(GameTime gameTime)
    {
        GraphicsDevice.Clear(Color.Black);

        // the backgrounds are scrolled by setting LinearWrap (so the UV coordinates wrap) and then we change the start coordinates for the rectangle source            
        spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.LinearWrap, null, null);            
        spriteBatch.Draw(background1, Vector2.Zero, new Rectangle((int)scroll.X, (int)scroll.Y, background1.Width, background1.Height), Color.White);
        spriteBatch.Draw(background2, Vector2.Zero, new Rectangle((int)scroll.X*2, (int)scroll.Y*2, background2.Width, background2.Height), Color.White);
            
        map.Draw(); // D R A W   T I L E S 
        if (EDIT_MODE) editor.Draw(gameTime); //Draw Editor Interface
        spriteBatch.End();

        base.Draw(gameTime);
    }
    #endregion
}   

Step 32) Make a level... I'm gonna save mine as lev1 (remember .txt is automatically added)
[Note: my txt file goes into Projects\TileGame\TileGame\TileGame\bin\x86\Debug\Content]

Here's a level map I made:

lev1


Project Source Code [Load Lev1 to see mine ;) ]


Home Page