RyanJK5
RyanJK5

Reputation: 1

Collision correction pushing player in the wrong direction

I'm currently working on a 2D game using an ECS with a character that's able to move left and right and make a small jump. I'm using a velocity component to control the player's movement, which is updated like this during every game tick:

private const float MaximumVerticalVelocity = 15f;
private const float VerticalAcceleration = 1f;
private const float MaximumHorizontalVelocity = 10f;
private const float HorizontalAcceleration = 1f;

private void move(GameTime gameTime, int entityID) {
    float speed = HorizontalAcceleration;
    var hitbox = (RectangleF) _colliderMapper.Get(entityID).Bounds;
    var keyInputs = _inputMapper.Get(entityID);
    var velocity = _velocityMapper.Get(entityID);
    var position = _positionMapper.Get(entityID);

    updateVelocity(velocity, 0, VerticalAcceleration);
    if (Keyboard.GetState().IsKeyDown(keyInputs[PlayerAction.Jump]) &&
        hitbox.Bottom < Game.Main.MapHeight && hitbox.Bottom > 0 && (
        !Game.Main.TileIsBlank(position.X, hitbox.Bottom) ||
        !Game.Main.TileIsBlank(hitbox.Right - 1, hitbox.Bottom)
        )) {
            velocity.DirY = -11f;
    }
    if (Keyboard.GetState().IsKeyDown(keyInputs[PlayerAction.Sprint])) {
        speed *= 2;
    }

    bool leftDown = Keyboard.GetState().IsKeyDown(keyInputs[PlayerAction.Left]);
    bool rightDown = Keyboard.GetState().IsKeyDown(keyInputs[PlayerAction.Right]);
    if (leftDown && !(rightDown && velocity.DirX <= 0)) {
        updateVelocity(velocity, -speed, 0);
    }
    else if (!leftDown && velocity.DirX <= 0) {
        updateVelocity(velocity, speed, 0, 0, MaximumVerticalVelocity);
    }
    if (rightDown && !(leftDown && velocity.DirX >= 0)) {
        updateVelocity(velocity, speed, 0);
    }
    else if (!rightDown && velocity.DirX >= 0) {
        updateVelocity(velocity, -speed, 0, 0, MaximumVerticalVelocity);
    }
    if (!leftDown && !rightDown && velocity.DirX != 0) {
        if (velocity.DirX < 0) {
            updateVelocity(velocity, speed, 0, 0, MaximumVerticalVelocity);
        }
        else {
            updateVelocity(velocity, -speed, 0, 0, MaximumVerticalVelocity);
        }
    }
    position.X += velocity.DirX;
    position.Y += velocity.DirY;
}

private void updateVelocity(Velocity velocity, float x, float y, float xLimit, float yLimit) {
    if ((x >= 0 && velocity.DirX + x < xLimit) || (x < 0 && velocity.DirX + x > -xLimit)) {
        velocity.DirX += x;
    }
    else {
        if (x >= 0) {
            velocity.DirX = xLimit;
        }
        else {
            velocity.DirX = -xLimit;
        }
    }
    
    if ((y >= 0 && velocity.DirY + y < yLimit) || (y < 0 && velocity.DirY + y > -yLimit)) {
        velocity.DirY += y;
    }
    else {
        if (y >= 0) {
            velocity.DirY = yLimit;
        }
        else {
            velocity.DirY = -yLimit;
        }
    }
}

private void updateVelocity(Velocity velocity, float x, float y) => 
        updateVelocity(velocity, x, y, MaximumHorizontalVelocity, MaximumVerticalVelocity);

The player and every tile are in a collision system provided by the framework (MonoGame.Extended). All of them have rectangular hitboxes. This is the current code I'm using to resolve collisions when the player collides with a tile:

private void onCollision(int entityID, object sender, CollisionEventArgs args) {
        if (args.Other is StaticCollider) {
            var velocity = _velocityMapper.Get(entityID);
            var position = _positionMapper.Get(entityID);
            var collider = _colliderMapper.Get(entityID);
            var intersection = collider.RectBounds.Intersection((RectangleF) args.Other.Bounds);
            var otherBounds = (RectangleF) args.Other.Bounds;

            if (intersection.Height > intersection.Width) {
                if (collider.RectBounds.X < otherBounds.Position.X) {
                    position.X -= intersection.Width;
                }
                else {
                    position.X += intersection.Width;
                }
                velocity.DirX = 0;
            }
            else {
                if (collider.RectBounds.Y < otherBounds.Y) { 
                    position.Y -= intersection.Height;
                }
                else {
                    position.Y += intersection.Height;
                }
                velocity.DirY = 0;
            }
            collider.RectBounds.X = position.X;
            collider.RectBounds.Y = position.Y;
        }
    }

The issue is that when the player jumps and lands on the tile in such a way that the width of the intersection is shorter than the height, the player is pushed sideways rather than upwards. (shown here and here) What do I do in this situation?

Upvotes: 0

Views: 95

Answers (1)

user10316640
user10316640

Reputation:

The line of code:

if (intersection.Height > intersection.Width)

Only works if the rectangles are:

  1. Square: Check
  2. Same size: Problem here.

Done properly his gives the following four collision zones, formed from the blue diagonals:

A red square with blue diagonal lines perfectly intersecting the corners.

This is the actual test you are performing (not to scale):

Red square with blue diagonals intersecting several pixels horizontally inset from the corners

The reason it moves to the right is the order the collision checks occur.

Solution

Since the aspect ratio is the same, width / height = 1, for both objects:

// ...
var adj = (float)collider.RectBounds.Width / otherBounds.Height;

if (intersection.Height * adj > intersection.Width) {
// ...

If they are not the same aspect ratio: Add a second adj2 variable:

// ...
var adj = (float)collider.RectBounds.Width / otherBounds.Height;
var adj2 = (float)otherBounds.Height / collider.RectBounds.Width;
if (intersection.Height * adj > intersection.Width * adj2) {
// ...

I will say that your programming style/approach/methodology, ECS, does not scale beyond small games.

In C# put the methods inside of the classes they will be used in or in a base class and derive classes from it, if they are not related use an interface.

Mappers are not needed, the just take up space and time being on the heap.

Function calls are expensive:

If a multiple calls to the same function, Keyboard.GetState() returns the same value store the value in a local variable.

Minimize the number of casts and dots, .: like args.Other.Bounds. The parameter CollisionEventArgs args needs to be simplified to: Rectanglef otherBounds

Upvotes: 0

Related Questions