RayTracing Collision Detection For 2d tile games

Hello every one as you will know collision detection can be a real pain the backside.

Iv recently gone back to a old project. Its a 2d grid based platformer. Anyhow the collision detection is a messy hack, Stuff added for what happens if you collide with a tile on x and y at same time, what happens when your object is traveling super fast plus other hacks.

Anyhow a few years ago I implemented this fast tray trace for my Minecraft cone test. the paper is here http://www.cse.yorku.ca/~amana/research/grid.pdf

Well I thought that would be good if I could implment a good collisoin detection based on this. Iv tryed before and not got it perfect, but I think this time I have. Its been a pain to get working but iv made it very flexible so it will fit just about anyone’s projects with very little code

It has the following benifits.
1: its fast
2: super fast object do not tunnel or skip tiles.
3: its very accurate

Here is a video of it working https://youtu.be/qn-WMqO9tR4
the projectile is moving at more than 40 pixels per update. and the tile width is 20 pixels.

Bellow is the code for thouse who are interested. Its very late now so tommorow ill zip and upload the project

Hope its of some use to someone. Your comments always welcome.

using Microsoft.Xna.Framework;
namespace Riddlersoft.Core.Collisions
{
    public enum CollisionDirection
    {
        Horizontal,
        Vertical,
    }

    public struct Collision
    {
        public Point GridLocation { get; internal set; }
        public Coordinate ObjectCoordinate { get; internal set; }
        public CollisionDirection Direction { get; internal set; }
        public object Tile { get; internal set; }
        public object Grid { get; internal set; }
    }
}
using Microsoft.Xna.Framework;

namespace Riddlersoft.Core.Collisions
{
    /// <summary>
    /// this structur contains both a position and velocity for an 2d game object
    /// </summary>
    public struct Coordinate
    {
        /// <summary>
        /// the position of the object
        /// </summary>
        public Vector2 Position;
        /// <summary>
        /// the velocity of the object
        /// </summary>
        public Vector2 Velocity;

        /// <summary>
        /// creates a new coordinate instance
        /// </summary>
        /// <param name="position">the start position of the object</param>
        /// <param name="velocity">the start velocity of the object</param>
        public Coordinate(Vector2 position, Vector2 velocity)
        {
            Position = position;
            Velocity = velocity;
        }
    }
}

using System;
using Microsoft.Xna.Framework;

namespace Riddlersoft.Core.Collisions
{
    public class RayCollisionDetector
    {
        /// <summary>
        /// the cell or tile size
        /// </summary>
        private int _cellSize;

        /// <summary>
        /// the amount of bounce we want after a collisoin
        /// 0 is no bound 1 is perfect bounce
        /// </summary>
        private float _amountOfBounce = 1;
        public float AmountOfBounce
        {
            get
            {
                return _amountOfBounce;
            }
            set
            {
                _amountOfBounce = MathHelper.Clamp(value, 0, 1);
            }
        }

        /// <summary>
        /// the size of the grid you want to do collision check on
        /// usual the total size of the grid
        /// </summary>
        private Rectangle _gridBounds;

        /// <summary>
        /// this function fires as soon as an object enters the cell
        /// return false to ignore collision of tile ie air
        /// return true to enable the collision
        /// OnCellEnter(CollisionDirection dir, Point gridLocation, Object sender);
        /// </summary>
        public Func<CollisionDirection, Point, object, bool> OnCellEnter;

        /// <summary>
        /// this action fires for prossesing the collision with the cell
        /// ProssesCellColision(Collision col);
        /// </summary>
        public Action<Collision> ProssesCellColision;

        /// <summary>
        /// creates a new instace of the collision detector
        /// </summary>
        /// <param name="cellSize">the size in pixels of each cell</param>
        /// <param name="gridBounds">the size of the 2d array</param>
        public RayCollisionDetector(int cellSize, Rectangle gridBounds)
        {
            _cellSize = cellSize;
            _gridBounds = gridBounds;
        }


        private float CalculateDistance(float pos, float vel)
        {
            if (vel > 0)
                return (float)Math.Ceiling(pos / _cellSize) * _cellSize - pos;

            return pos - (float)Math.Floor(pos / _cellSize) * _cellSize;
        }

        private float CalculateTravelTime(float distance, float vel)
        {
            if (vel == 0)
                return float.PositiveInfinity;

            return distance / Math.Abs(vel);
        }

        private bool ValidateHorizontalColision<T>(Point p, Vector2 vel, float travelTime, T[,] grid)
        {
            if (travelTime > 1) //if no collision
                return false;

            int dir = Math.Sign(vel.X);
            Point tile = new Point(p.X + dir, p.Y);

            if (tile.X <= _gridBounds.X || tile.X >= _gridBounds.Width)
                return true;

            if (OnCellEnter != null)
                return OnCellEnter(CollisionDirection.Horizontal, tile, grid[tile.X, tile.Y]);
            
           // if (grid[p.X + dir, p.Y]) //if we can walk here there is not a collision
                //return false;

            //   if (grid[p.X, p.Y]) //if this side block is passible we have a collision
            //  return true;
            return true;
        }

