MonoGame (new XNA) Tutorial: MeoMotion Character Animation (Vector Bone and Distortion Animator) Page 2

(page 1, PAGE 2, page 3)

So far (on page 1) we've set up support for drawing distorted quads as well as transformed vertices which we'll be using with MeoMotion.cs which we'll now start making - so right click MeoMotor (in solution explorer), click add, click class, type MeoMotion and click add.

Step 10) Right click content and add the original .png spritesheet (or combined spritesheets .png image if you combined projects) of your MeoMotion animation project to the content pipeline.

NOTE: that this tutorial is for loading .txt versions of the project. In the final release of your project (for this version) you'll want to place your animation .txt's into the same folder as the executable.

Next copy the .txt file of the exported animation into the same folder as the executable. At this stage of project development, it will likely be in Bin then in x86 then in Debug.


Step 11) In MeoMotion.cs we'll start setting up a constructor and a Load_TXT function which we'll test out to make sure it is able to access the animation file ok. We'll start with this:

ContentManager content;  
public SpriteBatchDistort batch;       
public Texture2D tex; 
public int num_parts, max_parts, total_animations; //number of sprite parts, maximum parts that may be used in a render batch, number of animations

// C O N S T R U C T O R 
public MeoMotion(ContentManager _content, SpriteBatchDistort spriteBatchDistort) {
    content = _content;   
    batch = spriteBatchDistort;
    max_parts=0;         
}


// 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)
// -- apon final release of your project you'll need to make sure this exported file is in the same folder as the executable
public void Load_TXT(string FileName, float rescale)
{
    if (FileName.EndsWith(".txt")) { } else FileName += ".txt";            
    if (!File.Exists(FileName))  { Console.WriteLine("File not found: " + FileName); return; }      
    using (StreamReader reader = new StreamReader(FileName))
    {
        string line = reader.ReadLine(); string[] strs;
        strs = line.Split(',');
        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; 
        }
        string image_filename = Path.GetFileName(strs[1]);
        image_filename = Path.GetFileNameWithoutExtension(image_filename);
        Console.WriteLine("File: " + FileName + " opened. Will be loading .png file: " + image_filename + " using the content pipeline");
    }
}

Then in Game1.cs we'll add this variable: MeoMotion meo;
Also, we'll add this to Initialize: meo = new MeoMotion(Content, distortBatch);
And this to LoadContent: meo.Load_TXT("ComboTest"); // (whatever you named it)
And then try it out and check the output box to see if everything seems to be working ok so far. If it works ok you can remove the last Console.WriteLine("File:"+FileName+.....etc. because it's only for testing.
Note again you may need to resolve some underlined bits by right-click and resolve to add the using's


Step 12) Now we'll complete loading the txt by using reader to read to end of the stream and use switch-case on each read line to determine what it is reading and how to respond. The reason I like doing it this way, is it allows us to change the file format and class or structure without having to rewrite the code and old formats will work even if the main program has been updated to include new data. Before we can continue loading, we'll need to add some classes to hold and organize our data. Since they are simple, we'll just add them above the MeoMotion class in the same file (MeoMotion.cs) like so:

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


    // Holds an individual key for a part in an animation
    class Key
    {        
        public int     part;            // if non-negative tells to swap parts 
        public int     order;          // index for draw order
        public Vector2 scale;      // sprite size
        public float   rot;             // sprite rotation 
        public Vector2 pos;          // sprite position
        public float   alpha;          // transparency
        public Vector2 o1, o2, o3, o4; // vertex offsets/distortions
        public bool    active;         // active (for hiding parts not animated)
    }


    // Holds an animation (and all key and part manipulation info) 
    class MeoAnimation
    {
        public string animation_name; // used when need to identify which animation index to use (using dictionary)
        public int num_keys;               // total keys in this animation sequence
        public bool looping;                // is a loop type animation?
        public int[] times;                  // time of key_index
        public Key[,] keys;           // keyframes (part index, key index)
        public int key1;                // current key
        public int key2;                // next key 
        public int root;                 // index of root bone (could be useful) 
        public int start_part;       // section of parts this animation works on
        public int end_part;         // "                                      "
        public float timer;           // used in the example Play() method for interpolating between keys

        // add customization properties here:
public float default_speed = 1.0f; // 100%
public Vector2 default_offset = Vector2.Zero; } // Holds final rendering data for a Draw method class Final { public int order; // which index to use ( drawing order ) public int part; // index of actual part/rect to render public float alpha; // transparency public Vector2 v1, v2, v3, v4; // transformed vertices public bool hide; // don't draw? } class MeoMotion { ContentManager content; public SpriteBatchDistort batch; public Texture2D tex; public int num_parts, max_parts, total_animations; // Note: max_parts can be used elsewhere to allocate memory for final renders Final[] final; // used in the example play and draw methods public SpritePart[] parts; // sprite part list public MeoAnimation[] anim; // all the animations public Dictionary<string, int> lookup = new Dictionary<string, int>(); // used to find animation index of a named animation

