MonoGame (new XNA) Tutorial:
Platformer Game Programming Tutorial                         PAGE 2

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

Page2:

11) In solution explorer, right-click on the folder: Map Related Stuff and Add>>Class "Editor"

The Editor will need access to some things:
The map, its tiles, sheet parts, the input class, (eventually a player class), eventually a monster manager
Also a string containing the save-time (to show verification that the file saved at whatever time)
And a boolean to toggle on/off for showing colliders

Code Snippet
  1. class Editor
  2. {        
  3.     Map        mp;                                     // reference to Map in Game1
  4.     Tile[,]    tiles;                                  // reference to tiles in Map
  5.     Sheet[]    sheet;                                  // refer to sheet data in Game1
  6.     Input      inp;                                    // refer to input from Game1
  7.     //Player     player;                                 // later we'll want to refer to player made in Game1 (not made yet)
  8.     //MonsterSys monsterSys;                             // later we'll want to refer to monsterSys in Game1 (not made yet)
  9.     string     time_saved = "-";                       // when was the file saved (to confirm file save worked)
  10.     bool       show_colliders;                         // hit f12 to see collision map

The constructor will set the above fields to refer to the ones made in Game1... since Player class and MonsterSys don't exist yet, those are commented out and there as reminders to add them later.

Code Snippet
  1. // C O N S T R U C T
  2. //public Editor(Map map, Input inpt, /*Player playr,*/ Sheet[] sht /*, MonsterSys monst*/)
  3. public Editor(Map map, Input inpt, Sheet[] sht)
  4. {
  5.     mp         = map;
  6.     tiles      = mp.tiles;
  7.     inp        = inpt;
  8.     //player     = playr;
  9.     sheet      = sht;
  10.     //monsterSys = monst;
  11. }
 

12) Update()... first we'll start with some basic map navigation controls:

Code Snippet
  1. //------------
  2. // U P D A T E   ( editor input )
  3. //------------
  4. public void Update()
  5. {
  6.     mp.scroll_offset = Vector2.Zero;
  7.     bool shift_down = inp.shift_down;
  8.     if (inp.Keypress(Keys.Right) || (shift_down && inp.Keydown(Keys.Right))) { mp.loc.X++; Game1.background_pos.X--; }
  9.     if (inp.Keypress(Keys.Left) || (shift_down && inp.Keydown(Keys.Left))) { mp.loc.X--; Game1.background_pos.X++; }
  10.     if (inp.Keypress(Keys.Down) || (shift_down && inp.Keydown(Keys.Down))) { mp.loc.Y++; }
  11.     if (inp.Keypress(Keys.Up) || (shift_down && inp.Keydown(Keys.Up))) { mp.loc.Y--; }
  12.  
  13.     // PREVENT OUT OF BOUNDS
  14.     if (mp.loc.X >= Map.TILES_WIDE) { mp.loc.X = Map.TILES_WIDE - 1; Game1.background_pos.X++; }
  15.     if (mp.loc.X < 0) { mp.loc.X = 0; Game1.background_pos.X--; }
  16.     if (mp.loc.Y >= Map.TILES_HIGH) { mp.loc.Y = Map.TILES_HIGH - 1; }
  17.     if (mp.loc.Y < 0) { mp.loc.Y = 0; }

a) The map scroll_offset in editor should always be 0. Each time we press a direction, it will shift the tiles 1 in that direction (changes the map location by 1 tile)... there's no smooth scrolling in edit mode to make it simple to edit.
If shift key is held, the map location will change faster (because it checks if the key is held instead of pressed)... so good to keep this in mind if you want to move through your map really fast.
Background is moved horizontally too just so it looks better (didn't bother with Y direction since it's mostly a horizontal map)

map location is then tested to see if it's out of bounds and if so, adjusted to prevent ... problems... (and it un-moves the background just so it doesn't keep moving when you hit the world edge)

b) We'll also need controls for adding and deleting tiles for the map... And we'll allow setting a tile to solid too (like if you want a background image to be something you could stand on for example):

Code Snippet
  1. // DELETE TILE
  2. if (inp.Keypress(Keys.Delete) || inp.Keypress(Keys.Back)) { mp.DeleteTile(); /*will also delete monsters later*/ }
  3.  
  4. // SET COLLIDER
  5. if (inp.Keydown(Keys.Insert)) mp.SetType(); // sets tile as solid by default
  6.  
  7. // ADD TILE (refer to sheet manager)
  8. if (inp.Keypress(Keys.Q)) mp.AddTile(1); if (inp.Keypress(Keys.W)) mp.AddTile(2); if (inp.Keypress(Keys.E)) mp.AddTile(3);
  9. if (inp.Keypress(Keys.R)) mp.AddTile(4); if (inp.Keypress(Keys.T)) mp.AddTile(5); if (inp.Keypress(Keys.Y)) mp.AddTile(6);
  10. if (inp.Keypress(Keys.U)) mp.AddTile(7); if (inp.Keypress(Keys.I)) mp.AddTile(8); if (inp.Keypress(Keys.O)) mp.AddTile(9);
  11. if (inp.Keypress(Keys.P)) mp.AddTile(10); if (inp.Keypress(Keys.A)) mp.AddTile(11); if (inp.Keypress(Keys.S)) mp.AddTile(12);
  12. if (inp.Keypress(Keys.D)) mp.AddTile(13); if (inp.Keypress(Keys.F)) mp.AddTile(14); if (inp.Keypress(Keys.G)) mp.AddTile(15);
  13. if (inp.Keypress(Keys.H)) mp.AddTile(16);
  14. // (add more later)

The keys and numbers are kept track of in comments in SheetMgr, so copying that down to paper might be handy for editing (maybe with thumbnail images) ... altho trial and error work too... ;)
Eventually you may want to make a tiles sheet image pop up optionally, where you can select parts by mouse.

We'll need to get the world coordinate on the map for placing players, NPC's, and Monsters (since they're tracked by world coord):

Code Snippet
  1. // GET WORLD POS OF TILE (to edit)
  2. Vector2 world_coord = Conv.tile_to_world(mp.loc);
  3.  
  4. // ADD CHARACTER / MONSTER  
  5. // LATER: Add keypress test to place a monster at the world coordinate
  6.  
  7. // PLACE PLAYER
  8. if (inp.Keypress(Keys.M))
  9. {
  10.     mp.startData.x = mp.loc.X; mp.startData.y = mp.loc.Y;
  11.     //player.pos = world_coord + new Vector2(-32, -32);    // this should work after we've made the player class
  12. }


For now I just left comments about adding monsters and players... but you can set the start location for the player which is saved to file. The placement position will always be a tile center (thus -32, -32 since the origin [or root bone] will be in the center of each character)

And we'll add a toggle for showing colliders using the F12 key:

// SHOW HELPERS (ie: colliders)

if (inp.Keypress(Keys.F12)) show_colliders = !show_colliders;

c) Also we'll want to be able to edit each individual tile's properties such as:
- Rotation
- Scale
- Offset (reposition)
- Overlap (if it is drawn over top of characters or not)
And we'll set the Home key to reset all the properties to defaults.

Code Snippet
  1. // EDIT A TILE
  2. if (tiles[mp.loc.X, mp.loc.Y].index > 0)
  3. {
  4.     if (inp.Keydown(Keys.OemPeriod)) tiles[mp.loc.X, mp.loc.Y].rot += 0.01f;
  5.     if (inp.Keydown(Keys.OemComma)) tiles[mp.loc.X, mp.loc.Y].rot -= 0.01f;
  6.     if (inp.Keydown(Keys.OemPlus)) tiles[mp.loc.X, mp.loc.Y].scale += new Vector2(0.01f, 0.01f);
  7.     if (inp.Keydown(Keys.OemMinus)) tiles[mp.loc.X, mp.loc.Y].scale -= new Vector2(0.01f, 0.01f);
  8.     if (inp.Keydown(Keys.NumPad8)) tiles[mp.loc.X, mp.loc.Y].offset.Y--;
  9.     if (inp.Keydown(Keys.NumPad2)) tiles[mp.loc.X, mp.loc.Y].offset.Y++;
  10.     if (inp.Keydown(Keys.NumPad4)) tiles[mp.loc.X, mp.loc.Y].offset.X--;
  11.     if (inp.Keydown(Keys.NumPad6)) tiles[mp.loc.X, mp.loc.Y].offset.X++;
  12.     if (inp.Keypress(Keys.PageUp)) tiles[mp.loc.X, mp.loc.Y].overlap = true;
  13.     if (inp.Keypress(Keys.PageDown)) tiles[mp.loc.X, mp.loc.Y].overlap = false;
  14.     // reset tile modifications:
  15.     if (inp.Keypress(Keys.Home))
  16.     {
  17.         tiles[mp.loc.X, mp.loc.Y].offset = sheet[tiles[mp.loc.X, mp.loc.Y].index].offset;
  18.         tiles[mp.loc.X, mp.loc.Y].rot = 0;
  19.         tiles[mp.loc.X, mp.loc.Y].scale = Vector2.One;
  20.     }
  21. }

Rotation is in radians so small increments. Right now, we're only using uniform scaling but non-uniform would be a useful feature to add later to make the levels look more organic (more variety). This is also useful for sculpting in shadow maps and light maps.
The default offset of the image can be changed (more useful for non-collision images...) .. this way you could group a bunch of items close together (like flowers for example).

d) Soon we'll add file saving and loading... we'll provide the input option... for now, you may want to comment out the unavailable methods until you've made them... or not since we'll make them soon anyway...

Code Snippet
  1.    // SAVE LEVEL MAP
  2.     if (shift_down && inp.Keypress(Keys.D4))
  3.     {
  4.         SaveLevel(Game1.LEVEL_NAME); // not made yet, but we'll make a SaveLevel method soon
  5.     }
  6.  
  7.     // LOAD LEVEL MAP
  8.     if (shift_down && inp.Keypress(Keys.D1)) if (File.Exists(Game1.LEVEL_NAME))
  9.     {
  10.         LoadLevel(Game1.LEVEL_NAME); // not made yet, but we'll make a LoadLevel method soon
  11.     }
  12.  
  13. } // ^^^ Update ^^^ (editor)


13) Draw Locators... will optionally provide rectangle images to show where player or monster start locations are.
For now, we don't have a monster draw method yet so we'll just add a comment reminder.

Code Snippet
  1. //-------------------------
  2. // D R A W  L O C A T O R S (ie: player starting position on editor map)
  3. //-------------------------
  4. public void DrawLocators(SpriteBatch spriteBatch, Texture2D tiles_image, Vector2 screen_center)
  5. {
  6.     // show the tile location:
  7.     spriteBatch.Draw(tiles_image, screen_center, new Rectangle(960, 0, 63, 63), new Color(100, 100, 100, 100), 0f, Vector2.Zero, 1f, SpriteEffects.None, 0f);
  8.     // show player starting location if visible:
  9.     if ((mp.startData.x > mp.a1) && (mp.startData.x < mp.a2) && (mp.startData.y > mp.b1) && (mp.startData.y < mp.b2))
  10.     {
  11.         float x = mp.sx + 64.0f * (mp.startData.x - mp.a1);
  12.         float y = mp.sy + 64.0f * (mp.startData.y - mp.b1);
  13.         spriteBatch.Draw(tiles_image, new Vector2(x, y), new Rectangle(960, 0, 63, 63), Color.Yellow, 0f, Vector2.Zero, 1f, SpriteEffects.None, 0f);
  14.     }                                              
  15.  
  16.     // MONSTERS LOCATORS:
  17.     //monsterSys.Draw();                             // shows monsters
  18.     mp.DrawTiles(show_colliders, edit_mode: true); // show monster start locations
  19. }

i) At the center tile (tile area to edit) we draw a partially transparent copy of the square indicator from the tiles_image (you may have a different source rect for yours).
ii) if the start location is on screen (checking a1[horizontal start tile], a2[horizontal end tile], b1[vertical start], b2[vertical end])
then we should show it at it's location. To get the screen location, we find out how many tiles over it is from the start location and multiply by 64 to get the position in pixels instead of tiles (adding that pixel distance to the screen's start location for tile drawing).
iii) Can't draw the monsters yet, but we can redo the DrawTiles loop telling it to only draw the colliders(if on) and show where the monsters are... if either of these are true, the draw loop will not redraw the tile images -- it will only show the locators over top (assuming the tiles were already drawn in a previous DrawTiles call).
iv) Go back to Game1.cs and look for E D I T  M O D E  only - - - - - and change:
if (gameState==GameState.edit) { } to :

Code Snippet
  1. // E D I T  M O D E  only  - - - - -
  2. if (gameState==GameState.edit)
  3. {
  4.     editor.DrawLocators(spriteBatch, tiles_image, screen_center);
  5. } // - - - - - - - - - - - - - - - -


14) In Editor.cs we'll want to show Instructions... we'll put a black partly transparent rectangle fill under the words so it's a bit easier to read them. This code assumes the source rectangle as the top-left pixel of the game map is a valid pixel (don't use a clear one).

Code Snippet
  1. //---------------------------------
  2. // D R A W  I N S T R U C T I O N S
  3. //---------------------------------
  4. public void DrawInstructions(SpriteBatch spriteBatch, Texture2D far_background, SpriteFont font, int screenH) {
  5.     spriteBatch.Draw(far_background, new Rectangle(0, 0, 200, screenH), new Rectangle(0, 0, 1, 1), new Color(0, 0, 0, 150)); // DARK RECTANGLE (makes text easier to read)
  6.     spriteBatch.DrawString(font, "A-Z = add tile", new Vector2(10, 10), Color.LimeGreen);            
  7.     spriteBatch.DrawString(font, "del = delete (set empty)", new Vector2(10, 30), Color.LimeGreen);
  8.     spriteBatch.DrawString(font, "ins = set collider", new Vector2(10, 50), Color.LimeGreen);
  9.     spriteBatch.DrawString(font, "$ = Save to: " + Game1.LEVEL_NAME + "  -- time saved: " + time_saved, new Vector2(10, 70), Color.LimeGreen);
  10.     spriteBatch.DrawString(font, "! = Load from: " + Game1.LEVEL_NAME, new Vector2(10, 90), Color.LimeGreen);
  11.     spriteBatch.DrawString(font, "* = Load from backup", new Vector2(10, 110), Color.LimeGreen);
  12.     spriteBatch.DrawString(font, "M = player position", new Vector2(10, 130), Color.LimeGreen);
  13.     spriteBatch.DrawString(font, "< > rotate", new Vector2(10, 150), Color.LimeGreen);
  14.     spriteBatch.DrawString(font, "- + scale", new Vector2(10, 170), Color.LimeGreen);
  15.     spriteBatch.DrawString(font, "Page Up/Dn = overlap", new Vector2(10, 190), Color.LimeGreen);
  16.     spriteBatch.DrawString(font, "numpad = offset", new Vector2(10, 210), Color.LimeGreen);
  17.     spriteBatch.DrawString(font, "home = reset tile", new Vector2(10, 230), Color.LimeGreen);
  18.     spriteBatch.DrawString(font, "enter = test level", new Vector2(10, 250), Color.LimeGreen);
  19.     spriteBatch.DrawString(font, "F12 - show colliders", new Vector2(10, 270), Color.LimeGreen);
  20.     spriteBatch.DrawString(font, "1-9 = add characters", new Vector2(10, 290), Color.LimeGreen);
  21. }

Now that we have this we can go into Game1.cs and change the comment:
// SHOW EDITOR INSTRUCTIONS
To:

Code Snippet
  1. else
  2. {   // EDITOR INSTRUCTIONS
  3.     editor.DrawInstructions(spriteBatch, far_background, font, screenH);                
  4. }



