Platformer Collison Detection Across Platform Seams

Topics: User Forum
Dec 23, 2010 at 4:24 AM

I have been successful in creating a platformer sample for the SimpleSample testing project.  I adopted an idea from Robert Dodd at this link: http://boxycraft.wordpress.com

I am using the player wheel to check for collisions to make sure that the player is on the ground so that it can jump.  Two things happen that I cannot figure out.  The first is when the player travels into the wall and two cotact points are created on the wheel.  The _isOnGround flag is set to false and does not change back after leaving the wall.  The same goes for a player traveling across a seam in two platforms.

Here is my player class:

using System.Collections.Generic;
using FarseerPhysics.Dynamics;
using FarseerPhysics.Dynamics.Contacts;
using FarseerPhysics.Dynamics.Joints;
using FarseerPhysics.Factories;
using FarseerPhysics.DemoBaseXNA;
using Microsoft.Xna.Framework;

namespace FarseerPhysics.DemoBaseXNA.DemoShare
{
    public class Player
    {
        private const float DemoUnitsPerDisplayUnit = 10.0f;
        private const float PlayerWidth = 20.0f;
        private const float PlayerHeight = 32.0f;

        private Vector2 _position;
        private float _playerWidth; 
        private float _playerHeight;
        private float _playerBodyHeight;

        private Fixture _playerBody;
        private Fixture _playerWheel;       
        private RevoluteJoint _motorJoint;
        private FixedAngleJoint _fixedAngleJoint;
        
        private bool _isOnGround;

        private int _ticks;
        private int _ticksCollision;

        public Player(World world, Vector2 position)
        {
            ConvertUnits.SetDisplayUnitToSimUnitRatio(DemoUnitsPerDisplayUnit);

            _position = ConvertUnits.ToSimUnits(position);
            _playerWidth = ConvertUnits.ToSimUnits(PlayerWidth);
            _playerHeight = ConvertUnits.ToSimUnits(PlayerHeight);

            _playerBodyHeight = _playerHeight - (_playerWidth / 2);            
            // Create a body for the player.
            _playerBody = FixtureFactory.CreateRectangle(world, _playerWidth, _playerBodyHeight, 1.0f);
            _playerBody.Body.BodyType = BodyType.Dynamic;
            _playerBody.Body.Position = _position - Vector2.UnitY * (_playerWidth / 4);
                        
            // Create a wheel for the player to control ground movement.
            _playerWheel = FixtureFactory.CreateCircle(world, _playerWidth / 2, 1.0f);
            _playerWheel.Body.BodyType = BodyType.Dynamic;
            _playerWheel.Body.Position = _playerBody.Body.Position - Vector2.UnitY * (_playerBodyHeight / 2);
            _playerWheel.Friction = float.MaxValue;
            
            // Controls speed of player along ground.
            _motorJoint = JointFactory.CreateRevoluteJoint(world, _playerBody.Body, _playerWheel.Body, Vector2.Zero);
            _motorJoint.MotorEnabled = true;
            _motorJoint.MaxMotorTorque = 1500.0f;
            _motorJoint.MotorSpeed = 0.0f;

            // Keeps player upright.
            _fixedAngleJoint = JointFactory.CreateFixedAngleJoint(world, _playerBody.Body);
            
            // Delegates subscriptions that help identify whether the player is on the ground.
            _playerWheel.OnCollision += OnWheelCollision;
            _playerWheel.OnSeparation += OnWheelSeparation;

            _isOnGround = false;
        }
        
        public Body Body
        {
            get { return _playerBody.Body; }
        }

        public RevoluteJoint Motor
        {
            get { return _motorJoint; }
            set { _motorJoint = value; }
        }        
        
        public bool IsOnGround
        {
            get { return _isOnGround; }
            set { _isOnGround = value; }
        }

        private bool OnWheelCollision(Fixture f1, Fixture f2, Contact contact)
        {
            _isOnGround = true;
            return true;
        }

