MonoGame (new XNA) Tutorial:
Platformer Game Programming Tutorial

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

Page4:

53) Let's go back to Game1.cs and add in some of the new things created in the appropriate places:
a) At the top where the variables are, let's add Hud hud; like so near Input inp:

Code Snippet
  1. //INPUT
  2. Input                 inp;                
  3.  
  4. //HUD
  5. Hud                   hud;                                  // heads up display (shows life, player status, etc)
(inp should already be there)

b) We don't have a crystal effect but we will soon so you can leave this uncommented:
Code Snippet
  1. //RENDERTARGETS
  2. RenderTarget2D        MainTarget;                           // render to a standard target and fit it to the desktop resolution
  3. RenderTarget2D        sprite_target;                        // any sprites reflected onto crystals are drawn on here also for crystal effect
  4.  
  5. //FX
  6. Effect                crystal_fx;                           // reflective crystal effect
(you'll already have MainTarget)

c) Make sure all of these are included somewhere in the variables:
Code Snippet
  1. // PLAYER / MEOMOTION / CAMERA        
  2. static public Vector2 rescale = Vector2.One; // adjusts character scale to screen resolution (static so if altered, all creatures can be scaled in all classes)
  3. static public Vector2 cam_pos;               // world position of camera
  4. Player                player;                // hero-player control and collision detection/response
  5. MeoMotion             meo;                   // meo = MeoMotion manager which loads and stores vector-based blended keyframe animations
- rescale will be optionally set to adjust for resolution
- add camera position(if didn't already), the player, and a MeoMotion system (loads/stores MeoMotion animations)

d) We don't have a MonsterSys to manage monsters yet, but we will very soon so you can leave this uncommented:
Code Snippet
  1. // MONSTERS / NPC's
  2. MonsterSys            monsterSys;            // holds and controls monsters or npc's
  3.  
  4. // FRAME ANIMATION STUFF / EXPLOSIONS        
  5. ExplodeSys            explodeSys;            // tracks explosion frames for all explosions within it

e) Initialize a render target to put animated sprites on (this is needed in part to create certain effects):
Code Snippet
  1. quadBatch     = new QuadBatch(Content, GraphicsDevice, "QuadEffect", "FontTexture", screenW, screenH); // setup distortable quad class
  2.  
  3. // INIT RENDER TARGETS
  4. sprite_target = new RenderTarget2D(GraphicsDevice, screenW, screenH);
  5.  
  6. rnd = new Random(); // INIT UTILS
(you'll already have quadBatch and rnd setup so just ignore those)

f) Create meo and explodeSys:
Code Snippet
  1.     map        = new Map(sheet, spriteBatch);            
  2.  
  3.     // INIT MEOMOTION
  4.     meo        = new MeoMotion(Content, quadBatch);                            // setup MeoMotion class       
  5.  
  6.     // INIT EXPLOSIONS
  7.     explodeSys = new ExplodeSys(spriteBatch);
  8.  
  9.     base.Initialize();
  10. }
(you already have map created)

54) Go to Game1.cs LoadContent():
a) after LOAD GRAPHICS section, add these (Note that Monster stuff isn't made yet but I will explain the idea):
Code Snippet
  1. // LOAD HUD
  2. hud = new Hud(spriteBatch, font);                                       // init and load heads up display (player status stuff)
  3. hud.Load(Content);
  4.  
  5. // LOAD EXPLOSIONS
  6. explodeSys.Load(Content, 16);
  7.  
  8. // CREATE PLAYER
  9. player    = new Player(screen_center, quadBatch, map, inp, hud, explodeSys); // create a player somewhere
  10.  
  11. // CREATE MONSTER SYSTEM
  12. monsterSys = new MonsterSys(Content, quadBatch, map, player, explodeSys);
  13. monsterSys.Load(1, Content);                                             // load monsters for level 1
  14.  
  15. // INIT EDITOR
  16. editor    = new Editor(map, inp, player, sheet, monsterSys);             // do this here because editor needs player and player needs hud and hud needs font (and font cannot be null)

(editor should be created already)
- make the heads-up display and load it's graphics
- load explosions (use 16 frames)
- create a player
- You may want to comment this for now but:
1) monsterSys will manage monsters and will need to load graphics (MeoMotion inside will need Content), will need to draw distortables (needs quadBatch), will need access to the map and player info, and need access to the explosion manager
2) Load monsters for level 1 (and need Content to load sounds)

55) We haven't made a Crystal.fx yet but we can prepare for it:
a)

Code Snippet
  1. // SET IMAGE FOR TILES
  2. map.SetTilesImage(tiles_image);                                         // this might be used in multiple places so send a reference to map but store tiles here
  3.  
  4. // FX
  5. crystal_fx = Content.Load<Effect>("Crystal");
  6. crystal_fx.Parameters["screen"].SetValue(new Vector2(screenW,screenH)); // screen(target) resolution
  7. crystal_fx.Parameters["BackMap"].SetValue(far_background);              // image to make shimmer from
  8.  
  9. // later we will want update to check if loading a new area is needed... and then have it call a method to load based on the level or area (game-state in update)
  10. sheet_mgr.Setup_Sheet_Level_1(ref sheet);
(map.SetTilesImage ... and ... sheet.mgr.Setup <--- should already exist)

- Loads crystal.fx (later placed in Content folder although if you wanted you could set it in a Shaders folder in there too)
- Send the display resolution as a float2
- It will need its regular texture, a reflection texture (the sprite target), and a background texture (to make shiney effects)... so for now we can send it the background one since it won't change... so the texture inside called BackMap will be set to the far_background which contains the stars which will work nicely for creating a shimmering shine effect as the crystals pass by.

b) After map.AddBorder,
Code Snippet
  1. // LOAD AND SETUP PLAYER ANIMATIONS:
  2. float resize = 0.60f; rescale = new Vector2(resize, resize);           // 60% size - scale characters overall
  3. meo.Adjust_Scale_For_ScreenResolution(ref rescale, screenW, screenH);  // adjust final scale for target resolution
  4. player.Load(meo, tiles_image);
- Starting with a resize value of 60% (desired default size), the Adjust method in meo will adjust the size for whatever resolution you change the game to run in (for target display)
- player loads the player related things and keeps track of the image containing some spell effect stuff that it'll use (in this case I put the additive spell effect (particle effect) images on the same sheet as the tiles - but this is totally up to you)

c) Load sounds and start playing some music (optional to do here or elsewhere)
Code Snippet
  1.     // L O A D  L E V E L
  2.     editor.LoadLevel(LEVEL_NAME);
  3.                
  4.  
  5.     // LOAD SOUND AND MUSIC
  6.     Sound.Load(Content);
  7.     MediaPlayer.Play(Sound.music);                                        // already set to repeat in Load
  8.  
  9. }
(should already have: editor.LoadLevel)
- load common sounds (and default music) [didn't need to make a Sound object because use of static]
- play default music (volume set to about half already inside Sound.cs)

d) In Update(), make sure to include these before GameState.play (if didn't make already):
Code Snippet
  1. map.UpdateVars();
  2. explodeSys.Update();
  3.  
  4. switch (gameState) {

e) In the gameState play mode, make sure you have all these now:
Code Snippet
  1. case GameState.play:
  2.     //-----------------
  3.     // P L A Y  M O D E  (input/updates)                                        
  4.  
  5.     // CHECK FOR GAMESTATE CHANGES
  6.     if (inp.Keypress(Keys.E)) { gameState = GameState.edit;      GraphicsDevice.SetRenderTarget(sprite_target);  GraphicsDevice.Clear(Color.TransparentBlack); }
  7.     if (hud.lives < 0)          gameState = GameState.game_over;
  8.  
  9.     // PLAYER INPUT AND ANIMATION UPDATES:
  10.     player.Update();
  11.  
  12.     // MOVE CAMERA
  13.     cam_pos += (player.pos - cam_pos) * 0.1f;                 // smooth pan camera toward player's world position
  14.  
  15.     // MATCH WORLD VIEW TO CAMERA
  16.     map.world_to_camera(cam_pos, ref background_pos);         // (what you see in the world based on where camera is)
  17.  
  18.     // PLAYER COLLISION DETECTION / RESPONSE:
  19.     player.WorldCollisions();
  20.  
  21.     // UPDATE BOUNCERS (spring platforms)
  22.     map.bounceMgr.Update();
  23.  
  24.     // INIT ANY NEW ANIMATION:
  25.     player.SetAnimation(gameTime);
  26.     
  27.     // MONSTER AI AND ANIMATION:
  28.     monsterSys.Update(gameTime);                     
  29.  
  30.     break;

- if number of lives less than 0 - game over
- update the player
- update world camera position by finding the difference between where the player is and camera is and shrink that distance by 10% each frame (so if 100, 10% = move to 90... and 10% 90 = move to 81... and 10% 81 = 72.9, etc)... so it quickly homes in on player and but slows down as it approaches player (smooth panning with ease-in)
- tell the map where the camera is (so it updates loc and scroll_offset for tiles) and retrieve the appropriate background position based on it
- process world collision detections/responses for player (sometimes called: "CDCR" or Collision Detection / Collision Response)
- update the bouncers (spring platform animations)
- based on player motion status, update the player animation selection if it has changed
- update monster motions (once we've made this - we'll implement this soon)

f) for GameState.edit, make sure you have all these now:

Code Snippet
  1. case GameState.edit:
  2.     //----------------------
  3.     // E D I T O R   M O D E  (input/updates)
  4.     cam_pos = map.loc.ToVector2() * 64;
  5.     monsterSys.Update(gameTime, false);                                // false = don't engage AI in editor mode
  6.     editor.Update();
  7.  
  8.     // SWITCH TO PLAY MODE
  9.     if (inp.Keypress(Keys.Enter)) gameState = GameState.play;
  10.     break;   //---------------- end editor mode
- camera world pos = tile location(x,y) * (64,64)
- update the monsters (false will tell it to ignore AI since this is edit mode) [not made yet]
- update the editor
- enter to go back to play mode

g) In Draw() at the first GameState.play check, we'll draw any monsters or players (or other sprites) onto a render target called sprite_target:
Code Snippet
  1. // DRAW SPRITES TO A SPRITE TARGET (so we can use it in crystal fx):
  2. if (gameState==GameState.play) //PLAY MODE
  3. {
  4.     GraphicsDevice.SetRenderTarget(sprite_target);
  5.     GraphicsDevice.Clear(Color.TransparentBlack);
  6.  
  7.     // DRAW MONSTERS
  8.     monsterSys.Draw();
  9.  
  10.     // DRAW PLAYER(S)
  11.     player.Draw();
  12. }
  13. //-------------------

