MonoGame / FNA / XNA Tutorial:
Platformer Game Programming Tutorial

With Effects and Smooth Animation
(Page1, Page2, Page3, Page4, Page5)

Page1:

In this tutorial we'll create a platformer game including:
- crystal fx shader
- frame animator __ (for explosions)
- particle system for fireball spells __ (could add heat haze effect later)
- MeoMotion animated characters __ (could use traditional if you like)
- AI __ (and discuss how to make more advanced)
Example Scene

(If you wish to make a translated version of this tutorial - feel free to do so)

You'll need:
- At least some c# programming knowledge
- Download/install Monogame
- At least some background knowledge of: programming, graphics, and dealing with GPU

1) Start a new Project (I'm using Monogame Cross-Platform Desktop) and name your game. Proceed by tidying up comments in Game1.cs however you like.

2) Go to solution explorer, right-click your project name (ie: Platformer) and Add >> New Folder __ and call it 'User Interface'
__ now right click on that folder and Add >> class __ and call it 'Input' __ then double click into input.cs and add this:

Code Snippet
  1. public class Input
  2. {
  3.     public KeyboardState kb, okb;
  4.     public bool shift_down,  control_down,  alt_down;
  5.     public bool shift_press, control_press, alt_press;
  6.     public bool old_shift_down, old_control_down, old_alt_down;
  7.  
  8.     public Input()
  9.     {
  10.         // may want to disable windows key temporarily in here (or not)
  11.     }
  12.  
  13.  
  14.     [MethodImpl(MethodImplOptions.AggressiveInlining)]
  15.     public bool Keypress(Keys k) { if (kb.IsKeyDown(k) && okb.IsKeyUp(k)) return true; else return false; }
  16.     [MethodImpl(MethodImplOptions.AggressiveInlining)]
  17.     public bool Keydown(Keys k)  { if (kb.IsKeyDown(k)) return true; else return false; }
  18.  
  19.  
  20.     //------------
  21.     // U P D A T E
  22.     //------------
  23.     public void Update()
  24.     {
  25.         old_alt_down = alt_down; old_shift_down = shift_down; old_control_down = control_down;
  26.         okb = kb;  
  27.         kb = Keyboard.GetState();
  28.         shift_down   = false;    shift_press   = false;
  29.         control_down = false;    control_press = false;
  30.         alt_down     = false;    alt_press     = false;
  31.         if (kb.IsKeyDown(Keys.LeftShift)   || kb.IsKeyDown(Keys.RightShift))   shift_down   = true;
  32.         if (kb.IsKeyDown(Keys.LeftControl) || kb.IsKeyDown(Keys.RightControl)) control_down = true;
  33.         if (kb.IsKeyDown(Keys.LeftAlt)     || kb.IsKeyDown(Keys.RightAlt))     alt_down     = true;
  34.         if ((shift_down)   && (!old_shift_down))   shift_press   = true;
  35.         if ((control_down) && (!old_control_down)) control_press = true;
  36.         if ((alt_down)     && (!old_alt_down))     alt_press     = true;
  37.     }
  38.         
  39. }

Tip: When you first type KeyboardState, you may notice it is underlined. Right click it and resolve: using Microsoft.Xna.Framework.Input; __ this will automatically add the necessary 'using' at the top of the module and help the compiler.

kb, okb are the keyboard state and old keyboard state which are compared to decide if a button is held or tapped (recent press).
Method Implementation Options for Aggressive Inlining basically tell the compiler to try to place these commands directly in the method rather than calling them as their own methods (if it can). This could be (sort-of) compared to #define macros in C/C++. Keypress checks if a key was tapped. Keydown only tells if it is currently down.

Update is called in Game1 each loop to keep control / key status info up to date. The reason for this is module is partly to make code easier to read in other modules checking gamer input and also to set things up for player-input mapping in the future (if need)

3) Go into Game1.cs (main)
Add the Input class: Input inp; and make it in Initialize() [ie: inp = new Input(); ]
Then use inp in Update as follows:

Code Snippet
  1. //------------
  2. // U P D A T E
  3. //------------   
  4. protected override void Update(GameTime gameTime)
  5. {
  6.     inp.Update();
  7.     if (inp.Keypress(Keys.Escape)) Exit();     // <-- CHANGE LATER - (to go to menu state - then provide exit option in menu)
  8.  
  9.     base.Update(gameTime);
  10. }

 

4) Setup display.
We'll be setting a target resolution (ie: 1024 x 768). You could use a lower resolution target for a more retro look or higher if you like. A standard target resolution is useful because it's easy to program and design everything based around that resolution and it can be scaled in the final output to match the actual display resolution for the device.

In Game1.cs, we'll add some useful parameters:

Code Snippet
  1. //DISPLAY
  2. const int  SCREENWIDTH = 1024, SCREENHEIGHT = 768;     // TARGET FORMAT
  3. const bool FULLSCREEN  = false;                        // not fullscreen because using windowed fill-screen mode        
  4. GraphicsDeviceManager  graphics;                       
  5. PresentationParameters pp;
  6. SpriteBatch            spriteBatch;

While you could use fullscreen mode, it may be better to use a borderless window mode that looks like fullscreen mode for a variety of possible reasons (ie: some security-software can cause exclusive fullscreen mode to crash).

In our various modules, we'll also might frequently need to know the current resolution so we'll include some static variables for global access to them:

Code Snippet
  1. static public int      screenW, screenH;
  2. static public Vector2  screen_center;

In the contructor of Game1, we'll create the backbuffer to match the current device resolution so it covers the entire screen.
This is not the same as the target resolution I mentioned. Everything will be rendered to a target which is then applied to the final backbuffer at the end. Normally you'd only use a backbuffer. [ a backbuffer does page flipping and only shows finished renders ]

For convience, we'll also keep globals for the screenRect (rectangle that matches target resolution ie: 0,0,1024,768) and the original desktop, resolution rectangle. ie: (0,0,1680,1050)
The MainTarget holds our standard target resolution before being applied to the device's backbuffer.

Code Snippet
  1. //SOURCE RECTANGLES
  2. Rectangle             screenRect, desktopRect;              // render target size, desktop screen size
  3.  
  4. //RENDERTARGETS
  5. RenderTarget2D MainTarget;                           // render to a standard target and fit it to the desktop resolution

Code Snippet
  1. //------------------
  2. // C O N S T R U C T
  3. //------------------
  4. public Game1()
  5. {            
  6.     // Set a display mode that is windowed but is the same as the desktop's current resolution (don't show a border)...
  7.     // This is done instead of using true fullscreen mode since some firewalls will crash the computer in true fullscreen mode
  8.     int initial_screen_width  = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Width-10;
  9.     int initial_screen_height = GraphicsAdapter.DefaultAdapter.CurrentDisplayMode.Height-10;
  10.     graphics = new GraphicsDeviceManager(this)
  11.     {
  12.         PreferredBackBufferWidth  = initial_screen_width,
  13.         PreferredBackBufferHeight = initial_screen_height,
  14.         IsFullScreen = false,       PreferredDepthStencilFormat = DepthFormat.Depth16
  15.     };
  16.     Window.IsBorderless   = true;
        Content.RootDirectory = "Content";
    }

Gets the default resolution (ie: desktop res), and sets the members for the graphics device to prefer that resolution and just use windowed mode (also set to allow DepthStencil format - ie: 16bit depth - we won't actually use this in this tutorial but could be usefull for more advanced things later - ie: occlusion fx)
Also set window to borderless so it looks like a regular fullscreen mode.

In the following code, we set our main render target with our game's standard resolution.
SideNote: For safety, I keep the habit of checking the presentation parameters for the backbuffer that was created in the constructor above. For example, sometimes if you create a fullscreen mode with preferred resolution, it will create the closest compatible allowed resolution - so you need to copy the true backbuffer dimensions (screen_width, screen_height) from the presentation parameters (because you might not get what you asked for).

In this case, screenW & screenH refer to the target's resolution. We capture the rectangles for source/destination purposes later.
Since the tile-grid has "tiles" of size 64x64, I subtract (32,32) to get the coordinate of what would be the corner of a tile centered on the screen. This will come in handle later for scrolling the tile-grid system relative to the screen's center.

Code Snippet
  1. //--------
  2. // I N I T
  3. //--------
  4. protected override void Initialize()
  5. {
  6.     // SETUP SPRITEBATCH AND GET TRUE DISPLAY
  7.     spriteBatch   = new SpriteBatch(GraphicsDevice);            
  8.     MainTarget    = new RenderTarget2D(GraphicsDevice, SCREENWIDTH, SCREENHEIGHT);            
  9.     pp            = GraphicsDevice.PresentationParameters;
  10.     SurfaceFormat format = pp.BackBufferFormat;             
  11.     screenW       = MainTarget.Width;   
  12.     screenH       = MainTarget.Height;
  13.     desktopRect   = new Rectangle(0, 0, pp.BackBufferWidth, pp.BackBufferHeight);
  14.     screenRect    = new Rectangle(0, 0, screenW, screenH);
  15.     screen_center = new Vector2(screenW / 2.0f, screenH / 2.0f) - new Vector2(32f, 32f);                   // subtracting half of the size of a tile since this is for tile calculations



5) Background(s) Parallax Scrolling
We'll need to have a couple background layers (as Texture2D) and something to track their movements.
In the variables for Game1.cs, (perhaps under Input inp;), add the following:

Code Snippet
  1. //TEXTURES
  2. Texture2D             far_background, mid_background;
  3. Texture2D             tiles_image;
  4.  
  5. //POSITIONS
  6. static public Vector2 background_pos;

I'm making background_pos as static (rather than passing it) so that when we add an editor class or others, it'll be easy to access and move the backgrounds when desired.
tiles_image - stores all the images related to the game map (ie: grass, trees, crystals, etc)

You'll need to go into a paint program of some kind (Photoshop, Gimp, Krita, Sai, etc...) and make a background (ie: far background of stars in a sunset) calling it "background_stars" for example...

To make a seamless repeating tile or background:
A) Make a background and make a layer copy of it