        private void OnWheelSeparation(Fixture f1, Fixture f2)
        {
            _isOnGround = false;            
        }
    }
}

And here is my DemoScreen:

using System.Text;
using FarseerPhysics.DemoBaseXNA;
using FarseerPhysics.DemoBaseXNA.DemoShare;
using FarseerPhysics.DemoBaseXNA.ScreenSystem;
using FarseerPhysics.Dynamics;
using FarseerPhysics.Factories;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;

namespace FarseerPhysics.SimpleSamplesXNA
{
    internal class Demo8Screen : GameScreen, IDemoScreen
    {
        private Player _player;

        private const float JumpForce = 350.0f;
        private const float MaxMotorSpeed = 30.0f;
        private const float AirMovementForce = 500.0f;

        #region IDemoScreen Members

        public string GetTitle()
        {
            return "Demo8: Platformer Player";
        }

        public string GetDetails()
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine("This demo shows how to combine a box with ");
            sb.AppendLine("a circle to create a player.");
            sb.AppendLine(string.Empty);
            sb.AppendLine("GamePad:");
            sb.AppendLine("  -Jump: A Button");
            sb.AppendLine("  -Move: Left Thumbstick");
            sb.AppendLine(string.Empty);
            sb.AppendLine("  -Jump: Space Bar");
            sb.AppendLine("  -Move: Arrow Keys");
            return sb.ToString();
        }

        #endregion

        public override void Initialize()
        {
            World = new World(new Vector2(0, -20));            
            base.Initialize();
        }

        public override void LoadContent()
        {
            _player = new Player(World, Vector2.Zero);
            World.Gravity = 75.0f * -Vector2.UnitY;
            CreateObstacles();
            
            base.LoadContent();
        }

        private void CreateObstacles()
        {
            Fixture[] rect = new Fixture[4];

            for (int i = 0; i < 4; i++)
            {
                rect[i] = FixtureFactory.CreateRectangle(World, 6, 1.5f, 1);
            }
            rect[0].Body.Position = new Vector2(-9, -5);
            rect[1].Body.Position = new Vector2(-8, 7);
            rect[2].Body.Position = new Vector2(9, -7);
            rect[3].Body.Position = new Vector2(7, 5);
        }

        public override void HandleInput(InputState input)
        {
            _player.Motor.MotorSpeed = 0.0f;

            Vector2 _jumpImpulse = Vector2.UnitY * JumpForce;
            Vector2 _airMovementForce = Vector2.Zero;

            // Handle player input while player is on the ground.
            if (_player.IsOnGround)
            {
                if (input.CurrentKeyboardState.IsKeyDown(Keys.Right) && input.CurrentKeyboardState.IsKeyDown(Keys.Left))
                {
                    _player.Motor.MotorSpeed = 0.0f;
                }
                else
                {
                    if (input.CurrentKeyboardState.IsKeyDown(Keys.Right))
                    {
                        _player.Motor.MotorSpeed = -MaxMotorSpeed;
                    }
                    if (input.CurrentKeyboardState.IsKeyDown(Keys.Left))
                    {
                        _player.Motor.MotorSpeed = MaxMotorSpeed;
                    }
                }

                if (input.CurrentKeyboardState.IsKeyDown(Keys.Space) && input.LastKeyboardState.IsKeyUp(Keys.Space))
                {
                    _player.Body.ApplyLinearImpulse(ref _jumpImpulse);
                }
            }

            // Handle player input while player is in the air
            if (!_player.IsOnGround)
            {
                if (input.CurrentKeyboardState.IsKeyDown(Keys.Right) && input.CurrentKeyboardState.IsKeyDown(Keys.Left))
                {
                    _airMovementForce = Vector2.Zero;
                }
                else
                {
                    if (input.CurrentKeyboardState.IsKeyDown(Keys.Right))
                    {
                        _airMovementForce = Vector2.UnitX * AirMovementForce;
                    }
                    if (input.CurrentKeyboardState.IsKeyDown(Keys.Left))
                    {
                        _airMovementForce = -Vector2.UnitX * AirMovementForce;
                    }
                }

                _player.Body.ApplyForce(_airMovementForce);
            }

            base.HandleInput(input);
        }
    }
}
It might be simple, but I just can't figure out what I'm doing wrong here. Any help would be much appreciated!
Dec 24, 2010 at 12:37 AM