IF play:
- set render target to sprite_target:
-- clear it (transparent black)
-- draw monsters
-- draw player

h) Next we set out render target to the MainTarget (of the resolution that our game's designed for)
Code Snippet
  1. //-------------------
  2.  
  3. GraphicsDevice.SetRenderTarget(MainTarget);          // this is like a substitute backbuffer set to a desired resolution (stretched to true backbuffer later)
  4.  
  5. // SKIP TO DRAW_OTHER_STATES IF NEEDED:
  6. if ((gameState != GameState.edit) && (gameState != GameState.play)) goto DRAW_OTHER_STATES; // an old tactic to avoid superfluous programming (use goto very sparingly and very cautiously)
- set render target to MainTarget
- if edit or play mode continue... otherwise skip ahead to DRAW_OTHER_STATES label (try to avoid goto when you can)

*DRAWS BACKGROUNDS*

i) After drawing the tiles, we can pass in some values that our crystal shader will need (in typical shader lingo, these are referred to as uniforms (GLSL) or constant buffers[ie: cbuffer, tbuffer] (HLSL) which are aligned and packed for optimal transfer rates and act as constants during each process run of the shader (but can be updated by the CPU software each frame) )
Code Snippet
  1. //-------------------            
  2. spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.None, RasterizerState.CullNone);
  3. map.DrawTiles();      // D R A W  T I L E S
  4. spriteBatch.End();
  5. //-------------------
  6.  
  7. // APPLY A CRYSTAL EFFECT TO THESE:                                     
  8. crystal_fx.Parameters["ReflectMap"].SetValue(sprite_target);            
  9. spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.None, RasterizerState.CullNone, crystal_fx);
  10. map.DrawCrystals();   // D R A W  C R Y S T A L S
  11. spriteBatch.End();
- draw tiles (may already have done this)
- set the texture in the shader called "ReflectMap" to the sprite_target (treating the render target for sprites as a texture)
- pass in the Begin, the crystal_fx (so its pixel shader replaces the default one until End() )
- draw the crystals with the effect

j) Now we can draw the sprite layer over the scene followed by any overlapping stuff and finally any additive blends that we want overtop of that:
Code Snippet
  1. spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.None, RasterizerState.CullNone);
  2. // SHOW SPRITES
  3. spriteBatch.Draw(sprite_target, Vector2.Zero, Color.White);            
  4. // DRAW OVERLAPS
  5. map.DrawOverlaps();            
  6. spriteBatch.End();
  7.  
  8. // DRAW ADDITIVE EFFECTS
  9. spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive, SamplerState.LinearClamp, DepthStencilState.None, RasterizerState.CullNone);
  10. explodeSys.Draw();
  11. spriteBatch.End();  spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.None, RasterizerState.CullNone);
- draw sprites layer from sprite_target (player, monsters, particles)
- draw overlapping tiles (ie: foreground trees)
- draw overlapping explosions with additive blending so they look cool
Include this:
Code Snippet
  1. // E D I T  M O D E  only  - - - - -
  2. if (gameState==GameState.edit)
  3. {
  4.     editor.DrawLocators(spriteBatch, tiles_image, screen_center);
  5. } // - - - - - - - - - - - - - - - -
  6. #if SHOW_PLAYER_COLLIDER          
  7. if ((player.loc.X > map.a1) && (player.loc.X < map.a2) && (player.loc.Y > map.b1) && (player.loc.Y < map.b2)) {       // if in drawn tiles
  8.     Vector2 CDpos = new Vector2(map.sx + 64.0f * (player.loc.X - map.a1), map.sy + 64.0f * (player.loc.Y - map.b1));  // get collision position (corner)
  9.     CDpos -= map.scroll_offset;
  10.     spriteBatch.Draw(tiles_image, CDpos, new Rectangle(960, 0, 63, 63), Color.Yellow, 0f, Vector2.Zero, 1f, SpriteEffects.None, 0f);
  11.     monsterSys.DrawColliders(spriteBatch, tiles_image);
  12. }           
  13. #endif
After we've made the MonsterSys, you can later come back to Game1.cs and Add (at the very very very TOP of the file):
#define SHOW_PLAYER_COLLIDER... this will unlock this section of code for compilation so that you can test the collision regions of the characters. For now you can include this line as a comment: //define SHOW_PLAYER_COLLIDER, and uncomment it later as needed.
CDpos (collision detection position), uses the player location to determine where to draw the tile occupation rectangle (while player is on-screen)... monsterSys.DrawColliders will loop through all the monsters drawing their colliders (body hit region and attack regions)... you can later adjust these properties as needed.

k) You may have all of the following but I suggest checking over it to be sure( ie: hud.Draw() ):
Code Snippet
  1. // TEXT / HUD ---
  2. if (gameState==GameState.play) {
  3.     spriteBatch.DrawString(font, "Press E to go to map editor", new Vector2(1, screenH-50), Color.DarkGreen); spriteBatch.DrawString(font, "Press E to go to map editor", new Vector2(0, screenH-49), Color.GreenYellow);
  4.     hud.Draw();                
  5. }
  6. else
  7. {   // EDITOR INSTRUCTIONS
  8.     editor.DrawInstructions(spriteBatch, far_background, font, screenH);                
  9. }
  10. spriteBatch.End();
  11.  
  12. DRAW_OTHER_STATES:                                        // (label for goto to avoid superfluous programming [use sparingly and cautiously] )
  13. //----------------
  14. switch (gameState)
  15. {
  16.     case GameState.game_over:                             // maybe add fade transitions later
  17.         spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.LinearWrap);
  18.         GraphicsDevice.Clear(Color.Black);
  19.         spriteBatch.DrawString(font, "G A M E   O V E R", screen_center + new Vector2(-80, -20), Color.LimeGreen);
  20.         spriteBatch.DrawString(font, "  (press enter)  ", screen_center + new Vector2(-62, 20), Color.LimeGreen);
  21.         spriteBatch.End();
  22.         break;
  23. }
  24.  
  25.  
  26. // DRAW TEMP BACKBUFFER (MainTarget) to TRUE BACKBUFFER IN MAXIMIZED WINDOWED MODE (this resolves some rare but possible compatibility problems):
  27. GraphicsDevice.SetRenderTarget(null);
  28. spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Opaque, SamplerState.LinearWrap, DepthStencilState.None, RasterizerState.CullNone);
  29. spriteBatch.Draw(MainTarget, desktopRect, Color.White);
  30. spriteBatch.End();


-------------------------
56) In Solution Explorer, over your c# project_name, right-click and Add>>Folder: "Monsters"
a) Right-Click the folder "Monsters" and Add>>Class: "Monster" (which defines and processes monsters and their AI)
b) Right-Click the folder "Monsters" and Add>>Class: "MonsterSys" (holds the monsters and their sounds and runs their loops)

57) Go into Monster.cs
a) Add an enumeration for types of monsters, then in the class itself, add the following constants and enums:
Code Snippet
  1. enum MonsterType { None, Mouster, Hellcat }
  2.  
  3. class Monster
  4. {
  5.     const float GRAVITY  = 0.25f, MAX_FALL_SPEED = 20;        // using same in player class
  6.     const float MAX_JUMP = -12;
  7.     
  8.     public enum Act      { idle, walk, run, attack, jump, ouch }         // later add things like jump, duck, guard, or whatever
  9.     public enum Mode     { wander, run_away, patrol }                    // what mode of behavior the monster AI is using
- 2 actual monster types: Mouster, Hellcat
- gravity, maximum falling speed, and maximum upward jump speed are constants (as in player) but you could make them dynamic
- monsters can potentially do animations / Actions: idle, walk, run, attack, jump, ouch (more can be added)
- monsters have decision modes: wander, run_away, patrol
Code Snippet
  1. int screenW, screenH;
  2.  
  3. // MEO / ANIMATION
  4. MeoMotion         meo;            // reference to MonsterSys's meoMotion player        
  5. MeoPlayer         meo_play;       // for controlling enemy character animations (unique instance)
  6. bool              flip;           // flip horizontally                
  7. Vector2           rescale;        // adjusts meo player image scale for resolution (and movement speed)
  8. Act               motion, last_motion; // if motion changes, it will play the corresponding animation
  9. Mode              mode;           // wander, chase, escape, patrol
  10. int               walk_index;     // remember index of walk/run to adjust animation speed        
  11. int               jump_index;     // remember index of "jump" if it exists
  12. int               ouch_index;
  13. int               time, total_time;           // current time doing an action, total time to continue that action
  14. bool              must_finish_animation;      // if you want an animation to finish completely before allowing switch to the new one     
- display width/height stored locally for convenience
MonsterSys has a MeoMotion object to hold all monster animations (refers to), flip effect, rescale for resolution, current animation, previous animation, decision mode, walk, jump, ouch animation indices, timing for how long to carry out a decision to do a motion, finish current animation before allowing next animation to take over
Code Snippet
  1. ExplodeSys        explode;        // <-- frame based animations (referenced from Game1)
  2.  
  3. // MAP / PLAYER
  4. Tile[,]           tiles;          // reference to tiles[,] from map
  5. BounceMgr         bounceMgr;      // refer to Game1's bounceMgr (for spring platforms)
  6. Player            player;         // refer to player
(explosion manager, map's tiles, bounce manager, player)
Code Snippet
  1. // MONSTER        
  2. MonsterType       type;           // which type of monster                        
  3. public bool       dead;           // if it's dead, skip processing it
  4. public int        life;           // life status
  5. public Vector2    pos, vel;       // world position, velocity
  6. public Vector2    target_vel;     // velocity goal (either random or deliberate)
  7. public Vector2    old_pos;        // previous position of monster
  8. public Vector2    screen_pos;     // where monster would appear on screen  
  9. public Point      loc, old_loc;   // approximate map tile coordinate of monster
  10. public Rectangle  bbox;           // bounding box for collision detection
  11. public Rectangle  attack_box;     // for collision detection of attack from monster                       
  12. bool              grounded;       // monster on ground or not            
  13. int               attack_key;     // actual animation key where attack begins to be harmful
  14. int               invincible;     // cycles of invincibility (counts down to 0)
  15.  
  16. // SOUND        
  17. Vector2     pan;              // stereo pan (left-right speaker volumes based on monster position)
(monster type, dead, monster life-bar, position, velocity, velocity to aquire, old position, screen position, map location, previous map location, bounding box, attack region, on the ground, first keyframe where attack can harm(after anticipation), invincibility count-down, stereo pan (volume is all left for -1 and all right for +1) )

58) Monster constructor will need a start position, the type of monster to make,
and access to:
MeoMotion in MonsterSys, Quadbatch to draw distortables, map, player, and the explosion manager:
Code Snippet
  1. // CONSTRUCT
  2. public Monster(Vector2 Pos, MeoMotion Meo, QuadBatch QBatch, MonsterType monster_type, Map map, Player playr, ExplodeSys ex)
  3. {
  4.     screenW = Game1.screenW;  screenH = Game1.screenH;  rescale = Game1.rescale;
  5.     meo        = Meo;                        
  6.     tiles      = map.tiles;
  7.     bounceMgr  = map.bounceMgr;
  8.     player     = playr;
  9.     explode    = ex;
  10.  
  11.     type       = monster_type;            
  12.     string sheet_name = get_monster_properties(type);
  13.     meo_play   = new MeoPlayer(sheet_name, Pos, Meo, QBatch);
  14.     pos        = Pos;       old_pos = pos;
  15.     screen_pos = Conv.world_to_screen(pos);
  16.     loc        = Conv.GetTileCoord(pos);
  17.     dead       = false;
  18.     customize_monster();
  19.     motion     = Act.idle;
  20.     mode       = Mode.wander;
  21. }

- make some values local for convenience
- get references
- set the monster type
- get monster properties of desired monster (return the sheet name for setup of meoPlayer):
--- this determines the monster's collision regions(bbox's), first attack keyframe, and lifebar
- create a meoPlayer using the sheet name (and providing position, meo, & quadBatch)
- set start position and find from that:
--- screen position and map location
- customize the monster's animations (based on type)
--- perhaps adjust certain animation speeds or refer certain index (like walk index) to a different animation name (like "run")
- set starting animation to idle and initial decision mode to wander

