Movable Platforms
I've got a few requests to do this one and I did a quick search and found that someone had already done it in the XNA forums. The post is here: http://forums.xna.com/forums/t/23694.aspx so all credit goes to him for getting it to work. I'll do a run-through on how to get it working in your game. It's pretty straight forward but I'll try to make it as easy as I can so you don't get lost. The code here is his and I'm just relaying it so I won't explain it as I go.
Here's a video showing what you should end up with.
First off you'll need a new class. Here's the new MovableTile class:
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace MovingPlatforms
{
class MovableTile
{
private Texture2D texture;
private Vector2 origin;
public Level Level
{
get { return level; }
}
Level level;
public Vector2 Position
{
get { return position; }
}
Vector2 position;
public Vector2 Velocity
{
get { return velocity; }
}
Vector2 velocity;
/// <summary>
/// Gets whether or not the player's feet are on the MovableTile.
/// </summary>
public bool PlayerIsOn { get; set; }
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);
}
}
public FaceDirection Direction
{
get { return direction; }
set { direction = value; }
}
FaceDirection direction = FaceDirection.Left;
public TileCollision Collision
{
get { return collision; }
set { collision = value; }
}
private TileCollision collision;
private Rectangle localBounds;
private float waitTime;
private const float MaxWaitTime = 0.1f;
private const float MoveSpeed = 120.0f;
public MovableTile(Level level, Vector2 position, TileCollision collision)
{
this.level = level;
this.position = position;
this.collision = collision;
LoadContent();
}
public void LoadContent()
{
texture = collision == TileCollision.Platform ?
Level.Content.Load<Texture2D>("Tiles/Platform") :
Level.Content.Load<Texture2D>("Tiles/BlockB0");
origin = new Vector2(texture.Width / 2.0f, texture.Height / 2.0f);
// Calculate bounds within texture size.
localBounds = new Rectangle(0, 0, texture.Width, texture.Height);
}
public void Update(GameTime gameTime)
{
float elapsed = (float)gameTime.ElapsedGameTime.TotalSeconds;
// Calculate tile position based on the side we are moving towards.
float posX = Position.X + localBounds.Width / 2 * (int)direction;
int tileX = (int)Math.Floor(posX / Tile.Width) - (int)direction;
int tileY = (int)Math.Floor(Position.Y / Tile.Height);
if (waitTime > 0)
{
// Wait for some amount of time.
waitTime = Math.Max(0.0f, waitTime - (float)gameTime.ElapsedGameTime.TotalSeconds);
if (waitTime <= 0.0f)
{
// Then turn around.
direction = (FaceDirection)(-(int)direction);
}
}
else
{
//If we're about to run into a wall that isn't a MovableTile move in other direction.
if (Level.GetCollision(tileX + (int)direction, tileY) == TileCollision.Impassable ||
Level.GetCollision(tileX + (int)direction, tileY) == TileCollision.Platform)
{
velocity = new Vector2(0.0f, 0.0f);
waitTime = MaxWaitTime;
}
else
{
// Move in the current direction.
velocity = new Vector2((int)direction * MoveSpeed * elapsed, 0.0f);
position = position + velocity;
}
}
if (level.movableTiles.Count > 0)
{
//If we're about to run into a MovableTile move in other direction.
foreach (var movableTile in level.movableTiles)
{
if (BoundingRectangle != movableTile.BoundingRectangle)
{
if (BoundingRectangle.Intersects(movableTile.BoundingRectangle))
{
direction = (FaceDirection)(-(int)direction);
velocity = new Vector2((int)direction * MoveSpeed * elapsed, 0.0f);
}
}
}
}
}
public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
{
spriteBatch.Draw(
texture,
Position,
null,
Color.White,
0.0f,
origin,
1.0f,
SpriteEffects.None,
0.0f);
}
}
}
After you add that to your project, and change the namespace to match yours, we can move onto Player.cs.
At the top of HandleCollisions(...) you should see something similar to the follow code:
// Get the player's bounding rectangle and find neighboring tiles. Rectangle bounds = BoundingRectangle; int leftTile = (int)Math.Floor((float)bounds.Left / Tile.Width); int rightTile = (int)Math.Ceiling(((float)bounds.Right / Tile.Width)) - 1; int topTile = (int)Math.Floor((float)bounds.Top / Tile.Height); int bottomTile = (int)Math.Ceiling(((float)bounds.Bottom / Tile.Height)) - 1; // Reset flag to search for ground collision. isOnGround = false;
After that code, add this:
//For each potentially colliding movable tile.
foreach (var movableTile in level.movableTiles)
{
// Reset flag to search for movable tile collision.
movableTile.PlayerIsOn = false;
//check to see if player is on tile.
if ((BoundingRectangle.Bottom == movableTile.BoundingRectangle.Top + 1) &&
(BoundingRectangle.Left >= movableTile.BoundingRectangle.Left - (BoundingRectangle.Width / 2) &&
BoundingRectangle.Right <= movableTile.BoundingRectangle.Right + (BoundingRectangle.Width / 2)))
{
movableTile.PlayerIsOn = true;
}
bounds = HandleCollision(bounds, movableTile.Collision, movableTile.BoundingRectangle);
}Underneath the "private void HandleCOllisions()" method, add this method:
private Rectangle HandleCollision(Rectangle bounds, TileCollision collision, Rectangle tileBounds)
{
Vector2 depth = RectangleExtensions.GetIntersectionDepth(bounds, tileBounds);
if (depth != Vector2.Zero)
{
float absDepthX = Math.Abs(depth.X);
float absDepthY = Math.Abs(depth.Y);
// Resolve the collision along the shallow axis.
if (absDepthY < absDepthX || collision == TileCollision.Platform)
{
// If we crossed the top of a tile, we are on the ground.
if (previousBottom <= tileBounds.Top)
isOnGround = true;
// Ignore platforms, unless we are on the ground.
if (collision == TileCollision.Impassable || IsOnGround)
{
// Resolve the collision along the Y axis.
Position = new Vector2(Position.X, Position.Y + depth.Y);
// Perform further collisions with the new bounds.
bounds = BoundingRectangle;
}
}
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;
}
}
return bounds;
}
Now onto Level.cs
Near the top add the following:
public List<MovableTile> movableTiles = new List<MovableTile>();
Let's add a case for our movable tiles so we can add them to our levels. In the "LoadTile(char tileType, int x, int y)" method let's add the following case:
// Moving platform - Horizontal
case 'M':
return LoadMovableTile(x, y, TileCollision.Platform);
We have a new method (LoadMovableTile(...)) so let's create that method below the LoadTile(...) method:
private Tile LoadMovableTile(int x, int y, TileCollision collision)
{
Point position = GetBounds(x, y).Center;
movableTiles.Add(new MovableTile(this, new Vector2(position.X, position.Y), collision));
return new Tile(null, TileCollision.Passable);
}
Since we have our own list of movable tiles that means we need to update/draw them ourselves instead of having our DrawTiles(...) method draw them. In the Update(...) method let's add the following after the UpdateEnemies(...) call:
UpdateMovableTiles(gameTime);
Since we don't have an UpdateMovableTiles(...) method yet, let's add it below the Update(...) method:
private void UpdateMovableTiles(GameTime gameTime)
{
foreach (MovableTile tile in movableTiles)
{
tile.Update(gameTime);
if (tile.PlayerIsOn)
{
//Make player move with tile if the player is on top of tile
player.Position += tile.Velocity;
}
}
}
Now that our movable tiles will update themselves, let's draw them now! In the Draw(...) method let's add the following call right after "DrawTiles(spriteBatch);"
foreach (MovableTile tile in movableTiles)
tile.Draw(gameTime, spriteBatch);
That's it for the additions, not you'll just have to open the level you want to add movable tiles to (0.txt for example) and add 'M's wherever you want movers at. This example has them moving horizontally only and once they hit another mover or a block they'll wait for the specified amount of time and move in the other direction. If you want to change the time they wait at each end, find this line in your MovableTile class:
private const float MaxWaitTime = 0.1f;
and change 0.1f to whatever you'd like!
I don't have any plans on modifying this to add vertical movers so that's something you'll have to tackle yourself. ![]()
December 16th, 2011 - 13:19
Most of it looks good, but when it says “In HandleCollisions(…) find this line:” I’m not sure what to do. There’s some more code in the block you included than in the starter kit, but when I add in the extra code I just get errors. Any ideas?
December 18th, 2011 - 22:42
It seems a bunch of this post actually got removed when I moved the site over a while ago. I updated the post so everything should work correctly now!
December 19th, 2011 - 13:00
Thanks this really helps =)