Sgt. Conker We are "absolutely fine"

5Sep/100

Article: I Can Has Platformer? (Part 4)

by Casey Young

I Can Has Platformer Part 4 Final ImageWelcome back to the fourth part of my series on how to create a simple platformer game. In this article, we give our hero a purpose to live.

Please refer to my third article to grab the final project before continuing this part, you will need that code to edit along with to make things work. =D

From the last part, we gave our hero some motivation and made him animated. He is all hyped up and ready to go, but nothing to do. In this part we are going to give our hero some obsticles, and add some baddies for him to fend off! First things first, grab this sprite sheet of our hero's ruffian friend, The MuffinMan!

I Can Has Platformer Part 4, MuffinMan

Save this file under Content/Sprites, in a new directory called Enemies. While we are grabbing the new assets, grab a new sprite sheet of our hero, and override the previous one under Content/Sprites/Player.

I Can Has Platformer Part 4, Player

Now with the two sprite sheets saved and loaded into our project, let's start creating the Enemy class. Our enemy class will be very similar to the player class, but not as complex. Start off by creating a new class file named Enemy, and have it inherit our AnimatedSprite class. Let's start off with the public properties and private fields of the file.

using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace ScribblePlatformer
{
    class Enemy : AnimatedSprite
    {
        private SpriteEffects flip = SpriteEffects.None;
        private string currentAnim = "Walk";
        private const float MoveSpeed = 128.0f;
        private Rectangle localBounds;
        bool isAlive;
        bool isCompletelyDead;
        Level level;
        Vector2 position;
        Vector2 velocity;

        /// <summary>
        /// Is the enemy alive, or dead?
        /// </summary>
        public bool IsAlive
        {
            get { return isAlive; }
        }

        /// <summary>
        /// Is enemy completely dead, if so, flag for removal from level
        /// </summary>
        public bool IsCompletelyDead
        {
            get { return isCompletelyDead; }
        }

        /// <summary>
        /// What level are we on, used for collision and interaction
        /// with the level's entities.
        /// </summary>
        public Level Level
        {
            get { return level; }
        }

        /// <summary>
        /// What position the enemy is in the world
        /// </summary>
        public Vector2 Position
        {
            get { return position; }
            set { position = value; }
        }

        /// <summary>
        /// The enemy's movement velocity
        /// </summary>
        public Vector2 Velocity
        {
            get { return velocity; }
            set { velocity = value; }
        }

        /// <summary>
        /// Gets a rectangle which bounds this enemy in world space.
        /// </summary>
        public Rectangle BoundingRectangle
        {
            get
            {
                int left = (int)Math.Round(Position.X - Origin.X) + localBounds.X;
                int top = (int)Math.Round(Position.Y - Origin.Y) + localBounds.Y;

                return new Rectangle(left, top, localBounds.Width, localBounds.Height);
            }
        }

When we want to create a new Enemy, we want to tell our system what type of enemy it is. This article doesn't use this feature, yet, but I am implementing it now so you can add new enemies with just a simple modifications.

		/// <summary>
        /// Constructs a new Enemy.
        /// </summary>
        public Enemy(Level _level, Vector2 _position, string _enemy)
        {
            level = _level;
            position = _position;
            isAlive = true;
            isCompletelyDead = false;

            LoadContent(_enemy);
        }

In our LoadContent function for our Enemy class, this is where it can load a different sprite sheet for a different enemy, right now we just have MuffinMan, so it is the default sprite sheet to load. We are also giving our MuffinMan 2 sets of animations, Walk and Dead. Walk is the default because he likes to pace back and forth. When our hero kills the MuffinMan, he will run through a death animation.

        /// <summary>
        /// Loads the player sprite sheet.
        /// </summary>
        public void LoadContent(string _enemy)
        {
            string enemyString = string.Empty;
            switch (_enemy)
            {
                case "e":
                    enemyString = "Sprites/Enemies/muffinman";
                    break;
                default:
                    enemyString = "Sprites/Enemies/muffinman";
                    break;
            }
            // Load animated textures.
            Texture2D tex = Level.Content.Load<Texture2D>(enemyString);
            if(!SpriteTextures.Contains(tex))
                SpriteTextures.Add(tex);

            Animation anim = new Animation();
            anim.LoadAnimation("Walk", 0, new List<int>
			{
				0,
				1
			}, 4, true);
            SpriteAnimations.Add("Walk", anim);

            anim = new Animation();
            anim.LoadAnimation("Dead", 0, new List<int>
			{
				2,
				3,
                4,
                5
			}, 36, false);
            anim.AnimationCallBack(DeadAnimEnd);
            SpriteAnimations.Add("Dead", anim);

            // Calculate bounds within texture size.
            // subtract 10 from width and height to remove a 5px buffer
            // around the enemy.
            int width = FrameWidth - 10;
            int left = (FrameWidth - width) / 2;
            int height = FrameHeight - 10;
            int top = FrameHeight - height;
            localBounds = new Rectangle(left, top, width, height);

            SpriteAnimations[currentAnim].ResetPlay();
        }

Notice that the Dead animation sequence has a callback function tied to it. This is when we know the death animation is done, and tell the level to remove the enemy from itself. So let's implement that function, which just sets a flag saying the enemy is completely killed off. Also, let's creat a function for our level to call telling the enemy that he just got squished.

        /// <summary>
        /// Called when the player has squished us.
        /// </summary>
        public void OnKilled()
        {
            isAlive = false;
            SpriteAnimations[currentAnim].Stop();
            currentAnim = "Dead";
            SpriteAnimations[currentAnim].ResetPlay();
        }

        /// <summary>
        /// Called when Dead animation is done, letting the level know to remove.
        /// </summary>
        public void DeadAnimEnd()
        {
            isCompletelyDead = true;
        }

The last two things left for our Enemy class is the simple ai and telling it to draw. Our enemy, the MuffinMan just likes to walk back and forth, and dosn't like to fall off ledges. So in our update function, let's have him move back and forth, and keep him on ledges.

        /// <summary>
        /// Paces back and forth along a platform.
        /// </summary>
        public void Update(GameTime _gameTime)
        {
            SpriteAnimations[currentAnim].Update(_gameTime);
            if (isAlive)
            {
                float elapsed = (float) _gameTime.ElapsedGameTime.TotalSeconds;

                // Calculate tile position based on the side we are walking towards.
                int direction = 1;
                if (Velocity.X < 0)
                    direction = -1;
                float posX = Position.X + localBounds.Width/2*direction;
                int tileX = (int) Math.Floor(posX/Tile.Width) - direction;
                int tileY = (int) Math.Floor(Position.Y/Tile.Height);

                // If we are about to run into a wall or off a cliff, reverse direction.
                if (Level.GetCollision(tileX + direction, tileY - 1) == TileCollision.Impassable ||
                    Level.GetCollision(tileX + direction, tileY + 1) == TileCollision.Passable ||
                    Level.GetCollision(tileX + direction, tileY) == TileCollision.Impassable)
                {
                    direction *= -1;
                }

                // Move in the current direction.
                velocity = new Vector2(direction*MoveSpeed*elapsed, 0.0f);
                position = position + velocity;
            }
        }

The Draw function is very similar to our Player's Draw function. We want to draw our enemy with the correct frame of animation and flip him if he is moving the opposite direction.

        /// <summary>
        /// Draws the animated enemy.
        /// </summary>
        public void Draw(GameTime _gameTime, SpriteBatch _spriteBatch)
        {
            Rectangle source = GetFrameRectangle(SpriteAnimations[currentAnim].FrameToDraw);

            // Flip the sprite to face the way it is moving.
            if (Velocity.X > 0)
                flip = SpriteEffects.FlipHorizontally;
            else if (Velocity.X < 0)
                flip = SpriteEffects.None;

            // Draw the enemy.
            _spriteBatch.Draw(SpriteTextures[0], position, source, Color.White, 0.0f, Origin, 1.0f, flip, 0.0f);
        }
[csharp]

And that is our Enemy class.  Pretty short and easy, no?  Now let's edit our Player class, and get him ready for his battles with the MuffinMan.  We want to implement a new property to tell the level that he is completely dead and have him respawn.

[csharp]
        /// <summary>
        /// Is player completely dead?
        /// </summary>
        public bool IsCompletelyDead
        {
            get { return isCompletelyDead; }
        }
        bool isCompletelyDead;

In the Player Class's LoadContent, we want to load up a new animation sequence, his death sequence called Dead. We don't want it to loop, just like the Enemy's animation, and have it call a function when done. Add the new animation sequence after the Jump sequence.

            anim = new Animation();
            anim.LoadAnimation("Dead", 0, new List<int>
			{
				13,
				14,
				15
			}, 6, false);
            anim.AnimationCallBack(DeadAnimEnd);
            SpriteAnimations.Add("Dead", anim);

The Player Class's Reset function needs to reset our new property we added, so set it back to false. Our Reset should look like the following:

		/// <summary>
		/// Resets the player to life.
		/// </summary>
		/// <param name="_position">The position to come to life at.</param>
		public void Reset(Vector2 _position)
		{
			Position = _position;
			Velocity = Vector2.Zero;
			isAlive = true;
		    isCompletelyDead = false;
			SpriteAnimations[currentAnim].Stop();
			currentAnim = "Idle";
			SpriteAnimations[currentAnim].ResetPlay();
		}

We need to make sure to only handle input when our hero is alive, so we need to modify the Update function to only call the GetInput function when the hero is alive. The updated Update function should look similar to this:

		/// <summary>
		/// Handles input, performs physics, and animates the player sprite.
		/// </summary>
		public void Update(GameTime _gameTime)
		{
            if (isAlive)
            {
                GetInput();
            }
		    ApplyPhysics(_gameTime);

			SpriteAnimations[currentAnim].Update(_gameTime);

			// Clear input.
			movement = 0.0f;
			isJumping = false;
		}

Now to handle our hero's death, we need to implement the Dead animation's callback and also create a function to get called if our hero runs into the MuffinMan.

        /// <summary>
        /// Called when the player has been killed.
        /// </summary>
        public void OnKilled()
        {
            isAlive = false;
            SpriteAnimations[currentAnim].Stop();
            currentAnim = "Dead";
            SpriteAnimations[currentAnim].ResetPlay();

        }

        /// <summary>
        /// Called when the player has been killed.
        /// </summary>
        public void DeadAnimEnd()
        {
            isCompletelyDead = true;
        }

That should be all for the player. There is just a few things left to do. We need to tell the Level to load up an enemy, and implement the collision detections. Let's load up the Level1.txt file and make some spawn points for the MuffinMan. We are going to use the letter 'e' for the MuffinMan.

.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.
.,.,.,.,.,.,.,.,.,.,.,e,.,.,.,.,.,.,.,.
.,.,.,.,.,.,.,.,.,r,b,g,o,y,.,.,.,.,.,.
.,b,.,e,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.
.,r,y,o,G,B,R,.,.,.,.,.,.,.,.,.,.,.,.,.
.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.
.,.,.,.,.,.,.,.,.,O,Y,R,.,.,.,.,.,.,.,.
.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.,.
.,.,.,.,R,B,G,.,.,.,g,.,.,.,B,G,O,.,.,.
.,+,.,.,.,.,.,.,.,.,b,.,.,.,.,.,.,.,e,.
r,r,b,b,g,g,o,o,y,y,r,b,g,o,y,r,b,g,o,y

If we try to run this right now, our Level will complain that it doesn't know what to do with the value of 'e'. Let's load up the Level class and make it aware of our new friend here. First off, let's add two lists of enemies one for active, and another for removing from active.

        private List<Enemy> enemies = new List<Enemy>();
        private List<Enemy> deadEnemies = new List<Enemy>();

In the LoadTile function, we need to add a case for 'e', and have it load up an Enemy. So add this case to the switch.

	// enemies spawns
	case 'e':
		return LoadEnemyTile(_x, _y, "e");

Now with that added, we need to implement this new function. It creates an enemy, adds it to the enemy list, and creates a blank tile on the map.

        /// <summary>
        /// Instantiates an enemy and puts him in the level.
        /// </summary>
        private Tile LoadEnemyTile(int _x, int _y, string _enemy)
        {
            Vector2 position = new Vector2((_x * 64) + 48, (_y * 64) + 20);
            enemies.Add(new Enemy(this, position, _enemy));

            return new Tile(String.Empty, 0, TileCollision.Passable);
        }

In the Update function, we want to check if we need to respawn our hero, and also we need to update our Enemies. The updated Update function should look something like this:

		/// <summary>
		/// Updates all objects in the world, performs collision between them,
		/// and handles the time limit with scoring.
		/// </summary>
		public void Update(GameTime _gameTime)
		{
            Player.Update(_gameTime);

            if (Player.IsCompletelyDead)
                Player.Reset(start);

            UpdateEnemies(_gameTime);
		}

The new Update function has a new call in it we need to implement. Inside this new function, we are going to tell each enemy to update, and check if he has been squished by our hero, or has achieved victory in killing our hero. If there are any enemies that have finished thier death animation, we want to remove them from the main update list.

        /// <summary>
        /// Animates each enemy and allow them to kill the player.
        /// </summary>
        private void UpdateEnemies(GameTime _gameTime)
        {
            foreach (Enemy enemy in enemies)
            {
                enemy.Update(_gameTime);

                if (player.IsAlive && enemy.IsAlive)
                {

                    // Touching an enemy instantly kills the player
                    if (enemy.BoundingRectangle.Intersects(Player.BoundingRectangle))
                    {
                        if (enemy.BoundingRectangle.Y > Player.BoundingRectangle.Y)
                        {
                            OnEnemyKilled(enemy);
                        }
                        else
                        {
                            OnPlayerKilled();
                        }
                    }
                }
                else
                {
                    if(enemy.IsCompletelyDead)
                        deadEnemies.Add(enemy);
                }
            }

            if (deadEnemies.Count > 0)
            {
                foreach (Enemy deadEnemy in deadEnemies)
                {
                    enemies.Remove(deadEnemy);
                }
                deadEnemies.Clear();
            }
        }

We are almost done here, we now need to implent the new functions that we are calling in UpdateEnemies, namely OnEnemyKilled and OnPlayerKilled.

        /// <summary>
        /// Called when the player is killed.
        /// </summary>
        private void OnPlayerKilled()
        {
            Player.OnKilled();
        }

        /// <summary>
        /// Called when an enemy is killed.
        /// </summary>
        private void OnEnemyKilled(Enemy _enemy)
        {
            _enemy.OnKilled();
        }

The last thing to do is tell our active enemies to draw themselves. To do this, we need to modify the Draw function and call Draw for each enemy in the list.

		public void Draw(GameTime _gameTime, SpriteBatch _spriteBatch)
		{
			DrawTiles(_spriteBatch);
            player.Draw(_gameTime, _spriteBatch);

            foreach (Enemy enemy in enemies)
                enemy.Draw(_gameTime, _spriteBatch);
		}

That should do it. Our hero now has some Muffin butt to kick! Build the project and run it. Our hero should now be able to jump on top of the MuffinMan to squish him, or die running into him.

I Can Has Platformer Part 4 Final Image

In the next article, we are going to add some collectibles that our hero will try to collect. To get a hold of the project as it should be by the end of this part, click here.

Stay tuned for the 5th part of the "I Can Has Platformer" series...

Part 1

Part 2

Part 3

Part 5

About ElementCy

Private Slaq0r