59) We'll want the option to change monsters (like if a monster dies and we want to switch the dead one for a new one at another location):

Code Snippet
  1. // C H A N G E  M O N S T E R
  2. public void ChangeMonster(Vector2 Pos, MonsterType monster_type) {
  3.     pos        = Pos;        old_pos = pos;
  4.     screen_pos = Conv.world_to_screen(pos);
  5.     loc        = Conv.GetTileCoord(pos);            
  6.     type = monster_type;
  7.     meo_play.sheet_name = get_monster_properties(type);             
  8.     dead = false;
  9.     customize_monster();
  10.     motion = Act.idle;
  11. }
Using a start position and desired monster type:
- set position data (world, screen, map)
- what kind of monster
- get the sheet name (based on type) and other type-specific properties (like collision boxes)
- make alive
- adjust animation properties and set as idle

60) If the monster jumps, determine if a bounce sound is played (spring), if it should keep current animation(busy), set as not on ground (won't allow new jump if it already is doing one)
Code Snippet
  1. //------------------------
  2. // M O N S T E R   J U M P
  3. //------------------------
  4. // play_bounce [plays bounce sound], keep_motion [don't change animations]
  5. void MonsterJump(bool play_bounce = false, bool keep_motion = false)
  6. {
  7.     if (motion == Act.jump) return;
  8.     if ((!keep_motion)&&(jump_index>0)) motion = Act.jump;            
  9.     grounded = false;
  10.     if (!play_bounce) Sound.jump.Play(pan.Y, 0.9f, pan.X); else Sound.bounce.Play(pan.Y, 0.9f, pan.X);
  11.     if (must_finish_animation)
  12.     {
  13.         vel.Y = MAX_JUMP; // need to trigger from here instead of state because waiting for animation to finish                
  14.     }
  15. }

- already jumping (return)
- if allowed to, set as jump animation (if jump index is zero then there is no actual animation for the jump - just a velocity change)
- no longer grounded
- if regular jump - play jump sound - but if bouncer caused the jump - play bounce sound
- if waiting for animation/motion to finish - set a jump velocity for now (otherwise it set one already before calling MonsterJump)

61) When a monster recieves damage (lifebar reduced based on power of attack):

Code Snippet
  1. //-----------------------------
  2. // M O N S T E R  D A M A G E D
  3. //-----------------------------
  4. void MonsterDamaged(int life_loss)
  5. {
  6.     vel.Y = -6f;
  7.     meo_play.SetAnimation(ouch_index, flip); must_finish_animation = true; motion = Act.ouch;
  8.     life-=life_loss;
  9.     invincible = 100;
  10.     
  11.     // (sounds could be referenced with indexing[] too if you have lots of monster types)
  12.     switch (type)  {
  13.         case MonsterType.Mouster: MonsterSys.monster_ouch1.Play(pan.Y, 0.9f, pan.X); break;
  14.         case MonsterType.Hellcat: MonsterSys.monster_ouch2.Play(pan.Y, 0.9f, pan.X); break;
  15.     }
  16.  
  17.     if (life <= 0.0f)
  18.     {
  19.         dead = true;
  20.         // ADD EXPLOSION:
  21.         explode.Add_Explosion(pos);
  22.         Sound.explode.Play(pan.Y, 0.9f, pan.X);
  23.     }
  24. }
- monster hops a bit and plays the ouch animation (must finish it and make sure motion state is ouch)
- loses some life
- briefly invincible while damaged
- depending on monster, type: play the ouch sound based on monster's current pan setting
(we could have passed in a reference to the sound also, from MonsterSys, and set it during get_monster_properties)

IF lifebar is empty:
- dead, add explosion(at world position), play explosion sound (using monster's current pan setting)

62) The monster may sometimes decide to change speeds... let's make something to help decide on a new random speed:
Code Snippet
  1. // GET RANDOM SPEED
  2. float GetRandomSpeed()
  3. {
  4.     float x = Game1.rnd.Next(12)-6;
  5.     if (x > 0) if (x <  4) x =  4;
  6.     if (x < 0) if (x > -4) x = -4;
  7.     return x;
  8. }
- get a random number between -6 and +6
- if positive... make it (+4 to +6) ... and if negative (-4 to -6) [vary these as you like]

63) When we start collision detection, it will be annoying to check for strange circumstances (like boundary violations) all the time, so we'll make a compact helper method to determine if there's a solid there or not:
Code Snippet
  1. // SOLID (collision helper)
  2. bool Solid(int x, int y)
  3. {            
  4.     if ((x < 0) || (x >= Map.TILES_WIDE) || (y < 0) || (y >= Map.TILES_HIGH)) return true; // out of bounds is an obstacle
  5.     if (tiles[x, y].is_solid) return true;
  6.     return false;
  7. }
- this also reduces something like if (tiles[x,y].is_solid to: if Solid(x,y) which makes the code look better (altho not quite as efficient but it definately won't matter).

64) We'll also need a couple helper methods to determine if a monster should jump on or over something or down onto something... but to do that it will need something to help it determine its surroundings a bit better:
a) Need a test for a vertical strip of tiles to see if it can land on something or not:
Code Snippet
  1. // CONTAINS SOLID (test a vertical strip of tiles to see if a solid tile exists to land on)
  2. int ContainsSolid(int x, int y1, int y2)
  3. {            
  4.     if (x < 0) x = 0; if (y1<0) y1=0;
  5.     if (x >= Map.TILES_WIDE) x = Map.TILES_WIDE - 1; if (y2 >= Map.TILES_HIGH) { y2 = Map.TILES_HIGH - 1; if (y1 > y2) y1 = y2; }
  6.     if (y2 < y1) { int tmp = y1; y1 = y2; y2 = tmp; }  // this should never happen (just in case)
  7.     int y = y1;
  8.     do
  9.     {
  10.         if (tiles[x, y].spikes) return 2;   // don't jump down onto spikes (monster's not that dumb)
  11.         if (tiles[x, y].is_solid) return 1;
  12.         if (tiles[x, y].stand_on) return 1;
  13.         y++;
  14.     } while (y < y2);            
  15.     return 0;
  16. }
Given map X coordinate and a starting and ending Y coordinate:
- clip the x and y coordinates to check to be within the map boundaries
- y2 should be greater than y1 for the test (if not, swap the 2 values)
Loop from y1 to y2:
- if spikes return a 2 to prevent monster from jumping on them
- if it's something that you can stand on, return a 1 to indicate that it's ok to proceed
At this point we know there's nothing good to jump onto so return a 0 to indicate this

b) It will also be handy to determine if a vertical strip of tiles is empty or not:
Code Snippet
  1. // CONTAINS EMPTY
  2. bool ContainsEmpty(int x, int y1, int y2)
  3. {            
  4.     if (x < 0) x = 0; if (y1 < 0) y1 = 0;
  5.     if (x >= Map.TILES_WIDE) x = Map.TILES_WIDE - 1; if (y2 >= Map.TILES_HIGH) { y2 = Map.TILES_HIGH - 1; if (y1 > y2) y1 = y2; }
  6.     if (y2 < y1) { int tmp = y1; y1 = y2; y2 = tmp; }  // this should never happen (just in case)
  7.     int y = y1;
  8.     do
  9.     {
  10.         if (!tiles[x, y].is_solid) return true;
  11.         y++;
  12.     } while (y < y2);
  13.     return false;
  14. }
- clip x,y to map
- swap y1,y2 if y2<y1
Loop y1 to y2:
- if NOT solid return true (empty)
otherwise return false (occupied)

c) same sort of helper for finding spikes:
Code Snippet
  1. // SPIKE (collision helper)
  2. bool Spike(int x, int y)
  3. {
  4.     if ((x < 0) || (x >= Map.TILES_WIDE) || (y < 0) || (y >= Map.TILES_HIGH)) return true;
  5.     if (tiles[x, y].spikes) return true;
  6.     return false;
  7. }
- if edge off map or spikes, return as true (hit spikes)
- NO spikes