15) Back to Editor.cs
We need to be able to save the map. Start with testing if the file already exists. If it does, it will make a backup copy of the file:
Code Snippet
  1. //-------------------
  2. // S A V E  L E V E L
  3. //-------------------
  4. void SaveLevel(string level_name)
  5. {
  6.     if (File.Exists(level_name)) File.Copy(level_name, Game1.BACKUP_NAME, true);

If you accidentally ruin your level... you can hit the * (shift 8) to load the backup copy.

Now we write out all the relevant data for our level to a txt file (stored in the Bin directory of Debug or Release [depending on which you're building right now]) For example it may be in: Platformer\bin\DesktopGL\x86\Debug\Content
stored as "lev1.txt" or whatever you named it in Game1.cs... (that's monogame 3.5 but newer versions will say AnyCPU [instead of x86 or x64])

Code Snippet
  1. using (StreamWriter writer = new StreamWriter(level_name))
  2. {
  3.     //write start data (player position)
        writer.Write(mp.startData.x.ToString() + ",");  writer.Write(mp.startData.y.ToString() + ",");  writer.WriteLine();
  4.  
  5.     for (int y = 0; y < Map.TILES_HIGH; y++)
  6.     {
  7.         for (int x = 0; x < Map.TILES_WIDE; x++)
  8.         {
  9.             int temp;
  10.             if ((tiles[x, y].index != 0) || (tiles[x, y].type != TileType.empty)) // store only tiles with used information
  11.             {
  12.                 writer.Write("XY,"); writer.Write(x.ToString() + ","); writer.Write(y.ToString() + ",");  writer.WriteLine();
  13.                 writer.Write("INDEX,"); writer.Write(tiles[x, y].index.ToString() + ",");  writer.WriteLine();
  14.                 temp = (int)tiles[x, y].type;
  15.                 writer.Write("TYPE,"); writer.Write(temp.ToString() + ",");  writer.WriteLine();
  16.                 writer.Write("OFFSET,"); writer.Write(tiles[x, y].offset.X.ToString() + ",");  writer.Write(tiles[x, y].offset.Y.ToString() + ",");  writer.WriteLine();
  17.                 writer.Write("ROT,"); writer.Write(tiles[x, y].rot.ToString() + ",");  writer.WriteLine();
  18.                 writer.Write("SCALE,"); writer.Write(tiles[x, y].scale.X.ToString() + ",");  writer.Write(tiles[x, y].scale.Y.ToString() + ",");  writer.WriteLine();
  19.                 writer.Write("OVERLAP,"); writer.Write(tiles[x, y].overlap.ToString() + ",");  writer.WriteLine();                            
  20.             }
  21.             if (tiles[x, y].monster_start != MonsterType.None)   // store monsters start locations for tiles that have them
  22.             {
  23.                 temp = (int)tiles[x, y].monster_start;
  24.                 writer.Write("MONSTER,"); writer.Write(x.ToString() + ","); writer.Write(y.ToString() + ","); writer.Write(temp.ToString() + ","); writer.WriteLine();
  25.             }
  26.         }
  27.     }
  28. }
  29. time_saved = DateTime.Now.ToShortTimeString();


Using a stream writer, we write to "lev1.txt" (eventually when the game is done and you know the format won't change you can make a binary writer/reader)
The file will always start with the player's start data x,y tile coordinate... for that reason I didn't write a token.
We loop through all the tiles and if a tile has some valid property (not 0 or not empty), then we write data for it to file:
Starting with the XY token to tell which tile it is (ie: tile[x,y]) ... the comma's are important (as well as the ending WriteLine();) so don't forget those.
Everything's written as a string ... so we write all the tile data...
... then we see if the tile has a monster on (start location) ... and if so write the tile coord X,Y (ie: tile[x,y]) and the number for which enumeration it is.

We'll get a string for the time_saved which is shown in the DrawInstructions method... that way you can see the file was successfully saved (at whatever time) after you press '$'.
Don't forget the closing bracket for this method. ;p
}


16) Now we'll make a LoadLevel method which will interpret the data and build a level from it. Since we're using tokens to determine what type of data is loaded into the string, it's easy to use a switch statement to determine how to treat the data.
This way, no matter what changes you make to the file format, you'll always be able to load the old file format of older levels you made to update them to the new format easily without errors.

Code Snippet
  1. //-------------------
  2.  // L O A D  L E V E L
  3.  //-------------------
  4.  public void LoadLevel(string level_name)
  5.  {
  6.      if (!File.Exists(level_name)) return;
  7.      mp.ClearMap();

First make sure the file exists and clear off anything that might be on the map so it won't be mixed with whatever you were playing around with before (if you did).

a) Next we'll use a stream reader to read in the file one line at a time. For each line read, we'll split the string into parts at the commas. The very first line we already know is the player's map position (x,y) for startData (which we convert from string to int)
We'll also position the map/camera location at the player's start location.
Then we'll loop through the lines checking the tokens to determine what data it is and how to add it into the tiles:

Code Snippet
  1. using (StreamReader reader = new StreamReader(level_name))
  2. {
  3.     string line = reader.ReadLine(); string[] parts = line.Split(',');
  4.     mp.startData.x = Convert.ToInt32(parts[0]);
  5.     mp.startData.y = Convert.ToInt32(parts[1]);
  6.     mp.loc.X = mp.startData.x;  
  7.     mp.loc.Y = mp.startData.y; //position the map at start position of player
  8.     //player.pos = Conv.tile_to_world(mp.loc) + new Vector2(-32, -32);
  9.     int x = 0, y = 0;
  10.     while (reader.EndOfStream != true)
  11.     {
  12.         line = reader.ReadLine(); parts = line.Split(',');
  13.         switch (parts[0])
  14.         {
  15.             case "XY": x = Convert.ToInt32(parts[1]);  y = Convert.ToInt32(parts[2]);   break;
  16.             case "INDEX":   tiles[x, y].index    = Convert.ToInt32(parts[1]);           break;
  17.             case "TYPE":    tiles[x, y].type = (TileType)(Convert.ToInt32(parts[1]));   break;
  18.             case "OFFSET":  tiles[x, y].offset.X = Convert.ToInt32(parts[1]);   tiles[x, y].offset.Y = Convert.ToInt32(parts[2]); break;
  19.             case "ROT":     tiles[x, y].rot      = Convert.ToSingle(parts[1]);          break;
  20.             case "SCALE":   tiles[x, y].scale.X  = Convert.ToSingle(parts[1]);  tiles[x, y].scale.Y = Convert.ToSingle(parts[2]); break;
  21.             case "OVERLAP": tiles[x, y].overlap = Convert.ToBoolean(parts[1]);          break;
  22.             case "MONSTER": x = Convert.ToInt32(parts[1]); y = Convert.ToInt32(parts[2]);
  23.                   tiles[x, y].monster_start = (MonsterType)(Convert.ToInt32(parts[3])); break;
  24.         }
  25.     }
  26. }

Note: With enumeration types, we have to cast them from int to their appropriate type.

b) Now we'll need to preprocess some of this by looping through the tile data (skipping empty ones) and applying proper data for clusters of tiles (such as spikes, springs, solids, or platforms)... this is done to ensure the properties are correct for all tile clusters. In the future you may also check for things like water, breakables, etc... and set those up appropriately (keeping special-case tile treatment seperate from standard tile data). For now, we'll just keep it simple with this paranoid bit of code.
Note: We do NOT yet have a monster class so later we'll need to come back here and have it add the monsters at their world locations in this tile pre-process code:
Code Snippet
  1.     // PRE-PROCESS LOADED TILES (mainly for data specific to tile type that isn't saved to file [or fixing tile data] )
  2.     for (int b = 0; b < Map.TILES_HIGH; b++)
  3.     {
  4.         for (int a = 0; a < Map.TILES_WIDE; a++)
  5.         {
  6.             int i = tiles[a, b].index;
  7.             //if (tiles[a, b].monster_start != MonsterType.None)
  8.             // ADD IN THE MONSTERS AT START LOCATIONS
  9.             TileType typ = sheet[i].type;
  10.             if (typ == TileType.empty) continue;
  11.                                 
  12.             // PROVIDE DATA FOR TILE CLUSTER (based on corner tile)
  13.             if ((typ == TileType.solid)    || (typ == TileType.spring)
  14.              || (typ == TileType.platform) || (typ == TileType.spikes))
  15.             {
  16.                 for (int d = b; d < b+sheet[i].tiles_high; d++)
  17.                 {                            
  18.                     for (int c = a; c < a + sheet[i].tiles_wide; c++)
  19.                     {                                
  20.                         tiles[c, d].overlap = true; tiles[c, d].stand_on = true;
  21.                         if (typ == TileType.spikes) { tiles[c, d].spikes = true; tiles[c, d].is_solid = true; }
  22.                         else if (typ == TileType.solid) tiles[c, d].is_solid = true;
  23.                     }
  24.                 }                                                    
  25.             }
  26.         }
  27.     }
  28. }


17) Spring Platforms or "Bouncers"

In solution explorer, right-click the folder, "Map Related Stuff" and Add>>Class: "Bouncer"
a) We'll need:
x,y                   = the indices of which tile this affects (which one will have bounce animation)
original_offset = the starting offset the tile normally has
distance          = the maximum wave amplitude in pixels (how far it bounces up/down)
frequency        = will represent how fast wave is changing
f                      = keeps track of a value to give to sin(f) which determines what part of the wave it's at

Code Snippet
  1. // B O U N C E R  (spring platform)
  2. class Bouncer
  3. {
  4.     public int x, y;                    // tile to run bounce motion on
  5.     public Vector2 original_offset;        
  6.     double distance, frequency;         // controls wobble motion
  7.     double f;
  8.  
  9.     // CONSTRUCT
  10.     public Bouncer(int tile_x, int tile_y, Vector2 original_Offset, double start_wobble_distance, double wobble_frequency)
  11.     {
  12.         x = tile_x;     
  13.         y = tile_y;
  14.         original_offset = original_Offset;
  15.         distance        = start_wobble_distance;
  16.         frequency       = wobble_frequency;
  17.         f = 0;
  18.     }

To construct it, we take which tile to affect, the tile's regular offset, a wobble distance, and wobble speed...

b) Now we'll need to adjust the bouncer's offset using the wave: sin(f) * distance. And we'll want to decrease the distance of the wobble gradually until it we make it stop when it's close enough to the middle. I just picked -0.6 since it provides a nice rate of wobble decay - you can experiment with this if you like.
Code Snippet
  1.     // UPDATE
  2.     public bool Update(ref Vector2 offset)
  3.     {
  4.         offset.Y = original_offset.Y + (float)(distance * Math.Sin(f));
  5.         f += frequency;
  6.         distance-=0.6;
  7.         if (distance < 1.0) return false;
  8.         return true;
  9.     }
  10. }

The offset is a reference to the tile's offset so any changes here will affect the tile's offset but it'll be returned to normal when it's done animating. If we return false after the distance gets near 0, the bounce manager knows to remove this animation.


18) BounceMgr will manage bounce events... It'll need to be able to add new bounce events and update them all.
Instead of creating a new .cs module, since this should be compact, we'll just add it at the bottom under the Bouncer class.
We'll need:
- access to tiles[x,y] from the map.
- a list or array of bouncers

a) To construct it, we'll need to pass a reference to the tiles, and we'll provide a memory pool of up to 100 bouncers.

Code Snippet
  1. //--------------------------------------------
  2. // B O U N C E  M G R  (manages bounce events)
  3. //--------------------------------------------
  4. class BounceMgr
  5. {
  6.     Tile[,]   tiles;                                  // refers(not a new copy) to map's tiles
  7.     Bouncer[] bouncers;                               // tracks active bounce/spring platform animations
  8.     int       num_bouncers;                           // number of active bouncers (if more than one at a time)        
  9.  
  10.     // CONSTRUCT
  11.     public BounceMgr(Tile[,] Tiles)
  12.     {
  13.         tiles    = Tiles;
  14.         bouncers = new Bouncer[100];                  // allow up to 100 bouncers at once (spring platforms)
  15.     }

You may notice I keep mentioning the objects are actually referring to the originals created. I only keep repeating this because it seems like I've noticed a lot of people who thought passing these is slow (because they thought it copies the values)... if it were some big struct, then yes it would copy the values in like a new object copy, but as a class it only provides the memory location of the original object - so it's actually quick. I keep a reference to the objects in my classes to reduce how many times I'm passing variables in each loop (and less code to look at in each call).
( Note: You could use a list instead if you really wanted )

b) If the player jumps on a bouncer, it should Add a bouncer animation event... but we don't want to add one if it already started (thus return if it did).

Code Snippet
  1. // A D D  (B O U N C E)  (tile event for bouncey stuff)
  2. public void Add(int x, int y)
  3. {
  4.     if (tiles[x, y].event_active) return;                                      // <--already activated so return
  5.     bouncers[num_bouncers] = new Bouncer(x, y, tiles[x, y].offset, 25, 0.4);   // create new bouncer event
  6.     tiles[x, y].event_active = true;                                           // <--say it's activated
  7.     num_bouncers++;
  8. }


I set the standard bouncer added to have an distance of 25 pixels and an wave(angle) change speed of 0.4 radians.
Must remember to set event as active so it doesn't restart immediately.

c) Update all bouncers:
Return if none...
Loop {
If the current bouncer update returns false, remove it
}

Code Snippet
  1. // U P D A T E  (B O U N C E R S)
  2. public void Update()
  3. {
  4.     if (num_bouncers < 1) return;  // <-- no bouncers so return
  5.     int i = 0, bx, by;
  6.     while (i < num_bouncers)       
  7.     {
  8.         bx = bouncers[i].x; by = bouncers[i].y;                 // get tile coord of bouncer
  9.         if (!bouncers[i].Update(ref tiles[bx, by].offset))      // update (refer to tile's offset for updating it)  
  10.         { // UPDATE    (if false remove the bouncer)            
  11.             tiles[bx, by].offset = bouncers[i].original_offset; // done animating it so set to original position
  12.             tiles[bx, by].event_active = false;                 // no longer active
  13.             bouncers[i] = bouncers[num_bouncers - 1];           // replace with last item
  14.             if (num_bouncers > 0) num_bouncers--;               // one less active bouncer
  15.         }
  16.         i++;
  17.     }
  18. }
You might be wondering about the removal. It's just setting the current bouncer to be the last entry... and then reducing the
count to ignore the last one (which would be the same as current one now).


19) Go to Map.cs and in the fields where it says //MAP DATA after: public StartData startData; add the following (uncomment it):
Code Snippet
  1. public BounceMgr bounceMgr;                              // manages bounce events (triggered by players or monsters)

And in the Map() constructor near the bottom after the line: screen_center = Game1.screen_center; add the following (uncomment):
Code Snippet
  1. bounceMgr = new BounceMgr(tiles);                                     // init bounce manager (for spring tiles)


20) Go into Game1.cs and go to Update() and look for case GameState.play:
Look for:
// MATCH WORLD VIEW TO CAMERA
map.world_to_camera(cam_pos, ref background_pos); // (what you see in the world based on where camera is)
... under this add a comment as a reminder for later:
//
// PLAYER COLLISION DETECTION / RESPONSE
//
... and under this add the call to Update the bouncers on the map:

Code Snippet
  1. // UPDATE BOUNCERS (spring platforms)
  2. map.bounceMgr.Update();


Now, the map's bouncers will be updated... we could have put it in map.UpdateVars() but since I call that in editor mode also, I'll just keep them seperated since it's not used in editor mode. Both ways are probably fine though.

------------------------------------------------

21) QUADBATCH (similar to spriteBatch except allows quad distortions and unique colors at each vertex)
If you've already followed my older tutorials for this, you can skip to step 21...
I'm assuming you already understand that a graphics processor is designed to render 3D polygons by default - which is why there's a bit of added complexity to setting up our 2D rendering (on the plus side though, we have the option of putting 3D into a 2D scene or vice-versa)
The other difference between this and SpriteBatch is you pass in an entire Sprite Sheet (containing many images) into the Begin() and it will draw everything from that sheet until you use End(). This forces you to manually batch your draws carefully which reduces texture switching which may cause a tiny performance benefit. It also only changes gpu states if necessary.

a) In solution explorer, Right-Click on c# project_name and Add>>Class: "QuadBatch"
I've already made a QuadBatch before... so I'll show huge sections of code and briefly explain how they work. A prior understanding of how DirectX or OpenGL work will make it a lot easier to understand.

