0

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?

RyanJK5
  • 1
  • 1

1 Answers1

0

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

  • I tried adding this to the code, but it didn't seem to change the results. I even resized the player to be the same as the tile (40x40) and the issue was still happening. And about the ECS--yeah, I prefer a more traditional approach as well, I just saw the option is this framework and wanted to see what it's like to use it for the experience. Making everything fit together was definitely a pain and they still don't completely, lol. I'm probably going to refactor this if I decide to work on it long-term. – RyanJK5 Nov 04 '22 at 21:43
  • @RyanJK5, I have been using similar code for over 10 years for moving objects' bounces, with velocity correction code(not needed for static objects). The only thing left is gravity correction: Instead of: `velocity.DirY = 0;` try `velocity.DirY = -1;` Use the second version of the code and combine all collision surfaces , both vertical and horizontal into a single object. –  Nov 09 '22 at 01:42
  • What do you mean by the last sentence? – RyanJK5 Nov 09 '22 at 01:45
  • Make your object collision surfaces larger, combining blocks to provide a single surface collision. –  Nov 09 '22 at 03:06