65) UpdateAI() - we'll make a fairly basic AI that determines some actity options based on some of its surroundings.
a) We'll start with some basic motions(animations) such as idle, walk, jump, etc... and determine how those motions should gradually change to other activities based on current decision mode and what the environment is like.
Code Snippet
  1. // ----------------
  2. // U P D A T E  A I
  3. // ----------------
  4. void UpdateAI()
  5. {
  6.     int x = loc.X, y = loc.Y;
  7.  
  8.     // just a very basic simple AI for now (later could add pre-calculated path-node awareness(A* or other)
  9.     // - but limited range of vision and thus awareness of paths [don't make the AI overwhelmingly smart])
  10.     switch (motion)
  11.     {
  12.         case Act.idle: // I D L E ----------------                                     
  13.             if (last_motion != motion) {
  14.                 time = 0; total_time = Game1.rnd.Next(50, 140);                        
  15.             }
  16.             time++;
  17.             if (time > total_time) {
  18.                 target_vel.X = GetRandomSpeed();
  19.             }                    
  20.             break;

- for simplicity, get the location as x,y
- based on current motion:
-- Idle: if first time in idle: start a timer
-- if timer reaches time goal: set a random X velocity (somewhere else, this will trigger a different motion)

b) In walk and run mode the logic should be the same and there is a patrol or wander mode... so let's setup the logic:

Code Snippet
  1. case Act.walk: // W A L K / R U N ---------------------
  2. case Act.run:
  3.     if (last_motion != motion) {
  4.         time = 0; total_time = Game1.rnd.Next(80, 160);                        
  5.     }                    
  6.     if (mode == Mode.wander) {
  7.         time++;
  8.         if (time > total_time) {
  9.             time = 0; total_time = Game1.rnd.Next(90, 160);
  10.             target_vel.X = GetRandomSpeed();
  11.             if (total_time > 150) target_vel.X = 0f;              // force an idle in wander mode sometimes
  12.             if (total_time < 110) mode = Mode.patrol;             // switch between patrol and wander sometimes
  13.         }
  14.     }
  15.     else if (mode == Mode.patrol) {
  16.         time++;
  17.         if (time > total_time) {
  18.             time = 0; total_time = Game1.rnd.Next(90, 160);
  19.             if (total_time < 110) mode = Mode.wander;             // switch between patrol and wander sometimes
  20.         }
  21.     }
  22.     break;

IF first time (as walk or run), set a timer
IF Wander:
- IF timer done:
- - set new timer randomly:(90 to 160)
- - randomize X velocity
- - if timer > 150 set to stop moving
- - if timer < 110 change to Patrol mode
IF Patrol:
- IF timer done:
- - set new timer randomly:(90 to 160)
- - if timer < 110 change to Wander mode

c) If Jump motion, if first time trigger the jump velocity, and if wander mode - periodically adjust velocity.

Code Snippet
  1.     case Act.jump: // J U M P -------------------
  2.         if (motion != last_motion) {
  3.             vel.Y = (float)(-Game1.rnd.Next(6)-6);
  4.         }                    
  5.         if (mode == Mode.wander) {
  6.             time++;
  7.             if (time > total_time) {
  8.                 time = 0; total_time = Game1.rnd.Next(80, 160);
  9.                 target_vel.X = GetRandomSpeed();
  10.                 if (total_time > 150) { target_vel.X = 0f; }
  11.             }                          
  12.         }
  13.         break;
  14.     case Act.attack: // A T T A C K -------------------
  15.         if (meo_play.IsDoneAnimation()) motion = Act.idle;
  16.         break;
  17.     case Act.ouch:
  18.         if (meo_play.IsDoneAnimation()) motion = Act.idle;
  19.         break;
  20. }//^switch(motion)^

JUMP:
IF just changed to jump motion, pick a random jump velocity (between -(0-6) - 6) = (-6 to -12)
IF wander mode:
- IF timer done
- - setup new timer time
- - get a random velocity (left/right)
- - IF new timer > 150, enemy stops moving (will change to idle later)
ATTACK: IF attack finishes, change to idle
OUCH:     IF ouch finishes, change to idle

d) If monster has its feet on the ground, make some monster decisions based on mode:

Code Snippet
  1. if (grounded)
  2. {
  3.     switch (mode) {
i) Run away and wander mode use the same decisions in this case:
Code Snippet
  1. // --- W A N D E R ---
  2. case Mode.run_away:
  3. case Mode.wander:
  4.     if (target_vel.X < 0)                                          // going left
  5.     {                    
  6.         bool isSpike = Spike(x-1,y);
  7.         if (Solid(x - 1, y) && !isSpike) {                         // if obstacle (that's not spike)
  8.             if (ContainsEmpty(x - 1, y - 4, y - 1))                // see if can jump over it (or onto it)
  9.             {
  10.                 vel.Y = -12; MonsterJump();
  11.             }
  12.             else target_vel.X = 4;                                 // if not change direction
  13.         }
  14.         else if (isSpike) target_vel.X = 4;                        // change directions if spikes are directly beside monster (ie: x-1, y)
  15.  
  16.         int isSolid = ContainsSolid(x - 1, y + 1, y + 5);
  17.         if (isSolid == 0) target_vel.X = 4;                        // nothing to jump down onto so change directions
  18.         else if (isSolid == 2) { target_vel.X = -8; vel.Y = -12; MonsterJump(); }       // try to jump over the spikes (x-1, y+(1 to 5))
  19.     }

Going Left:
- if solid in the way:
- - IF can you jump over it or onto it? (1 left, 4 up to 1 up)
- - - - set jump velocity and intialize Monster Jump
- - ELSE change direction
- ELSE IF it's a spike, change direction
- GET if something solid to jump down onto (1 left, 1 down to 5 down)
- IF NO, change direction
- IF spikes, jump over them (or try)

ii) Left's do the same for Going Right:

Code Snippet
  1. else if (target_vel.X > 0)                                     // going right
  2. {
  3.     bool isSpike = Spike(x + 1, y);
  4.     if (Solid(x + 1, y) && !isSpike)                           // if obstacle that's not a spike
  5.     {
  6.         if (ContainsEmpty(x + 1, y - 4, y - 1)) {              // can jump over or onto ?
  7.             vel.Y = -12; MonsterJump();
  8.         }
  9.         else target_vel.X = -4;                                // no, so go other direction
  10.     }
  11.     else if (isSpike) target_vel.X = -4;                       // change directions if spikes are directly beside monster (ie: x+1, y)
  12.     int isSolid = ContainsSolid(x + 1, y + 1, y + 5);
  13.     if (isSolid == 0) target_vel.X = -4;                       // nothing to jump down onto so change directions
  14.     else if (isSolid == 2) { target_vel.X = 8; vel.Y = -12; MonsterJump(); }     // try to jump over the spikes (x+1, y+(1 to 5))
  15. }
(exact same idea as above but some values changed because going toward the right)

To take this a step further, you could use something like an A* algorythm by doing a sweeping test of an area based on what's there to determine choices and movements and provide a range/direction of vision to the monsters that may influence their behavior in a slightly more realistic way.

iii) When in patrol mode, the monster will only change directions when encountering obstacle:
Code Snippet
  1.         // --- P A T R O L ---
  2.         case Mode.patrol:
  3.             if (target_vel.X < 0)                                          // going left
  4.             {
  5.                 if (Solid(x - 1, y) || !tiles[x - 1, y + 1].stand_on || Spike(x-1,y+1)) // if obstacle or drop-off or spikes
  6.                 {
  7.                     target_vel.X = Game1.rnd.Next(3) * + 4;                // go the other way (randomize speed a bit)
  8.                 }                            
  9.             }
  10.             else if (target_vel.X > 0)                                     // going right
  11.             {
  12.                 if (Solid(x + 1, y) || !tiles[x + 1, y + 1].stand_on || Spike(x + 1, y + 1)) // if obstacle or drop-off or spikes
  13.                 {
  14.                     target_vel.X = -Game1.rnd.Next(3) * - 4;               // go the other way (randomize speed a bit)
  15.                 }                            
  16.             }                        
  17.         break;
  18.         
  19.     }//^switch(mode)^
  20. }//^grounded^
PATROL:
IF going left (check left):
- IF solid_obstacle or nothing-to-stand-on or spikes, change direction
IF going right (check right):
- if solid_obstacle or nothing-to-stand-on, or spikes, change direction

e) IF Chasing player (and wander or patrol mode), find out if close enough to attack and if so attack (and check for success):
Code Snippet
  1. // --- C H A S E ---
  2. if ((mode==Mode.wander)||(mode == Mode.patrol)) {
  3.     if ((pos.X > player.pos.X - 220) && (pos.X < player.pos.X + 220))
  4.     {
  5.         if ((pos.Y > player.pos.Y - 150) && (pos.Y < player.pos.Y + 150))
  6.         {                        
  7.             if (pos.X < player.pos.X) target_vel.X = 6;
  8.             if (pos.X > player.pos.X) target_vel.X = -6;   
  9.             float left  = pos.X+bbox.X,     player_left  = player.bbox.X;
  10.             float right = left+bbox.Width,  player_right = player.bbox.Z;
  11.             float top   = pos.Y+bbox.Y,     player_top   = player.bbox.Y;
  12.             float bot   = top+bbox.Height,  player_bot   = player.bbox.W;
  13.             const float xrange = 100, yrange = 70;
  14.             // IN RANGE TO TRY ATTACK?
  15.             if ((right>player_left-xrange)&&(left<player_right+xrange)&&
  16.                 (bot > player_top - yrange) && (top < player_bot - yrange))
  17.             {
  18.                 // BEGIN ATTACK
  19.                 if (last_motion != Act.attack)
  20.                 {
  21.                     motion = Act.attack;                                
  22.                     switch (type) // (sounds could be referenced with indexing[] too if you have lots of monster types)
  23.                     {
  24.                         case MonsterType.Mouster: MonsterSys.monster_attack1.Play(pan.Y, 0.8f, pan.X); break;
  25.                         case MonsterType.Hellcat: MonsterSys.monster_attack2.Play(pan.Y, 0.8f, pan.X); break;
  26.                     }
  27.                 }
  28.                 // DOES ATTACK HIT PLAYER?  // (only check if the animation's played past the first set of keys [****this may depend on how you made the attack keys****]            
  29.                 else if ((motion == Act.attack) && (meo_play.key1 > attack_key) && (player.invincible <= 0))
  30.                 {
  31.                     top = pos.Y + attack_box.Y; bot = top + attack_box.Height;
  32.                     if (!flip) { left = pos.X + attack_box.X; right = left + attack_box.Width; }
  33.                     else { right = pos.X - attack_box.X; left = right - attack_box.Width; }
  34.                     if ((right > player_left) && (left < player_right) && // IT HIT?
  35.                         (bot > player_top) && (top < player_bot))
  36.                     {
  37.                         player.HeroDamaged(0.2f);
  38.                     }
  39.                 }
  40.             }
  41.         }
  42.     }
  43. }