Code Snippet
  1. //FONTS : to create a font the surrounding colors must be green(0,255,0) with alpha space as 1 and green seperations spaces as 1    
  2. public enum Alignment { None, LeftAlign, RightAlign, CenterAlign }
  3. public struct FontData
  4. {
  5.     public float x1, y1, x2, y2;
  6.     public float w, h, iw;
  7. }
  8. public class QuadBatch
  9. {
  10.     //PUBLIC
  11.     public BlendState        blendState;
  12.     public SamplerState      samplerState;
  13.     public DepthStencilState depthStencilState;     // default is depth is off
  14.     public Texture2D tex = null, font_tex = null;
  15.     public Effect default_shader = null;
  16.     public Effect fx = null;                        // substitute pixel-shader effect
  17.     public float text_h_space, text_v_space;        // font spacing
  18.     public Vector2 origin_default;
  19.     public float depth, font_depth;
  20.     public int screenWidth, screenHeight;
  21.     //PRIVATE
  22.     private GraphicsDevice   device;
  23.     private VertexPositionColorTexture[] vertices;
  24.     private VertexPositionColorTexture[] fontverts;
  25.     private short[] indices;
  26.     private VertexBuffer vertexBuffer;
  27.     private IndexBuffer  indexBuffer;
  28.     private bool beginCalled;
  29.     private bool font_tex_loaded = false;
  30.     private int  vert_count, font_vert_count;
  31.     private FontData[] fd;
  32.     private float default_font_size = 0.5f;
  33.     private Matrix world, view, proj, font_world, font_view, font_proj, ViewProj, WorldViewProj, font_WorldViewProj;
- Alignments - how text / font is set to Align on the display target.
- FontData: x1,y2,x2,y2 (top-left and bottom-right coords of a letter on a texture (bitmap).
w,h = width, height, iw = width/2 (used for centering each letter on a position)

BlendState :    ie: NonPremultipied, Alpha blending (premultiplied alpha), Opaque (no transparency), Additive (good for special fx)
SamplerState: ie: Theses can be Point (rough pixels), Linear (smoother blend), Anisotropic (smoothest blend/filtering) and each can be either Clamp or Wrap (whether the sampling source will stop at the edge (repeating the last sampled color) or whether it will loop around in texture memory to sample from) and these factors apply if the texture coordinates are out of bounds for the texture itself. In other words... clamp to the edge or wrap/loop back to the beginning.
DepthStencilState: ie: default(good for 3D), depthRead(read-only z depth of pixel), none (default but turn it on if need z testing)
tex = the sprite sheet containing many images to extract from
font_tex = contains a texture composed of ASCII characters seperated by a key color for determining where letters start and end.
default_shader = just like spritebatch's default shader. Draws normally and tints by multiplying by vertex color.
fx = if fx is not null, any vertex shader or pixel shader included in it will over-ride the default one (if it's just a pixel shader for example, it will still use the default's vertex shader that was originally set).
text_h_space, v_space = how much space between each font character
origin_default = middle of image unless changed (rotates and scales around center - not the same as spriteBatch default behavior which does this at the corner by default)
depth, font_depth... the z depth (0.0f-1.0f) to draw to (I believe this is percent between near culling plane to far culling plane)
screenWidth, Height ... display dimensions
device = access to graphics processing unit (GPU) and associated functionality
vertices = a memory pool of vertices to be cached each time they fill up (if they fill before calling End)
fontVerts = same idea but applied only to texture fonts
indices = the indices should never change. Each index tells the processor which vertex comes next in the vertex list composing triangles (2 triangles for each quad)... by using indices we gain a bit of efficiency by preventing redundant vertex processing on the GPU
vertexBuffer = the thing that passes the vertices into the GPU (offset, quantity, etc)
indexBuffer = same sort of thing.
beginCalled = used to warn programmer of redundant calls to Begin (or other Begin-End related mistakes)
font_tex_loaded = true if it's loaded... so allow font use
vert_count, font_vert_count = how many vertices to draw so far.
fd[] = the font data for all the ASCII characters (see above class)
default_font_size... how fonts are scaled initially
world, view, proj matrices:
World matrix sort of determines where you are physically in the world... so you can move your entire world or quad vertices around with this. For 2D we'll leave this as an identity matrix (no change). You could use this to make earthquake effects and shake the world around if you wanted... could also use it to pan a small level across the camera's view (we won't use this technique).
View matrix makes the vertices relative to the camera's view (position/orientation) -- we'll keep the camera looking straight ahead for the 2D stuff (altho technically you can make some neat fx if you play with this a bit)
Proj (projection) matrix clips the vertices to the clipping planes of the viewing frustum (using w) - depths will be normalized between the near and far plane - coordinates are scaled from the center of the display based on their distance from the camera and this produces the perspective effect... Since we're doing this full 2D, we don't want camera-relative perspective scaling (although... depending on field of view (FOV) you could create neat effects with this) ... so we'll call a method to set up an orthographic camera (2D cam with no perspecitive/eye-distance-based scaling)
WorldViewProj is just the combined matrices (in that order) [so if these don't change we don't need to recalculate each time]

b) The constructor will need the target display dimensions (screenWidth/Height) and a default effect (we'll make this and call it "QuadEffect.fx") as well as a texture containing our custom font which we'll call "FontTexture.png")
Anyway, I'll show the code here and you can go down below it to read about what I'm doing referring back to the code as you like:

Code Snippet
  1. // C O N S T R U C T O R :
  2. public QuadBatch(ContentManager content, GraphicsDevice graphics, string Default_Effect_File, string FontTextureFile, int? ScreenWidth, int? ScreenHeight)
  3. {            
  4.     VertexDeclaration vertexDeclaration;            
  5.     default_shader = content.Load<Effect>(Default_Effect_File);
  6.     device = graphics;
  7.     if (ScreenWidth.HasValue) screenWidth = ScreenWidth.Value; else screenWidth = device.Viewport.Width;
  8.     if (ScreenHeight.HasValue) screenHeight = ScreenHeight.Value; else screenHeight = device.Viewport.Height;
  9.     beginCalled = false;            
  10.     vertices  = new VertexPositionColorTexture[8192]; //2048 sprites * 4 vertices per sprite IS 8192
  11.     fontverts = new VertexPositionColorTexture[8192]; //2048 sprites * 4 vertices per sprite IS 8192
  12.     vertexDeclaration = VertexPositionColorTexture.VertexDeclaration;
  13.     indices = new short[12288]; //2048 sprites * 6 indices per sprites IS 12288
  14.     int c = 0;
  15.     for (int i = 0; i < 8192; i += 8) //2048 * 4 verts per sprite(0,1,2,3) so loop in increments of 4
  16.     {
  17.         indices[c] = (short)(i + 0); c++; indices[c] = (short)(i + 1); c++; indices[c] = (short)(i + 2); c++;
  18.         indices[c] = (short)(i + 2); c++; indices[c] = (short)(i + 3); c++; indices[c] = (short)(i + 0); c++;
  19.         indices[c] = (short)(i + 4); c++; indices[c] = (short)(i + 5); c++; indices[c] = (short)(i + 6); c++;
  20.         indices[c] = (short)(i + 6); c++; indices[c] = (short)(i + 7); c++; indices[c] = (short)(i + 4); c++;
  21.     }
  22.     vertexBuffer = new DynamicVertexBuffer(graphics, typeof(VertexPositionColorTexture), 8192, BufferUsage.WriteOnly); //only plan to write to it
  23.     indexBuffer = new IndexBuffer(graphics, typeof(short), 12288, BufferUsage.WriteOnly);
  24.     indexBuffer.SetData(indices);
  25.     fd = new FontData[130]; font_depth = 0.0f;
  26.     origin_default = new Vector2(0.5f, 0.5f);
  27.     Use2DCamera();
  28.     PrepareFont(content.Load<Texture2D>(FontTextureFile));
  29.     blendState        = BlendState.AlphaBlend;
  30.     samplerState      = SamplerState.LinearClamp;
  31.     depthStencilState = DepthStencilState.None;
  32.     if (device.RasterizerState.CullMode != CullMode.None) device.RasterizerState = RasterizerState.CullNone;            
  33. }


VertexDeclaration sets the vertex format we're using (VertexPositionColorTexture) with the line:
vertexDeclaration = VertexPositionColorTexture.VertexDeclaration;
Wait... I'm not sure what sorcery this is... I think at one time doing this set up the vertex format automatically. I'm not sure it is even necessary now, because the type is declared when we make the vertexBuffer...we'll just leave it as is for now.
We then load the default effect (vertex and pixel shaders) "QuadEffect" (which we'll make after we finish this constructor)
Assign device as the GraphicsDevice
Since I want up to 2048 quads/sprites (before needing to cache/draw & restart building verts), I allocate 8192 vertices... might need lots of letters too for text...
The indices will go in a clockwise manner by default. [Keep in mind your cull setting and the winding direction of data you import. If 2D it's perfectly safe to turn culling to none.]
I'm not sure why I unrolled the loop to 2 quads per loop since this is not a time-critical loop... not that it matters...
To setup the vertexBuffer (which carries our vertices to the GPU), we'll set it as Dynamic because we want to keep updating the vertices each frame. HERE we set the format to VertexPositionColorTexture (keep in mind when making shaders) for 8192 vertices total and WriteOnly mode.
To setup the indexBuffer (provides indices to GPU), we just make a regular static (unchanging) buffer that can hold enough indices for 2048 sprites per cache(draws to buffer)/End()
We can send this data now and won't need to update it later because it doesn't change since the indices will always be in same progressive order.
Allocate fd for enough FontData to hold 130 characters. (default depth for text is at 0)
The origin_default is set to the sprite's center... but if origin isn't null, the actual origin position can be sent instead (ie: 20,30)
We'll add a method to setup the 2D camera.
PrepareFont method will take a look at the font texture file and fill the fd[] with font data based on where it's finding the letters.
A nice custom texture font can be made with a software like SpriteFont 2 by Nubik... there are others that you can use too. I picked a color of RGB(0, 255, 0) so the green channel is maxed out... any part of the font image with this color is considered a barrier and this can be used to determine where each new character starts or ends (as long as the vertical size is consistent which it should be). You can use my font in your own games if you like.
We then set some default device states with RasterizerState.CullNone (because we don't need backface culling for a 2D game typically)... However I suppose if you put actual 3D models into this, you'd have to make sure the vertices of the model exported are set to clockwise... because I'm doing the 2D with a clockwise vertex order. You'll know if it's wrong because the model will be see-through from the wrong side and show only the triangles facing away.

22) Before we continue though, open the monogame pipeline (double click Content.mgcb) and add the Font Texture you made (or downloaded ie: FontTexture.png) [note: the transparent pixels in the image below only look black because of the dark background but they're transparent]

font texture
You can add it to your content folder and go edit>>add>>existing item and click on it.

23) Also let's make the fx file for the default shaders. If you're using Visual Studio and have hlsl formatting, then it may be a bit easier to decipher what you're reading. Go to file>>new>>file>>hlsl and just give it any name like Shader1.hlsl
Once you're done making it, save as: QuadEffect.fx in the Content folder... and then you can delete (or store as backup) the original copy.

a) TexSampler will hold our texture (sprite sheet) ... I set it to linear and clamp by default. There's absolutely no mip mapping so it's NOT necessary to add that... just note that sprite scaling should be a bit smoother when sprites are increased in size with MagFilter as linear. You really don't need to set the filters here since they're already set elsewhere but I thought I'd show them here just so you know it can be done:

Code Snippet
  1. sampler TexSampler : register(s0)
  2. {
  3.     Texture = <Texture>;
  4.     AddressU = clamp;
  5.     AddressV = clamp;
  6.  
  7.     MinFilter = linear;
  8.     MagFilter = linear;
  9.     MipFilter = linear;
  10. };

So you could just as easily get away with just:
sampler TexSampler : register(s0)
{
    Texture = <Texture>;
}

b) Our vertices will be updated based on whatever the transform matrix value was set to in the c# code...
We'll also need a struct for the output format from the vertex shader to the pixel shader.
Note that these must be in the right order or the shader won't work:

Code Snippet
  1. float4x4 MatrixTransform;
  2. struct VSOutput
  3. {
  4.     float4 position     : SV_Position;
  5.     float4 color        : COLOR0;
  6.     float2 texCoord     : TEXCOORD0;
  7. };
Like I said, make sure these come in the order and size expected based on your Vertex Declaration and your output must exactly match your input to any of your pixel shaders.

c) A basic vertex shader, takes the vertices in the appropriate parameter format and multiplies those vertices by the transformation matrix to rotate,position,and scale them correctly into camera view. Since this is 2D, in reality, it does very little to them and no perspective adjustment (just orthographic viewing).
Code Snippet
  1. VSOutput SpriteVertexShader(float4 position    : SV_Position,  float4 color : COLOR0,  float2 texCoord    : TEXCOORD0)
  2. {
  3.     VSOutput output;         
  4.     output.position = position;
  5.     output.position = mul(position, MatrixTransform);
  6.     output.color = color;
  7.     output.texCoord = texCoord;     
  8.     return output;
  9. }

d) The pixel shader's also simple. It just samples a texel(pixel) from the current sampling coordinate (as it interpolates across the image) and multiplies the color by the corresponding interpolated value of the vertex colors... If all 4 vertices were blue then it would just multiply by blue (preferably not pure blue 0,0,1,0 cuz then the whole image is shades of blue)... if all white, then the resulting multiply would be r*1, g*1, b*1, a*1 because each channel is normalized to be (0-1) [ie: 0% to 100%] which would mean no change in color. If you used addition instead of multiply, it would be a different result (in which case you'd use saturate to clamp the values).
I digress... Anyway just thought I should point out too, that if for example, the top left corner was red and the bottom right was blue... then as it interpolates(blends) ... the value of color passed into the pixel shader would be something like purple near the middle of the image... so for example, IF the pixel color should be gray at that point, it would become tinted purple-ish... This could be used for colored lighting provided it's done carefully (no pure colors). However there are other tricks for that too. We'll be using the color value mainly to show when our characters got hurt (flash in red shades)
Code Snippet
  1. float4 SpritePixelShader(VSOutput input) : SV_Target0
  2. {             
  3.     float4 col;
  4.     col = tex2D(TexSampler, input.texCoord) *input.color;        
  5.     return col;       
  6. }

e) Add the technique and for cross-desktopGL project (openGL related) I'll use vs_3_0 and ps_3_0 ... for a windows only project I believe the common one to use now would be vs_5_0 and ps_5_0 ... but for dx9 compatibility it would be vs_4_0_level_9_1 & ps_4_0_level_9_1 and version 2 would be older less capable versions (not as many instructions). We'll tell it to use the latest OpenGL version like so (I believe it first translates to glsl and then a final compile):

Code Snippet
  1. technique QuadBatch
  2. {
  3.     pass
  4.     {
  5.         VertexShader = compile vs_3_0 SpriteVertexShader();
  6.         PixelShader  = compile ps_3_0 SpritePixelShader();
  7.     }
  8. }

Note: that in each shader - don't forget to give the methods a unique name. Like WaterPixelShader() or something like that.

f) Go back into the Monogame Pipeline and edit>>add>>existing item: "QuadEffect.fx"


24) Go back into QuadBatch.cs and add the following methods:

Code Snippet
  1. public void SetEffect(Effect F_ect)    { fx = F_ect; }
  2. public void SetWorld(Matrix World)     { world = World; WorldViewProj = world * ViewProj; }
  3. public void SetFontWorld(Matrix World) { font_world = World; font_WorldViewProj = font_world * font_view * font_proj; }

If you set a over-riding effect (vertex shader, pixel shader, or both) it will replace the default shader(s) until it's set to null.
Set world could be useful for making earthquakes... it updates world and the transformation matrix that the shader will use on the vertices. Same thing idea for fonts.


25) We need to setup the camera for 2D viewing (Orthographic) [We could make a Use3DCamera() too if we wanted]
Code Snippet
  1. // U S E  2 D  C A M E R A
  2. public void Use2DCamera()
  3. {
  4.     world = Matrix.Identity;                                    
  5.     proj = Matrix.CreateOrthographicOffCenter(0.0f, screenWidth, screenHeight, 0.0f, -2000f, 2000.0f); //near and far need to be huge for 3d world rotations to work (remember in this mode 2000 might actually be the size of the image so if you rotate it toward the camera part of it will cut off if this is too small)                        
  6.     Matrix halfPixelOffset = Matrix.CreateTranslation(-0.5f, -0.5f, 0); proj = halfPixelOffset * proj; //<--fixes half-pixel offset problem
  7.     view = Matrix.Identity; depth = 0.0f;
  8.     font_world = world; font_view = view; font_proj = proj;
  9.     ViewProj = view * proj; WorldViewProj = world * ViewProj; font_WorldViewProj = WorldViewProj;
  10. }

world matrix is set to identity (no change) ... same with view matrix (no change) ... cuz the the vertices themselves may move in many unique ways and so are updated dynamically, the camera faces down the z axis and doesn't really move... so then all we need to do is crop vertices from the culling frustum (the planes that make up the view) ... we don't need perspective distance_scaling so that's why this is set as an orthographic (Off_center so coordinates are relative to the top-left corner of the screen) ... I set a generous range for near and far clipping planes (so 3D mode will remain compatible).
the halfPixelOffset adjusts it so that the coordinates align better with the actual display pixels (looks better). Default z (depth) is 0 for vertices.
Builds a matrix in the correct order (world * view * proj) as WorldViewProj to be sent to the vertex shader as the MatrixTransform later.


26) Later we'll make a MeoPlayer that will make use of a method called DrawTransformedVertices which uses the character's position and 4 vertex offsets and a color to draw a distortable sprite-part.
a) Add this:

Code Snippet
  1. //  M E O  M O T I O N  C H A R A C T E R  D R A W   ( used in   M E O   P L A Y E R )
  2. //  D R A W  T R A N S F O R M E D  V E R T I C E S  ( assumes verts already transformed )        
  3. public void DrawTransformedVertices(Rectangle sourceRect, Vector2 scene_origin, Vector2 p1, Vector2 p2, Vector2 p3, Vector2 p4, Color c1, SpriteEffects flip = SpriteEffects.None)
  4. {
  5.     if (!beginCalled) { Console.WriteLine("BEGIN not called before QuadBatch Draw. Draw aborted."); return; }            
  6.     float u1, v1, u2, v2;
  7.  
  8.     p1 = (p1 + scene_origin); p2 = (p2 + scene_origin); p3 = (p3 + scene_origin); p4 = (p4 + scene_origin);            
  9.     u1 = sourceRect.X / (float)tex.Width; //gets the texture coords in terms of (0.0f-1.0f, 0.0f-1.0f)
  10.     v1 = sourceRect.Y / (float)tex.Height;
  11.     u2 = (sourceRect.X + sourceRect.Width)  / (float)tex.Width;
  12.     v2 = (sourceRect.Y + sourceRect.Height) / (float)tex.Height;
  13.     if ((flip & SpriteEffects.FlipVertically) != 0)
  14.     {
  15.         var temp = v2; v2 = v1; v1 = temp; //BR_Y                
  16.     }
  17.     if ((flip & SpriteEffects.FlipHorizontally) != 0)
  18.     {
  19.         var temp = u2; u2 = u1; u1 = temp; //BR_X                
  20.     }

- p1,p2,p3,p4 are added to player position (scene_origin) ... then just get where to sample from the texture-image:
- uv texture coordinates are needed ...  ie:  [ x / width ] ... gives u-coordinate (0.0 - 1.0) along the texture2D
   ... same idea for v-coord (ie: get the % along the height of the texture)
- to flip the image vertically or horizontally, it simply swaps the v-coords or u-coords
(When the graphics card renders a polygon, it will interpolate along the sampling coordinates on the image based on the uv coordinates of each vertex)

b) Now we just need to add the positions and uv-coordinates into the vertices (with the color):
Code Snippet
  1.     vertices[vert_count].Position = new Vector3(p1, depth);
  2.     vertices[vert_count].TextureCoordinate = new Vector2(u1, v1);   //upper-left texCoord
  3.     vertices[vert_count].Color = c1; vert_count++;
  4.     vertices[vert_count].Position = new Vector3(p2, depth);
  5.     vertices[vert_count].TextureCoordinate = new Vector2(u2, v1);   //upper-right texCoord
  6.     vertices[vert_count].Color = c1; vert_count++;
  7.     vertices[vert_count].Position = new Vector3(p3, depth);
  8.     vertices[vert_count].TextureCoordinate = new Vector2(u2, v2);   //lower-right texCoord
  9.     vertices[vert_count].Color = c1; vert_count++;
  10.     vertices[vert_count].Position = new Vector3(p4, depth);
  11.     vertices[vert_count].TextureCoordinate = new Vector2(u1, v2);   //lower-left texCoord
  12.     vertices[vert_count].Color = c1; vert_count++;
  13.     if ((vert_count + 1) >= 8192)
  14.     {
  15.         EndVerts(); beginCalled = true; vert_count = 0;
  16.     }
  17. } // DrawTransformedVertices (for MeoMotion)

As you can see, every time a draw is called, it adds 4 more vertices (a distortable quad).
If we should reach the end of our allocated system memory (vertex_count+1)>=8192, then we need to draw them to the render target (however a call to end resets beginCalled and we're not done so we make sure beginCalled is true so it can continue drawing)
Since these are all drawn now, we can reset the vertex count and continue by making another batch of sprites.


27) We'll add a couple more drawing methods which could come in handy... I'll just explain this first one and provide a link to the >> QuadBatch source code <<
The others use the same concepts but are more specialized. You can always remove ones you'll never use or add in new ones that suit your needs.
The first I'll show is the most complex one which allows vertices to be offset for an irregular shape and allows each point on the quad to have it's own color for color-blending across the image. This could come in handy for warping geometry ie: for swaying trees or ie: adding some subtle color blending corresponding to colored light sources in a scene (without using light maps on everything).
After this I'll show you some smaller simpler overloads(versions of Draw) that are handy too.
If you check the QuadBatch source, you'll notice there are numerous different kinds of overloads for a wide variety of drawing needs... I typically only add one each time I want a specific way to draw something... You may not need many of these.
ie:
various versions of:
Draw, DrawDepth, DrawColor, DrawColorDepth, DrawColorDistort,
DrawColorDistorDepth, DrawEntire, DrawDest
And some overloads for Begin also...

I probably use Draw and DrawDest the most. (DrawColor ones allow you to set 4 vertex colors)
But for this project, the MeoPlayer will mostly only use DrawTransformedVertices.
SpriteBatch will be used for all the other drawing routines (although we could have used QuadBatch instead).

a) So... here's the really complex version that allows you to distort and color each vertex differently:

Code Snippet
  1. //-------------------------------------
  2. // D R A W
  3. //-------------------------------------
  4. public void Draw(Rectangle? sourceRect, Vector2 pos, Vector2? origin, Vector2? Scale, float rot, int x1off, int y1off, int x2off, int y2off, int x3off, int y3off, int x4off, int y4off, Color c1, Color c2, Color c3, Color c4, SpriteEffects flip = SpriteEffects.None) {
  5.     if (!beginCalled) { Console.WriteLine("BEGIN not called before QuadBatch Draw. Draw aborted."); return; }
  6.     float w, h, o_x, o_y, x1, y1, x2, y2, x3, y3, x4, y4, u1, v1, u2, v2;
  7.     Vector2 scale = Vector2.One;
  8.     if (!sourceRect.HasValue) sourceRect = new Rectangle(0, 0, tex.Width, tex.Height);            
  9.     if (Scale.HasValue) scale = Scale.Value;

Setting the sourceRect to null would cause it to show the entire image (rather than a selected region)
pos = sprite position
origin = center of rotation/scale (if not set it will use the center unlike SpriteBatch)
Scale = x-scale, y-scale ... changes the size (if null it scales as 1,1 [no change])
rot = rotation in radians (0 to 2*PI) or (0 to 2*3.14) ... altho numbers passing 6.28 will just loop it
x1off, y1off,...etc... = offsets from sprite's position for each vertex
c1, c2,...etc... = colors for each vertex
flip = option to flip horizontally or vertically (default = none)
(Note: Passing an object reference that holds these would work, but you don't gain anything because you still need to copy values into the object in the first place... depending on how your game is coded though, this may be an option to keep in mind.)

b) So next we scale and rotate... but only perform those calculations if needed:

Code Snippet
  1. if ((scale.X != 1) || (scale.Y != 1)) {
  2.     w = sourceRect.Value.Width * scale.X; h = sourceRect.Value.Height * scale.Y;
  3.     if (origin.HasValue) { o_x = origin.Value.X * scale.X; o_y = origin.Value.Y * scale.Y; } else { o_x = w * origin_default.X; o_y = h * origin_default.Y; } // note: w and h are already scaled
  4.     //get scaled offsets:
  5.     float xo1 = x1off * scale.X, yo1 = y1off * scale.Y;  float xo2 = x2off * scale.X, yo2 = y2off * scale.Y;
  6.     float xo3 = x3off * scale.X, yo3 = y3off * scale.Y;  float xo4 = x4off * scale.X, yo4 = y4off * scale.Y;
  7.     //what is the point after offset
  8.     x1 = pos.X + xo1;     y1 = pos.Y + yo1;      //upper-left
  9.     x2 = pos.X + w + xo2; y2 = pos.Y + yo2;      //upper-right
  10.     x3 = pos.X + w + xo3; y3 = pos.Y + h + yo3;  //lower-right
  11.     x4 = pos.X + xo4;     y4 = pos.Y + h + yo4;  //lower-left                
  12. }  else { //same with no scaling:            
  13.     w = sourceRect.Value.Width; h = sourceRect.Value.Height;
  14.     if (origin.HasValue) { o_x = origin.Value.X; o_y = origin.Value.Y; } else { o_x = w * origin_default.X; o_y = h * origin_default.Y; }
  15.     //need positions of destination                
  16.     x1 = pos.X + x1off;     y1 = pos.Y + y1off;     //upper-left
  17.     x2 = pos.X + w + x2off; y2 = pos.Y + y2off;     //upper-right
  18.     x3 = pos.X + w + x3off; y3 = pos.Y + h + y3off; //lower-right
  19.     x4 = pos.X + x4off;     y4 = pos.Y + h + y4off; //lower-left
  20. }
  21. if (rot != 0f) {
  22.     float ox = pos.X + o_x, oy = pos.Y + o_y;
  23.     float cos = (float)Math.Cos(rot), sin = (float)Math.Sin(rot); //this is actually quite fast on a modern computer
  24.     float hd = x1 - ox, vd = y1 - oy;
  25.     x1 = ox + hd * cos - vd * sin;    y1 = oy + hd * sin + vd * cos;  hd = x2 - ox; vd = y2 - oy;
  26.     x2 = ox + hd * cos - vd * sin;    y2 = oy + hd * sin + vd * cos;  hd = x3 - ox; vd = y3 - oy;
  27.     x3 = ox + hd * cos - vd * sin;    y3 = oy + hd * sin + vd * cos;  hd = x4 - ox; vd = y4 - oy;
  28.     x4 = ox + hd * cos - vd * sin;    y4 = oy + hd * sin + vd * cos;
  29. }

If scale is not 1,1:
- Get a scaled version of width and height of the rectangle source (w,h) [this is how big it will be drawn to target]

- If an origin is set, the origin must be scaled too... If null - just use the center of the scaled rectangle dimensions
- get the scaled offsets
- get each point by adding the scaled offsets to the sprite position (as well as scaled w,h distances when needed)
So normally, the 4 points would be: (pos.X,pos.Y),(pos.X+w,pos.Y),(pos.X+w,pos.Y+h),(pos.X,pos.Y+h)
making the quad... but we need to add the scaled offsets to each one so:
x1 = pos.X + x01 or x1 = 100 + (scale 2 for -10 so:) -20 = 80 ... same sort of thing for the other points
IF NO scaling:
- just use regular values and get the 4 points using w,h (for right and bottom) and add the regular offsets
IF Rotation:
- get a rotation origin as sprite position(top-left corner) + origin (offset from corner to rotation point)
- store a cos and sin value for the rotation (using cos and sin)
- get horizontal distance and vertical distance from the first vertex to rotation origin as hd, vd
- ox, oy acts as the position the points will rotate around
- hd is the horizontal distance from origin to the horizontal position of the point to rotate...
- vd is the vertical distance from origin to the vertical position to rotate
So for point 1:
(x1,y1). <------hd------
                               |
                               | vd
                               |
                           (ox, oy)  [origin on target]
We'll rotate the hd vector and vd vector and combine them so the point is rotated (for y we have to swap the functions)
so for point 1 we get the new x1, y1 by:
x1 = ox + hd * cos - vd * sin;    y1 = oy + hd * sin + vd * cos
Understanding this completely requires some understanding of waves produced by sin and cos (how the numbers change) ... if you diagram it on paper, you'll see how the vectors end up rotating and you add them together to get the final x1,y1
- Get the next hd and vd for the next point.... ie: hd = x2 - ox; vd = y2 - oy;
- apply the same principle... repeat until done all 4 points
So now we've scaled, translated and rotated the vertices of the quad...

c) Now to check if we want to flip the image horizontally or vertically.. if so, we'll need to swap the uv texture coordinates.
First we get the uv coordinates (0-1, 0-1) by getting the % across the image for the left, top, right, and bottom positions of the source rectangle.

Code Snippet
  1. Rectangle tempRect = sourceRect.Value;
  2. u1 = tempRect.X / (float)tex.Width; //gets the texture coords in terms of (0.0f-1.0f, 0.0f-1.0f)
  3. v1 = tempRect.Y / (float)tex.Height;
  4. u2 = (tempRect.X + tempRect.Width) / (float)tex.Width;    v2 = (tempRect.Y + tempRect.Height) / (float)tex.Height;
  5. if ((flip & SpriteEffects.FlipVertically) != 0) {
  6.     var temp = v2; v2 = v1; v1 = temp;              
  7. }
  8. if ((flip & SpriteEffects.FlipHorizontally) != 0) {
  9.     var temp = u2; u2 = u1; u1 = temp;
  10.     
  11. }

d) Now just copy the positions, texture coordinates and each color into each vertex (incrementing vertex count):
Code Snippet
  1.    vertices[vert_count].Position = new Vector3(x1, y1, depth);  vertices[vert_count].TextureCoordinate = new Vector2(u1, v1);   //upper-left texCoord
  2.     vertices[vert_count].Color = c1; vert_count++;
  3.     vertices[vert_count].Position = new Vector3(x2, y2, depth);  vertices[vert_count].TextureCoordinate = new Vector2(u2, v1);   //upper-right texCoord
  4.     vertices[vert_count].Color = c2; vert_count++;
  5.     vertices[vert_count].Position = new Vector3(x3, y3, depth);  vertices[vert_count].TextureCoordinate = new Vector2(u2, v2);   //lower-right texCoord
  6.     vertices[vert_count].Color = c3; vert_count++;
  7.     vertices[vert_count].Position = new Vector3(x4, y4, depth);  vertices[vert_count].TextureCoordinate = new Vector2(u1, v2);   //lower-left texCoord
  8.     vertices[vert_count].Color = c4; vert_count++;
  9.     if ((vert_count + 1) >= 8192) {
  10.         EndVerts();   beginCalled = true; vert_count = 0;
  11.     }
  12. }//Draw (big)
Again... if we reached our allocated memory limit, we draw these out to render target and reset for another batch.


e) This is pretty much the same thing but without offsets (just a color variety):
Code Snippet
  1. //-------------------
  2. // D R A W - 4 colors (no distort)
  3. //-------------------
  4. public void Draw(Rectangle? sourceRect, Vector2 pos, Vector2? origin, Vector2? _scale, float rot, Color c1, Color c2, Color c3, Color c4, SpriteEffects flip = SpriteEffects.None) {
  5.     if (!beginCalled) { Console.WriteLine("BEGIN not called before QuadBatch Draw. Draw aborted."); return; }
  6.     Vector2 scale = Vector2.One;
  7.     float w, h, o_x, o_y, x1, y1, x2, y2, x3, y3, x4, y4, u1, v1, u2, v2;
  8.     if (!sourceRect.HasValue) sourceRect = new Rectangle(0, 0, tex.Width, tex.Height);            
  9.     if (_scale.HasValue) { scale = _scale.Value; }
  10.     if ((scale.X != 1) || (scale.Y != 1)) {
  11.         w = sourceRect.Value.Width * scale.X; h = sourceRect.Value.Height * scale.Y;                
  12.         if (origin.HasValue) { o_x = origin.Value.X * scale.X; o_y = origin.Value.Y * scale.Y; } else { o_x = w * origin_default.X; o_y = h * origin_default.Y; } //Note: w and h are already scaled sizes       
  13.     } else { // same but no scaling
  14.         w = sourceRect.Value.Width; h = sourceRect.Value.Height;                
  15.         if (origin.HasValue) { o_x = origin.Value.X; o_y = origin.Value.Y; } else { o_x = w * origin_default.X; o_y = h * origin_default.Y; } //Note: w and h are already scaled sizes                                        
  16.     }
  17.     x1 = pos.X; y1 = pos.Y;         //upper-left
  18.     x2 = pos.X + w; y2 = pos.Y;     //upper-right
  19.     x3 = pos.X + w; y3 = pos.Y + h; //lower-right
  20.     x4 = pos.X; y4 = pos.Y + h;     //lower-left                
  21.     if (rot != 0f) {
  22.         float ox = pos.X + o_x, oy = pos.Y + o_y;
  23.         float cos = (float)Math.Cos(rot), sin = (float)Math.Sin(rot); //this is actually quite fast on a modern computer               
  24.         float hd1 = x1 - ox, vd1 = y1 - oy;//top-left dif
  25.         float hd2 = x2 - ox, vd2 = y3 - oy;//bottom-right dif
  26.         x1 = ox + hd1 * cos - vd1 * sin;  y1 = oy + hd1 * sin + vd1 * cos;  x2 = ox + hd2 * cos - vd1 * sin;  y2 = oy + hd2 * sin + vd1 * cos;
  27.         x3 = ox + hd2 * cos - vd2 * sin;  y3 = oy + hd2 * sin + vd2 * cos;  x4 = ox + hd1 * cos - vd2 * sin;  y4 = oy + hd1 * sin + vd2 * cos;
  28.     }            
  29.     Rectangle tempRect = sourceRect.Value;             
  30.     u1 = (tempRect.X + 0.5f) / (float)tex.Width; //gets the texture coords in terms of (0.0f-1.0f, 0.0f-1.0f)
  31.     v1 = (tempRect.Y + 0.5f) / (float)tex.Height;
  32.     u2 = (tempRect.X + tempRect.Width - 0.5f) / (float)tex.Width;    v2 = (tempRect.Y + tempRect.Height - 0.5f) / (float)tex.Height;
  33.     if ((flip & SpriteEffects.FlipVertically) != 0) {
  34.         var temp = v2; v2 = v1; v1 = temp; //BR_Y                
  35.     }
  36.     if ((flip & SpriteEffects.FlipHorizontally) != 0) {
  37.         var temp = u2; u2 = u1; u1 = temp; //BR_X                
  38.     }
  39.     vertices[vert_count].Position = new Vector3(x1, y1, depth);    vertices[vert_count].TextureCoordinate = new Vector2(u1, v1);   //upper-left texCoord
  40.     vertices[vert_count].Color = c1; vert_count++;
  41.     vertices[vert_count].Position = new Vector3(x2, y2, depth);    vertices[vert_count].TextureCoordinate = new Vector2(u2, v1);   //upper-right texCoord
  42.     vertices[vert_count].Color = c2; vert_count++;
  43.     vertices[vert_count].Position = new Vector3(x3, y3, depth);    vertices[vert_count].TextureCoordinate = new Vector2(u2, v2);   //lower-right texCoord
  44.     vertices[vert_count].Color = c3; vert_count++;
  45.     vertices[vert_count].Position = new Vector3(x4, y4, depth);    vertices[vert_count].TextureCoordinate = new Vector2(u1, v2);   //lower-left texCoord
  46.     vertices[vert_count].Color = c4; vert_count++;
  47.     if ((vert_count + 1) >= 8192) {
  48.         EndVerts(); beginCalled = true; vert_count = 0;
  49.     }            
  50. }//Draw (secondary)

f) If you're using only 1 color for most sprites, I'd recommend adding one that only passes a single color (see QuadBatch source code)

g) Here's a version that does basically the same thing but uses a destination rectangle to determine how to put it in the scene:
Code Snippet
  1. //-----------
  2. // DRAW_DEST
  3. //-----------
  4. public void DrawDest(Rectangle? sourceRect, Rectangle destRect, Color c1, Color c2, Color c3, Color c4, SpriteEffects flip = SpriteEffects.None) {
  5.     if (!beginCalled) { Console.WriteLine("BEGIN not called before QuadBatch Draw. Draw aborted."); return; }
  6.     float w, h, x1, y1, x2, y2, x3, y3, x4, y4, u1, v1, u2, v2; // generate a TARGET WIDTH, HEIGHT based on the source
  7.     if (!sourceRect.HasValue) sourceRect = new Rectangle(0, 0, tex.Width, tex.Height);
  8.     w = sourceRect.Value.Width; h = sourceRect.Value.Height;            
  9.  
  10.     //need positions of destination            
  11.     x1 = destRect.X; y1 = destRect.Y;                  //upper-left
  12.     x2 = destRect.X + destRect.Width; y2 = destRect.Y; //upper-right
  13.     x3 = destRect.X + destRect.Width; y3 = destRect.Y + destRect.Height; //lower-right
  14.     x4 = destRect.X; y4 = destRect.Y + destRect.Height;     //lower-left              
  15.     Rectangle tempRect = sourceRect.Value;         
  16.     u1 = tempRect.X / (float)tex.Width; //gets the texture coords in terms of (0.0f-1.0f, 0.0f-1.0f)
  17.     v1 = tempRect.Y / (float)tex.Height;
  18.     u2 = (tempRect.X + tempRect.Width) / (float)tex.Width; v2 = (tempRect.Y + tempRect.Height) / (float)tex.Height;
  19.     if ((flip & SpriteEffects.FlipVertically) != 0) {
  20.         var temp = v2; v2 = v1; v1 = temp; //BR_Y                
  21.     }
  22.     if ((flip & SpriteEffects.FlipHorizontally) != 0) {
  23.         var temp = u2; u2 = u1; u1 = temp; //BR_X                
  24.     }
  25.     vertices[vert_count].Position = new Vector3(x1, y1, depth);  vertices[vert_count].TextureCoordinate = new Vector2(u1, v1);   //upper-left texCoord
  26.     vertices[vert_count].Color = c1; vert_count++;
  27.     vertices[vert_count].Position = new Vector3(x2, y2, depth);  vertices[vert_count].TextureCoordinate = new Vector2(u2, v1);   //upper-right texCoord
  28.     vertices[vert_count].Color = c2; vert_count++;
  29.     vertices[vert_count].Position = new Vector3(x3, y3, depth);  vertices[vert_count].TextureCoordinate = new Vector2(u2, v2);   //lower-right texCoord
  30.     vertices[vert_count].Color = c3; vert_count++;
  31.     vertices[vert_count].Position = new Vector3(x4, y4, depth);  vertices[vert_count].TextureCoordinate = new Vector2(u1, v2);   //lower-left texCoord
  32.     vertices[vert_count].Color = c4; vert_count++;
  33.     if ((vert_count + 1) >= 8192) {
  34.         EndVerts();  beginCalled = true;  vert_count = 0;
  35.     }            
  36. }//Draw (Draw Dest 4 color)

Here it creates the vertex positions from the destination rect... no scaling or rotation.

h) Again, you probably want to make a single color version of this (see QuadBatch source code for all versions of Draw if need)


28) Begin just sets what device states that End() will use to draw the vertices made so far.
a) This is the complete version:
Code Snippet
  1. public void Begin(Texture2D texture, BlendState blend_State, SamplerState sampler_State, DepthStencilState depth_StencilState, Matrix World, Effect efct)
  2. {
  3.     if (texture == null) { Console.WriteLine("QuadBatch Begin aborted because texture == null"); return; }
  4.     if (beginCalled) { Console.WriteLine("QuadBatch Begin already called."); return; }
  5.     tex = texture;
  6.     blendState = blend_State;
  7.     samplerState = sampler_State;
  8.     depthStencilState = depth_StencilState;
  9.     vert_count = 0; font_vert_count = 0; beginCalled = true;
  10.     world = World; WorldViewProj = world * ViewProj;
  11.     fx = efct;
  12. }


b) You'll likely want the reduced versions (overloads) of Begin. Most of the time, you'll probably only set a texture or a blend state before drawing. See QuadBatch source code to see the shorter Begin versions.
Note if fx is set it will over-ride the existing vertex or pixel shader set with default_shader (until set to null). Usually we only use a pixel shader (and so it will still use the default vertex shader).

29) The End() method will check if there's anything to render, set the device states, copy the vertex data into the vertex buffer, apply the default shaders (and if applicable apply an alternative shader), set the MatrixTransform needed by the vertex shader, set the texture, DrawTriangles (and call end font if there is font to show)
if device.Textures[0] = tex doesn't work [it should], try passing it as a value - I think this happened to me once before ... also note that you can't go device.Textures[1],[2],etc = some_texture -- you must instead pass textures by saying something like: shader.Parameters["NextTextureName"].SetValue(YourSecondTexture);

Code Snippet
  1. //E N D -----------------------------------------        
  2. public void End()
  3. {
  4.     if (!beginCalled) { Console.WriteLine("call to END without begin called. Aborting End."); return; }
  5.     if (vert_count >= 3)
  6.     { //nothing to draw
  7.         device.BlendState = blendState;      device.DepthStencilState = depthStencilState;      device.SamplerStates[0] = samplerState;
  8.         if (device.RasterizerState.CullMode != CullMode.None) { //don't bother unless necessary                                    
  9.             device.RasterizerState = RasterizerState.CullNone;
  10.         }
  11.         //Draw entire vertex list:                    
  12.         int triangle_count = vert_count / 2;
  13.         vertexBuffer.SetData(vertices, 0, vert_count);
  14.         device.SetVertexBuffer(vertexBuffer);
  15.         default_shader.CurrentTechnique.Passes[0].Apply(); //<--this must be applied first to get the vertex shader / world matrix transformation
  16.         default_shader.Parameters["MatrixTransform"].SetValue(WorldViewProj);                
  17.         if (fx != null) fx.CurrentTechnique.Passes[0].Apply(); //this will simply modify the pixel shader used (should make sure it multiplies by the vertex color internally)                
  18.         device.Textures[0] = tex; // <--- do this just before drawing only                
  19.         device.DrawUserIndexedPrimitives<VertexPositionColorTexture>(PrimitiveType.TriangleList, vertices, 0, vert_count, indices, 0, triangle_count);
  20.     }
  21.     if (font_vert_count > 0) EndFont();
  22.     beginCalled = false; fx = null;
  23. }

