Seeking more help with tile collision in XNA C#

Started by
7 comments, last by greencrayon 16 years, 9 months ago
Hi folks. I am still trying to get my head around tile-based collision detection. This is what I have at the moment. The collisions are detected ok but some wierd stuff happens. I dont think the player's position is being reset properly after a collision has taken place. This makes it impossible for the plyer to (scooch?) around objects as the player has to move away from the obstacle in order to move on. Also sometimes the player zooms across the screen and breaks the array boundary. I hope someone can help me. p.s forgive me if i got the source tags wrong :) This is the array for the walkable / non-walkable tiles.


       int[,] iMapWalkable = new int[mapHeight, mapWidth] { 
                             { 1, 1, 1, 1, 1, 1, 1, 1}, 
                             { 1, 0, 0, 0, 0, 0, 0, 1},
                             { 1, 0, 0, 0, 0, 0, 0, 1},
                             { 1, 0, 0, 1, 0, 0, 0, 1},
                             { 1, 0, 0, 0, 0, 0, 0, 1},
                             { 1, 0, 0, 0, 0, 0, 0, 1},
                             { 1, 0, 0, 0, 0, 0, 0, 1},
                             { 1, 1, 1, 1, 1, 1, 1, 1},
        };


And this is my code for moving the player and detecting collisions.


            //Find the tile that the player is on, on the X axis
            PlayerTileX = (int)playerLoction.X / tileWidth;
            //Find the tile that the player is on, on the Y axis
            PlayerTileY = (int)playerLoction.Y / tileHeight;

            if (keyboardState.IsKeyDown(Keys.Up))
            {
                if (Map[PlayerTileY, PlayerTileX] == 0 && Map[PlayerTileY,    PlayerTileX + 1] == 0)
                {
                    playerLoction.Y -= playerVelocity.Y * (float)gameTime.ElapsedGameTime.TotalSeconds;
                }
                else
                {
                    playerLoction.Y = (PlayerTileY + 1) * tileHeight;
                }
            }
            if (keyboardState.IsKeyDown(Keys.Down))
            {
                if (Map[PlayerTileY + 1, PlayerTileX] == 0 && Map[PlayerTileY + 1, PlayerTileX + 1] == 0)
                {
                    playerLoction.Y += playerVelocity.Y * (float)gameTime.ElapsedGameTime.TotalSeconds;
                }
                else
                {
                    playerLoction.Y = PlayerTileY * tileHeight;
                }
            }
            if (keyboardState.IsKeyDown(Keys.Left))
            {
                if (Map[PlayerTileY, PlayerTileX] == 0 && Map[PlayerTileY + 1, PlayerTileX] == 0)
                {
                    playerLoction.X -= playerVelocity.X * (float)gameTime.ElapsedGameTime.TotalSeconds;
                }
                else
                {
                    playerLoction.X = (PlayerTileX + 1) * tileWidth;
                }
            }
            if (keyboardState.IsKeyDown(Keys.Right))
            {
                if (Map[PlayerTileY, PlayerTileX + 1] == 0 && Map[PlayerTileY + 1, PlayerTileX + 1] == 0)
                {
                    playerLoction.X += playerVelocity.X * (float)gameTime.ElapsedGameTime.TotalSeconds;
                }
                else
                {
                    playerLoction.X = (PlayerTileX) * tileWidth;
                }
            }


Advertisement
Hi again folks. I am still having problems getting to grips with tile based collisions. I am using C# and XNA. Below is my complete Game1 class for the program. I managed to get the walkable/non-walkable areas working just fine. But I want to make it possible for my character to slide around the edges of the map and around obstacles. Could anyone give me some pointers on how to change my code to make sliding possible? Any help greatly appreciated.