IF within some general range (-220 to 220, -150 to 150):
- IF Left of player, go right
- IF Right of player, go left
- make some variables to making the bounding box tests easier to understand when reading them
- IF (right-side is within certain distance of player's left side) AND
      (left-side is within a certain distance of player's right side) AND
      (bottom-side is within a certain distance of player's top) AND
      (top-side is within a certain distance of player's bottom) :
- - IF (not already attacking)
- - - motion/animation = attack
- - - IF Mouster - play attack sound 1
- - - IF Hellcat - play attack sound 2
// (NOTE: you can also use a reference to the proper sound passed from MonsterSys so you don't need these switch statements)
- - IF (already attacking):
- - - IF facing right, adjust attack-box for right-facing attack
- - - IF facing left, adjust attack-box for left-facing attack
- - - IF bounding box's (player vs attack-box) overlap: Player takes 0.2 damage (could make monster dependent variable)

f) If the character's running away, set the appropriate direction to run:

Code Snippet
  1. else if (mode == Mode.run_away) // R U N  A W A Y  (simple for now [maybe later add logic to escape spells too])
  2. {
  3.     if (pos.X < player.pos.X) target_vel.X = -6;
  4.     if (pos.X > player.pos.X) target_vel.X = 6;   
  5. }

g) Gradually accelerate the monster's velocity to its target velocity... if the velocity is ZERO, set the animation state (motion) to idle .... but if moving (and idle) then set motion as walk (or run if Mouseter [not that it makes a difference in this case])
Code Snippet
  1.     // ADJUST VELOCITY / MOTION
  2.     if (vel.X < target_vel.X) vel.X +=0.5f;
  3.     if (vel.X > target_vel.X) vel.X -=0.5f;
  4.     if (vel.X == 0) { if (motion != Act.jump) motion = Act.idle; }
  5.     else if (motion==Act.idle)
  6.     {
  7.         motion = Act.walk;
  8.         if (type == MonsterType.Mouster) motion = Act.run;
  9.     }
  10. }
(also don't set to idle if in the middle of a jump)


66) Update will need to know whether it should actually update the AI, (it also takes gameTime in case you want to use this with your MeoPlayer animations instead of the 60fps standard.
a) Much like the player class, we'll want to store the old position and map location in case backtracking is needed. Sweeping vectors tests could be done for high-velocity characters (selecting for the intersection closest to the old_pos). Our characters don't move even a fraction the speed necessary to require this type of CDCR, but it's something to keep in mind.

Code Snippet
  1. //-------------
  2. // U P D A T E  ( monster motions / AI)
  3. //-------------
  4. public void Update(GameTime gameTime, bool update_AI = true)
  5. {
  6.     old_pos = pos;                // previous world position
  7.     old_loc = loc;                // previous map location   (in tiles)                   
  8.     
  9.  
  10.     if (update_AI)
  11.     {   
  12.         UpdateAI();              // eventually you may want to change this to dispatch a unique AI for different types of monsters
  13.     }
  14.  
  15.     // GRAVITY
  16.     if (vel.Y < MAX_FALL_SPEED) vel.Y += GRAVITY;
  17.     // SLOW DOWN MONSTER:                  
  18.     MeoAnimation walk_anim = meo.anim[walk_index]; // reference the walk for easier access
  19.     if (vel.X > 0) { vel.X -= 0.40f; if (vel.X < 0) vel.X = 0; walk_anim.speed = vel.X *  0.42f; }
  20.     if (vel.X < 0) { vel.X += 0.40f; if (vel.X > 0) vel.X = 0; walk_anim.speed = vel.X * -0.42f; } // animation speeds can only be positive (- x - = +)
  21.     // DETERMINE FLIP (assumes monsters initially facing left)
  22.     if (vel.X < -1) flip = true; else if (vel.X > 1) flip = false;
  23.     // MOVE MONSTER            
  24.     Vector2 tru_vel = vel * rescale; // factors character resize into velocity
  25.     pos += tru_vel;  
  26.     // STEREO PAN
  27.     pan = Sound.GetPan(screen_pos);
- store old position / map_location
- Update the AI (makes some decisions about animation and movement)
- apply gravity
- for simplicty let walk_anim represent meo.anim[walk_index]
- if going right: reduce velocity and adjust walking animation speed accordingly
- same idea for going left...
- determine if should flip_horizontally
- get the velocity (scaled based on resolution adjustment if using)
- move the monster
- get the stereo panning based on the monster's screen position

b) If an animation MUST finish, don't allow animation change until it's done (once it's done, set to idle first)..
If allowed and the desired animation just changed, set the new animation.
Code Snippet
  1. // ANIMATE:
  2. bool allow_switch = true;
  3. if (must_finish_animation)
  4.     if (!meo_play.IsDoneAnimation()) allow_switch = false; else { must_finish_animation = false; motion = Act.idle; }            
  5. if ((allow_switch)&&(motion != last_motion))
  6. {
  7.     switch (motion)
  8.     {
  9.         case Act.idle:   meo_play.SetAnimation("idle",   flip); break;
  10.         case Act.walk:   meo_play.SetAnimation("walk",   flip); break;
  11.         case Act.run:    meo_play.SetAnimation("run",    flip); break;
  12.         case Act.attack: meo_play.SetAnimation("attack", flip); break;
  13.         case Act.ouch:   meo_play.SetAnimation("ouch",   flip); break;
  14.         case Act.jump:   if (jump_index>0) meo_play.SetAnimation("jump",   flip); break;   // if jump animation exists (none for hellcat)
  15.     }
  16. }
  17. meo_play.flip = flip;
  18. last_motion = motion;
IF: determine if allowed to play a new animation (and if is actually new)
- IDLE: change / initialize idle animation (flip = true = face right... otherwise face left)
- WALK: same idea...
- same idea...
- (Note: IF a jump animation exists, play the jump) otherwise only the jump velocity is set (already did) )
- notify MeoPlayer the direction the monster should be facing (flip)
- remember which animation was playing for next loop to determine if a motion is a new one.

c) If the monster's on screen, Update it's MeoPlayer animation calculations. . . and if the monster's NOT invincible, see it got hurt:
Code Snippet
  1.     if ((screen_pos.X > bbox.X) && (screen_pos.X + bbox.X < screenW) && (screen_pos.Y > bbox.Y) && (screen_pos.Y + bbox.Y < screenH))
  2.         meo_play.Update(gameTime);                               // update character animations if they're going to be visible
  3.  
  4.     // TEST IF PLAYER ATTACK HITS THIS MONSTER
  5.     // P L A Y E R   A T T A C K E D   M O N S T E R   T E S T --------------------------
  6.     if (invincible <= 0)
  7.     {
  8.         if (mode == Mode.run_away) mode = Mode.patrol;
  9.         const int radius = 32, rad2 = 64; // radius of spell, radius * 2
  10.         Rectangle bound_rec = new Rectangle((int)(pos.X + bbox.X), (int)(pos.Y + bbox.Y), (int)bbox.Width, (int)bbox.Height);
  11.         int i = 0;
  12.         while (i < player.spells.Count)
  13.         {
  14.             Vector2 spos = player.spells[i].emit_world_pos;
  15.             Rectangle spell_rec = new Rectangle((int)(spos.X - radius), (int)(spos.Y - radius), rad2, rad2);
  16.             if (spell_rec.Intersects(bound_rec)) MonsterDamaged(player.spell_power);
  17.             i++;
  18.         }
  19.     }
  20.     else { invincible--; mode = Mode.run_away; }
  21.     // ^^^ player attacked monster test ^^^ ----------------------------------------------
  22.  
  23. } // ^^^ update ^^^
IF monster is visible on screen, make some MeoPlayer calculations for the animation.
IF NOT invincible:
- IF was running away, switch to patrol mode (run away while invincible [could extend this to timer or distance based])
- get current bounding rectangle (adding in position to bbox)
- LOOP spells:
- - get current bounding rectangle of spell (based on an approximate radius)
- - IF bounding rectangles of monster and spell overlap, apply damage to monster based on spell strength
IF invincible(due to damage)... countdown the invincibility and run away


