2
\$\begingroup\$

So I’m writing a player class and using floats to store the positon and velocity (Vector2) as well as using it when drawing. When it comes to collision I get the player bounds using this code: public Rectangle Bounds => new Rectangle((int)_pos.X, (int)_pos.Y, 12, 23); to test it against solid blocks.

The issue comes when I apply gravity to the player and the player moves in <1 increments per frame eg. first frame 0, second frame 0.25, etc. This causes the player to jitter when above a block due to him falling down little by little until the collision code (which uses ints) pushes it back up.

One solution I came across is to use the Bounds I calculated earlier for drawing, instead of using the position directly. However there is another problem (because the problems never stop coming)… Now the jitter occurs behind the scenes causing the ground detection to not work. How can I make the float positioning and int collision system work in harmony??

The player class' file:

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
 
public class Player
{
    public Rectangle Bounds
        => new Rectangle((int)_pos.X, (int)_pos.Y, 16, 16);
 
    Vector2 _prevPos;
    Vector2 _pos;
    Vector2 _vel;
 
    World _world;
 
    const float GRAVITY = 500;
    const float MOVE_SPEED = 100;
    const float JUMP_FORCE = 275;
    const float MAX_FALL_SPEED = 350;
 
    public Player(Vector2 position)
    {
        _pos = position;
        _world = Main.World;
    }
 
    public void Update(float dt)
    {
        // Gravity
        _vel.Y += GRAVITY * dt;
        _vel.Y = MathHelper.Clamp(_vel.Y, -MAX_FALL_SPEED, MAX_FALL_SPEED);
 
        // Horizontal movement
        if (Input.IsKeyHeld(Keys.A))
            _vel.X = -MOVE_SPEED;
        else if (Input.IsKeyHeld(Keys.D))
            _vel.X = MOVE_SPEED;
        else
            _vel.X = 0f;
 
        // Jumping
        if (Input.IsKeyDown(Keys.Space))
            _vel.Y = -JUMP_FORCE;
 
        // Calculate position
        _prevPos = _pos;
        _pos += _vel * dt;
 
        HandleCollisions();
 
        System.Console.WriteLine("Velocity: {0}", _vel);
    }
 
    void HandleCollisions()
    {
        Rectangle bounds = Bounds;
        Rectangle prevBounds = new Rectangle((int)_prevPos.X, (int)_prevPos.Y, bounds.Width, bounds.Height);
 
        // Only check blocks around player
        int left = bounds.Left / TileData.TILE_SIZE;
        int right = bounds.Right / TileData.TILE_SIZE;
        int top = bounds.Top / TileData.TILE_SIZE;
        int bottom = bounds.Bottom / TileData.TILE_SIZE;
 
        for (int x = left; x <= right; x++)
        {
            for (int y = top; y <= bottom; y++)
            {
                // Not a solid block
                if (_world.GetBlock(x, y) == -1)
                    continue;
 
                Rectangle blockBounds = new Rectangle(x * TileData.TILE_SIZE, y * TileData.TILE_SIZE, TileData.TILE_SIZE, TileData.TILE_SIZE);
 
                // Resolve collision on x axis
                if (bounds.Intersects(blockBounds))
                {
                    // Previously to left of block
                    if (prevBounds.Right <= blockBounds.Left)
                    {
                        _pos.X = blockBounds.Left - bounds.Width;
                        _vel.X = 0f;
                    }
                    // Previoulsy to right of block
                    else if (prevBounds.Left >= blockBounds.Right)
                    {
                        _pos.X = blockBounds.Right;
                        _vel.X = 0f;
                    }
                }
 
                // Perform further collision using new bounds
                bounds = Bounds;
 
                // Resolve collision on y axis
                if (bounds.Intersects(blockBounds))
                {
                    // Previously above block
                    if (prevBounds.Bottom <= blockBounds.Top)
                    {
                        _pos.Y = blockBounds.Top - bounds.Height;
                        _vel.Y = 0f;
 
                        // _isGrounded = true; Doesn't work due to float and int mismatch
                    }
                    // Previoulsy below block
                    else if (prevBounds.Top >= blockBounds.Bottom)
                    {
                        _pos.Y = blockBounds.Bottom;
                        _vel.Y = 0f;
                    }
                }
            }
        }
    }
 
    public void Draw(SpriteBatch spriteBatch)
    {
        spriteBatch.FillRectangle(Bounds, Color.White);
    }
}
\$\endgroup\$
1
  • 3
    \$\begingroup\$ I've edited your question to put the code into it. While I understand you don't want to cluster the question, the code tags make it readable within the question and it will avoid the question becoming non-understandable if the link becomes invalid. \$\endgroup\$
    – Sacha
    Commented Jun 28, 2021 at 8:23

1 Answer 1

1
\$\begingroup\$

You want to make it so that 'bottom' resolves into the next tile when there is a small increase in _pos.Y, the same goes for 'right' and _pos.X. This will also fix the edge case when you're exactly aligned within a tile ie. 'top' and 'bottom' will be within the same tile in that case.

So to calculate the left, right, top and bottom, do something like the following:

const int maxCells = 0x1000;            // Arbitrarily large. Beware float precision issues.
int pixels = maxCells * TileWidth;
int xLeft = (int)_pos.X / TileWidth;
int xRight = maxCells - (int)(pixels - _pos.X) / TileWidth;    // Aligns correctly within a tile in the edge case
int yTop = (int)_pos.Y / TileHeight;
int yBottom = maxCells - (int)(pixels - _pos.Y) / TileHeight;

Basically for 'bottom' and 'right' you're calculating the tile position from the other side and negating to give the desired result. The reason for subtracting from a constant is to keep everything in the positive range for the (int) cast.

With this you should be able to detect when 'bottom' enters the ground correctly.

\$\endgroup\$
1
  • \$\begingroup\$ Your code assumes TileWidth == TileHeight or in the given code TileData.TILE_SIZE == 16. It would be better to split the pixels variable into x and y with the second using TileHeight as the multiplier. As for maxCells use (int)(1/(MOVE_SPEED * dt) +1) where dt <= Game1.TargetElapsedTime for full precision at 60FPS and a movement speed of 100 use 0x1771. \$\endgroup\$
    – user122973
    Commented Oct 29, 2021 at 1:50

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .