Sgt. Conker We are "absolutely fine"

31Mar/102

Article: I Can Has Platformer? (Part 3)

by Casey Young

I Can Has Platformer Part 3 Final ImageWelcome back to the third part of my series on how to create a simple platformer game. In this article I take you on a little journey in making our hero animated.

Please refer to my second 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 added our hero in the game and had him stroll about the level. There was one thing missing though, he didn't really look like he was motivated at all. In this part we are going to spice up our hero a bit and make him animated! First off, you will need the new sprite sheet of our hero, which will replace our old image file.

Player Sprite Sheet

Save our new image file for our hero where the old one was, replacing the old file. If you don't remember, it is under the folder Sprites/Player in the Content project of our ScribblePlatformer game.

Also, save the platform image below. There is a few tweaks I made to them, mainly moving them up in the sheet to level off with the block images. Replace it with your older copy in the Tiles folder in the Content project of our ScribblePlatformer game.

Platforms

One last modification before we get into the animation, I have modified the level to make it more exciting. Modify the level1.txt file under the Levels folder of the Content project to the following:

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

Now let's get to the nitty gritty, Animations! Let's create a new class file and name it Animation. In this class, we will house a single animation for our AnimatedSprite class that we will be making after this. The animation class will hold everything we need to do a single animation, such as it's name, the frames to draw, and the order to draw. This class will also have a delegate to a function to call to notify when the animation is done for say 1shot animations, then wanting to switch back to another animation, or to chain animations together.

Let's start off by declaring our basic members and some of our public properties.

		/// <summary>
		/// The name of the animation.
		/// </summary>
		public string AnimationName { get; private set; }

		/// <summary>
		/// The id of the texture in the animatedSprite texture list
		/// </summary>
		public int TextureId { get; private set; }

		/// <summary>
		/// The current frame to draw.
		/// </summary>
		public int FrameToDraw { get { return frameList[currentFrame]; } }

		/// <summary>
		/// This is a callback function to call when the animation is done.
		/// </summary>
		public delegate void AnimationEndCallback();

		private AnimationEndCallback callBack;

		private List<int> frameList;
		private float totalElapsed;
		private float timePerFrame;
		private int currentFrame;
		private int frameCount;
		private int framesPerSec;
		private bool isPlaying;
		private bool loopAnimation;

To declare a new animation, we want to give the animation a name, let it know which texture from a list to draw from, a list of frames, how fast the animation will be, and does the animation loop.

		public void LoadAnimation(string _animationName, int _textureId, List<int> _frameList,
			int _framesPerSec, bool _loop)
		{
			AnimationName = _animationName;
			TextureId = _textureId;
			frameList = _frameList;
			frameCount = frameList.Count;
			framesPerSec = _framesPerSec;
			timePerFrame = (float)1 / framesPerSec;
			currentFrame = 0;
			totalElapsed = 0;
			loopAnimation = _loop;
		}

Now it knows what the animation is, we need to tell it to start and stop, and even reset if we want to. To do that, we want to have a few functions to do just that.

		public void Play()
		{
			isPlaying = true;
		}

		public void ResetPlay()
		{
			currentFrame = 0;
			totalElapsed = 0;
			isPlaying = true;
		}

		public void Stop()
		{
			isPlaying = false;
		}

We are almost done with the animation class. We need a way to set the delegate function so when the animation is done, it gets called.

		public void AnimationCallBack(AnimationEndCallback _callback)
		{
			callBack =  _callback;
		}

The final function we need for our animated class is an update function. This is the main guts, that gets called every update. This will handle switching frames, and calling the delegate if it is done and needs to call it.

		public void Update(GameTime _gameTime)
		{
			if(!isPlaying) return;

			totalElapsed += (float) _gameTime.ElapsedGameTime.TotalSeconds;

			if (totalElapsed <= timePerFrame) return;

			currentFrame++;
			currentFrame = currentFrame%frameCount;

			totalElapsed -= timePerFrame;

			if (!loopAnimation && currentFrame == 0)
			{
				isPlaying = false;
				if (callBack != null)
					callBack();
			}
		}