67) Need a method to determine world collisions ...
a) It will work similar to player's world collisions so we can borrow the concept and make a few adjustments. This is preferred over making a universal world-collision class just so there is freedom to make specific tweaks to special cases that may only apply to a player or only to a monster... for now they act almost the same.
Code Snippet
  1. //--------------------------------
  2. // W O R L D  C O L L I S I O N S (monster vs world)
  3. //--------------------------------
  4. bool  on_spikes;
  5. bool  last_grounded;
  6. float bottom;                   // character bottom from center
  7. public void WorldCollisions()
  8. {
  9.     // GET MONSTER'S SCREEN POSITION
  10.     meo_play.position = screen_pos = Conv.world_to_screen(pos);
  11.  
  12.     // get monster tile occupation:                                                 
  13.     loc.X = (int)pos.X / 64;
  14.     loc.Y = (int)pos.Y / 64;
  15.     int x = loc.X, y = loc.Y;
  16.  
  17.     // PREVENT ILLEGAL MEMORY ACCESS:
  18.     if ((x + 1 >= Map.TILES_WIDE) || (x - 1 < 0) || (y + 1 >= Map.TILES_HIGH) || (y - 1 < 0)) { pos = old_pos; loc = old_loc; }
  19.  
  20.     // GET MOVEMENT VECTOR:
  21.     Vector2 move = pos - old_pos;
  22.     // (! Assume velocity never exceeds tile size of 64 which is extremely fast [otherwise you need a sweep test for closest tile interactions] !)                                                  
  23.  
  24.     float BOUNCE_VEL = MAX_JUMP - 8f;                  // <-- how fast monster jumps up if it hits a spring platform
- on spikes - useful in more than 1 place - indicates if monster is currently on spikes
- last_grounded - if was grounded last time collisions were processed (determines if landing sound should be played)
- bottom - is set in get_monster_properties... (not made yet) - will be used to position monster at correct height when standing on something
- meo player's position and screen position(used here) are set to screen position based on world coord
- get map location from world coord ( /64 because tiles are 64x64) and set x,y for convenience
- clamp x,y to map
- get the movement vector (current position - old position)
- set a bouncer jump velocity (possible that MAX_JUMP is dynamic, otherwise this could be const too)

b) In much the same way player detected things above it and determined the collision response:
Code Snippet
  1. // MOVING UP
  2. if (move.Y < 0)
  3. {                
  4.     if (tiles[x, y - 1].is_solid)                  // WILL HIT SOLID (tile above)
  5.     {
  6.         if ((pos.Y + bbox.Y) < y * 64)             // top offset is distance from center of monster to top of it
  7.         {
  8.             if (tiles[x, y - 1].type == TileType.spring) { vel.Y = BOUNCE_VEL; bounceMgr.Add(x, y - 1); MonsterJump(true); }
  9.             vel.Y = 0; pos.Y = old_pos.Y;
  10.         }
  11.     }
  12.     if (tiles[x - 1, y - 1].is_solid)             // WILL HIT SOLID (up-left)
  13.     {
  14.         if ((pos.Y + bbox.Y) < y * 64)
  15.         {
  16.             if (tiles[x - 1, y - 1].type == TileType.spring) { vel.Y = BOUNCE_VEL; bounceMgr.Add(x - 1, y - 1); MonsterJump(true); }
  17.             if (pos.X < (x - 1) * 64 + 84) { vel.Y = 0; pos.Y = old_pos.Y; }
  18.         }
  19.     }
  20.     if (tiles[x + 1, y - 1].is_solid)             // WILL HIT SOLID (up right)
  21.     {
  22.         if ((pos.Y + bbox.Y) < y * 64)
  23.         {
  24.             if (tiles[x + 1, y - 1].type == TileType.spring) { vel.Y = BOUNCE_VEL; bounceMgr.Add(x + 1, y - 1); MonsterJump(true); }
  25.             if (pos.X > (x + 1) * 64 - 20) { vel.Y = 0; pos.Y = old_pos.Y; }
  26.         }
  27.     }
  28.     if (tiles[x, y].type == TileType.spring) { vel.Y = BOUNCE_VEL; bounceMgr.Add(x, y); MonsterJump(true); }
  29. }
(Note: the values 84 and -20 above seem to work ok still (like with player)... but you can tweak them as you see fit)

c) Keep in mind, that pos is the position of the center of the monster. We get half_width to get the distance from the center to the edge of the bbox for convenience:
Code Snippet
  1. int half_width = bbox.Width / 2;   
  2.  
  3. // MOVING RIGHT            
  4. if (move.X > 0)
  5. {                
  6.     if (tiles[x + 1, y].is_solid)                     // WILL HIT SOLID (to right)
  7.     {
  8.         if ((pos.X + half_width) > (x + 1) * 64)
  9.         {
  10.             if (on_spikes) { if (tiles[x + 1, y].type != TileType.spikes) { pos.X = old_pos.X; x = old_loc.X; } }
  11.             else { pos.X = old_pos.X; x = old_loc.X; }
  12.         }
  13.     }
  14.     else if (tiles[x + 1, y - 1].is_solid)            // HIT SOLID (up-right)
  15.     {
  16.         if ((vel.Y < 0) || (pos.Y < y * 64 + 40))     // hardcoded as +40 (may need to change for other character sizes)
  17.             if ((pos.X + half_width) > (x + 1) * 64) { pos.X = old_pos.X; x = old_loc.X; }
  18.     }
  19. }
  20. // MOVING LEFT
  21. else if (move.X < 0)
  22. {
  23.     if (tiles[x - 1, y].is_solid)                     // WILL HIT SOLID (to left)
  24.     {
  25.         if ((pos.X - half_width) < x * 64)
  26.         {
  27.             if (on_spikes) { if (tiles[x - 1, y].type != TileType.spikes) { pos.X = old_pos.X; x = old_loc.X; } }
  28.             else { pos.X = old_pos.X; x = old_loc.X; }
  29.         }
  30.     }
  31.     else if (tiles[x - 1, y - 1].is_solid)            // HIT SOLID (up-left)
  32.     {
  33.         if ((vel.Y < 0) || (pos.Y < y * 64 + 40))
  34.             if ((pos.X - half_width) < x * 64) { pos.X = old_pos.X; x = old_loc.X; }
  35.     }
  36. }
IF moving right,
- IF Solid blocking:
- - IF edge of monster passes edge of tile:
- - - IF on spikes IF obstruction is NOT spikes(some other solid): set monster back to old position/location
- - - ELSE just set monster back to old position/location
- IF Solid may be blocking monster's head:
- - IF jumping or at least 24 pixels up from bottom of that tile:
- - - IF edge of monster passes edge of tile: Set monster to old position/location
IF moving left,
[Same idea but for tiles to the left and pos.X - half_width (edge of monster on left side) ]

d) Set on_spikes and grounded as false until determined.
IF moving down:
Code Snippet
  1. // MOVING DOWN
  2. on_spikes = false;
  3. grounded = false;                                         // reset grounded as false until we know:
  4. if (move.Y > 0)
  5. {
  6.     float offy = 0;                                    // <-possible offset for collision region (ie: if spikes: farther down)                
  7.     if (tiles[x, y].stand_on)                             // WILL HIT SOLID (can stand-on)
  8.     {
  9.         if (tiles[x, y].spikes) offy = 28; else offy = 0; // hit spikes 28 pixels down from tile top
  10.         if ((pos.Y + bottom) > y * 64 + offy)
  11.         {
  12.             pos.Y = y * 64 - bottom + offy; vel.Y = 0;    // set position so on top of tile                        
  13.             grounded = true;                              // grounded   
  14.             if (offy == 28) on_spikes = true;
  15.             if (tiles[x, y].type == TileType.spring) { vel.Y = BOUNCE_VEL; bounceMgr.Add(x, y); MonsterJump(true); }//spring
  16.         }
  17.     }
  18.     if (tiles[x, y + 1].stand_on)                             // WILL HIT SOLID (can stand-on)
  19.     {
  20.         if (tiles[x, y + 1].spikes) offy = 28; else offy = 0; // hit spikes 28 pixels down from tile top
  21.         if ((pos.Y + bottom) > (y + 1) * 64 + offy)
  22.         {
  23.             pos.Y = (y + 1) * 64 - bottom + offy; vel.Y = 0;  // set position so on top of tile                        
  24.             grounded = true;                                  // grounded   
  25.             if (offy == 28) on_spikes = true;
  26.             if (tiles[x, y + 1].type == TileType.spring) { vel.Y = BOUNCE_VEL; bounceMgr.Add(x, y + 1); MonsterJump(true); } // spring
  27.         }
  28.     }
  29.     if (tiles[x - 1, y].stand_on)                             // WILL HIT SOLID (can stand-on)
  30.     {
  31.         if (tiles[x - 1, y].spikes) offy = 28; else offy = 0; // hit spikes 28 pixels down from tile top
  32.         if ((pos.Y + bottom) > y * 64 + offy)
  33.         {
  34.             if (pos.X < (x - 1) * 64 + 84)
  35.             {
  36.                 pos.Y = y * 64 - bottom + offy; vel.Y = 0;    // set position so on top of tile                        
  37.                 grounded = true;                              // grounded   
  38.                 if (offy == 28) on_spikes = true;
  39.             }
  40.         }
  41.     }

IF IN a tile that can be stood on:
- IF spikes: set standing offset as 28 pixels from top of tile
- IF bottom of monster passes top of tile (@ standing position):
- - position monster so feet touch where it should stand
- - set as grounded (touching the ground)
- - IF on spikes... set on_spikes = true
- - IF it's a spring/bouncer, bounce monster up and trigger bouncer animation (play bounce sound = true)
IF tile BELOW can be stood on:
(same idea except applied to tile below)
IF tile to the left can be stood on:
Same idea except:
IF monster position's < 20 pixels right of right edge of the tile: (using + 84 as edge tolerance - you can adjust this for your chars)
- set monster standing above the tile (and set on_spikes if so)
Continuing:

Code Snippet
  1. if (tiles[x - 1, y + 1].stand_on)                         // down-left (can stand on)
  2. {
  3.     if (tiles[x - 1, y + 1].spikes) offy = 28; else offy = 0;
  4.     if ((pos.Y + bottom) > (y + 1) * 64 + offy)
  5.     {
  6.         if (pos.X < (x - 1) * 64 + 84)                    // allow monster to stand at the very edge (hard coded as 84 for now)
  7.         {
  8.             pos.Y = (y + 1) * 64 - bottom + offy; vel.Y = 0;
  9.             grounded = true;
  10.             if (offy == 28) on_spikes = true;
  11.             if (tiles[x - 1, y + 1].type == TileType.spring) { vel.Y = BOUNCE_VEL; bounceMgr.Add(x - 1, y + 1); MonsterJump(true); }                            
  12.         }
  13.     }
  14. }
  15. if (tiles[x + 1, y + 1].stand_on)                      // down-right (can stand on)
  16. {
  17.     if (tiles[x + 1, y + 1].spikes) offy = 28; else offy = 0;
  18.     if ((pos.Y + bottom) > (y + 1) * 64 + offy)
  19.     {
  20.         if (pos.X > (x + 1) * 64 - 20)                 // allow monster to stand at the very edge
  21.         {
  22.             pos.Y = (y + 1) * 64 - bottom + offy; vel.Y = 0;
  23.             grounded = true;
  24.             if (offy == 28) on_spikes = true;
  25.             if (tiles[x + 1, y + 1].type == TileType.spring) { vel.Y = BOUNCE_VEL; bounceMgr.Add(x + 1, y + 1); MonsterJump(true); }                            
  26.         }
  27.     }
  28. }
  29. if (tiles[x + 1, y].stand_on)                             // WILL HIT SOLID (can stand-on)
  30. {
  31.     if (tiles[x + 1, y].spikes) offy = 28; else offy = 0;  // hit spikes 28 pixels down from tile top
  32.     if ((pos.Y + bottom) > y * 64 + offy)
  33.     {
  34.         if (pos.X > (x + 1) * 64 - 20)
  35.         {
  36.             pos.Y = y * 64 - bottom + offy; vel.Y = 0;    // set position so on top of tile                        
  37.             grounded = true;                              // grounded   
  38.             if (offy == 28) on_spikes = true;
  39.         }
  40.     }
  41. }
IF tile to the left and below can be stood on:
Same idea applied to tile to left and below... (thus +84 for edge tolerance [ie: so monster can stand on the edge of a tile])
Same idea for tile to the right and below [x+1, y+1]
(used + 20 [thus 64+20 = 84] for left and now -20 for tile to the right)
so: IF monster is to the right of the tile - 20, then should be able to stand on it (now determine if spikes or bouncer or just solid)
Same idea checking tile to the right.

e) If monster is on spikes, monster can take damage (but if monster was a metal robot, you might want to check type and make so it doesn't take damage in that case)
Code Snippet
  1.         // HIT SPIKES:
  2.         if (on_spikes)
  3.         {
  4.             if (move.Y > 0)
  5.             {
  6.                 MonsterDamaged(1);
  7.             }
  8.         }
  9.         if ((on_spikes == false) && (motion != Act.jump) && (grounded) && (!last_grounded)) Sound.land.Play(pan.Y, 0.8f, pan.X);
  10.     }//^^^ move.Y>0 ^^^  
  11.     last_grounded = grounded;
  12.  
  13. } // ^^^ world collisions ^^^