Since onCollision is called each time a new fixture is in contact with your wheel, when the wall touches your wheel it calls, but then when the wheel stops touching the wall, onSeperate is called setting your on ground flag the false.

TO fix this, you need to probably do something like this:

Keep a list of fixtures currently in contact with the wheel, who's contact point is within, say the quarter of the circle (on a normal unit circle, this area is from degree 225 to 315, or 5/4 pi to 7/4 pi).

Then in the onSeperate function, remove the separating fixture (if its in the list) from the list populated in the onCollision. If the list's size is 0, you are no longer standing on any valid ground to jump out of.

 

 

 

I was originally going to post a simpler method that "can" solve the problem directly as stated, but introduces another problem.

I wouldn't recommend using this unless you don't mind the side affect:

If you keep a count of fixtures touching the wheel by incrementing a variable (int is fine) in onWheelCollision, and then decrementing the variable in onWheelSeperate, you then know if you are allowed to jump if the variable is == 0.

The problem with using this method though is if you are falling and touching the side of a wall, it allows you to jump.

Coordinator
Dec 24, 2010 at 3:09 PM

Remember we also have World.ContactManager.BeginContact() that works on a higher level than single fixtures.

Dec 24, 2010 at 4:17 PM

Thanks for the responses!

@Quintinon That is a very nice suggestion.  I llike limiting the collision to a specific angle, regardless of the method used to determine palyer jumping state. 

@genbox Thanks for the suggestion, will look into it.

I also found a clever method for determing player state (jumping, running, idle) that uses OnCollsion only.  The article is here.  The ground sensor method can be found in the sample code linked at the bottom of the page.

Dec 25, 2010 at 5:09 AM

haha, stupid me, the sensor method is what I use, just been a while since I implemented it or I would have posted it instead of all that complicated stuff :p

Jan 31, 2011 at 9:06 AM
Edited Jan 31, 2011 at 12:18 PM

Thanks for the code and the link to the original article. I have read them all and created some good working character movement.

Only I have two problems, and I thought maybe someone here can tell me how to fix it. I'm using F3.0 (I know it's old, but my dev laptop won't run with W7, so can't update to XNA 4)

1. I have set restitution of both the character parts and all the walls to 0, but still it bounces a little. Specially when running into a wall (it bounces up and then falls down). I tried to make the wheel a tiny bit smaller, but this had no effect. I really need to get rid of this small bouncyness, as it effects many things in this game. I guess it might have to do with the revolute joint that connects the wheel with the body of the character. But how to overcome this?

2. When falling close to a wall, like sliding against a wall, the character sometimes gets stuck. I guess it collides with one of the edges of the blocks, that make up the wall. I tried to make the collision rectangles of the blocks a tiny bit smaller (so it forms small gabs in between them) and a tiny bit bigger (so they overlap a bit), but no real effect.

All help is appreciated!

Developer
Jan 31, 2011 at 9:42 AM
jordos wrote:

1. I have set restition of both the character parts and all the walls to 0, but still it bounces a little. Specially when running into a wall (it bounces up and then falls down). I tried to make the wheel a tiny bit smaller, but this had no effect. I really need to get rid of this small bouncyness, as it effects many things in this game. I guess it might have to do with the revolute joint that connects the wheel with the body of the character. But how to overcome this?

Have you tried making your character heavier and/or increasing your gravity? In general it is hard to achieve pixel perfect non bounciness if you use a physics engine, as almost everything tries to behave physically correct, which does not translate very well to classic oldschool platformers. As Farseer uses an iterative solver it might never be "perfect".