SpritePart holds info about each part that makes up the character (arm, hand, head, ect...). We're mostly just interested in rect, and m1,m2,m3,m4 although you could use pivot and parent info to do some customized behavior like to make the head look toward something or to make the character point a weapon.

The Key class holds the actual key information for a time. Our update (called play) will interpolate between 2 keyframes to make the animation blend smoothly between 2 keys. It does this by determining the percentage of time between the 2 times of the keys and if for example it is 20%(0.2f) key1 and 80%(0.8f) key2 then it will blend those positions,alpha,rotation,etc by that amount.

MeoAnimation holds lists of Keys and times as well as tracking other information about a particular animation (ie: walk, run, idle, jump...) We will make a lookup dictionary to identify animation index by name, and will keep track if the animation is looping, what the 2 keys are that it is trying to blend... Also here, custom character properties can be added. For example if you wanted a certain animation for that character (breathing) to run slower -- or if you want a particular animation offset a bit (like if character animation isn't lining up right with other animations).

Final is the final parts and their corresponding transformed vertices after the keyframes have been blended. This is what will be drawn using the transformed vertices drawing function we made earlier.

We'll need to pass in SpriteBatchDistort from Game1 in the constructor so we can use it in both places. root_index is really only of interest if we really wanted to know exactly which part holds the root bone(starting bone) [we won't use this here]. timer is used for tracking animation timing for interpolation purposes(which you'll see how soon).

In the MeoMotion class after the above vars, you should already have this sort of thing:

#region CONSTRUCTOR
// C O N S T R U C T O R ------------------
public MeoMotion(ContentManager _content, SpriteBatchDistort spriteBatchDistort) {
    content = _content;
    batch = spriteBatchDistort;
    max_parts = 0;
}
#endregion

We're going to expand Load_TXT.
To understand it properly though: I should mention that MeoMotion animator can combine animation projects into a single export file (and when it does it can also combine sprite sheets into a single spritemap .png -- keep in mind that the export .TXT file tracks animation information partly based on the original sheet name.)
  
So for example:
If you combined Meo.prj (using MeoSheet.png) with Cider.prj (using CiderSheet.png) into Combo.TXT it would also export a png file called MeoSheet_combo.PNG which holds a combination of both spritesheets(MeoSheet+CiderSheet) in the original project folder.
  Inside the Combo.TXT it remembers the original sheet names - but this is only to keep track of which part of the new PNG sheet to use for each animation. Later you'll see how we use a dictionary to lookup an animation based on both the original sheet name and the actual animation name. One other reason this is good is because both animation projects contain different characters which may use the same animation names (ie: "walk") - and so this way we know which character to animate.
So if I wanted I could say:
animation_index = lookup("CiderSheet","walk");
or
animation_index = lookup("MeoSheet","walk");
or
animation_index = lookup("Hero","jump");
...
They are technically all on the same sheet - it just provides a way of organizing the data.


Step 13) Make these changes to Load_TXT and I'll explain what it does.
At the beginning you'll notice when it finds "COMBO_FILENAME" it could load max_key but since we aren't going to use it, I commented it out (maximum number of keys used by any animation in the file). I also included an else section for older formats which do not support combined project exports.