bck1
B) Hide the original layer and position 4 copies so each one is half off the canvas and a corner is touching the center.

bk_make
C) Use smudge and clone-copy (air-brush mode) brushes to blend the seams so they disappear. Now when you scroll this image it will not have any visible seams. Result would be something like this:

nk2
Make sure the final background is suitable for your resolution (although you can stretch a smaller one if you want with a destination-Rectangle). The map location(grid coord) and scroll offset (0-64,0-64) will be used to calculate the exact scroll value for the background. If you're using a background that's a scene(ie: hills, trees, etc), then you can't scroll infinitely in the vertical direction. For this you'd need a horizontal seamless scroller but the vertical part would simply be taller than the target's Y resolution -- and so you'd scroll the far background a percent% of it's scrollable height based on how many tiles high the game map/grid can be. Optionally, you could also have it so it only scrolls horizontally - but that might not look as good.

And then a middle background for clouds or anything you may want to use additive blending with (if not you'll later want to change the blend style to AlphaBlend or nonpremultiplied for the middle layer since I'm going to set it to additive for the clouds)... call your middle background "mid_background". In the example below, I just used a medium-hard brush to paint circular blobs, and used dodge and burn tools to add shadow and light:

clouds

You'll also need to create a set of tiles. You can make each one in a seperate file and copy and scale them onto your tile sheet.
Set your guide grid to 64 x 64 so you can see where the tile images align as you place them on your tiles_image sheet. ie:
tile_example1
The grid lines are just guides - most graphics software gives you this option (with a hotkey to toggle on or off).
The above example shows an example of finding the tile coordinate information for setting up tiles. The red square on the leaves shows an estimate that the solid or relevant part of the tile is 1 64x64 tile (and the yellow corner dot marks where the top-left tile exists on the sheet). And for example, some could be 2 wide and 2 high (as with most square blocks on this sheet). The offset is the distance from where the tile will exist to where it should be plotted. I'll explain more later and refer back to this image. In the above I'm using the "Info window" to provide coordinates as well as width and height of rectangle selections. This tool will be very helpful for setting up your tile sheets.

NOTE: For creating the crystal effect (for above tile sheet):

bump1
You'll need to learn how to create a normal map. There are programs that can help you do this (as well as plugins) from the 2D original (altho a more realistic one can be made from a 3D model but that would take too long to explain here). It is possible to do it manually - to edit the coloring on a copy of the crystal such that red will indicate lighting on top-right, green for bottom-left and blues sort of in the middle. To do this, I'd make 3 layer copies (1 green shaded, 1 red shaded, 1 blue shaded [Adjustments tools]) and alpha-erase parts of each layer to get the right blends of colors. These colorings tell the effect what angle light is hitting the crystals at so it can calculate how to render effects over the bumpy surface.

So let's load the images:

Code Snippet
//--------
// L O A D
//--------
protected override void LoadContent()
{
    // LOAD GRAPHICS:            
    far_background = Content.Load<Texture2D>("background_stars");
    mid_background = Content.Load<Texture2D>("mid_background");
    tiles_image    = Content.Load<Texture2D>("tiles1");

If you're used to C/C++, you may notice almost nothing is put into UnloadContent (altho eventually it may be used to dispose certain things)... The reason for this is c# & monogame use a garbage collector to manage memory and will dispose of objects no longer used, on their own. There are exceptions though(or times you may want to force disposal) but that's another topic - it is worth looking into.

And in Draw():
Code Snippet
GraphicsDevice.SetRenderTarget(MainTarget);      // like substitute backbuffer set to desired resolution (stretched to true backbuffer later)

Above we set our standard resolution render target and everything drawn to it will be stretched onto the backbuffer later. Now we just need to draw the background layers. The far one does not need transparency so it's set as Opaque. The closer one is additive blended (black is transparent and colors are added together with background). Normally I'd suggest using alpha blending instead but I wanted to have the cloud colors change a bit based on background intensity (like the sunset).

Code Snippet
  1. // DRAW OPAQUE FAR BACKGROUND
  2. spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Opaque, SamplerState.LinearWrap);      // wrapped background 1
  3. spriteBatch.Draw(far_background, screenRect,
  4.     new Rectangle((int)(-background_pos.X*0.5f), 0, far_background.Width, far_background.Height), Color.White); // scroll at half speed: *0.5
  5. spriteBatch.End();
  6. // DRAW MID BACKGROUND(S)
  7. spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive, SamplerState.LinearWrap);    // wrapped background 2       
  8. spriteBatch.Draw(mid_background, screenRect,
        new Rectangle((int)(-background_pos.X), (int)-background_pos.Y, mid_background.Width, mid_background.Height), Color.White);
  9. spriteBatch.End();


The State: LinearWrap

What this does is when the texture memory is sampled beyond the edge of the texture, it wraps around to the other side. This is kind of like how you use mod or %. Example: if the width was 100 and you sampled at x=305 then it would sample from 5.
In doing this, we can change the source rectangle we're sampling from on the Texture2D and it will wrap.

  For far background, I multiply by 0.5 to reduce the horizontal scrolling by half. I don't scroll the Y direction because the level is mostly horizontal anyway and it's a distant background, but for the mid background I allow Y scrolling.
  You may notice the destination rectangle is always the screenRect -- that's because since we're scrolling the sample-source, we don't need to scroll the destination for these.

Code Snippet
  1.     // DRAW TEMP BACKBUFFER (MainTarget) to TRUE BACKBUFFER IN MAXIMIZED WINDOWED MODE (this resolves some rare compatibility problems):
  2.     GraphicsDevice.SetRenderTarget(null);
  3.     spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Opaque, SamplerState.LinearWrap, DepthStencilState.None, RasterizerState.CullNone);
  4.     spriteBatch.Draw(MainTarget, desktopRect, Color.White);
  5.     spriteBatch.End();
  6.     
  7.     base.Draw(gameTime);
  8. }
 

The above code sets our rendering to the backbuffer and we simply copy MainTarget onto the backbuffer (destination rectangle: desktopRect). This stretches our preferred standard game resolution onto the device's window-mode resolution as though we're in full screen mode.


6) Temporary Testing:

In Update():

Code Snippet
  1. if (inp.Keydown(Keys.Left)) background_pos.X++;
  2. if (inp.Keydown(Keys.Right)) background_pos.X--;
  3. if (inp.Keydown(Keys.Up)) background_pos.Y++;
  4. if (inp.Keydown(Keys.Down)) background_pos.Y--;


These are just for testing scrolling. Later, the player movement will determine these.

- - - - - -

7) Game map editor prerequisites...

a) So first in Game1.cs, we'll add the names of the level we want to edit and test. These are in game1 just so it's quick and easy to change level for editing without having to remember which class these are in.

Code Snippet
  1. // CHANGE THIS FOR MAKING A NEW LEVEL                        (use backup to retrieve a copy of old one if you accidentally save over it)
  2. static public string LEVEL_NAME  = @"Content\\lev1.txt";     // this is stored in the BIN/execute version of Content
  3. static public string BACKUP_NAME = @"Content\\backup.txt";

Later level loading might be by switch(name) or just a standard name like "lev" with a number on it(ie: "lev1").

For now, levels will be txt files with a type of switch loading that makes it simple to modify file format while still being able to load old levels. Later when the game's near completion, you may want to change it into a binary format once you're certain that there's no new types of data to add.
We'll set a backup file too so if you accidentally save something you didn't want to or you liked your old level better, you can load as "backup" and it will load from the backup file (still saves to "lev1" tho)

We'll also need to know which GameState we're in (ie: editing, playing, etc.) so we'll define some states in Game1.cs:

Code Snippet
  1. // GAME STATES:
  2. enum GameState { edit, play, game_over, menu, load_level }
  3. GameState gameState    = GameState.edit;

b) Now go into Solution Explorer and right click over your c# project-name, and Add >> Folder and call it "Map Related Stuff".
We'll need to define sheet parts or tile images... I'll just call these objects "Sheet" (short for Sheet_Part).
Under Map Related Stuff:
Right-click and Add a class and call it "Sheet"
We'll also need a class called "Tile", so Add that too.

Above the tile class, you will want to define some tile types like this:

Code Snippet
  1. // add more types to here which tell us how to draw and collide with the tile
  2. public enum TileType { empty, solid, reflector, spring, platform, spikes }
  3.  
  4. //--------
  5. // T I L E  
  6. //--------
  7. class Tile
  8. {


c) Sheet(part) will keep track of:
which tile type it is: TileType, (solids can be collided with, reflectors are for crystal effects, springs are for bouncers, etc...)
where on the actual sheet it is: Rectangle,
how many tiles_wide and tiles_high will be considered occupied when this sheet part is added to a map location
the offset from it's map position to draw it at: Vector2 offset;