Now we can move on over to our AnimatedSprite class. A few items will be ripped out of our player class, because our player will be inheriting from AnimatedSprite. We only want to hold the basic items that make up an animated sprite, that would be things such as the spritesheets and the animations. We could go with just 1 texture2d for the spritesheet, but I have built the animated sprite to have multiple spritesheets just in case we needed more than what 1 sheet could hold. There is no logic in the AnimatedSprite class, so this should be pretty small file. Create a new class file, and call it AnimatedSprite, and enter the following into it.

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

namespace ScribblePlatformer
{
	class AnimatedSprite
	{
		// This holds our texture to the sprite.
		public List<Texture2D> SpriteTextures { get; set; }

		public int FrameWidth;
		public int FrameHeight;
		private int framesPerRow;

		public Dictionary<string, Animation> SpriteAnimations;

		public AnimatedSprite()
		{
			FrameWidth = 96;
			FrameHeight = 96;
			framesPerRow = 4;
			SpriteTextures = new List<Texture2D>();
			SpriteAnimations = new Dictionary<string, Animation>();
		}

		/// <summary>
		/// Gets the Frame's origin
		/// </summary>
		public Vector2 Origin
		{
			get { return new Vector2(FrameWidth / 2.0f, FrameHeight / 2.0f); }
		}

		public Rectangle GetFrameRectangle(int _frameNumber)
		{
			return new Rectangle(
			  (_frameNumber % framesPerRow) * FrameWidth,
			  (_frameNumber / framesPerRow) * FrameHeight,
			  FrameWidth,
			  FrameHeight
			);

		}
	}
}

Now we need to modify our Player class to make it an animated object. Like I said earlier, we will make Player inherit AnimatedSprite. As for the private members, we will be adding a few more and removing a few. For starters, we can get rid of the Texture2D that used to hold the players texture. This is replaced with the List of Texture2D's in AnimatedSprite. We need to have a variable to hold what the current animation is, and we need to know which way the hero is facing.

		private string currentAnim = "Idle";

		private SpriteEffects flip = SpriteEffects.None;

Next we need to modify the LoadContent function to load up the spritesheet, and add a few animations for our hero. For this simple project, our hero will be able to walk, jump, and idle.

		/// <summary>
		/// Loads the player sprite sheet and sounds.
		/// </summary>
		public void LoadContent()
		{
			// Load animated textures.
			SpriteTextures.Add(Level.Content.Load<Texture2D>("Sprites/Player/player"));

			Animation anim = new Animation();
			anim.LoadAnimation("Idle", 0, new List<int>
			{
				0,
				11,
				0,
				12
			}, 7, true);
			SpriteAnimations.Add("Idle", anim);

			anim = new Animation();
			anim.LoadAnimation("Walking", 0, new List<int>
			{
				0,
				1,
				2,
				3,
				4,
				5,
				6
			}, 7, true);
			SpriteAnimations.Add("Walking", anim);

			anim = new Animation();
			anim.LoadAnimation("Jump", 0, new List<int>
			{
				7,
				8,
				9,
				10,
				9,
				8,
				7
			}, 20, false);
			anim.AnimationCallBack(JumpAnimEnd);
			SpriteAnimations.Add("Jump", anim);

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

Notice on the jump animation, we are giving the animation a Callback, so it will change animations when it finishes the jump animation. So we need to declare that now in our Player class.

		public void JumpAnimEnd()
		{
			currentAnim = "Idle";
			SpriteAnimations[currentAnim].Play();
		}

When the player resets, we want to restart stop the current animation, and start the starting animation, which in this case is idle.

		/// <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;
			SpriteAnimations[currentAnim].Stop();
			currentAnim = "Idle";
			SpriteAnimations[currentAnim].ResetPlay();
		}

