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
classEditor
{
Mapmp; // reference to Map in Game1
Tile[,] tiles; // reference to tiles in Map
Sheet[] sheet; // refer to sheet data in Game1
Inputinp; // refer to input from Game1
//Player player; // later we'll want to refer to player made in Game1 (not made yet)
//MonsterSys monsterSys; // later we'll want to refer to monsterSys in Game1 (not made yet)
stringtime_saved="-"; // when was the file saved (to confirm file save worked)
boolshow_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.
12) Update()... first we'll start with some basic map navigation controls:
Code Snippet
//------------
// U P D A T E ( editor input )
//------------
publicvoidUpdate()
{
mp.scroll_offset=Vector2.Zero;
boolshift_down=inp.shift_down;
if (inp.Keypress(Keys.Right) || (shift_down&&inp.Keydown(Keys.Right))) { mp.loc.X++; Game1.background_pos.X--; }
if (inp.Keypress(Keys.Left) || (shift_down&&inp.Keydown(Keys.Left))) { mp.loc.X--; Game1.background_pos.X++; }
if (inp.Keypress(Keys.Down) || (shift_down&&inp.Keydown(Keys.Down))) { mp.loc.Y++; }
if (inp.Keypress(Keys.Up) || (shift_down&&inp.Keydown(Keys.Up))) { mp.loc.Y--; }
// PREVENT OUT OF BOUNDS
if (mp.loc.X>=Map.TILES_WIDE) { mp.loc.X=Map.TILES_WIDE-1; Game1.background_pos.X++; }
if (mp.loc.X<0) { mp.loc.X=0; Game1.background_pos.X--; }
if (mp.loc.Y>=Map.TILES_HIGH) { mp.loc.Y=Map.TILES_HIGH-1; }
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
// DELETE TILE
if (inp.Keypress(Keys.Delete) ||inp.Keypress(Keys.Back)) { mp.DeleteTile(); /*will also delete monsters later*/ }
// SET COLLIDER
if (inp.Keydown(Keys.Insert)) mp.SetType(); // sets tile as solid by default
// ADD TILE (refer to sheet manager)
if (inp.Keypress(Keys.Q)) mp.AddTile(1); if (inp.Keypress(Keys.W)) mp.AddTile(2); if (inp.Keypress(Keys.E)) mp.AddTile(3);
if (inp.Keypress(Keys.R)) mp.AddTile(4); if (inp.Keypress(Keys.T)) mp.AddTile(5); if (inp.Keypress(Keys.Y)) mp.AddTile(6);
if (inp.Keypress(Keys.U)) mp.AddTile(7); if (inp.Keypress(Keys.I)) mp.AddTile(8); if (inp.Keypress(Keys.O)) mp.AddTile(9);
if (inp.Keypress(Keys.P)) mp.AddTile(10); if (inp.Keypress(Keys.A)) mp.AddTile(11); if (inp.Keypress(Keys.S)) mp.AddTile(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);
if (inp.Keypress(Keys.H)) mp.AddTile(16);
// (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
// GET WORLD POS OF TILE (to edit)
Vector2world_coord=Conv.tile_to_world(mp.loc);
// ADD CHARACTER / MONSTER
// LATER: Add keypress test to place a monster at the world coordinate
// PLACE PLAYER
if (inp.Keypress(Keys.M))
{
mp.startData.x=mp.loc.X; mp.startData.y=mp.loc.Y;
//player.pos = world_coord + new Vector2(-32, -32); // this should work after we've made the player class
}
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
// EDIT A TILE
if (tiles[mp.loc.X, mp.loc.Y].index>0)
{
if (inp.Keydown(Keys.OemPeriod)) tiles[mp.loc.X, mp.loc.Y].rot+=0.01f;
if (inp.Keydown(Keys.OemComma)) tiles[mp.loc.X, mp.loc.Y].rot-=0.01f;
if (inp.Keydown(Keys.OemPlus)) tiles[mp.loc.X, mp.loc.Y].scale+=newVector2(0.01f, 0.01f);
if (inp.Keydown(Keys.OemMinus)) tiles[mp.loc.X, mp.loc.Y].scale-=newVector2(0.01f, 0.01f);
if (inp.Keydown(Keys.NumPad8)) tiles[mp.loc.X, mp.loc.Y].offset.Y--;
if (inp.Keydown(Keys.NumPad2)) tiles[mp.loc.X, mp.loc.Y].offset.Y++;
if (inp.Keydown(Keys.NumPad4)) tiles[mp.loc.X, mp.loc.Y].offset.X--;
if (inp.Keydown(Keys.NumPad6)) tiles[mp.loc.X, mp.loc.Y].offset.X++;
if (inp.Keypress(Keys.PageUp)) tiles[mp.loc.X, mp.loc.Y].overlap=true;
if (inp.Keypress(Keys.PageDown)) tiles[mp.loc.X, mp.loc.Y].overlap=false;
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
// SAVE LEVEL MAP
if (shift_down&&inp.Keypress(Keys.D4))
{
SaveLevel(Game1.LEVEL_NAME); // not made yet, but we'll make a SaveLevel method soon
}
// LOAD LEVEL MAP
if (shift_down&&inp.Keypress(Keys.D1)) if (File.Exists(Game1.LEVEL_NAME))
{
LoadLevel(Game1.LEVEL_NAME); // not made yet, but we'll make a LoadLevel method soon
}
} // ^^^ 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
//-------------------------
// D R A W L O C A T O R S (ie: player starting position on editor map)
mp.DrawTiles(show_colliders, edit_mode: true); // show monster start locations
}
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 :
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).
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
//-------------------
// S A V E L E V E L
//-------------------
voidSaveLevel(stringlevel_name)
{
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
using (StreamWriterwriter=newStreamWriter(level_name))
{
//write start data (player position) writer.Write(mp.startData.x.ToString() +","); writer.Write(mp.startData.y.ToString() +","); writer.WriteLine();
for (inty=0; y<Map.TILES_HIGH; y++)
{
for (intx=0; x<Map.TILES_WIDE; x++)
{
inttemp;
if ((tiles[x, y].index!=0) || (tiles[x, y].type!=TileType.empty)) // store only tiles with used information
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
//-------------------
// L O A D L E V E L
//-------------------
publicvoidLoadLevel(stringlevel_name)
{
if (!File.Exists(level_name)) return;
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
using (StreamReaderreader=newStreamReader(level_name))
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
// PRE-PROCESS LOADED TILES (mainly for data specific to tile type that isn't saved to file [or fixing tile data] )
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
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.
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
//--------------------------------------------
// B O U N C E M G R (manages bounce events)
//--------------------------------------------
classBounceMgr
{
Tile[,] tiles; // refers(not a new copy) to map's tiles
Bouncer[] bouncers; // tracks active bounce/spring platform animations
intnum_bouncers; // number of active bouncers (if more than one at a time)
// CONSTRUCT
publicBounceMgr(Tile[,] Tiles)
{
tiles=Tiles;
bouncers=newBouncer[100]; // allow up to 100 bouncers at once (spring platforms)
}
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
// A D D (B O U N C E) (tile event for bouncey stuff)
publicvoidAdd(intx, inty)
{
if (tiles[x, y].event_active) return; // <--already activated so return
bouncers[num_bouncers] =newBouncer(x, y, tiles[x, y].offset, 25, 0.4); // create new bouncer event
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
// U P D A T E (B O U N C E R S)
publicvoidUpdate()
{
if (num_bouncers<1) return; // <-- no bouncers so return
inti=0, bx, by;
while (i<num_bouncers)
{
bx=bouncers[i].x; by=bouncers[i].y; // get tile coord of bouncer
if (!bouncers[i].Update(reftiles[bx, by].offset)) // update (refer to tile's offset for updating it)
{ // UPDATE (if false remove the bouncer)
tiles[bx, by].offset=bouncers[i].original_offset; // done animating it so set to original position
tiles[bx, by].event_active=false; // no longer active
bouncers[i] =bouncers[num_bouncers-1]; // replace with last item
if (num_bouncers>0) num_bouncers--; // one less active bouncer
}
i++;
}
}
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
publicBounceMgrbounceMgr; // 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
bounceMgr=newBounceMgr(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
// UPDATE BOUNCERS (spring platforms)
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
//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
- 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:
if (device.RasterizerState.CullMode!=CullMode.None) device.RasterizerState=RasterizerState.CullNone;
}
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]
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
samplerTexSampler:register(s0)
{
Texture=<Texture>;
AddressU=clamp;
AddressV=clamp;
MinFilter=linear;
MagFilter=linear;
MipFilter=linear;
};
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
float4x4MatrixTransform;
structVSOutput
{
float4position :SV_Position;
float4color :COLOR0;
float2texCoord :TEXCOORD0;
};
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).
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
float4SpritePixelShader(VSOutputinput):SV_Target0
{
float4col;
col=tex2D(TexSampler,input.texCoord)*input.color;
returncol;
}
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
techniqueQuadBatch
{
pass
{
VertexShader=compilevs_3_0SpriteVertexShader();
PixelShader =compileps_3_0SpritePixelShader();
}
}
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:
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
// U S E 2 D C A M E R A
publicvoidUse2DCamera()
{
world=Matrix.Identity;
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)
MatrixhalfPixelOffset=Matrix.CreateTranslation(-0.5f, -0.5f, 0); proj=halfPixelOffset*proj; //<--fixes half-pixel offset problem
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
// 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 )
// 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 )
- 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):
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:
if (!sourceRect.HasValue) sourceRect=newRectangle(0, 0, tex.Width, tex.Height);
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:
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
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
RectangletempRect=sourceRect.Value;
u1=tempRect.X/ (float)tex.Width; //gets the texture coords in terms of (0.0f-1.0f, 0.0f-1.0f)
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
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
}
x1=pos.X; y1=pos.Y; //upper-left
x2=pos.X+w; y2=pos.Y; //upper-right
x3=pos.X+w; y3=pos.Y+h; //lower-right
x4=pos.X; y4=pos.Y+h; //lower-left
if (rot!=0f) {
floatox=pos.X+o_x, oy=pos.Y+o_y;
floatcos= (float)Math.Cos(rot), sin= (float)Math.Sin(rot); //this is actually quite fast on a modern computer
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
//E N D -----------------------------------------
publicvoidEnd()
{
if (!beginCalled) { Console.WriteLine("call to END without begin called. Aborting End."); return; }
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)
device.Textures[0] =tex; // <--- do this just before drawing only
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
//E N D T E X T-----------------------------------------
publicvoidEndFont() //works like end except internally
{
if (!beginCalled) { Console.WriteLine("call to ENDText without begin called. Aborting End."); return; }
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)
device.Textures[0] =tex; // <--- do this just before drawing only
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.
if (ya>= (high-1)) { done=true; Console.WriteLine("Error finding font hieght"); }
} while (!done);
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
ya=4;
finished=false;
do
{
col=FontColorData[xa+ya*wide];
if ((col.R<5) && (col.G>249) && (col.B<5) && (col.A>249)) xa++;
average_width=average_width/ i *1.2f; //*1.2 to boost spacing a bit
}//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
publicfloatDefault_Font_Size { get { returndefault_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.
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):
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:
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.
if (align==Alignment.LeftAlign) { pos.X=spacing; px=spacing; } //one space from left side of screen
else {
floatmax_x=px, xx=px;
intq=0;
do {
if (max_x<xx) max_x=xx;
varc=text[q];
if ((c=='\n') || (c=='\r')) { xx=pos.X; q++; continue; } if ((c<33) || (c>127)) { xx+=spacing; q++; continue; }
intii= (int)c; w=fd[ii].w*scale.X;
xx+= (w/2+kern_space); q++;
} while (q<text.Length); if (max_x<xx) max_x=xx;
floattext_width=max_x-px;
if (align==Alignment.CenterAlign)
{
pos.X=screenWidth/2-text_width/2; px=pos.X;
}
elseif (align==Alignment.RightAlign)
{
pos.X=screenWidth-text_width-spacing; px=pos.X;
}
}
}
- 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
inta=0, i=0;
do
{
varc=text[a];
i= (int)c;
if ((c=='\n') || (c=='\r')) { px=pos.X; py+=v_space; a++; continue; }
if ((c<33) || (c>127)) { px+=spacing; a++; continue; }
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.
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.
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):
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
//UTILS
staticpublicRandomrnd;
and update the map variable section to this:
Code Snippet
//MAP DATA (put this in a class later)
constintMAX_SHEET_PARTS=300; // maximum allowed sprite-sheet parts for tiles image
Sheet[] sheet; // sprite sheet data for tiles (could use a list)
SheetManagersheet_mgr; // where a level's sheet definitions can be edited
Mapmap; // holds all the tiles map stuff
Editoreditor; // 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:
(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
rnd=newRandom(); // INIT UTILS
inp=newInput(); // 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
// INIT EDITOR
editor=newEditor(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)
// SET IMAGE FOR TILES
map.SetTilesImage(tiles_image); // this might be used in multiple places so send a reference to map but store tiles here
- 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
// L O A D L E V E L
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
caseGameState.edit:
//----------------------
// E D I T O R M O D E (input/updates)
cam_pos=map.loc.ToVector2() *64;
// UPDATE MONSTERS
editor.Update();
// SWITCH TO PLAY MODE
if (inp.Keypress(Keys.Enter)) gameState=GameState.play;
break; //---------------- end editor mode
All we really did was add editor.Update();
e) In Draw() where you left the comment: // EDITOR_INSTRUCTIONS, add:
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
// Holds info about sprite parts
classSpritePart
{
publicstringname;
publicRectanglerect; // source rectangle on sprite sheet
publicVector2pivot; // pivot/origin to rotate around
publicintparent; // what sprite was parent (useful when programming vector tracking [like head looking or pointing weapon] )
publicVector2m1, m2, m3, m4; // model points around origin(0,0) before transforms
}
- 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
// Holds an individual key for a part in an animation
classKey
{
publicintpart; // if non-negative tells to swap parts
publicboolactive; // active (for hiding parts not animated)
}
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
// Holds an animation (and all key and part manipulation info)
classMeoAnimation
{
publicstringanimation_name; // used when need to identify which animation index to use (using dictionary)
publicintnum_keys; // total keys in this animation sequence
publicboollooping;//, stopped;// is a loop type animation?, stopped or playing
publicintroot; // index of root bone (could be useful)
publicintstart_part; // section of parts this animation works on
publicintend_part; // " "
publicfloattimer; // used in the example Play() method for interpolating between keys
// add customization properties here:
publicfloatspeed=1.0f; // 100%
publicVector2offset=Vector2.Zero;
}
- 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
// Holds final rendering data for the Draw method
classFinal
{
publicintorder; // which index to use ( drawing order )
publicintpart; // index of actual part/rect to render
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
classMeoMotion
{
ContentManagercontent;
publicQuadBatchbatch;
publicTexture2Dtex;
publicintnum_parts, max_parts, total_animations;
Final[] final; // used in the example play and draw methods
publicSpritePart[] parts; // sprite part list
publicMeoAnimation[] anim; // all the animations
publicDictionary<string, int>lookup=newDictionary<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
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
// 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
if (FileName.EndsWith(".txt")) { } elseFileName+=".txt";
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
using (StreamReaderreader=newStreamReader(FileName))
{
stringanim_name="";
a=-1;//0; // animation index
stringline=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; }
parts=newSpritePart[num_parts]; //allocate parts anim=newMeoAnimation[total_animations]; //allocate animations
intp=0, pi=-1, first_index=0; // part index, timeline part index, first index within a sheet section
intk=0; // key index
stringcurrent_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)
boolfirst=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
do
{
line=reader.ReadLine(); strs=line.Split(',');
switch (strs[0])
{
case"SPRITESHEET_FILENAME": current_sheetname=Path.GetFileNameWithoutExtension(strs[1]); first=true; break; // SET FIRST so we know to set the first index
case"TOTAL_NUM_PARTS": part_count=Convert.ToInt32(strs[1]); if (part_count>max_parts) max_parts=part_count; break;
- 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)
anim[a].keys[pi, k].part=Convert.ToInt32(strs[1]) +first_index; // offset by first_index
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...
- 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
}
} 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; } elseanim[a].key2=0; a++; } while (a<total_animations);
final=newFinal[num_parts];
a=0; do { final[a] =newFinal(); a++; } while (a<num_parts);
}//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
// G E T I N D E X --------------------------------------
if (!lookup.TryGetValue(sheetname+animation_name, outvalue)) {
if (show_error_messages) Console.WriteLine("Animation not found in dictionary: "+sheetname+animation_name);
return0;
}
returnvalue;
}
- 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
classMeoPlayer
{
publicboolPREMULTIPLY_ALPHA=true;
constfloatHIDE_ALPHA_UNDER=0.02f; // minimum alpha (anything lower is clipped from calculations)
privatefloattimer; // for interpolating between keys
privatebooldone_anim; // used in IsDoneAnimation() to tell if at least one animation cycle has finished
privateintpart_count;
privateFinal[] final; // holds final animation data
publicQuadBatchbatch;
MeoMotionmeo; // 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
// C O N S T R U C T O R
/// <summary>
/// Creates a new character -- note: character can be changed by changing sheet_name and then setting an animation
/// </summary>
/// <param name="SheetName">used to determine which character on the spritesheet to use (uses the old original sheet name to refer to it)</param>
/// <param name="Position">default position of character</param>
/// <param name="Meo">instance of meo to use</param>
/// <param name="spriteBatchDistort">instance of spriteBatchDistort to use for drawing</param>
publicMeoPlayer(stringSheetName, Vector2Position, MeoMotionMeo, QuadBatchquadBatch) //, int max_onscreen_characters)
final=newFinal[(meo.max_parts+1)]; // create a memory pool big enough to hold any character's render data
inta=0; do { final[a] =newFinal(); a++; } while (a< (meo.max_parts+1)); // allocate
batch=quadBatch;
}
- 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
// S E T A N I M A T I O N
/// <summary>
/// Set a character animation by name with option to set flip horizontally
/// </summary>
publicvoidSetAnimation(stringAnimationName, boolFlip=false, boolplay_backward=false) // note: normally, if changing character direction - just change public variable flip = !flip
{
if (AnimationName=="none") { active=false; return; } // usually better to just change public var active to false for this
- 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
// see if at the end of an animation cycle
publicboolIsDoneAnimation()
{
if (last_reverse!=reverse) returnfalse; // just changed playback direction so not done
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
// U P D A T E
// 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)
- 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
if (reverse)
{
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
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
else
{
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
if (timer>t2) // ready to switch keys to interpolate between
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
time_dif=t2-t1; // total time between both keys
if (time_dif<=0) time_dif=0.0001f; // prevent unlikely possibility of division by zero
percent= (timer-t1) /time_dif; // what is the percentage (0-1) [for interpolation]
Vector2pos, scale, o1, o2, o3, o4;
floatrot;
floatx1, x2, x3, x4;
floaty1, y2, y3, y4;
inti=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
do
{
final[i].order=meo.anim[a].keys[i, k1].order;
if (meo.anim[a].keys[i, k1].active==false) { final[i].hide=true; i++; continue; } // (part not shown - skip - go to do)
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
// HERE YOU COULD ADD EXTRA PROGRAMMED ROTATION RESPONSES (like trailing hair, tail, etc - or vector based weapon pointing)
// 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)
if (rot!=0f)
{
floatcos= (float)Math.Cos(rot), sin= (float)Math.Sin(rot); // rotate points around origin and then add the position where they belong
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
final[i].v2.X=pos.X+x2; final[i].v2.Y=pos.Y+y2;
final[i].v3.X=pos.X+x3; final[i].v3.Y=pos.Y+y3;
final[i].v4.X=pos.X+x4; final[i].v4.Y=pos.Y+y4;
}
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
final[i].v1+=o1; // add the distortion offsets of the points
final[i].v2+=o2;
final[i].v3+=o3;
final[i].v4+=o4;
if (flip) // flip horizontally:
{
final[i].v1.X=-final[i].v1.X;
final[i].v2.X=-final[i].v2.X;
final[i].v3.X=-final[i].v3.X;
final[i].v4.X=-final[i].v4.X;
}
i++;
} while (i<part_count);
last_reverse=reverse;
}
- 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
// D R A W
// Customizable draw for character. Could make other draw overloads or pass in vars to control drawing behavior for specific characters.
publicvoidDraw(Colorcolor)
{
if (!active) return;
inti=0, n=0, p;
Colorcol;
n=0;
do
{
i=final[n].order;
if (final[i].hide) { n++; continue; }
p=final[i].part; // may switch parts during animation
color.A= (byte)(final[i].alpha*255.0f); // alpha transparency from 0-255
if (PREMULTIPLY_ALPHA) col=Color.FromNonPremultiplied(color.ToVector4()); // make sure the properties of the image in content is set to premultiply alpha true
elsecol=color;
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
n++;
} while (n<part_count);
}
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