30) EndFont and EndVerts basically do the same thing... except EndFont renders out triangles composed from the fontverts for text. Also it only sets device states if they weren't already set in the original End() which calls this if there are fonts to render too.
EndVerts is the same as End except it will not change beginCalled or reset the fx being used (or call to render fonts)... where End() will do those things whether or not there was actually something to render.

Code Snippet
  1. //E N D  T E X T-----------------------------------------
  2. public void EndFont() //works like end except internally
  3. {
  4.     if (!beginCalled) { Console.WriteLine("call to ENDText without begin called. Aborting End."); return; }
  5.     if (font_vert_count < 3) return; // nothing to draw
  6.     if (vert_count < 3) {            // not set up previously:
  7.         device.BlendState = blendState;      device.DepthStencilState = depthStencilState;      device.SamplerStates[0] = samplerState;
  8.         if (device.RasterizerState.CullMode != CullMode.None) { //don't bother unless necessary                                    
  9.             device.RasterizerState = RasterizerState.CullNone;
  10.         }
  11.     }
  12.     //Draw entire vertex list:                
  13.     int triangle_count = font_vert_count / 2;
  14.     vertexBuffer.SetData(fontverts, 0, font_vert_count);
  15.     device.SetVertexBuffer(vertexBuffer);
  16.     default_shader.CurrentTechnique.Passes[0].Apply();
  17.     default_shader.Parameters["MatrixTransform"].SetValue(font_WorldViewProj);           
  18.     device.Textures[0] = font_tex; // <--- do this just before drawing only
  19.     device.DrawUserIndexedPrimitives<VertexPositionColorTexture>(PrimitiveType.TriangleList, fontverts, 0, font_vert_count, indices, 0, triangle_count);
  20. }
  21.  
  22. public void EndVerts()
  23. {
  24.     if (!beginCalled) { Console.WriteLine("call to END without begin called. Aborting End."); return; }
  25.     beginCalled = false;
  26.     if (vert_count < 3) return;
  27.     device.BlendState = blendState;      device.DepthStencilState = depthStencilState;      device.SamplerStates[0] = samplerState;
  28.     if (device.RasterizerState.CullMode != CullMode.None) { //don't bother unless necessary                                
  29.         device.RasterizerState = RasterizerState.CullNone;
  30.     }
  31.     //Draw entire vertex list:                
  32.     int triangle_count = vert_count / 2;//sprite_count * 2;            
  33.     vertexBuffer.SetData(vertices, 0, vert_count);
  34.     device.SetVertexBuffer(vertexBuffer);
  35.     default_shader.CurrentTechnique.Passes[0].Apply(); //<--this must be applied first to get the vertex shader / world matrix transformation
  36.     default_shader.Parameters["MatrixTransform"].SetValue(WorldViewProj);            
  37.     if (fx != null) fx.CurrentTechnique.Passes[0].Apply(); //this will simply modify the pixel shader used (should make sure it multiplies by the vertex color internally)                
  38.     device.Textures[0] = tex; // <--- do this just before drawing only
  39.     device.DrawUserIndexedPrimitives<VertexPositionColorTexture>(PrimitiveType.TriangleList, vertices, 0, vert_count, indices, 0, triangle_count);
  40. }


31) Technically, we'll just use regular SpriteFonts for this project, but to be complete, I'll explain what the PrepareFont method does in QuadBatch... (you can skip this if you aren't interested and just get the source for QuadBatch)

a) PrepareFont will examine the font texture (looking for green (0,1,0) as dividers) and figure out where all the characters are and build an array of FontData structs to keep track of where those letters were on the texture.

Code Snippet
  1. //-----------------------------------------------------------------------------------------
  2. private float average_width;
  3. public float Average_Width { get { return average_width; } }
  4. // P R E P A R E  F O N T
  5. public void PrepareFont(Texture2D newfont_tex, int fontType = 0)
  6. {
  7.     if (newfont_tex == null) { Console.WriteLine("Invalid texture for font."); return; }
  8.     Color[] FontColorData;
  9.     font_tex = newfont_tex;
  10.     FontColorData = new Color[font_tex.Width * font_tex.Height];
  11.     font_tex.GetData<Color>(FontColorData);
  12.     font_tex_loaded = true;
  13.     int wide = font_tex.Width, high = font_tex.Height, font_height = 49;
  14.     if (fontType == 0) { font_height = 49; }
  15.     Color col;
  16.     float w = 1.0f / wide, h = 1.0f / high;
  17.     int i = 32;
  18.     int xa = 0, ya = 2;
  19.     bool go_down = false, finished = false;
  20.     col = (Color)FontColorData[0];
  21.     average_width = 1f;
  22.     bool done = false;
  23.     do
  24.     {
  25.         ya++; //<--must do first<--
  26.         col = FontColorData[3 + ya * wide];
  27.         if ((col.R < 5) && (col.G > 249) && (col.B < 5) && (col.A > 249)) { done = true; }
  28.         if (ya >= (high - 1)) { done = true; Console.WriteLine("Error finding font hieght"); }
  29.     } while (!done);
  30.     font_height = ya - 2;

I added a private global of average_width only used with font stuff (which is why it's down here).
Color[] FontColorData = a list of all the colors that make up the texture (font image).
font_tex.GetData<Color>(FontColorData)... does exactly what it says... fills FontColorData with all the colors.
By default the font_height is set to 49 at first but set to ya-2 after processing.
Note: starting index = 32 (because first character is 32 in ASCII)
Do {
  - check pixel color [ x=3, y=ya(starting at 2)]
  - if the pixel is extremely green [(ie: 0, 255, 0)<--what a spacer shoud be] then we're done (we've found a green pixel)
}
What this does it it starts checking at a pixel location (3,2) that should be clear (0,0,0,0) and scans downward (3,3),(3,4),(3,5),etc..
until it finds the first green barrier below the first row of characters.
Now it knows the font_height... (ya-2)

b) Next we need to scan through the rows and figure out the font data:

Code Snippet
  1.     ya = 4;
  2.     finished = false;
  3.     do
  4.     {
  5.         col = FontColorData[xa + ya * wide];
  6.         if ((col.R < 5) && (col.G > 249) && (col.B < 5) && (col.A > 249)) xa++;
  7.         else
  8.         {
  9.             fd[i].x1 = (float)xa + 1; fd[i].y1 = (float)(ya - 2);
  10.             done = false;
  11.             do
  12.             {
  13.                 xa++; //<--must do first<--
  14.                 col = FontColorData[xa + ya * wide];
  15.                 if ((col.R < 5) && (col.G > 249) && (col.B < 5) && (col.A > 249)) { done = true; }
  16.                 if (xa >= (wide - 1)) { done = true; go_down = true; }
  17.             } while (!done);
  18.             fd[i].x2 = (float)(xa - 2); fd[i].y2 = (float)(ya - 4 + font_height);
  19.             fd[i].w = fd[i].x2 - fd[i].x1; fd[i].h = fd[i].y2 - fd[i].y1;
  20.             average_width += fd[i].w; fd[i].iw = fd[i].w * 0.5f;
  21.             if (i < 130) i++; else { Console.WriteLine("index too high for font."); }
  22.         }
  23.         if ((xa >= (wide - 1)) || (go_down))
  24.         {
  25.             go_down = false;
  26.             xa = 1; ya = (ya + font_height) + 2;
  27.             if (ya >= high) finished = true;
  28.         }
  29.         if (i > 127) finished = true;
  30.     } while (!finished);
  31.     text_h_space = (fd[32].x2 - fd[32].x1) * default_font_size; text_v_space = (fd[32].y2 - fd[32].y1 - 2) * default_font_size;
  32.     average_width = average_width / i * 1.2f; //*1.2 to boost spacing a bit                
  33. }//PrepareFont

do {
- get color (ie: xa = 3, ya = 4)
- if green barrier, xa++
- else we started moving into a character so:
  {
     - record font data for top left corner of this character
     do {
      - advance xa until hit green barrier on the right side of the character
      - if we're at the far right side of the bitmap, go_down the map (and back to left side)
     }
     - record the bottom right corner of the character
     - record the width and height of this character (technically height shouldn't change)
     - start calculating the average width... and record the half-width (helps to center characters later)
     - get ready for next character(fd[i]) to record
     - if we've scanned the entire map, finished = true
}
set text_h_space (space between characters) as width of @ character * default_font_size (same idea for vertical spacing)
Set average_width to be the sum of widths / number_of_characters

32) It should be possible to get or set the default font size so:

Code Snippet
  1. public float Default_Font_Size { get { return default_font_size; }
       set { default_font_size = value; text_h_space = (fd[32].x2 - fd[32].x1) * default_font_size; text_v_space = (fd[32].y2 - fd[32].y1 - 2) *          default_font_size; }
    }

Just saying: default_font_size = value, won't work right so a set {} is used to ensure spacing is also adjusted accordingly.

33) Later we might make a text class which will automatically align text or center it, in which case it will be handy to be able to measure strings made with the font.
Code Snippet
  1. // M E A S U R E  S T R I N G  F A S T
  2. public Vector2 MeasureStringFast(string text)
  3. {
  4.     float kern_space = text_h_space * 0.25f;
  5.     float mult = default_font_size * 0.5f;
  6.     Vector2 size = new Vector2(1, fd[32].h * default_font_size);
  7.     size.X = text.Length * average_width;
  8.     size.X *= mult;
  9.     size.X += (kern_space * text.Length) + text_h_space * 0.5f;
  10.     return (size);
  11. }
size of a letter is first set to (1, height at default size)
and then the x value(width of string) is adjusted as the text's number of characters * the average_width of each character.
This is then adjusted for how the spacing will be... thus returning the width of the entire string in .X

34) This is a more accurate string measurement (if exact precision is desired):
Code Snippet
  1. // M E A S U R E  S T R I N G
  2. public Vector2 MeasureString(string text)
  3. {
  4.     float kern_space = text_h_space * 0.25f;
  5.     float mult = default_font_size * 0.5f;
  6.     Vector2 size = new Vector2(1, fd[32].h * default_font_size);
  7.     int a = 0;
  8.     do
  9.     {
  10.         size.X += fd[(int)text[a]].w;
  11.         a++;
  12.     } while (a < text.Length);
  13.     size.X *= mult;
  14.     size.X += (kern_space * text.Length) + text_h_space * 0.5f;
  15.     return (size);
  16. }
This one does the same except instead of using average_width, it adds up the exact width for the characters used.

Note: In QuadBatch I also have a MeasureMultiString which considers text where there are carriage returns and figures out the width and height of the rectangle the text group would occupy... You probably won't use it so it is not included here.
(I'll probably improve the MeasureString and DrawString methods in the future since I see some opportunities to improve them)

35) a) Just a few overloads for the DrawString method we're about to make:
Code Snippet
  1. public void DrawString(string text, Vector2 pos, Color color, Alignment align = Alignment.None) { DrawString(text, pos, new Vector2(default_font_size, default_font_size), color, align); }
  2. public void DrawString(string text, Vector2 pos, float scale, Color color, Alignment align = Alignment.None)
  3. {
  4.     DrawString(text, pos, new Vector2(scale, scale), color, align);
  5. }

b) Unfortunately the string drawing methods are a bit bulky. I'll show you some code and briefly explain what's happening...
The following one is accurate for varying character widths, but slower.
I recommend to use DrawStringFast when timing is important.
Code Snippet
  1. public void DrawString(string text, Vector2 pos, Vector2 scale, Color color, Alignment align = Alignment.None)
  2. {
  3.     if (!beginCalled) { Console.WriteLine("Can't DrawString. Begin not set."); return; }
  4.     if (!font_tex_loaded) { Console.WriteLine("Font texture not loaded. Aborting DrawString."); return; }
  5.     float px = pos.X, py = pos.Y;
  6.     float spacing = (fd[32].x2 - fd[32].x1) * scale.X, v_space = (fd[32].y2 - fd[32].y1 - 2) * scale.Y;
  7.     float kern_space = spacing / 4;
  8.     float w, h, tx1, tx2, ty1, ty2;
  9.     if (align != Alignment.None) {
  10.         if (align == Alignment.LeftAlign) { pos.X = spacing; px = spacing; } //one space from left side of screen
  11.         else {
  12.             float max_x = px, xx = px;
  13.             int q = 0;
  14.             do {
  15.                 if (max_x < xx) max_x = xx;
  16.                 var c = text[q];
  17.                 if ((c == '\n') || (c == '\r')) { xx = pos.X; q++; continue; } if ((c < 33) || (c > 127)) { xx += spacing; q++; continue; }
  18.                 int ii = (int)c; w = fd[ii].w * scale.X;
  19.                 xx += (w / 2 + kern_space); q++;
  20.             } while (q < text.Length); if (max_x < xx) max_x = xx;
  21.             float text_width = max_x - px;
  22.             if (align == Alignment.CenterAlign)
  23.             {
  24.                 pos.X = screenWidth / 2 - text_width / 2; px = pos.X;                       
  25.             }
  26.             else if (align == Alignment.RightAlign)
  27.             {
  28.                 pos.X = screenWidth - text_width - spacing; px = pos.X;
  29.             }
  30.         }
  31.     }

- spacing is scaled based on character size (and divided by 4 to get actual kern_space which we want to be about 1/4 size)
- If alignment is needed:
- if LeftAlign: set character at 1 space from left side of screen
- else:
- Need to find the maximum x position(right side of text), so loop through text characters:
{
- if carriage retrun, set xx back to start... if no image of character, add a space
- set w = character_width scaled
- move to next character position
}
(basically we just measured the string -- so text_width = max_x(right-most) - px(start_pos)
For CenterAlign, set the text pos.X at (half screen width) - (half text width)
For RightAlign, set text pos.X as (screen width) - text_width (minus 1 space)

c) Now we need to loop through the characters, fetching the index to use based on the text char ...
and get the scaled character sizes...
and get the texture coordinates of each character based on location/texture_dimension

Code Snippet
  1.     int a = 0, i = 0;
  2.     do
  3.     {
  4.         var c = text[a];
  5.         i = (int)c;
  6.         if ((c == '\n') || (c == '\r')) { px = pos.X; py += v_space; a++; continue; }
  7.         if ((c < 33) || (c > 127)) { px += spacing; a++; continue; }
  8.         w = fd[i].w * scale.X;
  9.         h = fd[i].h * scale.Y;
  10.         tx1 = fd[i].x1 / font_tex.Width; ty1 = fd[i].y1 / font_tex.Height; tx2 = fd[i].x2 / font_tex.Width; ty2 = fd[i].y2 / font_tex.Height;
  11.         fontverts[font_vert_count].Position = new Vector3(px, py, font_depth);              fontverts[font_vert_count].TextureCoordinate = new Vector2(tx1, ty1);   //upper-left texCoord
  12.         fontverts[font_vert_count].Color = color; font_vert_count++;
  13.         fontverts[font_vert_count].Position = new Vector3((px + w), py, font_depth);        fontverts[font_vert_count].TextureCoordinate = new Vector2(tx2, ty1);   //upper-right texCoord
  14.         fontverts[font_vert_count].Color = color; font_vert_count++;
  15.         fontverts[font_vert_count].Position = new Vector3((px + w), (py + h), font_depth);  fontverts[font_vert_count].TextureCoordinate = new Vector2(tx2, ty2);   //lower-right texCoord
  16.         fontverts[font_vert_count].Color = color; font_vert_count++;
  17.         fontverts[font_vert_count].Position = new Vector3(px, (py + h), font_depth);        fontverts[font_vert_count].TextureCoordinate = new Vector2(tx1, ty2);   //lower-left texCoord
  18.         fontverts[font_vert_count].Color = color; font_vert_count++;
  19.         if ((font_vert_count + 1) >= 8192) {  //Flush:
  20.             EndFont(); beginCalled = true; font_vert_count = 0;
  21.         }
  22.         px += (w / 2 + kern_space);
  23.         a++;
  24.     } while (a < text.Length);
  25. }