2. When falling close to a wall, like sliding against a wall, the character sometimes gets stuck. I guess it collides with one of the edges of the blocks, that make up the wall. I tried to make the collision rectangles of the blocks a tiny bit smaller (so it forms small gabs in between them) and a tiny bit bigger (so they overlap a bit), but no real effect.

This is a known problem that already exist within Box2D (Farseer is based on it) and cannot really be solved at the moment... maybe in a future Box2D update. For now your best chance is using Edge/Loop shapes with adjacency information. Try to model you environment in a way that walls, floors are made up of one edge, where possible e.g. try to move seams in your geometry to ceilings... that was my solution so far at least.

Only I have two problems, and I thought maybe someone here can tell me how to fix it. I'm using F3.0 (I know it's old, but my dev laptop won't run with W7, so can't update to XNA 4)

Just on a side note: Farseer 3.2 works perfectly well with XNA 3.1. It is just the samples, which won't run with 3.1 anymore. Furthermore there is a standalone Win XP download for XNA 4... so you don't need Win 7, you'll have to switch to Visual C# Express 2010 / VS 2010 though i guess (both also run on Win XP). You'll find it here: http://go.microsoft.com/fwlink/?LinkId=197288

Jan 31, 2011 at 10:27 AM
Edited Jan 31, 2011 at 12:19 PM

Hi Elsch, thanks very much for your reply.

1. Good tip about the bouniness. Will try to make the character heavier and see how far I can get. The main problem with the bounciness is that when the player jumps at the moment the character bounces up, it will jump higher.

2. Shame to hear there is no real solving for this. Building levels out of long edges instead of blocks would be alot better I reckon. But it would also require alot of work on the leveldesigner I made.
I will try make the body part of the character a capsule first...

Thanks for pointing out about XNA and Win XP. Strange how I thought this wasn't possible... I tried earlier to insert FP3.1, but that wouldn't let me build the dll's. Now, I will just jump to XNA 4.

Developer
Jan 31, 2011 at 11:48 AM
jordos wrote:

1. Good tip about the bouncyness. Will try to make the character heavier and see how far I can get. The main problem with the bouncyness is that when the player jumps at the moment the character bounces up, it will jump higher.

You can always set the y-component of your characters body to zero before jumping. Or apply an impulse equal to that to the character like: body.ApplyLinearImpulse(jumpImpulse - new Vector2(0f, body.LinearVelocity.Y - [some factor dependent on height above ground]);

2. Shame to hear there is no real solving for this. Building levels out of long edges instead of blocks would be alot better I reckon. But it would also require alot of work on the leveldesigner I made.
I will try make the body part of the character a capsule first...

If you have a tile-based environment with rectangular blocks I have some code for decomposing that to rectangles or loop shapes which are seamless for all walls and floors.

Jan 31, 2011 at 12:17 PM
Edited Jan 31, 2011 at 12:20 PM

About the jumping, setting the Y velocity to zero, or use the Y velocity in a calculation will work as long as the ground doesn't move. But if the character is on some platform that's moving up or down, it will give a very unnatural effect. I could set it to the same velocity as the floor he's jumping from, but then I need to keep a reference to the ground. But I could do that.

I have indeed a tile-based environment with rectangular blocks and I would really like to look into that code you have :) Thanks!

Jan 31, 2011 at 1:48 PM
Try setting the wheel friction to zero when the character is on a wall. The player bounces when hitting the wall because the wheel is pushing the character up it. Worked for me. As far as the character catching on tiles, I mitigated that by making the tile and character bigger (thereby making them heavier). I did this by modifying the conversion factor (WorldUnitsPerDisplayUnit) on my level importer then adjusting gravity and forces accordingly.
Jan 31, 2011 at 2:26 PM
nwhitesel wrote:
Try setting the wheel friction to zero when the character is on a wall. The player bounces when hitting the wall because the wheel is pushing the character up it. Worked for me. 