Add these to Sheet.cs:

Code Snippet
  1. // S H E E T
  2.     class Sheet
  3.     {
  4.         public TileType type;
  5.         public Rectangle rect;
  6.         public int tiles_wide, tiles_high;
  7.         public Vector2 offset;

And we'll add this constructor:

Code Snippet
  1.     /// <summary>
  2.     /// Setup a tile item from the tiles image sprite sheet.  
  3.     /// </summary>        
  4.     /// <param name="Type">(empty, solid, water, etc...)</param>
  5.     /// <param name="Tiles_wide">number of this item will occupy in width (ie: 3 tiles wide will be solid) </param>
  6.     /// <param name="Tiles_high">numver of tiles high that will be empty, solid, liquid, etc... </param>
  7.     /// <param name="top_left_corner">top left corner of first tile (ie: first solid tile corner) - this is used to calculate the offset of the image around the tiles it occupies</param>
  8.     public Sheet(int x, int y, int x2, int y2, TileType Type, int Tiles_wide, int Tiles_high, float top_left_corner_x, float top_left_corner_y)
  9.     {
  10.         int width =  x2 - x + 1;
  11.         int height = y2 - y + 1;
  12.         rect = new Rectangle(x, y, width, height);
  13.         type = Type;           
  14.         tiles_wide = Tiles_wide; tiles_high = Tiles_high;
  15.         offset = new Vector2(rect.X, rect.Y) - new Vector2(top_left_corner_x, top_left_corner_y);
  16.     }
  17. }


As the comments above sort of explain, it takes the top-left and bottom-right corners of the region on the sheet where we want to extract the tile image from... and the type of tile (solid, bouncey, etc), the number of tiles_wide and tiles_high to consider occupied (ie: 2x2 solid), and the top-left corner of the image where the solid(or occupied) tile would start... which it then uses to figure out the offset from the edge of the image (SEE TILES IMAGE ABOVE)... so later when looping through map tiles it can use the offset from the tile position to place the image correctly into the scene.

d) Although it's not recommended practice, let's add a SheetManager class right below the Sheet class in the same file (sheet.cs)...
(Since they're closely related and both will be very small, I prefer to keep them both in sheet.cs)

This class will just provide a place to set up sheet part entries, where it isn't cluttering up the other code:

Code Snippet
  1.     // S H E E T  M A N A G E R  (sheet definitions)
  2.     class SheetManager
  3.     {
  4.         int num_sheet_parts;
  5.  
  6.         public SheetManager() { num_sheet_parts = 0; }
  7.  
  8.         // S E T U P   S H E E T   L E V E L  1
  9.         public void Setup_Sheet_Level_1(ref Sheet[] sheet)
  10.         {
  11.             num_sheet_parts = 0;
  12.             int n = 0;
  13.             sheet[n] = new Sheet(0, 0, 1, 1, TileType.empty, 1, 1, 0f, 0f); n++;              // nothing 0
  14.             sheet[n] = new Sheet(0, 16, 255, 111, TileType.solid, 4, 1, 0f, 32f); n++;        // grass1 1 q
  15.             sheet[n] = new Sheet(0, 128, 127, 255, TileType.solid, 2, 2, 0f, 128f); n++;      // solid block (green) 2 w
  16.             sheet[n] = new Sheet(166, 128, 344, 255, TileType.spring, 1, 1, 224f, 160f); n++; // spring (leaves) 3 e
  17.             sheet[n] = new Sheet(448, 128, 575, 254, TileType.reflector, 1, 1, 480f, 160f); n++; // reflective crystals 4 r
  18.             sheet[n] = new Sheet(768, 128, 959, 319, TileType.solid, 3, 3, 768f, 128f); n++;  // big solid block 5 t
  19.             sheet[n] = new Sheet(0, 320, 63, 383, TileType.solid, 1, 1, 0f, 320f); n++;       // small solid block 6 y
  20.             sheet[n] = new Sheet(65, 321, 127, 383, TileType.empty, 1, 1, 64f, 320f); n++;    // leaf decoration 7 u
  21.             sheet[n] = new Sheet(131, 256, 213, 383, TileType.empty, 1, 1, 146f, 320f); n++;  // rose 8 i
  22.             sheet[n] = new Sheet(320, 256, 447, 383, TileType.solid, 2, 2, 320f, 256f); n++;  // gold block 9 o
  23.             sheet[n] = new Sheet(0, 512, 127, 639, TileType.solid, 2, 2, 0f, 512f); n++;      // green-yellow block 10 p
  24.             sheet[n] = new Sheet(128, 512, 256, 639, TileType.solid, 2, 2, 128f, 512f); n++;  // green-solid block 11 a
  25.             sheet[n] = new Sheet(257, 402, 511, 650, TileType.empty, 1, 1, 367f, 576f); n++;  // house 12 s
  26.             sheet[n] = new Sheet(576, 448, 702, 639, TileType.empty, 1, 1, 593f, 569f); n++;   // big rose 13 d
  27.             sheet[n] = new Sheet(704, 384, 911, 646, TileType.empty, 1, 1, 772f, 576f); n++;   // tree 14 f
  28.             sheet[n] = new Sheet(0, 448, 63, 479, TileType.platform, 1, 1, 0, 448); n++;       // platform 15 g            
  29.             sheet[n] = new Sheet(768, 704, 895, 831, TileType.spikes, 2, 2, 768, 704); n++;    // spikes 16 h            
  30.             // add more items here... (add more types later)
  31.             num_sheet_parts = n;
  32.         }
  33.     }
  34. }

Obviously your own sheet part entry coordinates will be different. Eventually you may want to setup a switch based Setup_Sheet_Level(ref Sheet[] sheet, int level)

C# Lesson (skip if already understand):
You may notice that it takes a reference to a Sheet[] array which will be created in Game1 and shared by reference to other classes. By default, normally, class objects don't require ref before them as they're assumed to be referencing the original (in other words it passes the memory location of the object rather than copying to entire new object (because copying would be slow and we just need the one object)).
Even though there are no structs used here, I should still mention that structs in c# are copied if directly sent, as they're considered a value_type kinda like int, float, etc... so better to use classes instead of structs if it is something you'll be passing a lot. Structs in c# are more for tiny data clusters that won't change often or transition. If you do plan to use structs in c# (sometimes useful), just keep in mind that if you assign struct_b = struct_a and then change parts of struct_b... don't expect that those same changes will be made in the original, because it won't be a reference... it'll be a unique copy in a different memory location.

So to recap,
- the first 4 coords = rectangle region of image on tile sheet
- next is the tile-type
- then how many tiles_wide and tiles_high should be considered occupied after placing this part (placed by top-left corner tile)
- lastly, the topleft corner (x,y) that would mark on the sheet where the first top-left corner tile would be.
ie: If the image was truly a size of 4x3 tiles... but we only want the middle part to be solid... we might say it's 2x1 and mark the top-left corner of that 2x1 region on the tiles sheet. (see above tiles image)... the constructor then figures out how to offset the image around the tile it will be positioned with.


You may notice in the coments that I provide a brief description of what image each sheet part refers to as well as the key that will be used to place that part. In a more advanced editor, you could also have the option of picking the part from a pop-up image of the tile-sheet, using the mouse. If the sheet image is huge, you may want to use keys 1-4 to bring up 4 different sections of the tiles image to pick parts from... or scale it but it might be hard to see some parts. You could even create an editor for adjusting the image rectangles graphically (ie: a file-based "data driven design" approach) but it is a lot of extra complexity and likely won't save much time (tricky to make precise) - so maybe not the best approach in this case.

e) Go back to tile.cs
We'll need these fields for the tile class:

Code Snippet
  1. //--------
  2. // T I L E  
  3. //--------
  4. class Tile
  5. {
  6.     public int       index;           // image index (for sheet rect)
  7.     public TileType  type;            // empty, solid, spring, etc...
  8.     public Vector2   scale;  
  9.     public Vector2   offset;          // adjusts position
  10.     public float     rot;             // rotation
  11.     public bool      overlap;         // should it overlap the characters
  12.     public bool      stand_on;        // can you stand on it?
  13.     public bool      is_solid;        // does it act like a solid object?
  14.     public bool      spikes;          // is it spikes (or lava)
  15.     public bool      event_active;    // bounce event or some other event is already active       
  16.     public MonsterType monster_start; // type of monster to initialize at this tile ( None is default )

index  = which sheet part (ie: bricks, grass, etc)
type   = the actual type (how to interact with it ... ie: solid, water, bouncer, spikes, etc... )
offset = x,y distance from its map location (default is based on sheet part's offset but can be adjusted in map editing)
overlap = tells it to memorize the processed tile but show it after other images so it overlaps in the scene
stand_on = this tile can be stood upon... collision editing might allow customization of this for a map tile
is_solid = will collide with it (also could be customized in collision editing if you wanted [ie: for secret rooms])
spikes = something that you partly stand in (or bounce off of) and hurts you
event_active = currently used to determine if a bouncer(spring-like platform) has already started animation __ could be used for other types too which might have animation effects when interacted with
monster_start = None by default (thus not saved to the txt file) but if used... it will determine that a monster of some type will be initialized at this location when the game starts (and thus saved to file)

But... we don't have a MonsterType yet so:

f) Right-click on c# project_name and click Add>>Folder and call it "Monsters" ... then right-click that and Add>>Class and call it "Monster" and above the empty class, we'll add an enum for MonsterType:

Code Snippet
  1. enum MonsterType { None, Mouster, Hellcat }
  2.  
  3. class Monster
  4. {

Now, in Tile.cs the MonsterType should no longer be underlined as it has become valid.


g) In Tile.cs:
We'll need a constructor for initializing tiles and a Clear method to reset a tile. (Note: index == 0 will mean the tile is empty and thus it is not written to file)

Code Snippet
  1.     public Tile(int Index, TileType Type)
  2.     {
  3.         index   = Index;
  4.         type    = Type;
  5.         scale   = Vector2.One;
  6.         monster_start = MonsterType.None;
  7.     }
  8.  
  9.     public void Clear()
  10.     {
  11.         index   = 0;               type = TileType.empty;
  12.         scale   = Vector2.One;
  13.         offset  = Vector2.Zero;    rot = 0;
  14.         overlap = false;           stand_on = false;
  15.         spikes  = false;           is_solid = false;
  16.         event_active = false;      monster_start = MonsterType.None;
  17.     }        
  18. }