namespace OMGTILESAGAIN{    /// <summary>    /// This is the main type for your game    /// </summary>    public class Game1 : Microsoft.Xna.Framework.Game    {        GraphicsDeviceManager graphics;        ContentManager content;        SpriteBatch spriteBatch;        Texture2D[] tiles = new Texture2D[2];        Texture2D playerTexture;        Vector2 playerLocation = new Vector2(32, 32);        Vector2 playerVelocity = new Vector2(50, 50);        KeyboardState keyboardState;        const int mapHeight = 8;        const int mapWidth = 8;        int tileWidth = 32;        int tileHeight = 32;        // tiles the player is touching        int PlayerTileX;        int PlayerTileY;        // map to be drawn        int[,] Map = new int[mapHeight, mapWidth] {                              { 1, 1, 1, 1, 1, 1, 1, 1},                              { 1, 0, 0, 0, 0, 0, 0, 1},                             { 1, 0, 0, 0, 0, 0, 0, 1},                             { 1, 0, 0, 0, 1, 0, 0, 1},                             { 1, 0, 0, 0, 0, 0, 0, 1},                             { 1, 0, 0, 0, 0, 0, 0, 1},                             { 1, 0, 0, 0, 0, 0, 0, 1},                             { 1, 1, 1, 1, 1, 1, 1, 1},        };        // Walkable/ non-walkable areas        int[,] Walkable = new int[mapHeight, mapWidth] {                              { 1, 1, 1, 1, 1, 1, 1, 1},                              { 1, 0, 0, 0, 0, 0, 0, 1},                             { 1, 0, 0, 0, 0, 0, 0, 1},                             { 1, 0, 0, 0, 1, 0, 0, 1},                             { 1, 0, 0, 0, 0, 0, 0, 1},                             { 1, 0, 0, 0, 0, 0, 0, 1},                             { 1, 0, 0, 0, 0, 0, 0, 1},                             { 1, 1, 1, 1, 1, 1, 1, 1},        };                public Game1()        {            graphics = new GraphicsDeviceManager(this);            content = new ContentManager(Services);        }        /// <summary>        /// Allows the game to perform any initialization it needs to before starting to run.        /// This is where it can query for any required services and load any non-graphic        /// related content.  Calling base.Initialize will enumerate through any components        /// and initialize them as well.        /// </summary>        protected override void Initialize()        {            // TODO: Add your initialization logic here            spriteBatch = new SpriteBatch(graphics.GraphicsDevice);            base.Initialize();        }        /// <summary>        /// Load your graphics content.  If loadAllContent is true, you should        /// load content from both ResourceManagementMode pools.  Otherwise, just        /// load ResourceManagementMode.Manual content.        /// </summary>        /// <param name="loadAllContent">Which type of content to load.</param>        protected override void LoadGraphicsContent(bool loadAllContent)        {            if (loadAllContent)            {                // TODO: Load any ResourceManagementMode.Automatic content                tiles[0] = content.Load<Texture2D>("whiteTile");                tiles[1] = content.Load<Texture2D>("blackTile");                playerTexture = content.Load<Texture2D>("redTile");            }            // TODO: Load any ResourceManagementMode.Manual content        }        /// <summary>        /// Unload your graphics content.  If unloadAllContent is true, you should        /// unload content from both ResourceManagementMode pools.  Otherwise, just        /// unload ResourceManagementMode.Manual content.  Manual content will get        /// Disposed by the GraphicsDevice during a Reset.        /// </summary>        /// <param name="unloadAllContent">Which type of content to unload.</param>        protected override void UnloadGraphicsContent(bool unloadAllContent)        {            if (unloadAllContent)            {                // TODO: Unload any ResourceManagementMode.Automatic content                content.Unload();            }            // TODO: Unload any ResourceManagementMode.Manual content        }        /// <summary>        /// Allows the game to run logic such as updating the world,        /// checking for collisions, gathering input and playing audio.        /// </summary>        /// <param name="gameTime">Provides a snapshot of timing values.</param>        protected override void Update(GameTime gameTime)        {            // Allows the game to exit            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)                this.Exit();            // TODO: Add your update logic here            //Find the tile that the player is on, on the X axis            PlayerTileX = (int)playerLocation.X / tileWidth;            //Find the tile that the player is on, on the Y axis            PlayerTileY = (int)playerLocation.Y / tileHeight;            keyboardState = Keyboard.GetState();            //move the player            if (keyboardState.IsKeyDown(Keys.Up))            {                if (Walkable[PlayerTileY, PlayerTileX] == 0 && Map[PlayerTileY, PlayerTileX + 1] == 0)                {                    playerLocation.Y -= playerVelocity.Y * (float)gameTime.ElapsedGameTime.TotalSeconds;                }            }            if (keyboardState.IsKeyDown(Keys.Down))            {                if (Walkable[PlayerTileY + 1, PlayerTileX] == 0 && Map[PlayerTileY + 1, PlayerTileX + 1] == 0)                {                    playerLocation.Y += playerVelocity.Y * (float)gameTime.ElapsedGameTime.TotalSeconds;                }            }            if (keyboardState.IsKeyDown(Keys.Left))            {                if (Walkable[PlayerTileY, PlayerTileX] == 0 && Map[PlayerTileY + 1, PlayerTileX] == 0)                {                    playerLocation.X -= playerVelocity.X * (float)gameTime.ElapsedGameTime.TotalSeconds;                }            }            if (keyboardState.IsKeyDown(Keys.Right))            {                if (Walkable[PlayerTileY, PlayerTileX + 1] == 0 && Map[PlayerTileY + 1, PlayerTileX + 1] == 0)                {                    playerLocation.X += playerVelocity.X * (float)gameTime.ElapsedGameTime.TotalSeconds;                }            }            base.Update(gameTime);        }                /// <summary>        /// This is called when the game should draw itself.        /// </summary>        /// <param name="gameTime">Provides a snapshot of timing values.</param>        protected override void Draw(GameTime gameTime)        {            graphics.GraphicsDevice.Clear(Color.CornflowerBlue);            // TODO: Add your drawing code here            spriteBatch.Begin();            // Draw the map            for (int y = 0; y < mapHeight; y++)            {                for (int x = 0; x < mapWidth; x++)                {                    spriteBatch.Draw(tiles[Map[y, x]], new Vector2(x * tileWidth, y * tileHeight), Color.White);                }            }            // Draw the player.            spriteBatch.Draw(playerTexture, playerLocation, Color.White);            spriteBatch.End();            base.Draw(gameTime);        }    }}
Check your movement code.

e.g
if (keyboardState.IsKeyDown(Keys.Up))            {                if (Walkable[PlayerTileY, PlayerTileX] == 0 && Map[PlayerTileY, PlayerTileX + 1] == 0)                {                    playerLocation.Y -= playerVelocity.Y * (float)gameTime.ElapsedGameTime.TotalSeconds;                }            }

Are you sure you are checking the tile you want to move to? Double check your Xs and Ys. Also make sure you check all the other parts.
Im not sure. But just for now, change all these:
playerLocation.Y -= playerVelocity.Y *(float)gameTime.ElapsedGameTime.TotalSeconds;


To:

playerLocation.Y = playerLocation.Y + 2;


Or the equivelent then tell us the results. I think the problem is in this line.
Game Development Tutorials, a few good game development tutorials located on my site.
Thanks for the replies guys. I tried your suggestion Butterman but the program behaves in the same way. I think my problem is that when the character sprite stops moving upon hitting a 1 tile, it stops with its corners over the tile. This makes it impossible to move left or right after hitting a tile above it.

But if I try to reset the location of the player sprite after a collision like this:

if (keyboardState.IsKeyDown(Keys.Up))            {                if (Walkable[PlayerTileY, PlayerTileX] == 0 && Walkable[PlayerTileY, PlayerTileX + 1] == 0)                {                    playerLocation.Y = playerLocation.Y - 2;                }                else                    playerLocation.Y = (PlayerTileY + 1) * tileHeight;            }


The player sprite is not always set outside the unwalkable tile. I have no idea why this is the case.
Don't confuse tile-indices with coordinates

Your player should be able to walk around freely, that is, not on a per-tile base, right? I mean, if tiles are 32x32 pixels, your player shouldn't be restricted to move in 32 pixel increments, is that correct?

If so, then you're approaching this from the wrong side. You're converting your player location into a tile coordinate, and then you're checking if the next tile is passable. See what's going wrong? Because you're only considering the player's position, you're treating him as a dot, and since you always check the next tile, you're treating his movement as if it always moves into the next tile, regardless of whether he really ends up in that tile or not.


Axis Aligned Bounding Boxes

Instead, it's better to use boxes for your player and your tiles - axis-aligned bounding boxes, or AABB's in short, are easy to use and they do the job nicely in most cases. So, you check your player box against the solid tile boxes, and if it intersects one, you should determine on which side it intersects least, which then tells you to which side you can push out your players box.

This gives multiple options:
  • You can save the scene as a collection of impassable boxes - you'll need less boxes, since you can combine neighbouring tiles into one box. This means you don't need a grid anymore, as you can use any size of boxes you want, anywhere.
  • You can stick to the grid array you're using and write code that checks which tiles the player currently occupies (depending on his size, he can occupy multiple tiles, so it's a little more involved than converting his position into an array index). After that, you check if the player occupies (intersects) impassable tiles, and if so, you should determine how far he intersects them, and push him back accordingly.