        private bool ValidateVerticalColision<T>(Point p, Vector2 vel, float travelTime, T[,] grid)
        {
            if (travelTime > 1) //if no collision
                return false;

            int dir = Math.Sign(vel.Y);
            Point tile = new Point(p.X, p.Y + dir);

            if (tile.Y <= _gridBounds.Y || tile.Y >= _gridBounds.Height)
                return true;

            if (OnCellEnter != null)
                return OnCellEnter(CollisionDirection.Horizontal, tile, grid[tile.X, tile.Y]);

            //  if (grid[p.X, p.Y + dir]) //if we can walk here there is not a collision
            // return false;

            //    if (grid[p.X, p.Y]) //if this side block is passible we have a collision
            //   return true;
            return true;
        }

        /// <summary>
        /// Handle colision for one update cycle
        /// </summary>
        /// <typeparam name="T">the tile type in your 2d array</typeparam>
        /// <param name="pos">the position of the object you want to check a colision with</param>
        /// <param name="vel">the velocity of the object you want to check a colision with</param>
        /// <param name="grid">the 2d array that represents you gameing grid</param>
        /// <returns>by default the collision detector will invert the velocity after a collision and also set the position.
        /// You can then set ether the pos and vel return to your object or you can handle it on your own
        /// </returns>
        public Coordinate HandleColision<T>(Vector2 pos, Vector2 vel, T[,] grid)
        {
           
            Vector2 preColVel = vel;
            //first calculate distance to next cell
            Vector2 distance = new Vector2(CalculateDistance(pos.X, vel.X),
                CalculateDistance(pos.Y, vel.Y));

            //now calculate travel time
            Vector2 travelTime = new Vector2(CalculateTravelTime(distance.X, vel.X),
                CalculateTravelTime(distance.Y, vel.Y));

            Vector2 blockTraveTime = new Vector2(_cellSize / Math.Abs(vel.X), _cellSize / Math.Abs(vel.Y));
            Point gridLocation = new Point((int)Math.Floor(pos.X / _cellSize), (int)Math.Floor(pos.Y / _cellSize));
            while (travelTime.X <= 1 || travelTime.Y <= 1)
            {
                bool HorCol = false, VerCol = false;

                if (travelTime.X < travelTime.Y)
                {
                    HorCol = ValidateHorizontalColision(gridLocation, vel, travelTime.X, grid);
                    if (!HorCol)
                    {
                        // deltaTraveTime += blockTraveTime;
                        travelTime.X += blockTraveTime.X;
                        //    if (deltaTraveTime.Y > deltaTraveTime.X)
                        gridLocation.X += Math.Sign(vel.X);
                        continue;
                    }
                }
                else
                {
                    VerCol = ValidateVerticalColision(gridLocation, vel, travelTime.Y, grid);
                    if (!VerCol)
                    {
                        //deltaTraveTime += blockTraveTime;
                        travelTime.Y += blockTraveTime.Y;
                        //  if (deltaTraveTime.Y < deltaTraveTime.X)
                        gridLocation.Y += Math.Sign(vel.Y);
                        continue;
                    }
                }

                if (HorCol) //logic for horizontal collision
                {
                    gridLocation.X += Math.Sign(vel.X);
                    if (ProssesCellColision != null) //if user has collision responce
                    {
                        Collision col = new Collisions.Collision()
                        {
                            Direction = CollisionDirection.Horizontal,
                            GridLocation = gridLocation,
                            ObjectCoordinate = new Coordinate(pos, vel),
                            Tile = grid[gridLocation.X, gridLocation.Y],
                            Grid = grid,
                        };
                        ProssesCellColision(col);

                    }
                    preColVel = vel * travelTime.X;
                    pos.X -= Math.Sign(vel.X); //take away 1 pixel so that we are not in the block
                    //this is requied otherwise we will get posistion errors

                    vel.X *= -_amountOfBounce;
                    break;
                }

                if (VerCol) //logic for vertical collisoins
                {
                    gridLocation.Y += Math.Sign(vel.Y);
                    if (ProssesCellColision != null) //if user has collision responce
                    {
                        Collision col = new Collisions.Collision()
                        {
                            Direction = CollisionDirection.Vertical,
                            GridLocation = gridLocation,
                            ObjectCoordinate = new Coordinate(pos, vel),
                            Tile = grid[gridLocation.X, gridLocation.Y],
                            Grid = grid,
                        };
                        ProssesCellColision(col);
                    }

                    preColVel = vel * travelTime.Y;
                    pos.Y -= Math.Sign(vel.Y); //take away 1 pixel so that we are not in the block
                    //this is requied otherwise we will get posistion errors

                    vel.Y *= -_amountOfBounce;
                    break;
                }

            }
            pos.X += preColVel.X;
            pos.Y += preColVel.Y;

            return new Collisions.Coordinate(pos, vel);
        }
    }
}