To make sure the hero gets animated, we need to call the current animation's update routine. We want to do this in the update function of our hero.

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

			ApplyPhysics(_gameTime);

			SpriteAnimations[currentAnim].Update(_gameTime);

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

Inside our GetInput we want to change the animations depending on what the user has inputed.

		/// <summary>
		/// Gets player horizontal movement and jump commands from input.
		/// </summary>
		private void GetInput()
		{
			// Get input state.
			GamePadState gamePadState = GamePad.GetState(PlayerIndex.One);
			KeyboardState keyboardState = Keyboard.GetState();

			// Get analog horizontal movement.
			movement = gamePadState.ThumbSticks.Left.X * MoveStickScale;

			// Ignore small movements to prevent running in place.
			if (Math.Abs(movement) < 0.5f)
				movement = 0.0f;

			// If any digital horizontal movement input is found, override the analog movement.
			if (gamePadState.IsButtonDown(Buttons.DPadLeft) ||
				keyboardState.IsKeyDown(Keys.Left) ||
				keyboardState.IsKeyDown(Keys.A))
			{
				movement = -1.0f;
			}
			else if (gamePadState.IsButtonDown(Buttons.DPadRight) ||
					 keyboardState.IsKeyDown(Keys.Right) ||
					 keyboardState.IsKeyDown(Keys.D))
			{
				movement = 1.0f;
			}

			// Check if the player wants to jump.
			isJumping =
				gamePadState.IsButtonDown(JumpButton) ||
				keyboardState.IsKeyDown(Keys.Space) ||
				keyboardState.IsKeyDown(Keys.Up) ||
				keyboardState.IsKeyDown(Keys.W);

			if (isJumping && currentAnim != "Jump")
			{
				SpriteAnimations[currentAnim].Stop();
				currentAnim = "Jump";
				SpriteAnimations[currentAnim].ResetPlay();
			}

			if(movement != 0 && currentAnim != "Jump" && currentAnim != "Walking")
			{
				SpriteAnimations[currentAnim].Stop();
				currentAnim = "Walking";
				SpriteAnimations[currentAnim].ResetPlay();
			}else if(currentAnim != "Jump" && movement == 0 && currentAnim == "Walking")
			{
				SpriteAnimations[currentAnim].Stop();
				currentAnim = "Idle";
				SpriteAnimations[currentAnim].ResetPlay();
			}
		}

The last thing we want to change is how we draw the player now. We now need to take in consideration which way the player is moving, and what animation the player is on.

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

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

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

There is one last thing we need to modify, and that is in the Level class. Remember that I had you download a new Platform tilesheet? Well, we need to modify the Level's GetBound function to compensate for the new placements of the tiles. Before we were adjusting by 20 for the platforms, this is no longer necessary because the platforms are now level with the tops of the blocks.

		/// <summary>
		/// Gets the bounding rectangle of a tile in world space.
		/// </summary>
		public Rectangle GetBounds(int _x, int _y)
		{
			if (_x < 0 || _y < 0 || _x >= Width || _y >= Height)
				return new Rectangle(_x * Tile.Width, _y * Tile.Height, Tile.Width, Tile.Height);

			return new Rectangle(_x * Tile.Width, (_y * Tile.Height) + 5, Tile.Width, Tile.Height-5);
		}

Now build and run, and now our hero looks more motivated with some animation.

I Can Has Platformer Part 3 Final Image

In the next article, we are going to add some baddies to interact with the our hero. To get a hold of the project as it should be by the end of this part, click here.

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

Part 1

Part 2

Part 4

Part 5

About ElementCy

Private Slaq0r
Comments (2) Trackbacks (5)
  1. Love your tutorials man. Can’t wait for the next one!

    BTW, scribbleguy’s kinda cute.

  2. Great tutorial,
    When will the next come? Part 1 : January / Part 2 : February / Part 3 : March / Part 4 : …
    Can’t wait more !! :p


Leave a comment


*