Adding Ladders to PSK
Ladders seem to be pretty common for the Platform Starter Kit (PSK) so I figured I'd post my implementation of how I got it working.
First of all I want to thank whoever I got the code from on the XNA forums in the first place (I've combined snippets from a few places in the XNA forums).
This is quite a long post so I tried splitting it up into 3 areas (split up by a horizontal line) with the name of the class the changes take place in. By the end of this you should be able to climb up/down ladders, have an animation for both and have a new key to use when building your levels so you can place ladders throughout.
Here's a video showing off what it should look like after this tutorial:
Player.cs
First off, we'll add these to the top of our Player.cs class:
1 2 3 4 5 6 7 8 9 10 11 12 | private const int LadderAlignment = 12; private bool isClimbing; public bool IsClimbing { get { return isClimbing; } } private bool wasClimbing; //This used to be private float movement; private Vector2 movement; |
Now in the Update(...) method there's a block of code that looks like this:
1 2 3 4 5 6 7 8 9 10 11 | if (IsAlive && IsOnGround) { if (Math.Abs(Velocity.X) - 0.02f > 0) { sprite.PlayAnimation(runAnimation); } else { sprite.PlayAnimation(idleAnimation); } } |
And we want to change Update(...) to look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | public void Update(GameTime gameTime) { GetInput(); ApplyPhysics(gameTime); //LADDER if (IsAlive) { //This if statement deals with running/idling if (isOnGround) { //If Velocity.X is > 0 in any direction, play runAnimation if (Math.Abs(Velocity.X) - 0.02f > 0) sprite.PlayAnimation(runAnimation); //Otherwise, sit still (idleAnimation) else sprite.PlayAnimation(idleAnimation); } //This if statement deals with ladder climbing else if (isClimbing) { //If he's moving down play ladderDownAnimation if (Velocity.Y - 0.02f > 0) sprite.PlayAnimation(ladderDownAnimation); //If he's moving up play ladderUpAnimation else if (Velocity.Y - 0.02f < 0) sprite.PlayAnimation(ladderUpAnimation); //Otherwise, just stand on the ladder (idleAnimation) else sprite.PlayAnimation(idleAnimation); } } //Reset our variables every frame movement = Vector2.Zero; wasClimbing = isClimbing; isClimbing = false; // Clear input. isJumping = false; } |
If you try to build at this point you'll get errors for ladderUpAnimation and ladderDownAnimation because we haven't added those yet but that's not until later. You'll also get a bunch of errors because we change movement from a float to a Vector2 so we'll address those issues now.
Our GetInput(...) should now look something like this. I also added another method below called IsAlignedToLadder() so don't forget to include that. I just commented out the previous movement lines so you can see what's changed.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | 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; movement.X = gamePadState.ThumbSticks.Left.X * MoveStickScale; movement.Y = gamePadState.ThumbSticks.Left.Y * MoveStickScale; // Ignore small movements to prevent running in place. //if (Math.Abs(movement) < 0.5f) // movement = 0.0f; if (Math.Abs(movement.X) < 0.5f) movement.X = 0.0f; if (Math.Abs(movement.Y) < 0.5f) movement.Y = 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; movement.X = -1.0f; } else if (gamePadState.IsButtonDown(Buttons.DPadRight) || keyboardState.IsKeyDown(Keys.Right) || keyboardState.IsKeyDown(Keys.D)) { //movement = 1.0f; movement.X = 1.0f; } //LADDER if (gamePadState.IsButtonDown(Buttons.DPadUp) || //gamePadState.ThumbSticks.Left.Y > 0.75 || keyboardState.IsKeyDown(Keys.Up) || keyboardState.IsKeyDown(Keys.W)) { isClimbing = false; if (IsAlignedToLadder()) { //We need to check the tile behind the player, //not what he is standing on if (level.GetTileCollisionBehindPlayer(position) == TileCollision.Ladder) { isClimbing = true; isJumping = false; isOnGround = false; movement.Y = -1.0f; } } } else if (gamePadState.IsButtonDown(Buttons.DPadDown) || //gamePadState.ThumbSticks.Left.Y < -0.75 || keyboardState.IsKeyDown(Keys.Down) || keyboardState.IsKeyDown(Keys.S)) { isClimbing = false; if (IsAlignedToLadder()) { // Check the tile the player is standing on if (level.GetTileCollisionBelowPlayer(level.Player.Position) == TileCollision.Ladder) { isClimbing = true; isJumping = false; isOnGround = false; movement.Y = 2.0f; } } } // Check if the player wants to jump. isJumping = gamePadState.IsButtonDown(JumpButton) || keyboardState.IsKeyDown(Keys.Space) || keyboardState.IsKeyDown(Keys.Up) || keyboardState.IsKeyDown(Keys.W); } //LADDER private bool IsAlignedToLadder() { int playerOffset = ((int)position.X % Tile.Width) - Tile.Center; if (Math.Abs(playerOffset) <= LadderAlignment && level.GetTileCollisionBelowPlayer(new Vector2( level.Player.position.X, level.Player.position.Y + 1)) == TileCollision.Ladder || level.GetTileCollisionBelowPlayer(new Vector2( level.Player.position.X, level.Player.position.Y - 1)) == TileCollision.Ladder) { // Align the player with the middle of the tile position.X -= playerOffset; return true; } else { return false; } } |
Now we need to make some changes inside of the ApplyPhysics(...) method to accommodate for more movement changes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | //Make sure to comment out these 2 lines: //velocity.X += movement * MoveAcceleration * elapsed; //velocity.Y = MathHelper.Clamp( // velocity.Y + GravityAcceleration * elapsed, // -MaxFallSpeed, // MaxFallSpeed); //Then add this underneath //LADDER if (!isClimbing) { if (wasClimbing) velocity.Y = 0; else velocity.Y = MathHelper.Clamp( velocity.Y + GravityAcceleration * elapsed, -MaxFallSpeed, MaxFallSpeed); } else { velocity.Y = movement.Y * MoveAcceleration * elapsed; } velocity.X += movement.X * MoveAcceleration * elapsed; |
Now we move onto HandleCollisions(...). Find this if statement:
1 2 | if (previousBottom <= tileBounds.Top) isOnGround = true; |
And replace it with this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | //LADDER // If we crossed the top of a tile, we are on the ground. if (previousBottom <= tileBounds.Top) { if (collision == TileCollision.Ladder) { if (!isClimbing && !isJumping) { // When walking over a ladder isOnGround = true; } } else { isOnGround = true; isClimbing = false; isJumping = false; } } |
A little further down you'll see this else if statement:
1 2 3 4 5 6 7 8 | else if (collision == TileCollision.Impassable) // Ignore platforms. { // Resolve the collision along the X axis. Position = new Vector2(Position.X + depth.X, Position.Y); // Perform further collisions with the new bounds. bounds = BoundingRectangle; } |
AFTER that you'll need to ADD this one:
1 2 3 4 5 6 7 8 9 10 11 12 | //LADDER else if (collision == TileCollision.Ladder && !isClimbing) { // When walking in front of a ladder, falling off a ladder // but not climbing // Resolve the collision along the Y axis. Position = new Vector2(Position.X, Position.Y); // Perform further collisions with the new bounds. bounds = BoundingRectangle; } |
Now we'll add those missing animations. At the top our Player class add these calls:
1 2 | private Animation ladderUpAnimation; private Animation ladderDownAnimation; |
And in LoadContent(...) add these below the existing ones. Notice how I'm using the celebrate animation for the ladder up animation and the death animation for the ladder down animation. You'll obviously want to change these for your game! I used them so I didn't have to make this post even longer by adding specific ladder up/down animations.
If you don't have a celebrate or death animation then temporarily replace it with a animation that you do have.
1 2 3 4 | ladderUpAnimation = new Animation( Level.Content.Load<Texture2D>("Sprites/Player/Celebrate"), 0.1f, false); ladderDownAnimation = new Animation( Level.Content.Load<Texture2D>("Sprites/Player/Die"), 0.1f, false); |
Tile.cs
We only have 2 small additions in here. In enum TileCollision, add the following. If you have others, just increase the highest number by 1. The number after it doesn't really matter but you have to make sure it doesn't overlap another one.
1 | Ladder = 3, |
Then add this line under one of the other const calls:
1 | public const int Center = Width / 2; |
And that's it for our Tile class! Level.cs is next.
Level.cs
Under our GetCollision(...) method add these 2 methods:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | public TileCollision GetTileCollisionBehindPlayer(Vector2 playerPosition) { int x = (int)playerPosition.X / Tile.Width; int y = (int)(playerPosition.Y - 1) / Tile.Height; // Prevent escaping past the level ends. if (x == Width) return TileCollision.Impassable; // Allow jumping past the level top and falling through the bottom. if (y == Height) return TileCollision.Passable; return tiles[x, y].Collision; } public TileCollision GetTileCollisionBelowPlayer(Vector2 playerPosition) { int x = (int)playerPosition.X / Tile.Width; int y = (int)(playerPosition.Y) / Tile.Height; // Prevent escaping past the level ends. if (x == Width) return TileCollision.Impassable; // Allow jumping past the level top and falling through the bottom. if (y == Height) return TileCollision.Passable; return tiles[x, y].Collision; } |
Now you'll want to add this next to the other case statements in LoadTile(...).
1 2 3 | //Ladder case 'H': return LoadTile("BlockA0", TileCollision.Ladder); |
And don't forget to add some 'H' characters to your level so you can try it out! I used 'BlockA0' as my ladder texture so you'll want to add your own ladder texture to the Tiles folder and replace "BlockA0" with name of your new ladder texture.
Everything should work at this point so build it and try it out!
April 25th, 2012 - 08:18
Thank you very helpful and interactive