// L O A D  _  T X T -------------------------------------------------------------------------------------------------------------
/// 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)
/// -- apon final release of your project you'll need to make sure this exported file is in the same folder as the executable
/// [ You can specify export folders in MeoMotion using export options ]
// rescale - to resize the how big the sprites will appear
public void Load_TXT(string FileName, Vector2 rescale, bool adjustOrder=false) 
{
    int a = 0;//, max_key=0;
    if (FileName.EndsWith(".txt")) { } else FileName += ".txt";
    if (!File.Exists(FileName)) { Console.WriteLine("File not found: " + FileName); return; } //return if NOT existing                 
    using (StreamReader reader = new StreamReader(FileName))
    {
        string anim_name = "";
        a = -1;//0; // animation index
        string line = reader.ReadLine(); string[] strs;
        strs = line.Split(',');
        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; } string image_filename = Path.GetFileName(strs[1]); image_filename = Path.GetFileNameWithoutExtension(image_filename); tex = content.Load<Texture2D>(image_filename); // load actual spritemap //continue loading txt: int current_root = 0, part_count = 0; if (strs[0] == "COMBO_FILENAME") { num_parts = Convert.ToInt32(strs[4]); parts = new SpritePart[num_parts]; //allocate parts total_animations = Convert.ToInt32(strs[5]); anim = new MeoAnimation[total_animations]; //allocate animations //max_key = Convert.ToInt32(strs[6]); } else { line = reader.ReadLine(); strs = line.Split(','); //reading TOTAL_NUM_PARTS num_parts = Convert.ToInt32(strs[1]); part_count = num_parts; if (part_count > max_parts) max_parts = part_count; parts = new SpritePart[num_parts]; //allocate parts line = reader.ReadLine(); strs = line.Split(','); //reading ROOT_INDEX current_root = Convert.ToInt32(strs[1]); line = reader.ReadLine(); strs = line.Split(','); //reading TOTAL_ANIMATIONS total_animations = Convert.ToInt32(strs[1]); anim = new MeoAnimation[total_animations]; //allocate animations } int p = 0, pi = -1, first_index = 0; // part index, timeline part index, first index within a sheet section int k = 0; // key index string current_sheetname = ""; // used when sheet data has been combined into one sheet (old sheet names - used for looking up animation) bool first = false; do { line = reader.ReadLine(); strs = line.Split(','); switch (strs[0]) { case "SPRITESHEET_FILENAME": current_sheetname = Path.GetFileNameWithoutExtension(strs[1]); first = true; break; // SET FIRST case "TOTAL_NUM_PARTS": part_count = Convert.ToInt32(strs[1]); if (part_count > max_parts) max_parts = part_count; break; case "ROOT_INDEX": current_root=Convert.ToInt32(strs[1]); break; case "PART_INDEX": p = Convert.ToInt32(strs[1]); if (p >= num_parts) {
Console.WriteLine("Meo file integrity problem: part index p>=part_count"); return; } if (first) { first = false; first_index = p; } // record the first index if this is the first entry of this sheet section break; case "PART_NAME": parts[p] = new SpritePart(); parts[p].name = strs[1]; break; 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; case "LOCAL_POINTS_M1M2M3M4": parts[p].m1.X = Convert.ToSingle(strs[1])*rescale.X; parts[p].m1.Y = Convert.ToSingle(strs[2])*rescale.Y; parts[p].m2.X = Convert.ToSingle(strs[3])*rescale.X; parts[p].m2.Y = Convert.ToSingle(strs[4])*rescale.Y; parts[p].m3.X = Convert.ToSingle(strs[5])*rescale.X; parts[p].m3.Y = Convert.ToSingle(strs[6])*rescale.Y; parts[p].m4.X = Convert.ToSingle(strs[7])*rescale.X; parts[p].m4.Y = Convert.ToSingle(strs[8])*rescale.Y; break; case "PART_PIVOT": parts[p].pivot.X = Convert.ToSingle(strs[1]); parts[p].pivot.Y = Convert.ToSingle(strs[2]); break; case "PART_PARENT": parts[p].parent = Convert.ToInt32(strs[1]) + first_index; break; // offset by first index //-------------------------------------------------------------------- case "ANIMATION_NAME": anim_name = strs[1]; break; case "ANIMATION_NUMBER": //a = Convert.ToInt32(strs[1]); a++; //safer to do this instead if (a >= total_animations) { Console.WriteLine("Error: animation index a>=total_animations"); return; } anim[a] = new MeoAnimation(); anim[a].animation_name = anim_name; anim[a].looping = false; anim[a].root = current_root + first_index; // offset by first_index anim[a].start_part = first_index; anim[a].end_part = first_index + part_count; lookup.Add(current_sheetname+anim_name, a); anim[a].key1 = 0; anim[a].key2=0; anim[a].timer=0; pi=-1; k=0; break; case "ANIMATION_KEY_COUNT": anim[a].num_keys = Convert.ToInt32(strs[1]); anim[a].keys = new Key[num_parts,anim[a].num_keys]; // allocate keys for this animation anim[a].times = new int[anim[a].num_keys]; // allocate times break; case "KEY": k = Convert.ToInt32(strs[1]); pi = -1; break; case "LOOPING": anim[a].looping = true; break; case "TIME": anim[a].times[k] = Convert.ToInt32(strs[1]); break; case "PART": pi++; anim[a].keys[pi, k] = new Key(); anim[a].keys[pi, k].part = Convert.ToInt32(strs[1]) + first_index; // offset by first_index anim[a].keys[pi, k].active = true; break; case "ORDER": anim[a].keys[pi, k].order = Convert.ToInt32(strs[1]); if (adjustOrder) anim[a].keys[pi, k].order += first_index; // in case the custom player needs the order from the entire list break; // offset by first_index case "NOT_ACTIVE": anim[a].keys[pi, k].active = false; break; 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; case "K_ROT": anim[a].keys[pi,k].rot = Convert.ToSingle(strs[1]); break; 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; case "K_ALPHA": anim[a].keys[pi, k].alpha = Convert.ToSingle(strs[1]); break; 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; 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; 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; 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; } } while (reader.EndOfStream != true); }//using reader if (total_animations > a) total_animations = a; //precautionary setting of second key (in case no other keys): a = 0; do { if (anim[a].num_keys > 1) { anim[a].key2 = 1; } else anim[a].key2 = 0; a++; } while (a < total_animations); final = new Final[num_parts]; a = 0; do { final[a] = new Final(); a++; } while (a < num_parts); }//Load_TXT