REMEMBER: Anytime you add a field to Tile, you'll need to make sure they are initialized correctly and that Clear is updated to reset it correctly. If you forget to reset the new variable in Clear, then when you delete a tile, it will still think that property for that tile location should have that attribute (or behavior)... which could be bad...

h) While it is probably not "proper" practice... I would suggest adding a class called ProcessedTile just below the tile class (in the same module)... this just keeps track of tiles that are to be overlapped into the scene. ie: When tiles are processed in the map drawing methods, it uses the field, overlap, to determine if it should draw it later and if so... it copies the calculated coordinates into a "ProcessedTile" which is to be drawn later. This is a small simple class that probably doesn't need to clutter up the Solution Explorer so I just add it in the Tile.cs file at the bottom after the Tile class:

Code Snippet
  1. // P R O C E S S E D  T I L E
  2. // stores final draw information for a tile that must be rendered later or with a different shader:
  3. class ProcessedTile
  4. {
  5.     public Vector2 pos;
  6.     public Vector2 scale;
  7.     public float rot;
  8.     public Rectangle rect;
  9.  
  10.     public void Add(Vector2 position, Rectangle srcRect, float rotation, Vector2 Size)
  11.     {
  12.         pos   = position;
  13.         rect  = srcRect;
  14.         rot   = rotation;
  15.         scale = Size;
  16.     }
  17. }

All it will need to draw the tile later... is the position, size, rotation, and source-rectangle... Later you might want to add color(tint), origin, or other instruction booleans if needed (but for simplicity, I'd leave only what's needed until you absolutely need to add other things)

i) While we're here... let's add a tiny class above the Tile class that keeps track of player start data. Since it's so small and we need almost nothing to start with, we'll just leave it in here. We really only need to know where the player starts on the map for now:

Code Snippet
  1. // Starting state of map:
  2. class StartData {
  3.     public int x, y; //player start position on tile map (will add more later)
  4.     public StartData(int X, int Y) { x = X; y = Y; }
  5. }    
  6.  
  7.  
  8. // add more types to here which tell us how to draw and collide with the tile
  9. public enum TileType { empty, solid, reflector, spring, platform, spikes }
  10.  
  11. //--------
  12. // T I L E  
  13. //--------
  14. class Tile
  15. {


j) Now right-click on "Map Related Stuff" in solution explorer... and Add>>Class called "Map"
Code Snippet
  1. class Map
  2. {
  3.     public const int TILES_WIDE = 100, TILES_HIGH  = 50;     // map dimensions
  4.     public const int TILE_SPAN_X = 14, TILE_SPAN_Y = 10;     // number of tiles processed on the screen (14 * 2= 28 horizontal, 10 * 2 = 20 verticle)
I added 2 constants for maximum map dimensions (since this is only a demonstration, I just made it small: 100x50)
The TILE_SPAN_X,Y constants determine how many tiles to process on screen at once relative to the center tile. ie: center-14 to center+14 in the horizontal direction and -10 to +10 in the vertical direction

k) We'll need some essential field members for the map:
Tile[,] tiles == ... the map's tiles[x,y]
startData == player starting data
Later we'll add a bounce manager to keep track of bouncey spring platforms (we'll just add a comment for now)
loc == a location point on the map which is where the camera is located (in terms of tiles[x,y])
sheet[] == referring to the sheet part data[ie: source rectangles] (we still need a call to create this in Game1.cs - we'll do it later)
overlapTiles[] == stored overlapping tile data to draw overlaps after drawing everything else
We'll also need a seperate set of Processed Tiles for crystals too, to distinguish them as ones with a particular effect on them.
scroll_offset == (0-64, 0-64) - scrolling from 0 to 64 in X and Y directions... It smoothly scrolls between these numbers and when it exceeds 64, it updates the tile coordinate and resets the scroll value to 0 ... so if each tile in memory is 64x64 then it will scroll between these values and then switch tiles. (ie: if (scroll_offset.X>64f) {scroll_offset.X=0; loc.X--;} or (ie: if (scroll_offset.X<0f) {scroll_offset.X=64f; loc.X++;} ...and same for Y...
We'll also need some variables to keep track of start and end tiles visible on screen (other methods may need to check on these)
ie: a1, b1, a2, b2
Also we'll need access to tiles_image from Game1 as well as spriteBatch... and we'll want to know where our center tile is on screen ideally...
So we'll add this to Map.cs:

Code Snippet
  1. // MAP DATA
  2. public Tile[,]   tiles;                                  // map tiles         
  3. public StartData startData;                              // info about starting state of map (like player start position)
  4. //public BounceMgr bounceMgr;                              // manages bounce events (triggered by players or monsters)        
  5. public Point     loc;                                    // tiles map location (used to track what section of tiles to draw)                
  6. Sheet[]          sheet;                                  // refer to the sheet data in Game1
  7. ProcessedTile[]  overlapTiles;                           // tracks tiles to overlap into scene
  8. ProcessedTile[]  crystals;                               // tracks crystals to be rendered differently                
  9. Color            crystal_color;
  10. int              overlap_count, crystal_count// number of layer2 tiles in the scene, num of crystals needing crystal shader                
  11. public Vector2   scroll_offset;                          // smooth scroll between tile section updates using this (0-64, 0-64)
  12. public int       a1, b1, a2, b2;                         // start and end of visible tiles
  13. public float     sx, sy;                                 // start coords (top-left) of tiles to draw on screen
  14.  
  15. Texture2D        tiles_image;                            // refers to tiles image from Game1
  16. Vector2          screen_center;                          // center of screen for a tile
  17. float            timer;                                  // using this to get a fluctuating wave from sin or cos        
  18. SpriteBatch      spriteBatch;                            // refer to spriteBatch in Game1        

We haven't made a bounce manager yet so that'll remain commented for now.

l) In the Constructor, we'll need to pass in references to the sheet parts and spriteBatch:
Code Snippet
  1. // C O N S T R U C T
  2. public Map(Sheet[] sht, SpriteBatch sprBatch)
  3. {
  4.     sheet       = sht;
  5.     spriteBatch = sprBatch;
  6.     tiles       = new Tile[TILES_WIDE, TILES_HIGH];                      // allocate memory

So we create a grid of tiles that's 100x50 in size... We'll allocate memory for ALL the tiles as empies because it could help prevent possible null related crash errors caused by possible bugs (ie: trying to access memory that's not allocated).
Code Snippet
  1. // fill it in with default values                     (prefer to avoid null values - totally empty tiles not recorded to file anyway)
  2. for (int b = 0; b < TILES_HIGH; b++)
  3. {
  4.     for (int a = 0; a < TILES_WIDE; a++)
  5.     {
  6.         tiles[a, b] = new Tile(0, TileType.empty);
  7.     }
  8. }            
  9. overlapTiles = new ProcessedTile[250];                                // up to 250 overlap tiles on screen at once            
  10. crystals     = new ProcessedTile[250];                                // up to 250 crystals in a scene at once
  11. for (int a = 0; a < 250; a++) overlapTiles[a] = new ProcessedTile();
  12. for (int a = 0; a < 250; a++) crystals[a]     = new ProcessedTile();
(Note: Only occupied tiles will be stored to file)

We'll set the starting location for the editor(camera's center tile) and by default put the player there too. We'll also need to get the screen center too (-32,-32) as the target center minus half a tile size.

Code Snippet
  1.     // EDITOR START LOCATION:
  2.     loc.X = 5; loc.Y = 5;
  3.     startData = new StartData(loc.X, loc.Y);                              // init player start position        
  4.     screen_center = Game1.screen_center;
  5.         
  6.     //bounceMgr = new BounceMgr(tiles);                                     // init bounce manager (for spring tiles)
  7. }

Later, after we make a "bounce manager" for the spring platforms, we can initialize it here too.

I decided to make a seperate method for setting the tiles_image (since it may be loaded after making the map object):
Code Snippet
  1. //----------------------------
  2. // S E T  T I L E S  I M A G E (texture sprite-sheet for tiles)
  3. //----------------------------
  4. public void SetTilesImage(Texture2D tilesPic)
  5. {
  6.     tiles_image = tilesPic;                                              // keep original image in Game1 so can use it in multiple circumstances/places
  7. }


m) We'll need to be able to Add and Delete a tile. Keep in mind that a tile may actually refer to a cluster of tiles based on the sheet part's dimensions (tiles_wide, tiles_high).
Deleting a tile (or tile-cluster) is easy enough:
Code Snippet
  1. //---------------------
  2.         // D E L E T E  T I L E
  3.         //---------------------
  4.         public void DeleteTile()
  5.         {
  6.             int i = tiles[loc.X, loc.Y].index;
  7.             for (int b = loc.Y; b < loc.Y + sheet[i].tiles_high; b++) {
  8.                 if (b >= (TILES_HIGH - 1)) break;
  9.                 for (int a = loc.X; a < loc.X + sheet[i].tiles_wide; a++) {
  10.                     if (a >= (TILES_WIDE - 1)) break;
  11.                     tiles[a, b].Clear();
  12.                 }
  13.             }            
  14.         }        
  15.         // CLEAR MAP
  16.         public void ClearMap() { for (int b = 0; b < TILES_HIGH; b++) { for (int a = 0; a < TILES_WIDE; a++) { tiles[a, b].Clear(); } } }
i) get's index at current editor [camera] location
ii) based on dimensions of the tile cluster, it clears out any tile on the map within the cluster