IF on spikes (and not jumping), monster takes damage.
IF NOT on spikes and NOT jumping and on the ground (but wasn't last loop), play a landing sound (since monster just hit the ground)
Remember what last grounded was for next time.

68) Draw the monster using MeoPlayer:
Code Snippet
  1. //--------
  2. // D R A W
  3. //--------
  4. public void Draw()
  5. {            
  6.     // IS VISIBLE? (if no return)
  7.     if ((screen_pos.X < bbox.X) || (screen_pos.X + bbox.X > screenW) || (screen_pos.Y < bbox.Y) || (screen_pos.Y + bbox.Y > screenH)) return;
  8.    
  9.     // DRAW MONSTER   
  10.     Color col = Color.White;
  11.     if (invincible > 0) {
  12.         int n = invincible % 10;
  13.         if (n > 5) col = new Color(255, 155, 155, 255); // flashing if invincible                
  14.     }       
  15.     meo_play.Draw(col);            
  16. }

IF off screen, return
IF invincible, make red for 5 loops and normal for 5 loops and repeat this.Red, White, Red, white, Red, White...(white = no change)
Draw the monster.

69) Draw colliders will be used for debugging purposes... this way you can see where the attack regions are and where the monster's body can be collided with by players (ie: if they attack)
Before we do this, we'll need to add some extensions to SpriteBatch for drawing bounding rectangles.

a) Right-Click on your c# project_name and Add>>Class: "Extensions"

b) In Extensions.cs, make the class static... ie: static class Extensions
i) Let's start by adding a DrawLine function. GPU's do everything by drawing triangles using a texture source, so we'll need a texture source that it can extract a pixel from. We'll need a point1 and point2 to draw the line between and a line color:
Code Snippet
  1. public static void DrawLine(this SpriteBatch spriteBatch, Texture2D tex, Rectangle pixel, Vector2 begin, Vector2 end, Color color, int thick = 1)
  2. {            
  3.     Vector2 delta = end - begin;
  4.     float rot = (float)Math.Atan2(delta.Y, delta.X);
  5.     if (pixel.Width > 0) { pixel.Width = 1; pixel.Height = 1; }
  6.     spriteBatch.Draw(tex, begin, pixel, color, rot, new Vector2(0, 0.5f), new Vector2(delta.Length(), thick), SpriteEffects.None, 0);
  7. }
So, delta is the vector(relative to 0,0)
Using rule, tan(angle) = Y/X for a triangle... get the angle of the vector using Atan(Y/X)
Draws a pixel from tex at begin point, scaled as: (X-scale: length, Y-scale: 1), and rotated [around (0,0.5)] to angle of rot
So basically it makes a pixel stretched out to the line length and rotates it based on the angle the line would be.

ii) DrawRectLines will be useful for drawing bounding boxes. In this case, since each line is straight verticle or horizontal, we can just modify the destination rectangle (taking from pixel as the source rect):
Code Snippet
  1. public static void DrawRectLines(this SpriteBatch spriteBatch, Texture2D tex, Rectangle pixel, Rectangle r, Color color, int thick = 1) {
  2.     spriteBatch.Draw(tex, new Rectangle(r.X, r.Y, r.Width, thick), pixel, color);
  3.     spriteBatch.Draw(tex, new Rectangle(r.X + r.Width, r.Y, thick, r.Height), pixel, color);
  4.     spriteBatch.Draw(tex, new Rectangle(r.X, r.Y + r.Height, r.Width, thick), pixel, color);
  5.     spriteBatch.Draw(tex, new Rectangle(r.X, r.Y, thick, r.Height), pixel, color);
  6. }

c) Going back into Monster.cs:
Let's show the monster's body bounding-box (where it is vulnerable), and it's attack bounding-box (where it's attacking bounding box is [what parts can effect players])
Code Snippet
  1. //-------------------------
  2. // D R A W  C O L L I D E R (s)  (for debugging collision detections)
  3. //-------------------------
  4. public void DrawCollider(SpriteBatch spriteBatch, Texture2D tex, Rectangle s_box)
  5. {
  6.     // IS VISIBLE? (if no return)
  7.     if ((screen_pos.X < bbox.X) || (screen_pos.X + bbox.X > screenW) || (screen_pos.Y < bbox.Y) || (screen_pos.Y + bbox.Y > screenH)) return;
  8.     
  9.     float left = screen_pos.X + bbox.X;
  10.     float top = screen_pos.Y + bbox.Y;
  11.     
  12.     Rectangle pixel_source = new Rectangle(896,0,1,1);
  13.     
  14.     // SHOW MONSTER'S VULNERABLE BBOX:
  15.     spriteBatch.DrawRectLines(tex, pixel_source, new Rectangle((int)left,(int)top, bbox.Width, bbox.Height), Color.White);            
  16.    
  17.     // SHOW PLAYER'S VULNERABLE BBOX:
  18.     spriteBatch.DrawRectLines(tex, pixel_source, s_box, Color.White);
  19.  
  20.     // SHOW MONSTER'S ATTACKING BBOX:
  21.     top = screen_pos.Y + attack_box.Y;
  22.     if (!flip) { left = screen_pos.X + attack_box.X; } else { left = screen_pos.X - attack_box.X - attack_box.Width; }
  23.     //Console.WriteLine("coord: " + left + "," + top + "," + attack_box.Width + "," + attack_box.Height);
  24.     spriteBatch.DrawRectLines(tex, pixel_source, new Rectangle((int)left, (int)top, attack_box.Width, attack_box.Height), Color.Red);  
  25. }
NOTE: It's not properly optimal to draw the player's bounding box every single time we draw the moster rects, but since this is only for debugging, we don't actually care... so I just put it in here (should put it in a DrawColliders for player class)
s_box - player's bounding box (where player can get hit)
-IF monster's screen position(+bbox.X) is off screen to the left OR it's off screen to the right OR off the top of screen or off bottom: then: return...
For simplicity get the left and top edges.
-Set a pixel source (this rectangle must be somewhere on the supplied texture map that is white)
Using the supplied texture and source, draw the monster's bounding box (where it can be hit)
-Show player's bounding box (s_box) [should probably move this to a DrawColliders in the player class to avoid redundancy]
-Get the left and top edges of the attack-bounding-box.
(If facing left, then left becomes X - attack_box.X - attack_box.Width (where attack_box.X is a negative offset) )
-Draw Attack bounding box (area of monster that when attacking, can hurt player)

d) Monster properties are subject to rescale also so let's make a helper to adjust scale based on resolution:
Code Snippet
  1. // RESCALE RECT (helper for Get Monster Properties)
  2. Rectangle Rescale_Rect(int X, int Y, int Width, int Height)
  3. {
  4.     Vector2 corner    = new Vector2(X,Y) * rescale;
  5.     Vector2 dimension = new Vector2(Width, Height) * rescale;
  6.     return new Rectangle((int)corner.X, (int)corner.Y, (int)dimension.X, (int)dimension.Y);
  7. }
(X,Y are offsets from a center so rescaling will work fine)
Just returns the rescaled version of a rectangle.

e) We'll now want to make that get_monster_properties method I kept mentioning. It will be used to determing bounding boxes for each monster, where the bottom of the monster is (adjust until feet seem to touch at the right height), and which animation frames the attack should effect the player (ie: charging an attack shouldn't hurt the player).
Code Snippet
  1. // G E T  M O N S T E R  P R O P E R T I E S  (and assign a bounding box)
  2. string get_monster_properties(MonsterType mType)
  3. {
  4.     string sheet_name = "mouster";                           
  5.     switch (mType)
  6.     {
  7.         case MonsterType.Mouster:
  8.             sheet_name = "mouster";                    
  9.             bbox       = Rescale_Rect(-150,-115,300,205);    // top-left from center, width, height (box around monster for collision detection [sizes using info in original image before dissection])
  10.             attack_box = Rescale_Rect(30, -130, 120, 180);   // top_left from center, width, height (for attack region that can hit player)                                                                                
  11.             bottom     = bbox.Y + bbox.Height-11;            // bottom = custom value (-52) to make sure it looks right when standing on something
  12.             attack_key = 1;                                  // which animation key does the attack begin to be dangerous
  13.             life       = 3;
  14.             break;
  15.         case MonsterType.Hellcat:
  16.             sheet_name = "hellcat";                    
  17.             bbox       = Rescale_Rect( -80, -135, 160, 170);
  18.             attack_box = Rescale_Rect(-190, -100, 380, 110);
  19.             bottom     = bbox.Y + bbox.Height-11;             // bottom = custom value (-48) so it looks right when standing on something
  20.             attack_key = 11;
  21.             life       = 2;
  22.             break;
  23.     }            
  24.     return sheet_name;
  25. }
Based on the type of Monster:
- need the associated sheet_name (used with animation lookups to get the correct index)
bbox = bounding box (vulnerable part of monster), attack_box = (dangerous part of monster)
- x,y = offset from center (center to top-left corner of bbox),   width,height = size of bbox
bottom = what part of the monster's feet impact the ground
attack_key = first keyframe where attack starts to hurt player (if in range)
life = life bar
sheet_name is returned so the constructor can set it (based on type)