It takes the animation_index (aquired using GetIndex), an animation play speed (where 1.0f = normal speed, or 2.0f=speed*2)
If the animation is stopped, no calculations are needed.
To make the code less crazy, I drop the current key values and key times into some simple variables.
key1 and key2 are the 2 current keyframes being interpolated between (if you were using quadratic interpolation you'd need to track 3 keys).

Step 14) It would be useful to have a method in MeoMotion class which helps adjust a character scaling based on current screen dimensions:

// ADJUST SCALE FOR SCREEN RESOLUTION
// a tool to scale position, size, and speed of characters to match display resolutions
public void Adjust_Scale_For_ScreenResolution(ref Vector2 rescale, int width, int height)
{
    // Assuming default is 800 x 600 (can change this)
    float DEFAULT_W = 800, DEFAULT_H = 600;
    rescale.X = (rescale.X * width) / DEFAULT_W;
    rescale.Y = (rescale.Y * height) / DEFAULT_H;
}     

Step 15) Make a few changes to Game1.cs first. Make sure you have these variables:

public class Game1 : Microsoft.Xna.Framework.Game
{
    const int SCREENWIDTH = 800, SCREENHEIGHT = 600; 
    const bool FULLSCREEN = false;                   
    GraphicsDeviceManager graphics;
    SpriteBatch           spriteBatch;
    SpriteFont             font;
    SpriteBatchDistort    distortBatch;
    static public KeyboardState kb, old_kb;
    static public MouseState    ms, old_ms;     
    Input inp;
    Texture2D background; 
    //-----------
    MeoMotion meo;        // meo = MeoMotion manager which loads and plays animations

Vector2 pos; // character position (could add in a player or character class later) float play_speed; // percentage 0-1 of play-rate (like if walking half speed it would be 0.5) bool flip = false; // flip character horizontally for facing a different direction //MeoPlayer characters[]; //will make this later
and construct should be something like this:
#region C O N S T R U C T
//-----------------------
public Game1()
{
    graphics = new GraphicsDeviceManager(this)
    {
        PreferredBackBufferWidth = SCREENWIDTH, PreferredBackBufferHeight = SCREENHEIGHT, IsFullScreen = FULLSCREEN,
        PreferredDepthStencilFormat = DepthFormat.Depth16
    };
    Content.RootDirectory = "Content";
    inp = new Input();
}
#endregion
We'll add a font so we can put instructions on the screen. Soon we'll add NPC/character stuff... (^-^)
#region I N I T
//-------------
protected override void Initialize()
{
    spriteBatch = new SpriteBatch(GraphicsDevice);
    font = Content.Load<SpriteFont>("SpriteFont1");
    distortBatch = new SpriteBatchDistort(Content, GraphicsDevice, true, "QuadEffect");            

    meo = new MeoMotion(Content, distortBatch);
    pos = new Vector2(SCREENWIDTH / 2 - 20, SCREENHEIGHT / 2); //just putting character at some place near middle of screen
            
    //allocate some example characters:
    characters = new MeoPlayer[1]; //allocate some characters
    base.Initialize();
}
#endregion

We haven't made a MeoPlayer class yet. It will allow you to assign a character and select and run animations stored in MeoMotion class.
The Load method should look something like this (and loading a background now - be sure to include it in the content pipeline):

#region L O A D        
//-------------
protected override void LoadContent()
{
    int index = 0; 
    background = Content.Load<Texture2D>("background");

    float resize = 0.86f; rescale = new Vector2(resize, resize);           // 86% size - scale character overall
    meo.Adjust_Scale_For_ScreenResolution(ref rescale, SCREENWIDTH, SCREENHEIGHT); // adjust final scale for screen resolution                        
    meo.Load_TXT("MeoBreath1", rescale); // load an example ( ComboTest.TXT must be placed with executable file 
[ie: in BIN and then in x86 and then in debug] )
characters[0] = new MeoPlayer("CiderSheet", new Vector2(100, 100), meo, distortBatch); // sets the character to use from the sheet // CUSTOMIZE CHARACTER PROPERTIES (to show we can) --- ie: could add flip_default too [see MeoAnimation] index = characters[0].GetIndex("idle"); meo.anim[index].default_offset = new Vector2(0.0f, 6.0f); meo.anim[index].default_speed = 0.6f; // 60% index = characters[0].GetIndex("blink"); meo.anim[index].default_offset = new Vector2(1.0f, -4.0f); meo.anim[index].default_speed = 0.7f; // 70% // UNCOMMENT TO TEST OTHER SUB-SHEET MEMBERS: //characters[0] = new MeoPlayer("KattySheet", new Vector2(100, 100), meo, distortBatch); //characters[0] = new MeoPlayer("MeoSheet", new Vector2(100, 100), meo, distortBatch); characters[0].SetAnimation("idle", flip); // set a starting animation } protected override void UnloadContent() { } #endregion

Index is for an animation index (if many characters are on the sheet, then GetIndex resolves the actual animation index using a combination of an original character sheet name (before combining them), and the animation name. charater[0] in the example was made to be a character based on the portion of the spritesheet called "CiderSheet" and using it's "idle" animation initially (using SetAnimation). In this example, I also did a lookup to get animation index for idle and blink and set some custom properties of those animations.
Now update the Update method ... ;)

#region U P D A T E
//-----------------
enum motion { none, walk, run, jump, idle }
motion hero_motion = motion.idle, last_hero_motion = motion.none; //idle by default (example file so far only has idle and walk) 

protected override void Update(GameTime gameTime)
{
    kb = Keyboard.GetState(); ms = Mouse.GetState();
    if (inp.EscapePressed()) this.Exit();            
            
    if (inp.KeyDown(Keys.W)) { if (characters[0].play_speed < 2.0f) characters[0].play_speed += 0.1f; } // adjust animation speed
    if (inp.KeyDown(Keys.Q)) { if (characters[0].play_speed > 0.4f) characters[0].play_speed -= 0.1f; }

    if (inp.KeyDown(Keys.Right))     
{ hero_motion = motion.walk; flip = true; pos.X += 3.3f * rescale.X * characters[0].play_speed; } // walk speed * speed modifier else if (inp.KeyDown(Keys.Left)) { hero_motion = motion.walk; flip = false; pos.X -= 3.3f * rescale.X * characters[0].play_speed; } else hero_motion = motion.idle; // WHICH ANIMATION: if (last_hero_motion != hero_motion) { switch (hero_motion) { case motion.idle: characters[0].SetAnimation("idle", flip); break; case motion.walk: characters[0].SetAnimation("walk", flip); break; } } //(only does this once) //later we could use arrow keys or gamepad analog stick to set play_speed to sync with movement: if (inp.KeyDown(Keys.B)) characters[0].SetAnimation("blink", flip); characters[0].position = pos; characters[0].flip = flip; //<--make sure flip is correct (because the animation may be the same but not the direction facing) characters[0].Update(gameTime); last_hero_motion = hero_motion; base.Update(gameTime); } #endregion

I should mention that in the example files with this tutorial, I made only an idle, walk, and blink for "CiderSheet" and only an idle for "MeoSheet" - and just so you aren't confused still, those are the names of the original sheets used in the original projects which are used to distinguish between characters -- but technically they are all on one png spritemap sheet called MeoSheet_combo.png with one export file called ComboTest.TXT (using export combiner) --- also you should understand that when combining characters to one sheet and one export file, it will make a copy of the first spritesheet used in the first project and rename it with _combo.png -- so if it was "Hero" and you added "Monster1" and "King" to the export it would create a combined png called "Hero_combo.png" -- but do not rename the png, or the export file won't be able to find it in the content pipeline (unless you manually modify the ComboFilename inside the .TXT export yourself after).

So now we can refer to character and what character is doing when we get an animation index. Later we'll make an MeoPlayer.cs class which has a Play (or Update) and Draw methods - but we'll add a "flip" arguement, to allow the character to be shown facing in different directions.

#region D R A W
//-------------
protected override void Draw(GameTime gameTime)
{
    spriteBatch.Begin();
    spriteBatch.Draw(background, new Rectangle(0, 0, SCREENWIDTH, SCREENHEIGHT), Color.White);
    spriteBatch.DrawString(font, "Use Q,W to change play speed (B to blink)", new Vector2(10, 30), Color.LimeGreen);
    spriteBatch.DrawString(font, "Use arrows to control walk", new Vector2(10, 50), Color.LimeGreen);
    spriteBatch.End();

    // (make sure the texture property is set to premultiplied in the content pipeline - unless you switch this to non-premultiplied)
    // [just make sure they match otherwise you can have dark edge artifacts] 
    // (left click image in content, expand content processor - set premultiply alpha to true)
    meo.batch.Begin(meo.tex, BlendState.AlphaBlend);                                    
            
    characters[0].Draw(Color.White); // DRAW CHARACTER

    meo.batch.End();

    old_kb = kb; old_ms = ms; 
    base.Draw(gameTime);
}
#endregion

The advantage of combining many characters on one sheet, is there will be less texture switching which may help performance. MeoMotion Export combiner allows a maximum size of 4096x4096 texture sizes at this time.

If you wanted, you could create an enum for your different characters. You could for example do unique NPC's like this: charater[npc.BOB], character[npc.ANN], or make a list of characters for enemies: FireCharacters[max_fire_monsters], and it will know to simply refer to which animations to use (rather than copying a bunch of memory) and have a few variables to decide what part of the animations to use and when for each character. It's actually more simple than it sounds as you'll soon see.
NOTE: On the next page I will show you how to create the MeoPlayer.cs class which allows creation of many characters and monsters which operate independantly. I will then include source files and example projects you can try out.

>>> Click HERE to go to PAGE 3 of MeoMotor tutorial <<<