And ClearMap() just clears every possible tile on the map to empty.

n) To add a tile or tile cluster, it's a similar principle, except we must make sure to delete any old tile clusters that may be at that location, set the desired index, and based on the index, we copy in the default image-offset(from tile corner)... In the loop, we can also check properties of the desired tile and determine whether it should be something that we can stand_on or if it's spikes or whatever as a default (based on sheet)... later if we wanted we could make a collision editor that could modify those properties (like for secret areas and such)
Code Snippet
  1. //---------------
  2.  // A D D  T I L E
  3.  //---------------
  4.  public void AddTile(int i)
  5.  {
  6.      DeleteTile();                                                          // Delete any old tile occupations that might overlap
  7.      tiles[loc.X, loc.Y].index  = i;
  8.      tiles[loc.X, loc.Y].offset = sheet[i].offset;
  9.      for(int b=loc.Y; b<loc.Y+sheet[i].tiles_high; b++) {
  10.          if (b >= (TILES_HIGH - 1)) break;
  11.          for (int a = loc.X; a < loc.X + sheet[i].tiles_wide; a++)
  12.          {
  13.              if (a >= (TILES_WIDE - 1)) break;
  14.              TileType type = sheet[i].type;
  15.              tiles[a, b].type = type;
  16.              if ((type == TileType.solid)    || (type == TileType.spring)
  17.               || (type == TileType.platform) || (type == TileType.spikes))
  18.              {
  19.                  tiles[a,b].overlap = true; tiles[a,b].stand_on = true;
  20.                  if (type == TileType.spikes) { tiles[a, b].spikes = true; tiles[a, b].is_solid = true; }
  21.                  else if (type == TileType.solid) tiles[a, b].is_solid = true;
  22.              }
  23.          }
  24.      }            
  25.  }
This may seem redundant, but, by having tiles[x,y] hold some properties that are already in sheet[i], we can customize certain tiles (non-default behaviors) and also access that information a bit easier (less cluttered code later).

It's nice too to have the option to modify a specific map-tile's type (solid, platform, empty, etc):
Code Snippet
  1. //-----------------
  2. // S E T  T Y P E
  3. //-----------------
  4. public void SetType(TileType type = TileType.solid)
  5. {
  6.     int a = loc.X, b = loc.Y;
  7.     tiles[a,b].type = type;
  8.     if ((tiles[a, b].type == TileType.solid) || (tiles[a, b].type == TileType.spring) || (tiles[a, b].type == TileType.platform) || (tiles[a, b].type == TileType.spikes)){
  9.         tiles[a, b].overlap = true; tiles[a, b].stand_on = true;
  10.         if (type == TileType.spikes) { tiles[a, b].spikes = true; tiles[a, b].is_solid = true; }
  11.         else if (type == TileType.solid) tiles[a, b].is_solid = true;
  12.     }
  13. }


And to set a monster at the tile location... we could just set the public field for monster_start, but we'll make a method for this since we may later want to add some other parameter setups (plus less code clutter later):

Code Snippet
  1. // S E T  M O N S T E R
  2. public void SetMonster(MonsterType monster)
  3. {
  4.     tiles[loc.X, loc.Y].monster_start = monster;
  5. }

o) When we first start making a level, it would be good to have a border around the level so that the characters can never go out of bounds when testing. You could also setup a test for this to be safe.
Let's add a method to automatically create a border:

Code Snippet
  1. //-------------------
  2. // A D D  B O R D E R
  3. //-------------------
  4. public void AddBorder(int i)
  5. {
  6.     for (int a = 0; a < TILES_WIDE; a++) { loc.X = a; loc.Y = 0; AddTile(i); loc.Y = TILES_HIGH - 1; AddTile(i); }
  7.     for (int a = 0; a < TILES_HIGH; a++) { loc.X = 0; loc.Y = a; AddTile(i); loc.X = TILES_WIDE - 1; AddTile(i); }
  8. }


You'll want to pass in a single solid block index (like a stone block or a little cloud or some 1x1 solid). This barrier will stop characters from falling out of the level. For level 1 sheet setup you'll see this comment:
// small solid block 6 y
This indicates that the sheet part being made is for a small solid block of index 6 (so we'll pass 6 into AddBorder) ... as long as you use a tile with: "TileType.solid, 1, 1" then it will work.
The 'y' just indicates that keypress of 'y' will place this tile in editor mode.

p) I suppose we'll need a map Update() method which might update various map related animations... for now I think we really only need to update color changes for crystals using a timer. The same timer could be used to update other fx too.
Using a sin (or cos), we can oscillate the RGB values over time. I'll use a 0.2 (out of 1.0) range of color change. That's 20% change plus a base amount of red, green, or blue and each color channel will also use a slightly different rotation offset in the sin also so I'll offset the green by timer+0.5 and the blue by timer+1.0 (keeping in mind that the wave spans 6.28 rads [360 degrees] between each repeat)... so adding 1.0 is only about a 16% difference in the wave result but at least it gives some color change variation.

Code Snippet
  1. //----------------------
  2. // U P D A T E   V A R S  (for updates that apply to both editor and play modes)
  3. //----------------------
  4. public void UpdateVars()
  5. {
  6.     // UPDATE CRYSTAL COLOR
  7.     timer += 0.05f;
  8.     double t = (double)timer;
  9.     // make color channels wave between 0.8 to 1.0 (slightly different wave for each channel)
  10.     crystal_color = new Color( (float)Math.Sin(t)*0.2f+0.8f,  (float)Math.Sin(t+0.5)*0.2f+0.8f,  (float)Math.Sin(t+1.0)*0.2f+0.8f,  255);
  11. }

The result of each sin will be some number within the span of +1 to -1 so the *0.2 will result in +0.2 to -0.2 + 0.8 (20% up or down from 80%) - meaning it will range 0.6 to 1.0 (153 to 255 in byte values) as the timer changes. The alpha stays at 255 since the transparency itself doesn't change (will use texture(bitmap) texels(pixels) to determine alpha(s))


q) We'll need a method to draw the map's tiles onto the scene... if we're in edit mode we should signal it to show boxes or indicators that are helpful in that mode (ie: collision regions, monster starting positions, etc...).
We'll also need to establish which section of tiles would be visible based on camera/map location (loc).