text loop:
- i = index of character in ASCII (we're using 32 to 127)
- if carriage return, start a new line and continue (go back to do { )
- get scaled character width,height
- tx1,ty1, tx2, ty2 = top-left and bottom-right texture coordinates (using location/texture_dimension)
- record the 4 font vertices (position, texture coord, color)
- advance the character's position by desired spacing amount
- if the vertex count reaches 8192, call EndFont to draw the vertices to the target (flush) and reset the vertex array for reload

36) Faster version! DrawStringFast ... this one relies on default_font_size setting (use Set to change) but if you'd like to pass Scale to it, you can use the DrawStringFastScaled which is almost identical excent the width/height are scaled for each character. (See QuadBatch source code to get the Scale version).
You may also notice that this one doesn't do alignments, uses average_width, and sets the spacing differently... the results generally look good - not noticably different than the auto-alignment version above.

Code Snippet
  1. public void DrawStringFast(string text, Vector2 pos, Color color)
  2. {
  3.     if (!beginCalled) { Console.WriteLine("Can't DrawString. Begin not set."); return; }
  4.     if (!font_tex_loaded) { Console.WriteLine("Font texture not loaded. Aborting DrawString."); return; }
  5.     float px = pos.X + average_width * 0.5f, py = pos.Y;
  6.     float spacing = (average_width + fd[32].w * 0.5f) * default_font_size * 0.5f, v_space = (fd[32].y2 - fd[32].y1 - 2) * default_font_size;
  7.     float w, h, tx1, tx2, ty1, ty2;
  8.     float inv_width = 1.0f / font_tex.Width, inv_height = 1.0f / font_tex.Height;
  9.     h = fd[32].h * default_font_size;
  10.     int a = 0, i = 0;
  11.     do
  12.     {
  13.         var c = text[a];
  14.         i = (int)c;
  15.         if ((c == '\n') || (c == '\r')) { px = pos.X; py += v_space; a++; continue; }
  16.         if ((c < 33) || (c > 127)) { px += spacing; a++; continue; }
  17.         w = fd[i].iw * default_font_size;
  18.         tx1 = fd[i].x1 * inv_width; ty1 = fd[i].y1 * inv_height; tx2 = fd[i].x2 * inv_width; ty2 = fd[i].y2 * inv_height;
  19.         fontverts[font_vert_count].Position = new Vector3(px - w, py, font_depth);        fontverts[font_vert_count].TextureCoordinate = new Vector2(tx1, ty1);   //upper-left texCoord
  20.         fontverts[font_vert_count].Color = color; font_vert_count++;
  21.         fontverts[font_vert_count].Position = new Vector3(px + w, py, font_depth);        fontverts[font_vert_count].TextureCoordinate = new Vector2(tx2, ty1);   //upper-right texCoord
  22.         fontverts[font_vert_count].Color = color; font_vert_count++;
  23.         fontverts[font_vert_count].Position = new Vector3(px + w, (py + h), font_depth);  fontverts[font_vert_count].TextureCoordinate = new Vector2(tx2, ty2);   //lower-right texCoord
  24.         fontverts[font_vert_count].Color = color; font_vert_count++;
  25.         fontverts[font_vert_count].Position = new Vector3(px - w, (py + h), font_depth);  fontverts[font_vert_count].TextureCoordinate = new Vector2(tx1, ty2);   //lower-left texCoord
  26.         fontverts[font_vert_count].Color = color; font_vert_count++;
  27.         if ((font_vert_count + 1) >= 8192) {  //Flush:
  28.             EndFont(); beginCalled = true; font_vert_count = 0;
  29.         }
  30.         px += spacing;
  31.         a++;
  32.     } while (a < text.Length);
  33. }

37) Draw line, Draw Rectangle...If you wanted, you could use quadbatch.Rect to show bounding rectangles instead of using a box sprite from your tiles_image, however you'd need to do it within a quadBatch begin-end with a texture set where the top-left 4(or more) pixels of the image need to be a tiny white square... I do this for most of my tile_images... so it works for me but you can modify this to work with your own texture maps. DrawLine allows you to define where to extract the white from using Rectangle pixel.
Code Snippet
  1. // D R A W  L I N E        
  2. public void DrawLine(Rectangle pixel, Vector2 start, Vector2 end, Color color, float thickness = 2f)
  3. {
  4.     Vector2 delta = end - start;
  5.     float rot = (float)Math.Atan2(delta.Y, delta.X);
  6.     if (pixel.Width > 0) { pixel.Width = 1; pixel.Height = 1; }
  7.     Draw(pixel, start, new Vector2(0, 0.5f), new Vector2(delta.Length(), thickness), rot, color);
  8. }
  9. public void Line(float x1, float y1, float x2, float y2, Color color, float thickness = 1f)
  10. {
  11.     Rectangle pixel = new Rectangle(1, 1, 1, 1);
  12.     Vector2 end = new Vector2(x2, y2);
  13.     Vector2 start = new Vector2(x1, y1);
  14.     Vector2 delta = end - start;
  15.     float rot = (float)Math.Atan2(delta.Y, delta.X);
  16.     Draw(pixel, start, Vector2.Zero, new Vector2(delta.Length(), thickness), rot, color);
  17. }
  18. public void Rect(Rectangle r, Color color, float thickness = 1f) {
  19.     Rect(r.X, r.Y, r.X + r.Width, r.Y + r.Height, color, thickness);
  20. }
  21. public void Rect(Rectangle r, Vector2 p, Color color, float thickness = 1f)
  22. {
  23.     Rect(p.X, p.Y, p.X + r.Width, p.Y + r.Height, color, thickness);
  24. }
  25. public void Rect(float x1, float y1, float x2, float y2, Color color, float thickness = 1f)
  26. {
  27.     Rectangle pixel = new Rectangle(1, 1, 1, 1);
  28.     Draw(pixel, new Vector2(x1, y1), Vector2.Zero, new Vector2((x2 - x1), thickness), 0f, color);
  29.     Draw(pixel, new Vector2(x2, y1), Vector2.Zero, new Vector2((y2 - y1), thickness), -4.712389f, color);
  30.     Draw(pixel, new Vector2(x2, y2), Vector2.Zero, new Vector2((x2 - x1), thickness), 3.1415926f, color);
  31.     Draw(pixel, new Vector2(x1, y2), Vector2.Zero, new Vector2((y2 - y1), thickness), -1.5707963f, color);
  32. }

DrawLine:
- get the vector of the line to draw (where start would be the rotation origin)
- You may remember that the angle in the triangle this makes can be determined with opposite over adjacent... tan(a) = o/a
So to get (a), you use Atan(o, a) or Atan2(delta.Y, delta.X)
Then we draw a pixel that's stretched to be the length of the line and rotated the right amount.
The Rect method(s) (outline rectangle):
- assumes pixel at 1,1 is white (could modify this to be a passed parameter)
- Draws to a destination rectangle to stretch it making the desired horizontal or vertical lines (using the correct angle of rotation)

38) DrawDest overload that allows scaling and rotation of destination... there may be rare situations where you need to match images to a destination rect but you still want to apply scaling,etc... after (see QuadBatch source code)... I won't go into detail how it works here but it does work like the Draw routines already discussed.

39) Draw Quad
a) Just draws a filled colored quad (any 4 vertices and can use 4 different colors):
Code Snippet
  1. // D R A W  C O L O R  Q U A D
  2. // Draw a colored quad using a blank pixel:
  3. public void DrawColorQuad(Rectangle pixel, Vector2 p1, Vector2 p2, Vector2 p3, Vector2 p4, Color c1, Color c2, Color c3, Color c4) {
  4.     if (!beginCalled) { Console.WriteLine("BEGIN not called before QuadBatch Draw. Draw aborted."); return; }
  5.     float u1, v1, u2, v2;
  6.     u1 = pixel.X / (float)tex.Width; //gets the texture coords in terms of (0.0f-1.0f, 0.0f-1.0f)
  7.     v1 = pixel.Y / (float)tex.Height;
  8.     u2 = (pixel.X + pixel.Width)  / (float)tex.Width;
  9.     v2 = (pixel.Y + pixel.Height) / (float)tex.Height;
  10.     vertices[vert_count].Position = new Vector3(p1, depth);  vertices[vert_count].TextureCoordinate = new Vector2(u1, v1);   //upper-left texC
  11.     vertices[vert_count].Color = c1; vert_count++;
  12.     vertices[vert_count].Position = new Vector3(p2, depth);  vertices[vert_count].TextureCoordinate = new Vector2(u2, v1);   //upper-right
  13.     vertices[vert_count].Color = c2; vert_count++;
  14.     vertices[vert_count].Position = new Vector3(p3, depth);  vertices[vert_count].TextureCoordinate = new Vector2(u2, v2);   //lower-right
  15.     vertices[vert_count].Color = c3; vert_count++;
  16.     vertices[vert_count].Position = new Vector3(p4, depth);  vertices[vert_count].TextureCoordinate = new Vector2(u1, v2);   //lower-left
  17.     vertices[vert_count].Color = c4; vert_count++;
  18.     if ((vert_count + 1) >= 8192) {
  19.         EndVerts(); beginCalled = true; vert_count = 0;
  20.     }            
  21. }

Simply gets the uv coords and fills in the 4 vertices to draw later.

b) DrawRect - same as DrawColorQuad except as:
public void DrawRect(Rectangle pixel, Rectangle r, Color col)
and just before filling vertices[]:
Vector2 p1, p2, p3, p4;
p1.X = r.X; p1.Y = r.Y; p2 = p1; p2.X += r.Width; p3 = p2; p3.Y += r.Height; p4 = p1; p4.Y += r.Height;

to get the 4 vertex positions before filling them.

You can add plenty of other custom drawing stuff now that you know how to do it.
For 3D stuff, you may want to use non-dynamic vertex buffer where possible... and a variety of other differences but that would be another tutorial.


40)Back in Game.cs (you may have made some of these changes already)

a) We'll add our new QuadBatch (to be used mainly with MeoMotion stuff [altho I use it for most things]):
i) Under //DISPLAY variables, perhaps under SpriteBatch, Add this:
QuadBatch     quadBatch;
and also in variables, under //POSITIONS we'll add a new variable category for various helpers.
Code Snippet
  1. //UTILS
  2. static public Random  rnd;
and update the map variable section to this:
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        

You should already have the first 4 and just need to add Editor.


b) In Initialize() somewhere after screenW and screenH have been set (perhaps after screen_center), add this:

Code Snippet
  1. quadBatch     = new QuadBatch(Content, GraphicsDevice, "QuadEffect", "FontTexture", screenW, screenH); // setup distortable quad class
(This assumes QuadEffect.fx and FontTexture.png have been added to Content using MonogamePipeline)

Just before creating the Input object, add the call to construct a Random:
Code Snippet
  1. rnd = new Random(); // INIT UTILS            
  2. inp = new Input();  // INIT INPUT
(inp = new Input(); sould already be there)

c) In LoadContent(), we'll want to setup our editor and map... just before setting the tiles image, set the editor up:
NOTE: Player class does not exist yet (so you'll either need to temporarily modify this or add an empty player class for now):
Code Snippet
  1. // INIT EDITOR
  2. editor    = new Editor(map, inp, /*player,*/ sheet/*, monsterSys*/); // do this here because editor needs player and player needs hud and hud needs //font (and font cannot be null)
  3.  
  4. // SET IMAGE FOR TILES
  5. map.SetTilesImage(tiles_image);                       // this might be used in multiple places so send a reference to map but store tiles here
- editor is set up using the map, input, /*player*/ sheet, /*monsterSys*/); ... later once Player and MonsterSys exist, don't forget to remove the /* */ (so they're no longer comments)
...
Somewhere under map.AddBorder(6), add the following:
Code Snippet
  1. // L O A D  L E V E L
  2. editor.LoadLevel(LEVEL_NAME);
So by default, the LoadLevel is called regardless of whether in edit mode or not (to load a default start level)

d) In Update() under GameState.edit,
change it to match this:
Code Snippet
  1. case GameState.edit:
  2.     //----------------------
  3.     // E D I T O R   M O D E  (input/updates)
  4.     cam_pos = map.loc.ToVector2() * 64;
  5.     // UPDATE MONSTERS
  6.     editor.Update();
  7.  
  8.     // SWITCH TO PLAY MODE
  9.     if (inp.Keypress(Keys.Enter)) gameState = GameState.play;
  10.     break;   //---------------- end editor mode

All we really did was add editor.Update();

e) In Draw() where you left the comment: // EDITOR_INSTRUCTIONS, add:

Code Snippet
  1. else
  2. {   // EDITOR INSTRUCTIONS
  3.     editor.DrawInstructions(spriteBatch, far_background, font, screenH);                
  4. }



-----------------------------------------------------------

41) MEO MOTION CLASS:
---------------------------------
Note: If you plan to use traditional animation(frame lists) instead of vector based animations(skeletons and mesh distortions) , you can refer to the Animator class we'll make later(for explosions) to make something that will work in place of this.

First thing you'll need though, is a character. You could draw, scan(or tablet) a character sketch, load into Photoshop, Krita, Gimp, Sai, MangaStudio, or whatever you like to use. There are burn/dodge shadow/lighting tools (and tutorials online) as well as tutorials on how to make professional looking inking (if that's what you like). I recommend trying to avoid being too generic with your character designs so best to try to come up with something people will enjoy.

a) After you've inked, painted, and shaded your character. You'll need to set it up for animation in MeoMotion (or Spriter or Spine or whatever one you're using)... or even just frame based animation done in your art software. If you wanted, you could also use 3D characters too and swap and animate their parts in these types of software (making it look like runtime 3D animation rendering)
I made this video to help you get started on your character:

ART SETUP (longer newer version):


IMPORT and BONES SETUP:


ART tips/setup (shorter older version):



b) I've made this video to make it easy to understand how to setup and animate a MeoMotion character (similar principle for other methods you might want to use) ... this includes an idle and run animation tutorial. You'll need to figure out the other animations on your own but at least you'll know how to figure it out using animation refence sheets.

ANIMATION with MeoMotion (newer longer version):



ANIMATING with MeoMotion (shorter older version):


c) I've updated the MeoMotion runtime stuff a tiny bit for this tutorial... and since it will be useful to understand how the code works (for adjusting animation to match controller input), I'll provide an explanation of how this works... If you want to trust the 2 MeoMotion classes (MeoMotion, MeoPlayer), then feel free to do so and skip this if you like...
The following video provides a brief overview of the classes used and where to place your files and how to dispatch your animations (and tips: like finding the animation names if you forgot them).

USING MeoMotion for your game (exporting and importing into game):
First half of video discusses animation techniques and second half is about exporting:



USING MeoMotion for your game (older version):



i) In your solution explorer, right click the c# project_name and Add>>Folder: "MeoMotion"

ii) Now right-click on MeoMotion and Add>>Class: "MeoMotion" ... this will store and load your MeoMotion keyframe animations.

iii) To be compact, we're going to hold multiple tiny classes used by MeoMotion within the same MeoMotion.cs file.
You may see how small these are and think of making an editor yourself... but I would caution that the editor code is massive and much more complex.
Above the MeoMotion class, type in (or paste) the following class:

Code Snippet
  1. // Holds info about sprite parts
  2. class SpritePart
  3. {
  4.     public string name;
  5.     public Rectangle rect;         // source rectangle on sprite sheet     
  6.     public Vector2 pivot;          // pivot/origin to rotate around
  7.     public int parent;             // what sprite was parent (useful when programming vector tracking [like head looking or pointing weapon] )
  8.     public Vector2 m1, m2, m3, m4; // model points around origin(0,0) before transforms
  9. }

- Each image on your sprite sheet is called a SpritePart (with names: arm, foot, head, etc)
- rect is the image source rectangle on the sheet
- pivot is the point it rotates the image around (like a forearm rotates around the elbow point)
- parent is what the part attaches to. Animated parts will always inherit transformations from the parent ... that is, whatever offsets or rotations are set will effect his part [the child] (and sometimes scale too depending)
- the image can be distorted for jiggly effects (like hair flow) so each image part has model vertices to form a distortable quad. Model points are rotated around 0,0 before transforming, so these points represent the distances from the origin to each point on the quad (as 4 vectors to rotate and transform (or scale) around 0,0) ... after these are done the part is pasted onto the end of the bone(vector2) of the parent.

iv) Add the following class between SpritePart and before MeoMotion class:
Code Snippet
  1. // Holds an individual key for a part in an animation
  2. class Key
  3. {        
  4.     public int     part;           // if non-negative tells to swap parts
  5.     public int     order;          // index for draw order
  6.     public Vector2 scale;          // sprite size
  7.     public float   rot;            // sprite rotation
  8.     public Vector2 pos;            // sprite position
  9.     public float   alpha;          // transparency
  10.     public Vector2 o1, o2, o3, o4; // vertex offsets/distortions
  11.     public bool    active;         // active (for hiding parts not animated)
  12. }

If you've done macromedia flash animation, spriter, spine, anime studio, or some 3D animation, this should be familiar.
At each loaded Keyframe (Key), the keyframe needs to know:
- if it should swap the part (any positive part #)
- the draw order (which layer this is)
- how to transform it for this key (rotate, scale, relative position)
- the alpha transparency (used to blend between animation images smoothly)
- the offsets of each vertex (after translations) for distortion purposes (squish & stretch)
- if is currently active (so if it isn't then it avoids animating it at this key).


v) After Key class and before the MeoMotion class, we'll add a MeoAnimation class to hold animations:

Code Snippet
  1. // Holds an animation (and all key and part manipulation info)
  2. class MeoAnimation
  3. {
  4.     public string animation_name; // used when need to identify which animation index to use (using dictionary)
  5.     public int num_keys;          // total keys in this animation sequence
  6.     public bool looping;//, stopped;// is a loop type animation?, stopped or playing
  7.     public int[] times;           // time of key_index
  8.     public Key[,] keys;           // keyframes (part index, key index)
  9.     public int key1;              // current key
  10.     public int key2;              // next key
  11.     public int root;              // index of root bone (could be useful)
  12.     public int start_part;        // section of parts this animation works on
  13.     public int end_part;          // "                                      "
  14.     public float timer;           // used in the example Play() method for interpolating between keys
  15.     
  16.     // add customization properties here:
  17.     public float   speed  = 1.0f; // 100%
  18.     public Vector2 offset = Vector2.Zero;
  19. }
- You can lookup animation names you've forgotten by using notepad and using Ctrl+F to find "ANIMATION_NAME" to see what animations were named inside. If you forgot to name your animations, they'll be named anim1, anim2, etc... so you might want to go back to MeoMotion and name ones you forgot to make names for... If you did combine multiple characters onto 1 project (and 1 new spritesheet automatically made for this), you'll need to Ctrl+F and find each instance of SPRITESHEET_FILENAME before looking up the ANIMATION_NAME... keeping in mind that you may have idle1, walk1, run1, jump1, for multiple characters... it distinguishes between characters by combining the original SPRITESHEET_FILENAME with the ANIMATION_NAME so that it doesn't get confused about which animation you wanted to use(ie: "Wizard.png" + "Idle1", or "Penguin.png" + "Idle1"). If you're only using single txt files and spritesheets for characters you can ignore this concept.
- num keys are how many active keyframes are in the animation
- looping ... cycle the animation - it isn't mandatory to use the default setting saved in MeoMotion (you can change this any time)
- times[] ... hold the time position for each key index to determine which set of keys to interpolate(blend) between.
- Key[,] keys ... is like this: keys[SpritePart index, Key index] ... so this way each part has a set of animation keys (if used)
- key1, key2 are the source and target keys to blend between. They call this tweening which is sort of short for animation-in-betweening, where it interpolates between the 2 keys to make them transition smoothly.
For example if an arm where 30 degrees and needed to move to 50 degrees ... as the time moved from the first key to the next, after 10% of the time it would be (90% 30 degrees + 10% 50 degrees) = 32 degrees ... and half time would be (50% key1, 50% key2)= 40 degrees, and later it arrives at (for example) (20% 30 degrees + 80% 50 degrees) = 46 degrees... so as time passes (which controls the percent) it blends (interpolates) motions between key frames. As you may already know: 90% of 30 it's actually (0.9 * 30) ... so 0.0f - 1.0f is 0% to 100%...
- root is the index of the root... might not need it but could be useful for adding a root based attachment or something...
- (start_part ...to... end_part) determine which group of parts this animation is using (each character has their own group)
- timer ... as mentioned above - it's used to interpolate (blend) between the 2 keys it's working on

Custom properties:
speed = default playback rate of all animations (perhaps all your animations were far too slow or far too fast in the editor)
offset = reposition this animation (like if ducking, jumping, etc and the relative root position was wrong in some of the animations... here you could adjust it as you observe it in the game to fix the animation's position to match better... this way you don't have to go back to the MeoMotion editor and re-export the changes... so...quick and easy tweaking).


vi) After this class, add another one (before MeoMotion class) called Final:
Code Snippet
  1. // Holds final rendering data for the Draw method
  2. class Final
  3. {
  4.     public int order;              // which index to use ( drawing order )
  5.     public int part;               // index of actual part/rect to render
  6.     public float alpha;            // transparency
  7.     public Vector2 v1, v2, v3, v4; // transformed vertices
  8.     public bool hide;              // don't draw?
  9. }