Yeah, that's why I tried to make the wheel somewhat smaller, so the body would collide with the wall, while the wheel would still not reach it. But this had no effect. I will try it again and make it even more smaller, while I'm also turning the body rectangle into a capsule. I'll also give your idea a try, however I'm not sure yet how to test if it's against a wall. Could work with a raycast...
Also, the character tends to bounce a bit when landing as well, so improving the weight should hopefully fix this.

nwhitesel wrote:
As far as the character catching on tiles, I mitigated that by making the tile and character bigger (thereby making them heavier). I did this by modifying the conversion factor (WorldUnitsPerDisplayUnit) on my level importer then adjusting gravity and forces accordingly.

 I hope this had to do with the weight, because I've already increased my scalingfactor. This did alot of good though, so I might increase it just a little bit more to see what happends. On the other hand I want to be able to put larger objects in as well and I read in the Box2D manual that objects should be between 0.1 and 10 meters. My blocks are already like 2 meters wide and only 32 pixels on screen.

Thanks for all the suggestions! Lot's to try out when I get home. Will let you know the results.

Developer
Jan 31, 2011 at 2:48 PM

Ok first off I have my Farseer 2.x code which generates boxes:

private class CollisionLayer : TileLayer

{
    public List<Rectangle> collisionBoxes;
    private bool[] tileVisited;

    public BuildGeometry(int mapWidth) : base()
    {
        collisionBoxes = new List<Rectangle>();
        // tiles is an array filled with 1 and 0 for 
        // colliding and non-collidign tiles
        tileVisited = new bool[tiles.Length];

        for (int i = 0; i < tiles.Length; ++i) 
        {
            if (!tileVisited[i] && tiles[i] != 0) 
            {
                Rectangle tempRect;
                tempRect.rect = new Rectangle((i % mapWidth) * tileSpacing, 
                                              i / mapWidth * tileSpacing, 
                                              tileSpacing, tileSpacing);
                tileVisited[i] = true;

                // Determine the longest horizontal row of connected tiles
                int width = 1;
                int height = 1;
                int j = i + 1;
                while (j % mapWidth > 0 && tiles[j] == tiles[i] && !tileVisited[j]) 
                {
                    tempRect.rect.Width += tileSpacing;
                    tileVisited[j] = true;
                    width++;
                    j++;
                }

                // Try extending the rectangle vertically
                bool searchRows = true;
                while (searchRows && i + mapWidth * height < tiles.Length) 
                {
                    for (j = i + mapWidth * height; j < i + width + mapWidth * height; j++) 
                    {
                        if (tiles[j] != tiles[i]) 
                        {
                        searchRows = false;
                        break;
                        }
                    }
                    if (searchRows) 
                    {
                        tempRect.rect.Height += tileSpacing;
                        for (j = i + mapWidth * height; j < i + width + mapWidth * height; j++) 
                        {
                            tileVisited[j] = true;
                        }
                        height++;
                    }
                }
            collisionBoxes.Add(tempRect);
            }
        }
    }
}

I don't have any screenshot unfortunately as I ported this to Farseer 3.x and migrated everything to edge shapes:

private List<Vertices> BuildCollisionShapeFromLayer(int[,] collisionRect)
{
    List<Edge> edgeList = new List<Edge>();
    for (int i = 0; i < collisionRect.GetLength(0); ++i)
    {
        for (int j = 0; j < collisionRect.GetLength(1); ++j)
        {
            if (collisionRect[i, j] == 1)
            {
                // Check the four adjacent tiles and add edges between colliding
                // and non-coliding tiles to a list
                if (i == 0 || collisionRect[i - 1, j] == 0)
                {
                    edgeList.Add(new Edge(new Vector2((float)i / 2f, (float)j / 2f), 
                                          new Vector2((float)i / 2f, (float)(j + 1) / 2f)));
                }
                if (i == collisionRect.GetLength(0) - 1 || collisionRect[i + 1, j] == 0)
                {
                    edgeList.Add(new Edge(new Vector2((float)(i + 1) / 2f, (float)(j + 1) / 2f), 
                                          new Vector2((float)(i + 1) / 2f, (float)j / 2f)));
                }
                if (j == 0 || collisionRect[i, j - 1] == 0)
                {
                    edgeList.Add(new Edge(new Vector2((float)(i + 1) / 2f, (float)j / 2f), 
                                          new Vector2((float)i / 2f, (float)j / 2f)));
                }
                if (j == collisionRect.GetLength(1) - 1 || collisionRect[i, j + 1] == 0)
                {
                    edgeList.Add(new Edge(new Vector2((float)i / 2f, (float)(j + 1) / 2f), 
                                 new Vector2((float)(i + 1) / 2f, (float)(j + 1) / 2f)));
                }
            }
        }
    }

    // Combine edges which share a point to a closed polygon
    List<Vertices> collisionShape = new List<Vertices>();
    while (edgeList.Count > 0)
    {
        Vertices output = new Vertices();
        output.Add(edgeList[0].EdgeStart);
        output.Add(edgeList[0].EdgeEnd);
        edgeList.RemoveAt(0);
        bool closed = false;
        int index = 0;
        while (!closed && edgeList.Count > 0)
        {
            if (output[output.Count - 1] == edgeList[index].EdgeStart)
            {
                if (edgeList[index].EdgeEnd == output[0])
                {
                    closed = true;
                }
                else
                {
                    output.Add(edgeList[index].EdgeEnd);
                }
                edgeList.RemoveAt(index);
                --index;
            }
            if (!closed && ++index == edgeList.Count)
            {
                index = 0;
            }
        }
        // remove colinear points from polygon
        output = SimplifyTools.CollinearSimplify(output);
        collisionShape.Add(output);
    }
    return collisionShape;
}

You can convert the closed polygons (e.g. Vertices objects) to a loop shape then and end up with something like this: http://i.imgur.com/ve8vq.png

I still sometimes have the getting stuck on seams problem for one sided platforms (they are generated elsewhere but the code is pretty similar to the above one), in cases like the one marked with a red circle. These T-junctions can be problematic depending from where your character is approaching an edge.

Jan 31, 2011 at 7:44 PM

You can determine if the player is on a wall the same way you would determine if the player is on the ground.  I do an OnCollision delegate on the square and check the collision angle using something similar to what is discussed here.

As far as the sizing of the player.  If you increase the player size, you can keep the sprite the same size by using a scaling factor when drawing.  If you are using an animation, create a scaling vector and pass it to your animation system.  If not, just use a scaling matrix and pass it to SpriteBatch.  There are a lot of discussions on here about this type of unit conversion.  I would check out some of those (e.g. this).

Feb 1, 2011 at 8:15 AM

OK, I found out the body part of the character had a higher density than the wheel part, therefore it bounced on landing. Changed that, it doesn't bounce anymore. Only a little bit when bumping into a wall, will try your suggestion, nwhitesel.  I do the on ground / in air determination with raycasts, so will use it for this purpose too.

About the other issue. I turned the rectangle body part into a capsule, but that didn't solve anything. Which is strange, or not? Also played with the scaling factor, but that wouldn't do much either. The character would still get stuck sometimes. I understand the difference between physics coordinates and visual coordinates, nwhitesel. I'm using a conversion anyway, because I work with the FlatRedBall engine which is 2.5d and doesn't work with pixels. But what I ment is, I can't increase that factor forever, as the physics size of things, like the blocks, will get too big. And I want to leave some room to be able to add bigger stuff. The Box2D manual states objects shouldn' be bigger than 10 metres.

Thanks for your code sample Elsch, I need a bit more time to try it out, but will do.

I was also thinking about turning the character into circles only. Like one wheel with motorpower on the bottom and maybe two above it without any motorpower, just rotating when needed. Then use these circles for wall collision and add a more representative rectangle for other types of collision, like bullets.