Code Snippet
  1. //-------------------
  2. // D R A W  T I L E S
  3. //-------------------
  4. public void DrawTiles(bool draw_colliders = false, bool edit_mode = false)
  5. {
  6.     // get the region of tiles to draw(top to bottom, left to right) on screen based on map position:
  7.     b1 = loc.Y - TILE_SPAN_Y;
  8.     b2 = loc.Y + TILE_SPAN_Y;
  9.     a1 = loc.X - TILE_SPAN_X;  
  10.     a2 = loc.X + TILE_SPAN_X;

a1 = left most tile, a2 = right most
b1 = top most tile, b2 = bottom most

loc is the map location the camera should be seeing. We'll need to add a world_to_camera method later so we can set loc based on the world coordinate of the camera (this would also adjust the scroll_offset accordingly too). For now, we'll focus on drawing tiles - but first let's make sure the tile group to draw isn't out of bounds of the map of tiles[x,y]:

Code Snippet
  1. // prevent out-of-bounds:
  2. if (b1 < 0) { b1 = 0; if (b2 < 0) b2 = 0; }
  3. if (b2 >= TILES_HIGH) { b2 = TILES_HIGH - 1; if (b1 >= TILES_HIGH) b1 = TILES_HIGH - 1; }
  4. if (a1 < 0) { a1 = 0; if (a2 < 0) a2 = 0; }
  5. if (a2 >= TILES_WIDE) { a2 = TILES_WIDE - 1; if (a1 >= TILES_WIDE) a1 = TILES_WIDE - 1; }

a1 and b1 might no longer be what they were if they had been out of bounds. Let's find out the number of tiles there are between the center tile and the edges:

Code Snippet
  1. // calculate start coordinates for drawing tiles:
  2. int bdif = loc.Y - b1; // how many tiles up   from middle of screen
  3. int adif = loc.X - a1; // how many tiles left from middle of screen

We can use this to find the screen coordinate where the first tile starts drawing (it will probably be off-screen to the top-left, but if the image is large enough, part of it will show on screen). Since each tile is 64x64 pixels, we multiply the number of tiles (adif or bdif) by 64.0f to get the pixel distance from the center:

Code Snippet
  1. sx = screen_center.X - adif * 64.0f; // calculate starting x coordinate of tiles shown on screen
  2. sy = screen_center.Y - bdif * 64.0f; // calculate starting y coordinate of tiles shown on screen


You may remember from before that screen_center represents where a centered tile would be on the screen (mainTarget) [which is why we previously translated the coord by (-32,-32) [half a tile size])
Now sx,sy represent the first position tiles will start drawing at when looping between b1 to b2 on the outer loop and a1 to a2 on the inner loop.
We'll need a few more variables to track our progress through the loops:

Code Snippet
  1. // draw the section of tiles that should be seen:
  2. float x, y;        // screen coord of tile
  3. int a, b, i;       // current tile indices and sheet index
  4. Sheet sh;          // reference to a sheet
  5. overlap_count = 0; // will count overlap tiles
  6. crystal_count = 0;
  7. Vector2 tile_pos;  // final tile position on screen (with offsets)

- a,b used with tiles[a,b] in the loop
- x,y tracking screen position of tile in loop
- i = sheet index
- sh = points to the sheet part (at index i)
- overlap_count, crystal_count ... just counts how many ProcessedTiles have been stored to be drawn later
- tile_pos ... the final draw position after offsets are added

We'll loop through outer loop b1 to b2 using b (while keeping track of the y position of the tiles to draw starting it at y=sy) and the inner loop from a1 to a2 using a... and at the start of each inner loop we set the x coord to sx
In each loop, we'll reposition the tiles based on scroll_offset (0-64,0-64) for smooth scrolling... so tile_pos will be the x,y of the tile + scroll_offset ... we'll update scroll_offset later based on camera position.
If the index read from the map's tile is 0... then it's empty and it can skip the loop this time (but only after indicators are drawn which we'll add later)
We'll also need to offset the image for the tile_pos by the tile[a,b].offset so that it's correctly situated about it's corresponding tile location. tile[a,b].offset is normally extracted from the sheet part's calculated image offset but can be further offset later using the editor... this way you can move objects away from their home tile and position, rotate, and scale them however you want.
Then we either store the tile_pos (and any other draw data) as a ProcessedTile (to be shown later)... or we draw it now if it's a background tile-part or just something that will be under the player or other characters.

Code Snippet
  1. b = b1; y = sy;
  2. while (b < b2)
  3. {
  4.     a = a1; x = sx;
  5.     while (a < a2)
  6.     {
  7.         i = tiles[a, b].index;
  8.         tile_pos = new Vector2(x, y) - scroll_offset; // x,y location - scrolling amount (0-64) [ ie: scroll 0-64 then new tile loc ]
  9.                                                
  10.         
  11.         // A C T U A L  T I L E  I M A G E S  A R E   D R A W N -------------------------------------------------------
  12.         if (i == 0) { a++; x += 64.0f; continue; }     // empty so skip to next one
  13.         sh = sheet[i];
  14.         tile_pos += tiles[a, b].offset;                // make sure to add the tile's offset (editor-customized placement of image)
  15.         // STORE OVERLAPS AND CRYSTALS ... ELSE DRAW IT
  16.         if (tiles[a, b].overlap) {
               overlapTiles
    [overlap_count].Add(tile_pos, sh.rect, tiles[a, b].rot, tiles[a, b].scale); overlap_count++;
            }
  17.         else if (tiles[a, b].type == TileType.reflector) {
               crystals[crystal_count].Add(tile_pos, sh.rect, tiles[a, b].rot, tiles[a, b].scale); crystal_count++;
            }
  18.         else spriteBatch.Draw(tiles_image, tile_pos, sh.rect, Color.White, tiles[a, b].rot, Vector2.Zero, tiles[a, b].scale,                               SpriteEffects.None, 0f);
  19.         //-------------------------------------------------------------------------------------------------------------
  20.         
  21.         a++; x += 64.0f;
  22.     }
  23.     b++; y += 64.0f;
  24. }

As you can see, the x and y coordinates move 64 pixels per loop with each (a,b) tile update.


Technically we could have said sheet[i].rect instead of pointing to sheet[i] and using sh.rect... we didn't really gain anything other than to shorten a few lines... but keep this idea in mind later because sometimes it does make a big difference in code clutter (as long as it's still easy to understand the code).
Also technically if you wanted, with a few changes, you could use Lists for overlapTiles and crystals (the ProcessedTiles)... performance difference would not be noticable.

spriteBatch draws from the tiles_image loaded in Game1.cs at the new tile position extracting from source-rectangle: sheet[i].rect, using tile's rotation, at origin (0,0), using tile's size (default 1,1), non-flipped, depth 0
Later you may have tiles like slopes that need to be flipped in which case you'll need to store that data also.

Let's add some indicators to this loop which indicate solid tiles, platforms, and monster starting locations... I'll extract from my tiles_image, a square with an X through it as an indicator. If your tiles_image has a different indicator size and position, you'll need to change the source rectangles. Here's the new code with indicators added before the the draw call and you'll notice it skips the tile drawing... well... the reason for this is in the editor mode, it will draw ALL the tiles first, and then if it is set to show colliders, it will call the draw method again with show_colliders set to true (and edit mode too obviously). With just edit mode it will draw monster starting locations over top of the scene and with show_colliders it will draw boxes showing collision regions over the scene (and it updates the loop and uses continue to skip redrawing the tiles). If you examine the code you'll probably understand what I mean better:

Code Snippet
  1. b = b1; y = sy;
  2. while (b < b2)
  3. {
  4.     a = a1; x = sx;
  5.     while (a < a2)
  6.     {
  7.         i = tiles[a, b].index;
  8.         tile_pos = new Vector2(x, y) - scroll_offset; // x,y location - scrolling amount (0-64) [ ie: scroll 0-64 then new tile loc ]
  9.  
  10.  
  11.         // E D I T O R   L O C A T I O N  H E L P E R S  ( EDIT MODES ONLY ) --------------------------------------------------------
  12.         // if draw_colliders is true then all we do is draw the colliders and continue (skips the other drawing since it was already drawn)
  13.         if (draw_colliders)
  14.         {
  15.             if ((tiles[a, b].is_solid) || (tiles[a, b].stand_on))
  16.             {
  17.                 Vector2 siz = Vector2.One;
  18.                 if (!tiles[a, b].is_solid) siz = new Vector2(1f, 0.4f);  // show passable platforms differently
  19.                 spriteBatch.Draw(tiles_image, tile_pos, new Rectangle(960, 0, 63, 63), Color.Purple,
                                     0f, Vector2.Zero, siz, SpriteEffects.None, 0f);
  20.                 a++; x += 64.0f; continue;
  21.             }
  22.         }
  23.         if (edit_mode)
  24.         {
  25.             if (tiles[a, b].monster_start != MonsterType.None)
  26.             {
  27.                 spriteBatch.Draw(tiles_image, tile_pos, new Rectangle(960, 0, 63, 63), Color.Yellow,
                                     0f
    , Vector2.Zero, 1f, SpriteEffects.None, 0f);
  28.             }
  29.             a++; x += 64.0f; continue;
  30.         }//-------------------------------------------------------------------------------------------------------------------------
  31.                                                
  32.         
  33.         // A C T U A L  T I L E  I M A G E S  A R E   D R A W N --------------------------------------------------------------------
  34.         if (i == 0) { a++; x += 64.0f; continue; }               // empty so skip to next one
  35.         sh = sheet[i];
  36.         tile_pos += tiles[a, b].offset;                          // make sure to add the tile's offset (editor-customized placement of image)
  37.         // STORE OVERLAPS AND CRYSTALS ... ELSE DRAW IT
  38.         if (tiles[a, b].overlap) { overlapTiles[overlap_count].Add(tile_pos, sh.rect, tiles[a, b].rot, tiles[a, b].scale); overlap_count++; }
  39.         else if (tiles[a, b].type == TileType.reflector) { crystals[crystal_count].Add(tile_pos, sh.rect, tiles[a, b].rot, tiles[a, b].scale); crystal_count++; }
  40.         else spriteBatch.Draw(tiles_image, tile_pos, sh.rect, Color.White, tiles[a, b].rot, Vector2.Zero, tiles[a, b].scale, SpriteEffects.None, 0f);
  41.         //-------------------------------------------------------------------------------------------------------------------------
  42.         
  43.         a++; x += 64.0f;
  44.     }
  45.     b++; y += 64.0f;
  46. }

The indicators would be colored more appropriately if the original box image was white or light-gray. Since it's red, it pretty much only shows up in shades of red. If it was purple originally, I could get shades of blue, purple, or red also... you get the idea... we need at least some color in each color channel to get desired color tints.

We need methods also for drawing the stored ProcessedTiles (like overlaps and crystals):

Code Snippet
  1. //-------------------------
  2. // D R A W  O V E R L A P S ( probably more than half the tiles are overlaps [drawn on top of character layer] )  
  3. //-------------------------
  4. public void DrawOverlaps()
  5. {
  6.     int i = 0;
  7.     while (i < overlap_count)
  8.     {
  9.         spriteBatch.Draw(tiles_image, overlapTiles[i].pos, overlapTiles[i].rect, Color.White, overlapTiles[i].rot, Vector2.Zero, overlapTiles[i].scale, SpriteEffects.None, 0f);
  10.         i++;
  11.     }
  12. }
Simply blasts the stored images onto the scene based on calculations made in DrawTiles... So after the background tiles and characters are all draw, the overlapping stuff can be drawn with this.
Same principle applies to crystals, except they're drawn immediately after the background tiles and they make use of a sprites render-target that holds all the character images. Afterwards the actual sprite target layer is drawn onto the scene, and finally the overlaps (above).
Here's the method:
Code Snippet
  1. //-------------------------
  2. // D R A W  C R Y S T A L S
  3. //-------------------------
  4. public void DrawCrystals()
  5. {
  6.     int i = 0;
  7.     while (i < crystal_count)
  8.     {
  9.         spriteBatch.Draw(tiles_image, crystals[i].pos, crystals[i].rect, crystal_color, crystals[i].rot, Vector2.Zero, crystals[i].scale, SpriteEffects.None, 0f);
  10.         i++;
  11.     }
  12. }

The effects (and needed sprite render-target) are set up in Game1 before calling this draw method. I'll show you how to do this later...

r) Right-Click on "Map Related Stuff" in solution explorer and Add>>Class "Conv"
In Conv.cs, change it to:
static public class Conv
{
}