This holds final render data for a part to be drawn (ie: which part to draw(in correct order), alpha blending, the final vertex positions to draw the distortable quad with, and... whether or not it should skip drawing it (ie: if alpha gets too low, don't bother).


vii) MeoMotion class will store and load our MeoMotion animations:
Code Snippet
  1. class MeoMotion
  2.     {
  3.         ContentManager content;          
  4.         public QuadBatch batch;
  5.         public Texture2D tex;            
  6.         public int num_parts, max_parts, total_animations;
  7.         Final[] final;                 // used in the example play and draw methods        
  8.         public SpritePart[] parts;     // sprite part list
  9.         public MeoAnimation[] anim;    // all the animations
  10.         public Dictionary<string, int> lookup = new Dictionary<string, int>(); // used to find animation index of a named animation

content - refers to the ContentManager for loading images
batch - refers to quadBatch in Game1.cs
tex - holds the sprite sheet for the characters loaded in the MeoMotion txt file.
num_parts = number of sprite parts (head, body, arm, etc)
max_parts = maximum number of parts held by a character at any given time (good for memory pool allocating elsewhere)
(for example if character 1 has 12 parts and character 2 has 25 and character 3 has 17... max_parts would be 25)
total_animations = number of animations (if more than 1 character this would be the sum of ALL animations combined)
final[] = holds the final render data for a play method
parts[] = holds all the SpriteParts that could be used to assemble characters
anim[] = holds every animation for the characters
lookup = provides a way to use the animation name to get it's index (if combined then it uses sheet_name+animation_name)

viii) CONSTRUCTOR - gets references to the content manager and quadBatch
Code Snippet
  1. #region CONSTRUCTOR
  2. // C O N S T R U C T O R ------------------
  3. public MeoMotion(ContentManager _content, QuadBatch quadBatch) {
  4.     content   = _content;
  5.     batch     = quadBatch;
  6.     max_parts = 0;
  7. }
  8. #endregion


ix) Load_TXT will load the MeoMotion file from the bin folder that the game's exe is in.
(ie: bin/DesktopGL/x86/Debug) ... this way we just say something like Meo.Load_TXT("hero", Vector.One);
Code Snippet
  1. // L O A D  _  T X T -------------------------------------------------------------------------------------------------------------
  2. /// NEED TO: place the export .TXT file in your project (in BIN and then in x86 and then in debug - or whatever your build place is)
  3. /// -- apon final release of your project you'll need to make sure this exported file is in the same folder as the executable
  4. /// [ You can specify export folders in MeoMotion using export options ]
  5. // rescale - to resize the how big the sprites will appear
  6. public void Load_TXT(string FileName, Vector2 rescale, bool adjustOrder=false)
  7. {
  8.     int a = 0;//, max_key=0;
  9.     if (FileName.EndsWith(".txt")) { } else FileName += ".txt";
  10.     if (!File.Exists(FileName)) { Console.WriteLine("File not found: " + FileName); return; } //return if NOT existing         

Before we continue we first check if the txt file exists

x) We then begin reading lines of code and interpretting them. The first few lines will be standard... so we'll just read those directly first. Using a stream reader (self-disposes after use) we'll see if it's the new Combo supporting format or an older format.

Code Snippet
  1. using (StreamReader reader = new StreamReader(FileName))
  2. {
  3.     string anim_name = "";
  4.     a = -1;//0; // animation index
  5.     string line = reader.ReadLine(); string[] strs;
  6.     strs = line.Split(',');
  7.     if ((strs[0] != "SPRITESHEET_FILENAME")&&(strs[0]!="COMBO_FILENAME")) { Console.WriteLine("Data=" + strs[0]); Console.WriteLine("Unexpected first line in " + FileName + " while trying to load as .TXT MeoMotion file."); return; }
  8.     string image_filename = Path.GetFileName(strs[1]);
  9.     image_filename = Path.GetFileNameWithoutExtension(image_filename);
  10.     
  11.     tex = content.Load<Texture2D>(image_filename);        // load actual spritemap
  12.     //continue loading txt:
  13.     int current_root = 0, part_count = 0;
  14.     if (strs[0] == "COMBO_FILENAME")
  15.     {
  16.         num_parts = Convert.ToInt32(strs[4]);       
  17.         total_animations = Convert.ToInt32(strs[5]);        
  18.     }
  19.     else
  20.     {
  21.         line = reader.ReadLine(); strs = line.Split(','); //reading TOTAL_NUM_PARTS
  22.         num_parts = Convert.ToInt32(strs[1]); part_count = num_parts; if (part_count > max_parts) max_parts = part_count;
  23.         line = reader.ReadLine(); strs = line.Split(','); //reading ROOT_INDEX
  24.         current_root = Convert.ToInt32(strs[1]);
  25.         line = reader.ReadLine(); strs = line.Split(','); //reading TOTAL_ANIMATIONS
  26.         total_animations = Convert.ToInt32(strs[1]);
  27.     }
  28.     parts = new SpritePart[num_parts];                 //allocate parts
        anim  = new MeoAnimation[total_animations];        //allocate animations               

  29.     int p = 0, pi = -1, first_index = 0; // part index, timeline part index, first index within a sheet section
  30.     int k = 0;                           // key index                                     
  31.     string current_sheetname = "";       // used when sheet data has been combined into one sheet (old sheet names - used for looking up animations which use different sections of a spritemap)
  32.     bool first = false;

- tests first line for valid formatting (first splits it by comma into a set of strings [])
- if so set the image filename(.png) = actual file's name (from strs[1])
- image_filename is the same without the .png (since we'll use content to load which doesn't need the extension)
- tex = load the image using content
IF combo (due to recent changes... all projects will have COMBO_FILENAME even if they are 1 character/creature)
- 4th string = number of sprite parts ... allocate this many parts
- 5th string = number of animations ... allocate this many animations
ELSE (older file format)
- same idea but extracting for an older format (includes root at the beginning)
- set some looping variables (first = index of first part found that's associated with the current sheet section[ie: current character])

xi) Start looping through the line reads and checking the tokens to see what kind of data is on each line:

Code Snippet
  1. do
  2. {
  3.     line = reader.ReadLine(); strs = line.Split(',');
  4.     switch (strs[0])
  5.     {
  6.         case "SPRITESHEET_FILENAME": current_sheetname = Path.GetFileNameWithoutExtension(strs[1]); first = true; break; // SET FIRST so we know to set the first index
  7.         case "TOTAL_NUM_PARTS": part_count = Convert.ToInt32(strs[1]); if (part_count > max_parts) max_parts = part_count; break;
  8.         case "ROOT_INDEX": current_root=Convert.ToInt32(strs[1]); break;
  9.         case "PART_INDEX": p = Convert.ToInt32(strs[1]); if (p >= num_parts) { Console.WriteLine("Meo file integrity problem: part index p>=part_count"); return; }
  10.             if (first) { first = false; first_index = p; } // record the first index if this is the first entry of this sheet section
  11.             break;
  12.         case "PART_NAME": parts[p] = new SpritePart(); parts[p].name = strs[1]; break;
  13.         case "PART_RECTANGLE": parts[p].rect.X = Convert.ToInt32(strs[1]); parts[p].rect.Y = Convert.ToInt32(strs[2]); parts[p].rect.Width = Convert.ToInt32(strs[3]); parts[p].rect.Height = Convert.ToInt32(strs[4]); break;
  14.         case "LOCAL_POINTS_M1M2M3M4":
  15.             parts[p].m1.X = Convert.ToSingle(strs[1])*rescale.X; parts[p].m1.Y = Convert.ToSingle(strs[2])*rescale.Y;
  16.             parts[p].m2.X = Convert.ToSingle(strs[3])*rescale.X; parts[p].m2.Y = Convert.ToSingle(strs[4])*rescale.Y;
  17.             parts[p].m3.X = Convert.ToSingle(strs[5])*rescale.X; parts[p].m3.Y = Convert.ToSingle(strs[6])*rescale.Y;
  18.             parts[p].m4.X = Convert.ToSingle(strs[7])*rescale.X; parts[p].m4.Y = Convert.ToSingle(strs[8])*rescale.Y;
  19.             parts[p].pivot = -parts[p].m1; break;                                    
  20.             break;
  21.         case "PART_PARENT": parts[p].parent = Convert.ToInt32(strs[1]) + first_index; break; // offset by first index
  22.         //--------------------------------------------------------------------

- read a line, split it inro strings (strs[0...n])
- check the token:
- SPRITESHEET_FILENAME - get name of the original project png sheet associated with this section[not necessarily the current one] (starting a new character so first = true)
- TOTAL_NUM_PARTS - get the count of parts associated with current character
- ROOT_INDEX - get the index of the root bone (root part)
- PART_INDEX (for each) - sets the current part index to read for
IF this first time reading an index for this character, record the first_index
- PART_NAME - get part's name (ie: leg, arm)
- PART_RECTANGLE - get the source rectangle (where the image is on the sheet)
- LOCAL_POINTS_M1M2M3M4 - get the "model" points (the positions of the 4 vertices that make the part
and set the pivot to be -M1.... this is because the model points are modeled around the origin as offsets from it. Since M1 is left and above the origin (a negative direction vector) the vector from M1(if it were at 0,0) to the origin would be M1*-1 (opposite direction) ... this way we don't need to check and process PART_PIVOT from the file and can be sure it's correct.
- PART_PARENT - index of which part will be the parent (add first_index for this character)

xii) Deciphering animation data:
Code Snippet
  1. //--------------------------------------------------------------------
  2. case "ANIMATION_NAME": anim_name = strs[1]; break;
  3. case "ANIMATION_NUMBER":
  4.     //a = Convertert.ToInt32(strs[1]);                                                        
  5.     a++; //safer to do this instead
  6.     if (a >= total_animations) { Console.WriteLine("Error: animation index a>=total_animations"); return; }
  7.     anim[a] = new MeoAnimation();
  8.     anim[a].animation_name = anim_name;
  9.     anim[a].looping = false;
  10.     anim[a].root = current_root + first_index;          // offset by first_index
  11.     anim[a].start_part = first_index;
  12.     anim[a].end_part = first_index + part_count;
  13.     lookup.Add(current_sheetname+anim_name, a);   // Console.WriteLine("adding: "+current_sheetname+anim_name+"    "+a);  // use to test names
  14.     anim[a].key1 = 0; anim[a].key2=0; anim[a].timer=0; pi=-1; k=0;                            
  15.     break;
  16. case "ANIMATION_KEY_COUNT":
  17.     anim[a].num_keys = Convert.ToInt32(strs[1]);
  18.     anim[a].keys = new Key[num_parts,anim[a].num_keys]; // allocate keys for this animation
  19.     anim[a].times = new int[anim[a].num_keys];          // allocate times                            
  20.     break;                                           
  21. case "KEY": k = Convert.ToInt32(strs[1]); pi = -1; break;
  22. case "LOOPING": anim[a].looping = true; break;
  23. case "TIME": anim[a].times[k] = Convert.ToInt32(strs[1]); break;
  24. case "PART": pi++; anim[a].keys[pi, k] = new Key();
  25.     anim[a].keys[pi, k].part = Convert.ToInt32(strs[1]) + first_index; // offset by first_index
  26.     anim[a].keys[pi, k].active = true; break;

- get new ANIMATION_NAME (used with dictionary later to find index)
- set ANIMATION_NUMBER (index for a new animation), asign the name, assign the root part (adding the first_index of the character), and assign the section of parts from the part list (start to end as: first_index to first_index+part_count)
... set a dictionary lookup value for this animation index to associate with the animation name (combined with original sheetname from original project so we can distinguish between each character or creature) ... later when you lookup the index, it will be based on the current sheetname(ie: "monster4") associated with the player object and lookup an animation by: "walk","run","idle", etc... (you'll see)
... new animation so set the keyframe animation stuff to defaults
ANIMATION_KEY_COUNT = get how many keys are in this animation
...allocate memory to have this many keyframes for the number of parts in the current character
...allocate the time values for this many keyframes
- LOOPING (does the animation loop or not) [ this is actually usually above ANIMATION_KEY_COUNT in the file]
- KEY index (following this will be the key data for each part so reset part index pi to -1)
- TIME = time to trigger a key section change [ie: (key1=3, key2=4) to become (key1=4, key2=5) ]
- PART = update the part index, create a new part key ( anim[a].key[pi, k] = new Key() )
... get actual part swap number(if used otherwise is -1) (for combined projects) by adding the first_index
... set part to be active by default at this key...

Code Snippet
  1. case "ORDER": anim[a].keys[pi, k].order = Convert.ToInt32(strs[1]);
  2.     if (adjustOrder) anim[a].keys[pi, k].order += first_index;         // in case the custom player needs the order from the entire list
  3.     break; // offset by first_index
  4.  
  5. case "NOT_ACTIVE": anim[a].keys[pi, k].active = false; break;
  6. case "K_SCALE": anim[a].keys[pi,k].scale.X = Convert.ToSingle(strs[1]); anim[a].keys[pi,k].scale.Y = Convert.ToSingle(strs[2]); break;
  7. case "K_ROT": anim[a].keys[pi,k].rot = Convert.ToSingle(strs[1]); break;
  8. case "K_POS": anim[a].keys[pi,k].pos.X = Convert.ToSingle(strs[1])*rescale.X; anim[a].keys[pi,k].pos.Y = Convert.ToSingle(strs[2])*rescale.Y; break;
  9. case "K_ALPHA": anim[a].keys[pi, k].alpha = Convert.ToSingle(strs[1]); break;
  10. case "K_VERT_OFF1": anim[a].keys[pi, k].o1.X = Convert.ToSingle(strs[1]) * rescale.X; anim[a].keys[pi, k].o1.Y = Convert.ToSingle(strs[2]) * rescale.Y; break;
  11. case "K_VERT_OFF2": anim[a].keys[pi, k].o2.X = Convert.ToSingle(strs[1]) * rescale.X; anim[a].keys[pi, k].o2.Y = Convert.ToSingle(strs[2]) * rescale.Y; break;
  12. case "K_VERT_OFF3": anim[a].keys[pi, k].o3.X = Convert.ToSingle(strs[1]) * rescale.X; anim[a].keys[pi, k].o3.Y = Convert.ToSingle(strs[2]) * rescale.Y; break;
  13. case "K_VERT_OFF4": anim[a].keys[pi, k].o4.X = Convert.ToSingle(strs[1]) * rescale.X; anim[a].keys[pi, k].o4.Y = Convert.ToSingle(strs[2]) * rescale.Y; break;
- ORDER - set the layer that this part should occupy at this keyframe
- NOT_ACTIVE - change the active status at this keyframe to false (won't show image for this frame section)
- K_SCALE - size for the part at this key
- K_ROT - rotation of part at this key around its pivot (and then attaches to parent)
- K_POS - position (of part at this keyframe) - rescaled to fit target resolution
- K_ALPHA - transparency at the key for the part (also used in image blending and if low enough it won't process or show it to be more optimal)
- K_VERT_OFF1,2,3,4 - distortion offsets of each vertex of the sprite part's quad (also scaled to fit target resolution)
Code Snippet
  1.                     
  2.             }
  3.         } while (reader.EndOfStream != true);
  4.     }//using reader
  5.     if (total_animations > a) total_animations = a;
  6.     //precautionary setting of second key (in case no other keys):
  7.     a = 0;
  8.     do { if (anim[a].num_keys > 1) { anim[a].key2 = 1; } else anim[a].key2 = 0; a++; } while (a < total_animations);            
  9.     final = new Final[num_parts];
  10.     a = 0; do { final[a] = new Final(); a++; } while (a < num_parts);            
  11. }//Load_TXT
- closes/disposes of reader after reaching end of stream (even if exception occurs)
- take a few precautions (ie: for each animation, key1 should be 0 and key2 should be 1 unless it has only 1 key)
- allocate final (based on num_parts)

xiii) We'll need a way to get the index of an animation based on character and animation name:
Code Snippet
  1. // G E T  I N D E X --------------------------------------
  2. // finds the animation index of a named animation
  3. public int GetIndex(string sheetname, string animation_name, bool show_error_messages=true)
  4. {
  5.     int value;
  6.     if (!lookup.TryGetValue(sheetname+animation_name, out value)) {
  7.         if (show_error_messages) Console.WriteLine("Animation not found in dictionary: "+sheetname+animation_name);
  8.         return 0;
  9.     }
  10.     return value;
  11. }

- lookup is a fairly efficient hash table based system for quickly finding associated information.
( a fast equivalent in C++ would be the unordered_map)
If you create more than one character animation and combine them into a file, it will need to know which character you're referring to. If you open the .txt export and search "SPRITESHEET_FILENAME" you'll find some long pathname followed by the original sprite sheet image you used (ie: bla/bla/bla/bla/wizzy.png") so the sheetname is actually just "wizzy"... you can hit f3 to search for the next instance of SPRITESHEET_FILENAME which (if you have more than 1) will be the name of the next character (by sheet name)... keep in mind that you're now using a single .png for a combined project which holds sprite parts for more than one character now... this is not the .png we refer to so just think about the original sprite sheet png's you were using to distinguish between each character before combining them.
So now, for example, you can lookup the index of an animation(ie: idle) for both characters in 1 file with:
index = GetIndex("cat_monster", "idle") or index = GetIndex("dog_monster", "idle")

42) M e o P l a y e r
a) In solution explorer, right-click on MeoPlayer and Add>>Class: "MeoPlayer"

b) Add these variables:

Code Snippet
  1. class MeoPlayer
  2. {
  3.     public bool PREMULTIPLY_ALPHA = true;
  4.     const float HIDE_ALPHA_UNDER = 0.02f; // minimum alpha (anything lower is clipped from calculations)        
  5.     //--------------------------------------------------------------------------------------------------
  6.     public  Vector2 position;         // character's position     
  7.     public  string  sheet_name;       // name of the original sheet used in project (without .png) [just for identification purposes]
  8.     public  int     animation_index;  // index into list of animations (ie: anim[animation_index])
  9.     public  float   play_speed;       // animation speed    
  10.     public  bool    stopped, flip, active, reverse, last_reverse;
  11.     public  int     key1;             // current key
  12.     public  int     key2;             // next key                
  13.     private float   timer;            // for interpolating between keys        
  14.     private bool    done_anim;        // used in IsDoneAnimation() to tell if at least one animation cycle has finished
  15.     private int     part_count;               
  16.     private Final[] final;            // holds final animation data        
  17.     
  18.     public QuadBatch batch;        
  19.     MeoMotion meo;                    // meo = original instance of MeoMotion manager which loads and contains all the animations

PREMULTIPLY_ALPHA: will be set to true by default, in which case you'll want to make sure the properties of your characters sprite sheet is set to premultiply alpha true as well.
HIDE_ALPHA_UNDER: anything to be drawn with a very low alpha (ie: 2%) will be removed from processing (can't see it anyway) position - character screen location
sheet_name - used to identify which character it is (if multiple characters on the sheet)
animation_index - which animation
play_speed - animation speed
stopped = status of not playing (can check on it from another module... probably won't use)
flip = for SpriteEffects.FlipHorizontally
active = can set false to disable the animation/drawing... (probably won't use)
reverse = play backwards
last_reverse ... if last_reverse(reverse during previous loop) is different than reverse now, then it might not be done playing... so when checking if animation is done, this needs to be considered..
key1, 2 - current and next keys (to blend or interpolate animation between)
timer - how much time has passed (used for interpolating between keys)
done_anim - triggers once each time a cycle finishes so this is checked using IsAnimationDone() to see if at least 1 looping animation has completed (and then it resets the status to false)
part_count - how many parts this character has
final[] - holds all the final animation data (the stuff needed to draw it correctly)

c) Constructor -- takes the SheetName(which character it is based on name of original image file used with the character in MeoMotion [before combining projects] (see the .txt export and use find SPRITESHEET to find each character name -- ignore the path and file extension -- ie: bla/bla/bla/bla/wizzy.png would be "wizzy")
Position = start position (on screen)
Meo = refer to MeoMotion class from Game1.cs
quadBatch = refer to QuadBatch from Game1.cs to use for drawing distortable quads

Code Snippet
  1. // C O N S T R U C T O R
  2. /// <summary>
  3. /// Creates a new character -- note: character can be changed by changing sheet_name and then setting an animation
  4. /// </summary>
  5. /// <param name="SheetName">used to determine which character on the spritesheet to use (uses the old original sheet name to refer to it)</param>
  6. /// <param name="Position">default position of character</param>
  7. /// <param name="Meo">instance of meo to use</param>
  8. /// <param name="spriteBatchDistort">instance of spriteBatchDistort to use for drawing</param>
  9. public MeoPlayer(string SheetName, Vector2 Position, MeoMotion Meo, QuadBatch quadBatch) //, int max_onscreen_characters)
  10. {
  11.     sheet_name = SheetName;
  12.     position = Position;
  13.     play_speed = 1.0f; part_count = 0; flip = false; reverse = false;
  14.     meo = Meo;        
  15.     final = new Final[(meo.max_parts + 1)]; // create a memory pool big enough to hold any character's render data
  16.     int a = 0; do { final[a] = new Final(); a++; } while (a < (meo.max_parts+1)); // allocate
  17.     batch = quadBatch;
  18. }

- set play speed to 1 by default and flip as none and reverse play as false.
- Allocate the maximum number of final render parts any of the characters would need.

d) Set Animation methods will set which animation to play, whether it should be flipped horizontally, and whether it should play in forward or reverse:
Code Snippet
  1. // S E T  A N I M A T I O N
  2. /// <summary>
  3. /// Set a character animation by name with option to set flip horizontally
  4. /// </summary>        
  5. public void SetAnimation(string AnimationName, bool Flip=false, bool play_backward=false) // note: normally, if changing character direction - just change public variable flip = !flip
  6. {
  7.     if (AnimationName == "none") { active = false; return; }    // usually better to just change public var active to false for this
  8.     animation_index = meo.GetIndex(sheet_name, AnimationName);  // Console.WriteLine("animation_index = " + animation_index);
  9.     part_count = meo.anim[animation_index].end_part - meo.anim[animation_index].start_part; // make sure we are using the correct part_count
  10.     flip = Flip; active = true; key1 = 0; key2 = 1; timer = 0; done_anim = false; stopped = false; reverse = false; last_reverse = false;
  11.     if (play_backward) StartReverse();
  12. }
  13. public void SetAnimation(int AnimationIndex, bool Flip = false, bool Active=true, bool play_backward = false) // (if we already know the index)
  14. {
  15.     if (!Active) { active = false; return; }
  16.     animation_index = AnimationIndex;
  17.     part_count = meo.anim[animation_index].end_part - meo.anim[animation_index].start_part; // make sure we are using the correct part_count
  18.     flip = Flip; active = true; key1 = 0; key2 = 1; timer = 0; done_anim = false; stopped = false; reverse = false; last_reverse = false;
  19.     if (play_backward) StartReverse();
  20. }

- part_count = end_part - start_part (for this animation [so if this animation is a different character then the groups are different])
- init all the play status and keyframes to 0 and 1 (start)
- start a reverse animation if play_backwards

e) IsDoneAnimation checks if a cycle of animation has completed... resets the status only when it is tested

Code Snippet
  1. // see if at the end of an animation cycle
  2. public bool IsDoneAnimation()
  3. {
  4.     if (last_reverse != reverse) return false;  // just changed playback direction so not done
  5.     if (done_anim) {
  6.         done_anim = false; return true;
  7.     }
  8.     return false;
  9. }
  10.  
  11.  
  12. public void StartReverse()
  13. {
  14.     key2 = meo.anim[animation_index].num_keys - 1; key1 = key2 - 1; done_anim = false; stopped = false; reverse = true;
  15.     timer = meo.anim[animation_index].times[key2];
  16. }

f) StartReverse() sets the keys at the end of the animation cycle... and reverse is now true.
- timer is also set to the final time (will time it backwards)

g) Update(GameTime gameTime)
Note: I just use the standard 60fps timing that the page flip updates are locked to (unless system performance starts to fail) ... but if you want to use gameTime, you can (better for online game synchronization)... however for standard games I prefer this for a few reasons. Anyway, comments are left in the code to show how to change it to use gameTime if you really need it.
Code Snippet
  1. // U P D A T E
  2. // Customizable update for character animation. Could have other updates with other behaviors too, or pass in vars to control behavior decisions.. (like point weapon or something)
  3. public void Update(GameTime gameTime)
  4. {            
  5.     if (!active) return;
  6.     int a = animation_index;
  7.     int k1 = key1, k2 = key2;
  8.     int t1 = meo.anim[a].times[k1], t2 = meo.anim[a].times[k2];
  9.     float time_dif, percent;
  10.     if (meo.anim[a].looping) done_anim = false;
- note: using a, k1, k2 just to reduce bulky code (should be pretty easy to understand still)
- t1 = time of first key, t2 = time of next key
- time_dif (difference between the 2 times)
- percent ( normalized percentage (0.0-1.0) between the 2 times based on timer )
Code Snippet
  1. if (reverse)
  2. {
  3.     timer -= 16.6666667f * (play_speed * meo.anim[a].speed);  //timer -= (gameTime.ElapsedGameTime.Milliseconds * (play_speed * meo.anim[a].default_speed)); // <-- (could use this also) track milliseconds that passed since start of first key                       
  4.     if (timer < t1)
  5.     {
  6.         key2 = key1;
  7.         key1--;
  8.         if (key1 < 0)
  9.         {
  10.             if (meo.anim[a].looping == false)
  11.             { // STOP ANIMATION
  12.                 key1 = 0; key2 = 0; stopped = true;
  13.                 timer = meo.anim[a].times[key1];
  14.                 done_anim = true; last_reverse = reverse; return;
  15.             }
  16.             else { key1 = meo.anim[a].num_keys - 2; key2 = key1 + 1; timer = meo.anim[a].times[key2]; done_anim = true; }  // LOOP ANIMATION
  17.         }
  18.         k1 = key1; k2 = key2; t1 = meo.anim[a].times[k1]; t2 = meo.anim[a].times[k2];
  19.     }                
  20. }

If animation is reverse:
- timer plays backwards at 16 milliseconds per loop [for 60fps] (speed up[or slow down] based on playback speeds)
- if timer reaches the key1 time (t1):
-- move key2 backwards... move key1 backwards
-- if key1 tries to go under 0 (start), it should be done playing backwards so:
--- if it is NOT supposed to loop, stop the animation (reseting everything for default forward playback) and set the flag done_anim
and remember what the previous reverse status was (true) ... done this update so return...
--- else it IS a loop so reset the keys to the end of the cycle (and timer) to prepare to play backward again (flag that a cycle finished)
-- set k1, k2 to the key settings, and get the new time intervals t1, t2

For FORWARD animation:

Code Snippet
  1. else
  2. {
  3.     timer += 16.6666667f * (play_speed * meo.anim[a].speed);  //timer += (gameTime.ElapsedGameTime.Milliseconds * (play_speed * meo.anim[a].default_speed)); // <-- (could use this also) track milliseconds that passed since start of first key                       
  4.     if (timer > t2) // ready to switch keys to interpolate between
  5.     {
  6.         key1 = key2;
  7.         key2++;
  8.         if (key2 >= meo.anim[a].num_keys)
  9.         {
  10.             if (meo.anim[a].looping == false)
  11.             { // STOP ANIMATION
  12.                 key2 = meo.anim[a].num_keys - 1; stopped = true; timer = 0; done_anim = true; last_reverse = reverse; return;
  13.             }
  14.             else
  15.             { // LOOP ANIMATION
  16.                 key1 = 0; key2 = 1; timer = 0; done_anim = true;
  17.             }
  18.         }                    
  19.         k1 = key1; k2 = key2; t1 = meo.anim[a].times[k1]; t2 = meo.anim[a].times[k2];
  20.     }                
  21. }

Else forward animation:
- advance timer
- if time update keys:
-- advance to next set of keyframes
-- if end of cycle:
---- if NOT looping: stop the animation [set done_anim] (and return)
---- else: reset the animation keys to loop it [set done_anim to signify that at least one cycle completed]

h) Setup variables to prepare looping through parts to animate them.

Code Snippet
  1. time_dif = t2 - t1;                     // total time between both keys
  2. if (time_dif <= 0) time_dif = 0.0001f;  // prevent unlikely possibility of division by zero
  3. percent = (timer - t1) / time_dif;      // what is the percentage (0-1) [for interpolation]
  4.  
  5. Vector2 pos, scale, o1, o2, o3, o4;
  6. float rot;
  7. float x1, x2, x3, x4;
  8. float y1, y2, y3, y4;            
  9. int i = 0, p;
- get time span between frames
- get the percent (0.0f-1.0f) that the timer has passed through the span (timer-[start_time]) / time_dif

i) Loop through the all the character's parts and blend their keyframes to create final render data for each one:
Code Snippet
  1. do
  2. {
  3.     final[i].order = meo.anim[a].keys[i, k1].order;
  4.     if (meo.anim[a].keys[i, k1].active == false) { final[i].hide = true; i++; continue; } // (part not shown - skip - go to do)
  5.     final[i].hide = false;                
  6.     final[i].part = meo.anim[a].keys[i, k1].part; p = final[i].part;
  7.     // interpolate the data between the 2 keyframes
  8.     final[i].alpha = MathHelper.Lerp(meo.anim[a].keys[i, k1].alpha, meo.anim[a].keys[i, k2].alpha, percent); // blend alpha transparency
  9.     if (final[i].alpha < HIDE_ALPHA_UNDER) { final[i].hide = true; i++; continue; }                
  10.     if (final[i].alpha > 1.0f) final[i].alpha = 1.0f;                                                        // precaution
  11.     pos = Vector2.Lerp(meo.anim[a].keys[i, k1].pos, meo.anim[a].keys[i, k2].pos, percent);                   // blend position                
  12.     rot = MathHelper.Lerp(meo.anim[a].keys[i, k1].rot, meo.anim[a].keys[i, k2].rot, percent);                // blend rotation
  13.     scale = Vector2.Lerp(meo.anim[a].keys[i, k1].scale, meo.anim[a].keys[i, k2].scale, percent);             // blend scale                
  14.     o1 = Vector2.Lerp(meo.anim[a].keys[i, k1].o1, meo.anim[a].keys[i, k2].o1, percent);                      // blend distortion offsets
  15.     o2 = Vector2.Lerp(meo.anim[a].keys[i, k1].o2, meo.anim[a].keys[i, k2].o2, percent);
  16.     o3 = Vector2.Lerp(meo.anim[a].keys[i, k1].o3, meo.anim[a].keys[i, k2].o3, percent);
  17.     o4 = Vector2.Lerp(meo.anim[a].keys[i, k1].o4, meo.anim[a].keys[i, k2].o4, percent);                
  18.     // calculate the transformed vertices from the above data                
  19.     x1 = meo.parts[p].m1.X * scale.X; y1 = meo.parts[p].m1.Y * scale.Y; // scale part points at origin(0,0)
  20.     x2 = meo.parts[p].m2.X * scale.X; y2 = meo.parts[p].m2.Y * scale.Y;
  21.     x3 = meo.parts[p].m3.X * scale.X; y3 = meo.parts[p].m3.Y * scale.Y;
  22.     x4 = meo.parts[p].m4.X * scale.X; y4 = meo.parts[p].m4.Y * scale.Y;
  23.     pos += meo.anim[a].offset; // adjust postions by default offset property

- get the display order (which layer is it)
- if the key is not active, hide it and skip this loop else show it
- get the sprite part to process
- do a linear interpolation between each keyframe's alpha based on percentage
- make sure the alpha is valid ... if too low, can't be seen so hide this part and go back to do {
- position = linear interpolation between the 2 keyframes (part origin will be positioned at this point)
- rotation = " "
- scale = " "
- offsets (o1,o2,o3,o4) = " "
m1,m2,m3,m4 are the model points or offsets from the origin for each vertex of the quad
- get those vectors based on the new scale value
- add a custom offset to the pos (applied to all parts to offset the character's image position from physical position) Zero by default

If there's rotation, calculate the rotation (using an origin at: pos):
Code Snippet
  1. // HERE YOU COULD ADD EXTRA PROGRAMMED ROTATION RESPONSES (like trailing hair, tail, etc - or vector based weapon pointing)
  2. // Note: Use: meo.parts[p].name to identify and if has children add child to end-point of parent-limb (and add parent rotation to child rotation also)               
  3. if (rot != 0f)
  4. {
  5.     float cos = (float)Math.Cos(rot), sin = (float)Math.Sin(rot);   // rotate points around origin and then add the position where they belong
  6.     final[i].v1.X = pos.X + x1 * cos - y1 * sin; final[i].v1.Y = pos.Y + x1 * sin + y1 * cos;
  7.     final[i].v2.X = pos.X + x2 * cos - y2 * sin; final[i].v2.Y = pos.Y + x2 * sin + y2 * cos;
  8.     final[i].v3.X = pos.X + x3 * cos - y3 * sin; final[i].v3.Y = pos.Y + x3 * sin + y3 * cos;
  9.     final[i].v4.X = pos.X + x4 * cos - y4 * sin; final[i].v4.Y = pos.Y + x4 * sin + y4 * cos;
  10. }
  11. else
  12. {
  13.     final[i].v1.X = pos.X + x1; final[i].v1.Y = pos.Y + y1;         // no rotation, so just put the points in the correct position
  14.     final[i].v2.X = pos.X + x2; final[i].v2.Y = pos.Y + y2;
  15.     final[i].v3.X = pos.X + x3; final[i].v3.Y = pos.Y + y3;
  16.     final[i].v4.X = pos.X + x4; final[i].v4.Y = pos.Y + y4;
  17. }
IF Rotation:
- get cos and sin values for the rotation
- final vertex.X = position.X + offset.X * cos - offset.Y * sin... same idea: .Y = pos.Y +x1 * sin + y1 * cos
As explained before, this is taking an distance_X and rotating it (ie: x1 * cos, y1 * sin) as well as taking a distance_y (pointed up) and rotating it ... and adding the 2 component vectors together... added to the origin vector (pos) to offset the rotation to where we want it to rotate around (instead of 0,0)... It may take a diagram to explain this properly, if you're not already familiar with it.
- same idea for each of the 4 vectors
Else:
- just make the offsets relative to position (add pos + offset for each) to get the translated vector
- repeat concept for all 4 vectors

Code Snippet
  1.         final[i].v1 += o1; // add the distortion offsets of the points
  2.         final[i].v2 += o2;
  3.         final[i].v3 += o3;
  4.         final[i].v4 += o4;
  5.         if (flip) // flip horizontally:
  6.         {
  7.             final[i].v1.X = -final[i].v1.X;
  8.             final[i].v2.X = -final[i].v2.X;
  9.             final[i].v3.X = -final[i].v3.X;
  10.             final[i].v4.X = -final[i].v4.X;
  11.         }
  12.         i++;
  13.     } while (i < part_count);
  14.     last_reverse = reverse;
  15. }

- add the interpolated distortion offsets onto each vertex
- if flip horizontally, need to reverse the X value (can do this because they're all relative to the root position of the character)
- remember if we were animating in reverse or not...

j) Now we just need to render out the final data:
Code Snippet
  1. // D R A W
  2. // Customizable draw for character. Could make other draw overloads or pass in vars to control drawing behavior for specific characters.
  3. public void Draw(Color color)
  4. {
  5.     if (!active) return;
  6.     int i = 0, n = 0, p;
  7.     Color col;             
  8.     n = 0;
  9.     do
  10.     {
  11.         i = final[n].order;
  12.         if (final[i].hide) { n++; continue; }       
  13.         p = final[i].part;                                  // may switch parts during animation
  14.         color.A = (byte)(final[i].alpha * 255.0f);          // alpha transparency from 0-255                
  15.         if (PREMULTIPLY_ALPHA) col = Color.FromNonPremultiplied(color.ToVector4()); // make sure the properties of the image in content is set to premultiply alpha true
  16.         else col = color;
  17.         batch.DrawTransformedVertices(meo.parts[p].rect, position, final[i].v1, final[i].v2, final[i].v3, final[i].v4, col); // draw transformed sprite parts at character's position
  18.         n++;
  19.     } while (n < part_count);
  20. }

do {
- get index of part for 1st layer (based on order)
- if part hidden, continue (goto do{ )
- get actual part #
- get the byte version of alpha
- if PREMULTIPLY_ALPHA ... make the premultiplied version of the color (tint)
otherwise just use color (which now uses: final[i].alpha)
- use quadBatch to DrawTransformedVertices:
(source rect using part #, character's position, ... relative vertices of the quad ..., color)
}
Just realized using col and color is a bit redundant ... could probably just modify color as needed


>>>NEXT PAGE (3)<<<

>>> Back to Game Design <<<