Collision has major problems

I have a problem with my collision. My character is 2 tiles tall (128x64) and moves in 8 pixel increments
I have a collision method set up, but there’s a really big problem
Whenever you approach a wall from the left, the collision does nothing until you are inside the wall, at which point it softlocks the game. I desperately need help. Can anyone offer some insight?
Code is below.

Movement

protected override void Update(GameTime gameTime)
{
            if ((currentkeyboardstate.IsKeyUp(Keys.D)) && (currentkeyboardstate.IsKeyUp(Keys.A)))
            {
                Fool.PlayerPosition.X = 0;
                Fool.PlayerVelocity.X = 0;
            }

            if (currentkeyboardstate.IsKeyDown(Keys.D))
                Fool.PlayerVelocity.X = 8;
            if (currentkeyboardstate.IsKeyDown(Keys.A))
                Fool.PlayerVelocity.X = -8;

            if (currentkeyboardstate.IsKeyUp(Keys.W))
            {
                Fool.PlayerVelocity.Y = -8;
            }
                
            if (currentkeyboardstate.IsKeyDown(Keys.W))
                Fool.PlayerVelocity.Y = +8;

            Fool.PlayerVelocity += Fool.PlayerAcceleration;
            Fool.PlayerPosition += Fool.PlayerVelocity;

            int tempx = map.ObjectGroups["PlayerLayer"].Objects["Player"].X + (int)(Fool.PlayerPosition.X);
            int tempy = map.ObjectGroups["PlayerLayer"].Objects["Player"].Y - (int)(Fool.PlayerPosition.Y);
            //now we have moved checkbounds
            
            if (CheckBoundsX() == false)
            {
                map.ObjectGroups["PlayerLayer"].Objects["Player"].X = tempx;
                CheckX = false;
            }
            else
            {
                Fool.PlayerPosition.X = 0;
                Fool.PlayerVelocity.X = 0;
                Fool.PlayerAcceleration.X = 0;
                CheckX = true;
            }
            if (CheckBoundsY() == false)
            {
                map.ObjectGroups["PlayerLayer"].Objects["Player"].Y = tempy;
                CheckY = false;
            }
            else
            {
                Fool.PlayerPosition.Y = 0;
                Fool.PlayerVelocity.Y = 0;
                Fool.PlayerAcceleration.Y = 0;
                CheckY = true;
            }

CheckX (horizontal check)

        public bool CheckBoundsX()
        {
            bool checkX = false;

            Rectangle playrec = new Rectangle(
                map.ObjectGroups["PlayerLayer"].Objects["Player"].X + (int)(Fool.PlayerPosition.X),
                map.ObjectGroups["PlayerLayer"].Objects["Player"].Y - (int)(Fool.PlayerPosition.Y),
                map.ObjectGroups["PlayerLayer"].Objects["Player"].Width,
                map.ObjectGroups["PlayerLayer"].Objects["Player"].Height
                );

            Vector2 centertile = new Vector2(
                (map.ObjectGroups["PlayerLayer"].Objects["Player"].X + (map.ObjectGroups["PlayerLayer"].Objects["Player"].Width)) / 64,
                (map.ObjectGroups["PlayerLayer"].Objects["Player"].Y + (map.ObjectGroups["PlayerLayer"].Objects["Player"].Height / 2)) / 64
                );

            if (collision.GetTile((int)centertile.X + 1, (int)centertile.Y) != 0)
            {
                Rectangle right = new Rectangle(
                    (int)(centertile.X + 1) * 64,
                    (int)centertile.Y * 64,
                    64,
                    64
                    );

                if (playrec.Intersects(right))
                    checkX = true;
            }

            if (collision.GetTile((int)centertile.X - 1, (int)centertile.Y) != 0)
            {
                Rectangle left = new Rectangle(
                    (int)(centertile.X - 1) * 64,
                    (int)centertile.Y * 64,
                    64,
                    64
                    );

                if (playrec.Intersects(left))
                    checkX = true;
            }
return CheckX;
}

CheckY (vertical)

public bool CheckBoundsY()
        {
                bool checkY = false;

                Rectangle playrec = new Rectangle(
                    map.ObjectGroups["PlayerLayer"].Objects["Player"].X + (int)(Fool.PlayerPosition.X),
                    map.ObjectGroups["PlayerLayer"].Objects["Player"].Y - (int)(Fool.PlayerPosition.Y),
                    map.ObjectGroups["PlayerLayer"].Objects["Player"].Width,
                    map.ObjectGroups["PlayerLayer"].Objects["Player"].Height
                    );

                Vector2 centertile = new Vector2(
                    (map.ObjectGroups["PlayerLayer"].Objects["Player"].X + (map.ObjectGroups["PlayerLayer"].Objects["Player"].Width / 2)) / 64,
                    (map.ObjectGroups["PlayerLayer"].Objects["Player"].Y + (map.ObjectGroups["PlayerLayer"].Objects["Player"].Height / 2)) / 64
                    );

            if (collision.GetTile((int)centertile.X, (int)centertile.Y - 1) != 0)
            {
                Rectangle top = new Rectangle(
                    (int)centertile.X * 64,
                    (int)(centertile.Y - 1) * 64,
                    64,
                    64
                    );

                if (playrec.Intersects(top))
                    checkY = true;
            }

            if (collision.GetTile((int)centertile.X, (int)centertile.Y + 1) != 0)
            {
                Rectangle bottom = new Rectangle(
                    (int)centertile.X * 64,
                    (int)(centertile.Y + 1) * 64,
                    64,
                    64
                    );

                if (playrec.Intersects(bottom))
                    checkY = true;
            }


            return checkY;
        }

Any help is welcome. Thanks in advance.

At a guess, this looks suspicious:

            Vector2 centertile = new Vector2(
                (map.ObjectGroups["PlayerLayer"].Objects["Player"].X + (map.ObjectGroups["PlayerLayer"].Objects["Player"].Width)) / 64,
                (map.ObjectGroups["PlayerLayer"].Objects["Player"].Y + (map.ObjectGroups["PlayerLayer"].Objects["Player"].Height / 2)) / 64
                );

You’re looking for the center tile, but getting something relative to (Player.X + Player.Width), which is the right-hand side of Player.

Isn’t PlayerX and PlayerY supposed to represent the top left corner of the sprite?

It is. So X plus Width is the right side. I think you’ve missed a divide by 2.

How could that be, though? If X is the top left, then + Width would cover the width of Player, and not go over into the right side. Plus, that would cause my collision issue to be on the side opposite to the one it’s on. My right side collision doesn’t work, so if the detection was being centred around the right side, it’d detect the wall too early and leave a spot that you can’t ever enter.

when your player is inside wall, set a breakpoint and inspect your values via debugger in your collision code.

That’s the first thing you should do. Check if any values are not how you would expect them, like if the correct tile is getting queried. We don’t know the value of half your variables in the code you presented, so there would be a lot of guessing and this will not necessarily yield a solution. Could be as simple as truncating because you divide by integers without rounding.

Most of us would just use the debugger to find wrong values and identify the issue - debugging is essential.

Once you identified a value which is not as expected, post it here, and we may be able to tell you why it could be the case

Okay, so, let’s make sure I understand your code…

Vector2 centertile = new Vector2(
                (map.ObjectGroups["PlayerLayer"].Objects["Player"].X + (map.ObjectGroups["PlayerLayer"].Objects["Player"].Width)) / 64,
                (map.ObjectGroups["PlayerLayer"].Objects["Player"].Y + (map.ObjectGroups["PlayerLayer"].Objects["Player"].Height / 2)) / 64
                );

“centerTile” is a vector correlating to this location on your player sprite, divided by 64 (64 presumably being the width of your game space in tiles):

--------
|      |
|      X
|      |
--------

You then floor this value with int, to get a tile. Assuming your player sprite is aligned with and exactly as wide as a tile, this will return the tile immediately to the right of your player.

I don’t know why this would create the symptoms you’re describing, but it seems like that isn’t what you’re looking for here.

I’ve bolded the assumptions because, like reiti.net stated, we don’t know half the variables in your code and so there’s a lot of guessing.

One last bit of advise (and please take this as advise rather than criticism), this code could use a good refactoring pass.

The variable names should match what they do, I’d suggest pushing the sprite coords to your player class so you don’t have to pull it from the object list, and some methods for converting between screen space coords and tile coords. You can’t mess up the arithmetic when you’re just calling GetTileUnderPosition(Player.CenterPosition);

You have got to be kidding me, The /2 was the only thing wrong with it. I removed it last time hoping it’d fix the problem with half-clipping into platforms, and it just made the issues worse.

Still, now I do have to fix the problem with half-clipping into platforms. Standing on the edge of one, or walking into one from mid air (where the bottom half is next to it but not both halves) causes the player to clip into the platform and not be able to move in the other axis (clipping by falling causes x movement to not work, and walking into it from the sides causes the y movement to not work)

I’m assuming it would have something to do with walking half-on and half-off the platform, which apparently causes the player to register the next tile as the center tile instead of the one they moved from, and hence to fall into the platform’s side as they’re no longer “on” the platform. It could be solved by truncation, but I think that would pose issues in movement later as the center tile would snap to the one left of it if you were just over the boundary, which would inevitably cause extra problems.

After some more research, I do believe firmly that truncating would not solve the issue. I think I may need a new collision method, as the current one would clearly not work, unless I am able to use non integers when referring to tile coordinates/positions in the tilemap.

What you need is rounding - not truncating. you want to be 0.4 is the left tile and 0.6 is the right tile (measured from center of sprite).

If you measure from left of the sprite, you can do truncation (basically a floor() ). If you measure from the right, you need a ceil(). Of course it only works if you force the calculations to float (just cast a part to float to box the calculation to float as simple as dividing by 2f instead of just 2) and not int … as in the latter it will always truncate. Debugger will tell you when in doubt. There is no need for guessing

Well wouldn’t that still pose problems for exact half-on half-off situations? If it rounds to one of 2 values, it will fall off on one of the two sides. and since 0.5 rounds up, it would always make the right edges of platforms unable to collide with the player, as when you get halfway off the platform, you’d fall through as the collision is now looking at the next tile, which would be past the platform’s edge. I will admit it solves the majority of the other situations, but this exact half on half off one will still be a problem

Not to mention the fact that I got my enemy hitbox code up and running only to find out that my enemy apparently doesn’t have a hitbox…

As long as your character is not limited to moving only to the right, that’s a problem you will always face, for collision you have to check 2 boundaries and not a single point. So in the edge case you actually have to check 2 tiles for collision and not just a single one. As long as you only check against a single tile, you will always fall through it, depending on which direction your character is moving. You need to check all collisions involved.

If your character is limited to only ever move full tiles, you only need to check 1 tile, as the character can only ever be on 1 tile - with smooth movement you have to check for all states inbetween and that may involve more than 1 collision tile

Aha, so I need one floor and one ceiling division. That way, I can cover both the tile on the left, and the tile on the right. With this, it prevents falling through for either scenario, as if I am partially on one platform, I am still on it, so falling through becomes impossible by checking both! Thank you!

Weird. It appears to have only fixed one edge (hang off left of platform, standing on the right), but not the other. I am already checking both tiles under the player, how could this be?

The flooring you already had in your code was getting you the tile directly beneath the center of your sprite. If you’ve just added a ceiling as well, then what you’ve done is get the tile immediately to the right of that tile. You’re still not checking the left.

Try thinking about it in terms of space instead of in terms of math. Which tiles do you actually want to check? The one directly under the left-hand side of your sprite, and the one directly under the right hand side of your sprite (assuming your player is less wide than a tile).

So, instead of getting the centerTile position, try getting the leftFootTile and rightFootTile positions.

My player is exactly a tile wide. Perhaps the flooring of the left side was done wrong. After all, I did floor both the X and Y coordinates for that. Perhaps only the X should have been floored.

Also, if floored, the tile directly beneath the center would give me the one to the left, surely?

Well… no. If you take a position (X+Width/2) divide by the number of tiles ((X+Width/2)/64) and then floor it ( (int)((X+Width/2)/64) ), you get the co-ordinates of the tile it’s in.

So with the code above you’re getting the tile the center of the sprite falls in. Here, a diagram might help:

image

As you can see, in the left and middle cases you’re good: you are checking the cells south of these cells for collisions with the ground. In the right case, you’re not checking the cell to the SW. Even though it is solid, the player will fall through it.

Here we go, I put together a simplified version of your collision code that might help you going forward:

Disclaimer: this code is completely untested.

        protected override void Update(GameTime gameTime)
        {
            //Grab a reference to the player object, since we're using it so often.
            var player = map.ObjectGroups["PlayerLayer"].Objects["Player"];

            //Note: grabbing objects from a global library like this is generally
            //bad practice. It would be better to run this code within 
            //Player.Update(), or feed the Player variable into this via a 
            //parameter or public variable.

            //Reset velocity to 0 at the start of every frame
            player.Velocity.X = 0;

            if (currentkeyboardstate.IsKeyDown(Keys.D))
                player.Velocity.X = 8;
            if (currentkeyboardstate.IsKeyDown(Keys.A))
                player.Velocity.X = -8;

            if (currentkeyboardstate.IsKeyUp(Keys.W))
                player.Velocity.Y = -8;
            if (currentkeyboardstate.IsKeyDown(Keys.W))
                player.Velocity.Y = +8;

            //Work out where the player will be next frame.
            Vector2 futurePlayerPosition = player.Position + Fool.PlayerVelocity;
            //Note: This assumes "player.Position" is the top left corner of the 
            //player sprite.
            Rectangle futurePlayerBounds = new Rectangle(futurePlayerPosition, player.Size); 
            
            if(DetectCollision(futurePlayerBounds))
            {
                //Collision detected, don't move to futurePosition.
            } 
            else
            {
                //No collisions, go ahead and move the player
                player.Position = futurePlayerPosition;
            }
        }
        int tileGridWidth = 64;
        int tileGridHeight = 64;
        public bool DetectCollision(Rectangle bounds)
        {
            //Since we the player is 2 tiles high and has smooth movement, it will 
            //potentially overlap with SIX tiles, any of which could be solid. 
            //Since we're checking for six, we might as well check for any number.
            //That way this code can be used with any size bounding box.
            int minTileCoodX = (int)(bounds.X / tileGridWidth);
            int maxTileCoodX = (int)((bounds.X + bounds.Width) / tileGridWidth);
            int minTileCoodY = (int)(bounds.Y / tileGridHeight);
            int maxTileCoodY = (int)((bounds.Y + bounds.Width) / tileGridHeight);

            //Loop through all the tiles between the Rectangle's minimum and 
            //maximum bounds
            for (int x = minTileCoodX; x <= maxTileCoodX; x++)
            {
                for (int y = minTileCoodY; y <= maxTileCoodY; y++)
                {
                    //And check if the tile is solid:
                    if (collision.GetTile(x, y) != 0)
                    {
                        //You don't need to do a ".Intersects()" check here. This
                        //tile is solid, and the rectangle bounds fall within it, 
                        //therefore a collision has been detected.
                        return true;
                    }
                }
            }

            return false;
        }

So, one floor (center)
One ceiling (right)
One floor and with x-1 (left)
And this should all work?

Sorry for not using your code, this is a coursework project, and we were instructed to research it ourselves, but just taking code feels a bit like cheating (plus retrofitting the rest of my code to work with the new method would probably waste time I don’t have)