This will contain universally available functions that can be used for conversions...
We'll need:
GetTileCoord which takes a world position and returns a tile location as a Point (indices x,y)
We'll make 2 versions of it ... one which also takes a reference to an offset. The offset is updated and can be used to get the correct scroll_offset based on the world_position. We'll apply this later for getting the tile location and scroll_offset based on camera position.
We'll also want an easy way to convert world coords to screen coords (and for Rectangles)... and maybe even a simple way to get a tile's world coordinates:

Code Snippet
  1. // -------------------------
  2. // G E T  T I L E  C O O R D  
  3. // -------------------------
  4. /// <summary>
  5. /// Gets the tile map coordinate that something occupies [ie: 16 tiles right, 12 tiles down]
  6. /// </summary>
  7. /// <param name="position"> The world coordinate (in pixels) </param>
  8. /// <param name="offset">   The pixel offset from the corner of the occupied tile location that is returned (ie: useful for smooth scrolling)  </param>
  9. /// <returns> The occupied tile location in tiles [ie: 6 tiles right, 4 tiles down] </returns>
  10. static public Point GetTileCoord(Vector2 position, ref Vector2 offset)
  11. {
  12.     Point tile;
  13.     tile.X = (int)position.X / 64;
  14.     tile.Y = (int)position.Y / 64;
  15.     offset.X = (int)(position.X - tile.X * 64);
  16.     offset.Y = (int)(position.Y - tile.Y * 64);
  17.     return tile;
  18. }
  19. static public Point GetTileCoord(Vector2 position)
  20. {
  21.     Point tile;
  22.     tile.X = (int)position.X / 64; tile.Y = (int)position.Y / 64;            
  23.     return tile;
  24. }

The first GetTileCoord (will be used with camera world position) is a little more complex than the second one since we also need the scroll_offset (0-64, 0-64)... So we get the tile that the position would exist at by dividing by 64(tile size) and then obtain the difference from the world-position-corner(tile * 64) of the closest tile to the actual world position. This difference would be the scroll offset.

Go back to Game1.cs and add this to the top with the other variables:

Code Snippet
  1. static public Vector2 cam_pos;               // world position of camera


This will be used to sort out where we're looking in in world coordinates (pixel distances) on the game map.

Go back to Conv.cs
and add these other conversions:

Code Snippet
  1.  //----------------------------
  2.  // W O R L D  T O  S C R E E N
  3.  //----------------------------
  4.  static public Vector2 world_to_screen(Vector2 world_position)
  5.  {            
  6.      return world_position - Game1.cam_pos + Game1.screen_center;
  7.  }
  8.  
  9.  // BBOX WORLD TO SCREEN
  10.  static public Rectangle bbox_world_to_screen(Vector4 bbox)
  11.  {
  12.      bbox.X = bbox.X - Game1.cam_pos.X + Game1.screen_center.X; bbox.Y = bbox.Y - Game1.cam_pos.Y + Game1.screen_center.Y;
  13.      bbox.Z = bbox.Z - Game1.cam_pos.X + Game1.screen_center.X; bbox.W = bbox.W - Game1.cam_pos.Y + Game1.screen_center.Y;
  14.      Rectangle s_box = new Rectangle((int)bbox.X, (int)bbox.Y, (int)(bbox.Z - bbox.X), (int)(bbox.W - bbox.Y));
  15.      return s_box;
  16.  }
  17.  
  18.  
  19.  //------------------------
  20.  // T I L E  T O  W O R L D
  21.  //------------------------
  22.  static public Vector2 tile_to_world(Point tile_loc)
  23.  {
  24.      return new Vector2(tile_loc.X * 64, tile_loc.Y * 64);
  25.  }

world_to_screen simply takes a world coordinate and figures out where it would be relative to the camera (as positioned over the screen center).
bbox_world_to_screen is the same idea. tile_to_world is just converting a tile location on the map to actual world coordinates (in pixels).

Now return to Map.cs
And we can add the following method:

Code Snippet
  1. //----------------------------
  2. // W O R L D  T O  C A M E R A -- MATCH WORLD VIEW TO CAMERA
  3. //----------------------------
  4. public void world_to_camera(Vector2 cam_pos, ref Vector2 background_pos)
  5. {
  6.     scroll_offset = Vector2.Zero;
  7.     loc = Conv.GetTileCoord(cam_pos, ref scroll_offset);       // get location of center of screen as a tile coordinate in the map (and map's scroll offset [0-64, 0-64])
  8.   nd: #0c0c0c">// W O R L D  T O  C A M E R A -- MATCH WORLD VIEW TO CAMERA
  9. //----------------------------
  10. public void world_to_camera(Vector2 cam_pos, ref Vector2 background_pos)
  11. {
  12.     scroll_offset = Vector2.Zero;
  13.     loc = Conv.GetTileCoord(cam_pos, ref scroll_offset);       // get location of center of screen as a tile coordinate in the map (and map's scroll offset [0-64, 0-64])
  14.     background_pos.X = (loc.X * 64 + scroll_offset.X) * -0.5f; // reminder: using wrap mode to draw background (so values will wrap)
  15.     background_pos.Y = (loc.Y * 64 + scroll_offset.Y) * -0.5f; // so set scroll of background to half of total world scroll at this location (half since it scrolls slower)
  16. }

This uses the camera's world position to get a camera tile location (to scroll tiles around) and the scroll offset(0-64,0-64)... It will know which section of tiles to draw based on the loc value and how to position them based on the scroll_offset.
scroll_offset is calculated appropriately by Conv.GetTileCoord
We also need to update the background scrolling based on how much we've moved through the level (and since it's wrapping we don't care how big these numbers get). We multiply by -0.5 to cut the distance covered in half (because backgrounds are farther away) and negative because it's changing the sample source position rather than the destination position (because sample-wrapping).

s) Go back to Game1.cs

Add the following variables up top somewhere:

Code Snippet
  1. //MAP DATA (put this in a class later)         
  2. const int             MAX_SHEET_PARTS = 300;                // maximum allowed sprite-sheet parts for tiles image        
  3. Sheet[]               sheet;                                // sprite sheet data for tiles (could use a list)
  4. SheetManager          sheet_mgr;                            // where a level's sheet definitions can be edited  
  5. Map                   map;                                  // holds all the tiles map stuff
  6. //Editor                editor;                               // map editor        
 
We haven't made an editor yet, so we'll leave a comment as a reminder for now.
We'll need our Sheet[] which actually holds the sheet parts (tile image data). sheet_mgr will just setup sheet data for each level.

In Initialize() we need to init these:
Code Snippet
  1. // INIT MAP
  2. sheet      = new Sheet[MAX_SHEET_PARTS];
  3. sheet_mgr  = new SheetManager();
  4. map        = new Map(sheet, spriteBatch);

In LoadContent():
// SET IMAGE FOR TILES
map.SetTilesImage(tiles_image); // this might be used in multiple places so send a reference to map but store tiles here

And add this in there too:
Code Snippet
  1. // later we will want update to check if loading a new area is needed... and then have it call a method to load based on the level or area (game-state in update)
  2. sheet_mgr.Setup_Sheet_Level_1(ref sheet);            
  3.  
  4. // MAKE SAFETY BORDER FOR MAP (using tile 6 - small block):
  5. map.AddBorder(6);

SetTilesImage set's our tile sheet (texture2D) and sheet_mgr sets up the rectangle sources and offsets and such for the images on that sheet (to be used as tiles or tile clusters) and AddBorder puts a safety barrier around the level so players don't fall out.

Go to Game1.cs Update()
After the Keypress(Escape) for Exit, Add the call to map update (which only updates crystal colors for now):
map.UpdateVars();

Also we'll need a GameState switch... right now this could be edit mode or play mode or game over ... etc...
Later you may want to add states to load_level(n), play_cut_scene(n) or special menus... You may even have states within states like a type of MenuState in play mode (ie: none, main_menu, inventory, spell_select, etc...)