finaly test program

using Microsoft.Xna.Framework;
using Riddlersoft.Core.Collisions;

namespace GridRayTrace
{
    public class Mob
    {
        public Vector2 Pos, Vel;

        RayCollisionDetector _rcd;
        float gravity = .08f;
        public Mob(Vector2 _pos, Vector2 _vel,bool[,] grid)
        {
            Pos = _pos;
            Vel = _vel;
            grid[(int)(Pos.X / 20), (int)(Pos.Y / 20)] = false; //make start tile empty space
            _rcd = new RayCollisionDetector(20, new Rectangle(0, 0, 500, 500));
            _rcd.OnCellEnter = OnCellCollision;
            _rcd.ProssesCellColision = OnCollision;
            _rcd.AmountOfBounce = .95f;
        }

        private void OnCollision(Collision col)
        {
         //   if (col.Direction == CollisionDirection.Horizontal )
            //    Vel.X *= -1;
           // else
             //   Vel.Y *= -1;

            bool[,] grid = col.Grid as bool[,];
            grid[col.GridLocation.X, col.GridLocation.Y] = false;
        }

        private bool OnCellCollision(CollisionDirection dir, Point gridLocation, object sender)
        {
            bool b = (bool)sender;
            if (b)
                return true;

            return false;
        }


        public void Update(float dt, bool[,] grid)
        {
           // Vel.Y += gravity;
            Coordinate newLoc = _rcd.HandleColision<bool>(Pos, Vel, grid);
            Pos = newLoc.Position;
            Vel = newLoc.Velocity;
        }
    }
}
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

using System;
namespace GridRayTrace
{

    public class Game1 : Game
    {
        private bool[,] _grid = new bool[50, 50];

        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        private Texture2D tex;

        Mob mob;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
            this.graphics.PreferredBackBufferWidth = 1000;
            this.graphics.PreferredBackBufferHeight = 1000;
        }

       
        protected override void Initialize()
        {
            // TODO: Add your initialization logic here
            
            for (int x = 0; x < 50; x++)
                for (int y = 0; y < 50; y++)
                {
                    _grid[x, y] = true;
                }

            mob = new GridRayTrace.Mob(new Vector2(304.5f, 204.3f), new Vector2(23f, 40f), _grid);
            base.Initialize();
        }

        
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);
            tex = new Texture2D(GraphicsDevice, 1, 1);
            tex.SetData<Color>(new Color[1] { Color.White });
            // TODO: use this.Content to load your game content here
        }

        
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        protected override void Update(GameTime gameTime)
        {
            float dt = (float)gameTime.ElapsedGameTime.TotalSeconds;

                mob.Update(dt, _grid);
            // TODO: Add your update logic here
            base.Update(gameTime);
        }


        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            spriteBatch.Begin();

            //draw grid
            for (int x = 0; x < 50; x++)
                for (int y = 0; y < 50; y++)
                {
                    if (!_grid[x, y])
                        spriteBatch.Draw(tex, new Rectangle(x * 20, y * 20, 20, 20), Color.White);
                    else
                        spriteBatch.Draw(tex, new Rectangle(x * 20, y * 20, 20, 20), Color.LightGreen);
                }

            for (int x = 0; x < 50; x++)
                spriteBatch.Draw(tex, new Rectangle(x * 20, 0, 1, 1000), Color.Black);

            for (int y = 0; y < 50; y++)
                spriteBatch.Draw(tex, new Rectangle(0, y * 20, 1000, 1), Color.Black);

            Vector2 dir = mob.Vel;
            dir.Normalize();
            for (int i = 1; i < 10; i++)
                spriteBatch.Draw(tex, new Rectangle(Convert.ToInt32(mob.Pos.X - 1 - dir.X * i),
                    Convert.ToInt32(mob.Pos.Y - 1 - dir.Y * i), 3, 3), Color.Blue);
            spriteBatch.Draw(tex, new Rectangle(Convert.ToInt32(mob.Pos.X - 1), Convert.ToInt32(mob.Pos.Y - 1), 3, 3), Color.Red);

            spriteBatch.Draw(tex, new Rectangle(Convert.ToInt32(mob.Pos.X - 1 + mob.Vel.X), Convert.ToInt32(mob.Pos.Y - 1 + mob.Vel.Y), 3, 3), Color.LightPink);
            spriteBatch.End();
            // TODO: Add your drawing code here

            base.Draw(gameTime);
        }
    }
}