f) We'll also want the customize_monster (which actually customizes animation properties):
Code Snippet
  1.     // C U S T O M I Z E  M O N S T E R
  2.     void customize_monster() {
  3.         int index;            
  4.         jump_index = meo_play.GetIndex("jump", false); // FALSE = don't show errors for this one since if no jump is available that will be ok
  5.         ouch_index = meo_play.GetIndex("ouch");
  6.         switch (type)
  7.         {
  8.             case MonsterType.Mouster:
  9.                 meo_play.SetAnimation("idle", flip); motion = Act.idle;
  10.                 index = meo_play.GetIndex("idle");   meo.anim[index].speed = 0.8f;  // slow down the idle a bit    
  11.                 walk_index = meo_play.GetIndex("run");                    
  12.                 break;
  13.             case MonsterType.Hellcat:
  14.                 meo_play.SetAnimation("idle", flip); motion = Act.idle;
  15.                 index = meo_play.GetIndex("idle");   meo.anim[index].speed = 0.8f;  // "
  16.                 walk_index = meo_play.GetIndex("walk");                    
  17.                 break;
  18.         }            
  19.     }
  20. }
- get jump_index (if available ... false means no error if not available) ... if there isn't one, we already coded it to not play an animation jump if jump_index<1 and instead just provide a jump velocity (and sound).
- get damaged index
- start by setting the idle animation (and motion as idle) ... some monsters you may want to start with something different.
- modify the default speed of the idle to be a bit slower
- one monster uses "run" as the walk (has no walk) and the other one obviously just uses "walk" for that (has no run).


----------------------
70) In solution explorer, right-click on the Monsters folder, and Add>>Class: "MonsterSys"
a) In MonsterSys.cs, we'll need to have it store the monster animations, all the different active monsters, monster sound effects... and it will need access to the player(s), the map, and the explosion manager.
Code Snippet
  1. // Loads monster sets, add / delete monsters / calls to draw/update monsters
  2. // In this version, Monsters are added using the map editor to indicate where monsters start... but you could make a off-screen spawn method too
  3. class MonsterSys
  4. {
  5.     const int   MAX_MONSTERS = 30;                    // most number of monsters allowed in a level                
  6.     
  7.     // FOR MEOMOTION ANIMATION
  8.     MeoMotion           meo;                        // MeoMotion class to contain monsters
  9.     public Monster[]    monsters;                   // unique monsters (which have their own MeoPlayers)         
  10.     int                 num_monst;        
  11.     Vector2             rescale;                    // adjusts character size and motion based on resolution
  12.  
  13.     // OTHER
  14.     QuadBatch           quadBatch;                  // reference to quadBatch (provided to player in Monster class)
  15.     Map                 mp;                         // reference to map   
  16.     Player              player;                     // reference to player
  17.     ExplodeSys          explode;                    // reference to explodeSys (explosion animations)
  18.  
  19.     // SOUND
  20.     static public SoundEffect monster_attack1;  // Mouseter attack sound
  21.     static public SoundEffect monster_attack2;  // HellCat " "
  22.     static public SoundEffect monster_ouch1;    // Mouster ouch
  23.     static public SoundEffect monster_ouch2;    // HellCat ouch

meo - contains each monster and their animations
monsters[] - holds all the monsters (for some number of monsters: num_monst)
quadBatch - used by MeoPlayer of monster to draw distortable quads

b)Construct will need to provide access for loading content, refer to: quadBatch, map, player, and explosion systems:

Code Snippet
  1. // C O N S T R U C T
  2. public MonsterSys(ContentManager Content, QuadBatch QBatch, Map map, Player playr, ExplodeSys ex)
  3. {
  4.     meo       = new MeoMotion(Content, QBatch);
  5.     monsters  = new Monster[MAX_MONSTERS];
  6.     num_monst = 0;
  7.     
  8.     quadBatch = QBatch;
  9.     mp        = map;
  10.     player    = playr;
  11.     explode   = ex;
  12. }
We'll allocate memory for up to 30 monsters for this example (could use lists too if you prefer)

c) Based on the level (or area, we can have it load different monster files and sound effects)
In this example, we only have level 1... so 1 case:
Code Snippet
  1. //--------
  2. // L O A D
  3. //--------
  4. public void Load(int lev, ContentManager Content)
  5. {
  6.     rescale = Game1.rescale * 0.5f; // make 50% smaller than original monster sizes
  7.     switch (lev) {
  8.         case 1:                     
  9.             meo.Load_TXT("Monsters1", rescale); // load characters/animations (.TXT must be with exe file [ie: BIN - Windows - debug/release] )
  10.             monster_attack1 = Content.Load<SoundEffect>("Sound/MousterHit");
  11.             monster_ouch1   = Content.Load<SoundEffect>("Sound/MousterOuch");
  12.             monster_attack2 = Content.Load<SoundEffect>("Sound/ChainsawHit");
  13.             monster_ouch2   = Content.Load<SoundEffect>("Sound/CatOuch");
  14.             break;
  15.     }                                    
  16. }
Note that the Monsters1.txt in this example is a combined project export containing data for both monster types (which it also placed the images for onto a combo sheet as a png which we put in Content) [see source code for sound effects and images]
The txt is placed in the same file as the exe (either debug or release folder until project is ready for final release)

d) We'll need a method to add new monsters to the scene:
Code Snippet
  1. //---------------------
  2. // A D D  M O N S T E R
  3. //---------------------
  4. public void AddMonster(MonsterType type, Vector2 Pos)
  5. {
  6.     // Check existing monsters (if any) and try to change any dead ones to the new one (if any)             
  7.     int i = 0;
  8.     while(i<num_monst) {
  9.         if (monsters[i].dead) {
  10.             monsters[i].ChangeMonster(Pos, type);                           // make alive and change to desired monster
  11.             return;                                                         // monster is added so return
  12.         }
  13.         i++;
  14.     }
  15.     // No existing monsters that are dead to replace - see if we can add a new one:
  16.     if (num_monst >= MAX_MONSTERS) return;                                    // maximum - can't add more to this level - return
  17.     monsters[num_monst] = new Monster(Pos, meo, quadBatch, type, mp, player, explode); // CREATE MONSTER
  18.     num_monst++;
  19. }

- takes type and starting position
- checks for any dead monsters and as soon as it finds one it puts the new monster in this slot (at a new position and type) [return]
- max sure not to exceed the maximum allocated number of monsters
- no monsters were dead so create a new monster supplying access to everything it will need
- increment
(Like I said you could use lists for this and it may even be preferable if your monster quantity will change a lot)

e) Deleting monsters [ FOR EDITOR ONLY - in play mode just set monster as dead = true ]

Code Snippet
  1. //------------
  2. // D E L E T E ( editor delete of monster by map-tile coordinate [ in the game mode you simply set it to dead=true ] )
  3. //------------
  4. public void Delete()
  5. {
  6.     int i = 0;
  7.     while (i < num_monst) {
  8.         if (monsters[i].loc == mp.loc) {        // if a monster occupies the editor tile location:
  9.             int a = i;
  10.             while (a < num_monst-1) {           // shift the list back 1 and delete the last entry since it becomes a duplicate of the 2nd-last    
  11.                 if (monsters[a + 1] != null) monsters[a] = monsters[a + 1]; else break;
  12.                 a++;
  13.             }
  14.             monsters[a] = null; num_monst--;    // delete last entry since they were copy-shifted back 1
  15.         }
  16.         i++;
  17.     }
  18. }
LOOP:
IF monster location is map location:
- - starting at current index LOOP:
- - - IF next monster(a+1) in list is valid, move it back to current index (a)
- - remove monster at very end of list (since all valid ones have moved back 1 overwriting the one we wanted to delete)

f) For Updating monsters we'll want to optionally update the AI and update collision detections:
Code Snippet
  1. //------------
  2. // U P D A T E  ( AI )
  3. //------------
  4. public void Update(GameTime gameTime, bool updateAI = true)
  5. {
  6.     int i = 0;
  7.     while (i < num_monst)
  8.     {
  9.         if (monsters[i].dead) { i++; continue; }
  10.         monsters[i].Update(gameTime, updateAI);                     // update all monster motions in world
  11.         monsters[i].WorldCollisions();                              // check for world collisions                
  12.         i++;
  13.     }
  14. }

LOOP
- skip the dead ones
- update the monster's choices and animations
- do world collision detections and responses (CDCR)

g) To draw all the monsters:

Code Snippet
  1. //--------
  2. // D R A W  
  3. //--------
  4. public void Draw()
  5. {
  6.     quadBatch.Begin(meo.tex, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.None);
  7.     int i = 0;
  8.     while (i < num_monst)
  9.     {
  10.         if (!monsters[i].dead)  monsters[i].Draw();                   //  ( draw visible monsters )
  11.         i++;
  12.     }
  13.     quadBatch.End();
  14. }
- monsters are drawn with distortion animations, so set the quadbatch using the texture stored in the local meo.tex which contains all the monster part images. Premultiplied texture map property so use AlphaBlend. Basic smoothing so use LinearClamp.
LOOP:
- if monster's alive, draw it...
- end the quadbatch [done using meo.tex since all monsters are drawn now]

h) Last thing we'd like to be able to do is show all the monster colliders:
Code Snippet
  1.     //---------------------------
  2.     // D R A W  C O L L I D E R S (for debugging collision detections)
  3.     //---------------------------
  4.     public void DrawColliders(SpriteBatch spriteBatch, Texture2D tx)
  5.     {            
  6.         // get the bounding box in screen coordinates:
  7.         // top left & bottom right coords - used to visualize if enemy attack hits you (monsters.cs):            
  8.         Rectangle sbox  = Conv.bbox_world_to_screen(player.bbox);                        
  9.         
  10.         int i = 0;
  11.         while (i < num_monst)
  12.         {
  13.             if (!monsters[i].dead) monsters[i].DrawCollider(spriteBatch,tx,sbox);  //  ( draw collision boxes for debugging )
  14.             i++;
  15.         }         
  16.     }        
  17. }
- supply access to the spriteBatch and a texture which has a tiny white rectangle in it somewhere that can be used as a pixel source-rectangle for drawing lines with.
- get the player's bounding box to send (later to do this properly you may want to move this part into a new DrawPlayerColliders method to avoid redundant player-box drawing... since this is just for debugging monsters, it is not a concern)
LOOP
- if the monster is alive, draw it's bounding boxes (body region, attack region) [for now also draws player region]


>>> NEXT PAGE(5) <<<

>>> Back to Game Design <<<