I think the first solution is the best here - although it's different from your current approach, it's easier to implement once you understand what it's all about, and it's a lot more flexible: after you've written your AABB-AABB collision check, you can use AABB's for just about anything, including enemies, shots, pickups, etc.


Notes

A note about your iMapWalkable array: you should write get and set functions for accessing these values, instead of directly accessing the array. The reason is that, if it's so easy to get outside the playable field, you should do bounds checking, and you probably want to return 'impassable' (1) if you're looking outside the array boundaries.

Another note: your player movement is heavily tied to your input handling. Have you thought about writing a Player class, that has functions like MoveLeft(), MoveRight(), etc?

As for tile-based collision... the solution I proposed isn't really a tile-based solution, but since you're not doing tile-based movement, I think that doesn't really matter anyway.


Other

@Butterman: that line isn't a source of trouble - it just corrects movement with the frametime, which is, in fact, recommendable. EDIT: Unless, of course, he's using the total time passed since the start of the game.

[Edited by - Captain P on July 26, 2007 5:29:07 PM]
Create-ivity - a game development blog Mouseover for more information.
Okieodkie. I switched form using tile-based collision to using bounding boxes as Captain P advised. Some things seem to be working a lot better now but I am still very stuck.

When I run the following code the collisions work fine, the player stops if it hits an obstacle in the X or Y direction. But. If the player is moving down and to the left or down and to the right the player is able to move through an obstacle. Looks kinda like Mario going down a tube. Can anyone see what I am doing wrong here? Than you for your time.

 public class Game1 : Microsoft.Xna.Framework.Game    {        GraphicsDeviceManager graphics;        ContentManager content;        SpriteBatch spriteBatch;        Texture2D PlayerTexture;        Block[] blocks;        KeyboardState keyboardState;        Vector2 playerLocation = new Vector2(0, 0);        Vector2 playerVelocity = new Vector2(100, 100);                int numberOfBlocks = 12;        enum Directions        {            up,            down,            left,            right,            none        }        Directions currentDirection = Directions.none;        public Game1()        {            graphics = new GraphicsDeviceManager(this);            content = new ContentManager(Services);        }        /// <summary>        /// Allows the game to perform any initialization it needs to before starting to run.        /// This is where it can query for any required services and load any non-graphic        /// related content.  Calling base.Initialize will enumerate through any components        /// and initialize them as well.        /// </summary>        protected override void Initialize()        {            // TODO: Add your initialization logic here            spriteBatch = new SpriteBatch(graphics.GraphicsDevice);            blocks = new Block[numberOfBlocks];            int blockDrawPositionX = 200;            int blockDrawPositionY = 300;            for (int i = 0; i < numberOfBlocks; i++)            {                blocks = new Block(new Vector2(blockDrawPositionX, blockDrawPositionY));                blockDrawPositionX = blockDrawPositionX + 32;            }            base.Initialize();        }        /// <summary>        /// Load your graphics content.  If loadAllContent is true, you should        /// load content from both ResourceManagementMode pools.  Otherwise, just        /// load ResourceManagementMode.Manual content.        /// </summary>        /// <param name="loadAllContent">Which type of content to load.</param>        protected override void LoadGraphicsContent(bool loadAllContent)        {            if (loadAllContent)            {                // TODO: Load any ResourceManagementMode.Automatic content                Block.BlockTexture = content.Load<Texture2D>("blackTile");                PlayerTexture = content.Load<Texture2D>("redTIle");            }            // TODO: Load any ResourceManagementMode.Manual content        }        /// <summary>        /// Unload your graphics content.  If unloadAllContent is true, you should        /// unload content from both ResourceManagementMode pools.  Otherwise, just        /// unload ResourceManagementMode.Manual content.  Manual content will get        /// Disposed by the GraphicsDevice during a Reset.        /// </summary>        /// <param name="unloadAllContent">Which type of content to unload.</param>        protected override void UnloadGraphicsContent(bool unloadAllContent)        {            if (unloadAllContent)            {                // TODO: Unload any ResourceManagementMode.Automatic content                content.Unload();            }            // TODO: Unload any ResourceManagementMode.Manual content        }        /// <summary>        /// Allows the game to run logic such as updating the world,        /// checking for collisions, gathering input and playing audio.        /// </summary>        /// <param name="gameTime">Provides a snapshot of timing values.</param>        protected override void Update(GameTime gameTime)        {            // Allows the game to exit            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)                this.Exit();            // TODO: Add your update logic here            keyboardState = Keyboard.GetState();            // Set the collidable corners of the player sprite            Rectangle PlayerRect = new Rectangle((int)playerLocation.X, (int)playerLocation.Y, PlayerTexture.Width, PlayerTexture.Height);            //Check for collisions            for (int i = 0; i < numberOfBlocks; i++)            {                // Set the collidable crners of the bock sprite.                Rectangle BlockRect = new Rectangle((int)blocks.blockLocation.X, (int)blocks.blockLocation.Y, Block.BlockTexture.Width, Block.BlockTexture.Height);                if (PlayerRect.Intersects(BlockRect))                {                    if (currentDirection == Directions.up)                    {                        playerLocation.Y = blocks.blockLocation.Y + Block.BlockTexture.Height;                    }                    if (currentDirection == Directions.down)                    {                        playerLocation.Y = blocks.blockLocation.Y - PlayerTexture.Height;                    }                    if (currentDirection == Directions.left)                    {                        playerLocation.X = blocks.blockLocation.X + Block.BlockTexture.Width;                    }                    if (currentDirection == Directions.right)                    {                        playerLocation.X = blocks.blockLocation.X - PlayerTexture.Width;                    }                }            }            // Move the player            if (keyboardState.IsKeyDown(Keys.Up))            {                playerLocation.Y -= playerVelocity.Y * (float)gameTime.ElapsedGameTime.TotalSeconds;                currentDirection = Directions.up;            }            if (keyboardState.IsKeyDown(Keys.Down))            {                playerLocation.Y += playerVelocity.Y * (float)gameTime.ElapsedGameTime.TotalSeconds;                currentDirection = Directions.down;            }            if (keyboardState.IsKeyDown(Keys.Left))            {                playerLocation.X -= playerVelocity.X * (float)gameTime.ElapsedGameTime.TotalSeconds;                currentDirection = Directions.left;            }            if (keyboardState.IsKeyDown(Keys.Right))            {                playerLocation.X += playerVelocity.X * (float)gameTime.ElapsedGameTime.TotalSeconds;                currentDirection = Directions.right;            }                         base.Update(gameTime);        }        /// <summary>        /// This is called when the game should draw itself.        /// </summary>        /// <param name="gameTime">Provides a snapshot of timing values.</param>        protected override void Draw(GameTime gameTime)        {            graphics.GraphicsDevice.Clear(Color.CornflowerBlue);            // TODO: Add your drawing code here                        spriteBatch.Begin();            spriteBatch.Draw(PlayerTexture, playerLocation, Color.White);            for (int i = 0; i < numberOfBlocks; i++)            {                blocks.Draw(spriteBatch);            }            spriteBatch.End();            base.Draw(gameTime);        }    }

You're checking for collisions first, and then you're updating your players location. This means that you'll always react to collisions one frame too late. First handle input and movement, and after moving, check if the final position is valid - if it isn't, correct it.

Also, your currentDirection approach is flawed. Indeed, when you press two buttons, down-right for example, your currentDirection ends up being Directions.Right, because that check comes after the down-key check. During the next frame, a collision-check is performed. Because currentDirection is Directions.Right, only the x component of your players position is updated, which means that the player can walk down into a tile. This currentDirection is never reset anywhere, so it's not really the current direction anyway, just the last used direction. It's not really the actual direction either, since it doesn't take diagonal movement into account, while that movement certainly is possible.

You could take a lazy approach, and just push the players rect outside the colliding rect, towards the side that the intersection is the least. It'll probably suffice for most cases. Or, you could write a function that takes a rect and a movement vector, and then checks all other rects to see if the given rect, after having been moved, doesn't collide with any rect. And when it does, that function knows how the given rect has moved, so it should be able to determine where to cap the movement. This function can then return a corrected movement vector, so you know how far your players rect has really moved. It'll take some thinking, but will probably give the best results.


And finally, some notes: you're creating a new Rectangle every time you call your Update() function. Why not store a player Rectangle immediatly, and use it's position component as your players position, updating that instead of using a Vector2 for it? Also, why limit your Rectangle to integer precision, when you're using floats or doubles for your Vector2?

Oh, and did you step through your code using a debugger? I can assure you, knowing how to use a debugger certainly helps you spot these things more easily. :)
Create-ivity - a game development blog Mouseover for more information.
Wow I done everything wrong. Sucks to be me :). Thanks for all the pointers Captain P, you have given me loads to work with. I will be back again with another mess tomorrow I am sure.

This topic is closed to new replies.

Advertisement