For now we'll add a few basic state switches:

Code Snippet
  1. switch (gameState)
  2. {
  3.     case GameState.play:
  4.         //-----------------
  5.         // P L A Y  M O D E  (input/updates)                                        
  6.  
  7.         // CHECK FOR GAMESTATE CHANGES
  8.         if (inp.Keypress(Keys.E)) { gameState = GameState.edit; }                    
  9.  
  10.         // MOVE CAMERA
  11.         //cam_pos += (player.pos - cam_pos) * 0.1f;                 // smooth pan camera toward player's world position
  12.  
  13.         // MATCH WORLD VIEW TO CAMERA
  14.         map.world_to_camera(cam_pos, ref background_pos);         // (what you see in the world based on where camera is)                    
  15.  
  16.         break;
  17.     case GameState.edit:
  18.         //----------------------
  19.         // E D I T O R   M O D E  (input/updates)
  20.         cam_pos = map.loc.ToVector2() * 64;                    
  21.  
  22.         // SWITCH TO PLAY MODE
  23.         if (inp.Keypress(Keys.Enter)) gameState = GameState.play;
  24.         break;   //---------------- end editor mode
  25.     case GameState.game_over:
  26.         // G A M E  O V E R  (input)
  27.         if (inp.Keypress(Keys.Escape) || inp.Keypress(Keys.Enter)) Exit(); // change to go to menu state later
  28.         break;
  29. }


I left a comment for how to smooth pan the camera toward the player... (we still need to make a player class).
I'll explain this in more detail later.
In edit mode we'll eventually add a call to editor.Update() [after we make the editor]... and in this we can get input from the user to change loc directly causing the tiles shown to change in accordance to what the center tile (loc) is. There's no smooth scrolling in editor mode so we just set the camera position to the map's world position [in pixels, so * 64]
E key goes to edit mode and Enter goes to play mode.
Game Over state allows exit only (for now), but later it would best to go to a title screen with a menu of options.
As you may remember from before, map.world_to_camera will update the loc (for camera tile location) and scroll_offset (for smooth scrolling) as well as modify background_pos so that it wrap-scrolls the background sample sources according to world_position.
Another reason you may want to scroll backgrounds based on world position(rather than updating based on scroll_offset changes), is because (for example) if you had a non-repeating middle-background that was taller than the mainTarget display, and you wanted it to scroll vertically... you'd need it to reach the bottom of the y scrolling when you reach the bottom of the level... and the top of the image when you reach the top of the level... In this case the y scrolling for the middle background would be a percentage of the middle background's scroll maximum distance based on the percentage down your player is toward the bottom of the level. However in that case you'd need a few extra variables.

8) In Game1.cs Draw()
We'll lay down the drawing frame-work and psuedocode so we know what order to draw things and how to insert our render targets and effects into the scene just right. Since many components aren't developed yet, they'll just be comment placeholders for now:

Code Snippet
  1.  // DRAW SPRITES TO A SPRITE TARGET (so we can use it in crystal fx):
  2.  if (gameState == GameState.play) //PLAY MODE
  3.  {
  4.      // SET RENDER TARGET FOR SPRITES (and clear it as TransparentBlack)
  5.      
  6.      // DRAW MONSTERS
  7.      
  8.      // DRAW PLAYER(S)                
  9.  }
  10.  //-------------------
  11.  
  12.  GraphicsDevice.SetRenderTarget(MainTarget); // like a substitute backbuffer at desired resolution (stretched to true backbuffer after)
  13.  
  14.  // SKIP TO DRAW_OTHER_STATES IF NEEDED:
  15.  if ((gameState != GameState.edit) && (gameState != GameState.play)) goto DRAW_OTHER_STATES; // an old tactic to avoid superfluous programming (use goto very sparingly and very cautiously)
  16.  
  17.  
  18.  //-------------------------------------only for edit and play gameStates -------------------------------:
  19.  // DRAW OPAQUE FAR BACKGROUND
  20.  spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Opaque, SamplerState.LinearWrap);      // wrapped background 1
  21.  spriteBatch.Draw(far_background, screenRect, new Rectangle((int)(-background_pos.X * 0.5f), 0, far_background.Width, far_background.Height), Color.White); // scroll at half speed so *0.5
  22.  spriteBatch.End();
  23.  // DRAW MID BACKGROUND(S)
  24.  spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive, SamplerState.LinearWrap);    // wrapped background 2       
  25.  spriteBatch.Draw(mid_background, screenRect, new Rectangle((int)(-background_pos.X), (int)-background_pos.Y, mid_background.Width, mid_background.Height), Color.White);
  26.  spriteBatch.End();
  27.  
  28.  
  29.  //-------------------            
  30.  spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.None, RasterizerState.CullNone);
  31.  map.DrawTiles();      // D R A W  T I L E S
  32.  spriteBatch.End();
  33.  //-------------------
  34.  
  35.  // APPLY A CRYSTAL EFFECT TO THESE:                                   
  36.  // ...SET CRYSTAL VALUES FOR CRYSTAL FX... ie: the render target for sprites that will be made(sprite_target) [see above in play mode]
  37.  // SPRITEBATCH BEGIN WITH CRYSTAL FX
  38.  //map.DrawCrystals();   // D R A W  C R Y S T A L S
  39.  // SPRITEBATCH END
  40.  
  41.  
  42.  spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.None, RasterizerState.CullNone);
  43.  
  44.  // SHOW SPRITES
  45.  // DRAW THE SPRITE TARGET INTO THE SCENE
  46.  
  47.  // DRAW OVERLAPS
  48.  map.DrawOverlaps();
  49.  
  50.  // DRAW ADDITIVE EFFECTS            
  51.  
  52.  // E D I T  M O D E  only  - - - - -
  53.  if (gameState == GameState.edit)
  54.  {
  55.      // EDITOR DRAW LOCATORS
  56.  } // - - - - - - - - - - - - - - - -
  57.  
  58.  // TEXT / HUD ---
  59.  if (gameState == GameState.play)
  60.  {
  61.      spriteBatch.DrawString(font, "Press E to go to map editor", new Vector2(1, screenH - 50), Color.DarkGreen); spriteBatch.DrawString(font, "Press E to go to map editor", new Vector2(0, screenH - 49), Color.GreenYellow);
  62.     // DRAW HUD HERE (heads up display... ie: lifebar)
  63.  }
  64.  else
  65.  {   // SHOW EDITOR INSTRUCTIONS                
  66.  }
  67.  spriteBatch.End();
  68.  
  69.  
  70.  DRAW_OTHER_STATES:                                        // (label for goto to avoid superfluous programming [use sparingly and cautiously] )
  71.  //----------------
  72.  switch (gameState)
  73.  {
  74.      case GameState.game_over:                             // maybe add fade transitions later
  75.          spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearWrap);
  76.          GraphicsDevice.Clear(Color.Black);
  77.          // SHOW TEXT FOR GAME OVER
  78.          spriteBatch.End();
  79.          break;
  80.  }
  81.  
  82.  
  83.  // DRAW TEMP BACKBUFFER (MainTarget) to TRUE BACKBUFFER IN MAXIMIZED WINDOWED MODE (this resolves some rare but possible compatibility problems):
  84.  GraphicsDevice.SetRenderTarget(null);
  85.  spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Opaque, SamplerState.LinearWrap, DepthStencilState.None, RasterizerState.CullNone);
  86.  spriteBatch.Draw(MainTarget, desktopRect, Color.White);
  87.  spriteBatch.End();


We still don't have a font, so spriteBatch.DrawString won't work just yet.

9) Fonts... Right-click on Content.mgcb to open the Monogame Pipeline and click Edit>>Add>>New Item and select spriteFont and name it Font. You can open the file and modify the font's properties. I changed it to size 14, and bold so it's easy to read.

Go to the top of Game1.cs and under //DISPLAY parameters (under SpriteBatch spriteBatch;) add the following:
SpriteFont font;


Now under LoadContent(), under //LOAD GRAPHICS section, add:
font = Content.Load<SpriteFont>("Font");

You've now loaded the font to use with DrawString in the above Draw code.

10) Game Over state... now that we have a font... for now, let's just put a simple Game Over message on the screen. Just go into Draw() and replace // SHOW TEXT FOR GAME OVER with the DrawStrings you see below:
Code Snippet
  1. DRAW_OTHER_STATES:                                        // (label for goto to avoid superfluous programming [use sparingly and cautiously] )
  2. //----------------
  3. switch (gameState)
  4. {
  5.     case GameState.game_over:                             // maybe add fade transitions later
  6.         spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearWrap);
  7.         GraphicsDevice.Clear(Color.Black);
  8.         spriteBatch.DrawString(font, "G A M E   O V E R", screen_center + new Vector2(-80, -20), Color.LimeGreen);
  9.         spriteBatch.DrawString(font, "  (press enter)  ", screen_center + new Vector2(-62, 20), Color.LimeGreen);
  10.         spriteBatch.End();
  11.         break;
  12. }

The above just offsets the 2 lines of text 20 pixels above the screen center and 20 below it (the x offsets depend on text size).
So, centering the strings may depend a bit on the message length and font size
You could make a text class to handle this automatically too:
ie: text.DrawCentered("G A M E   O V E R", 0, -20, Color.Lime);

>>>NEXT PAGE (2)<<<

>>> Back to Game Design <<<