0

In my project I'm trying to implement discrete AABB collisions between two solid objects. The code I have works fine for two objects of the same size, but for two objects of different sizes the collision happens when the two sprites are clearly not touching. The distance between the two sprites when they are colliding is greater the more difference there is between the sizes of the objects.

Why does that happen and how can I change the code so collisions work as intended for two objects of different sizes?

The relevant code the way it currently is:

The Box struct represents a bounding box. It also has methods for checking intersections. Intersections are checked by the way of negation

public struct Box
{
    #region data
    public Vector2 TopLeft { get; set; }
    public Vector2 BottomRight{ get; set; }
    public float Width
    {
        get
        {
            return Math.Abs(BottomRight.X - TopLeft.X);
        }
    }
    public float Height
    {
        get
        {
            return Math.Abs(BottomRight.Y - TopLeft.Y);
        }
    }
    #endregion
    #region c'tor
    public Box(Vector2 tl, Vector2 br) : 
        this()
    {
        this.TopLeft = tl;
        this.BottomRight = br;
    }
    public Box(float top, float bottom, float left, float right) :
        this(new Vector2(left, top), new Vector2(right, bottom))
    {

    }
    public Box(Vector2 tl, float width, float height) :
        this(tl, new Vector2(tl.X + width, tl.Y + height))
    {

    }
    #endregion
    #region methods
    public bool Intersects(Box other)
    {
        return (IntersectsX(other) && IntersectsY(other)) || IsContained(other);
    }
    public bool IntersectsY(Box other)
    {
        return !((TopLeft.Y <= other.TopLeft.Y && BottomRight.Y <= other.TopLeft.Y) || (TopLeft.Y >= other.BottomRight.Y && BottomRight.Y >= other.BottomRight.Y));
    }
    public bool IntersectsX(Box other)
    {
        return !((TopLeft.X <= other.TopLeft.X && BottomRight.X <= other.TopLeft.X) || (TopLeft.X >= other.BottomRight.X && BottomRight.X >= other.BottomRight.X));
    }
    public bool IsContained(Box other)//checks if other is contained in this Box
    {
        return (TopLeft.X > other.TopLeft.X) && (TopLeft.X < other.BottomRight.X) && (TopLeft.Y > other.TopLeft.Y) && (TopLeft.Y < other.BottomRight.Y) &&
               (BottomRight.X > other.TopLeft.X) && (BottomRight.X < other.BottomRight.X) && (BottomRight.Y > other.TopLeft.Y) && (BottomRight.Y < other.BottomRight.Y);
    }
    #endregion
}

An object with a collider has to implement the IBoundingBoxCollider interface: public interface IBoundingBoxCollider { #region data Box BoundingBox { get; } bool SolidCollider { get; set; } bool StaticCollider { get; set; } float DistanceTraveledThisFrame { get; } #endregion #region methods void CheckCollision(IBoundingBoxCollider other); #endregion }

A character is represented by the Character class. This class has the CheckCollisions and UpdateCollider methods. UpdateCollider is also called in the class constructor. The Character class inherits from AnimatedObject, the class that handles animation, which inherits from DrawableObject, which handles drawing sprites to the screen. DrawableObject has the Position, Rotation and Scale properties. The Draw and Update methods handle static events in Game1 which are called on Draw and Update accordingly.

public virtual void CheckCollision(IBoundingBoxCollider other)
    {
        if (BoundingBox.Intersects(other.BoundingBox))
        {
            //on collision
            float newX = Position.X, newY = Position.Y;
            if (directionLastMoved == Directions.Up || directionLastMoved == Directions.Up_Left || directionLastMoved == Directions.Up_Right)
                newY = other.BoundingBox.BottomRight.Y;
            if (directionLastMoved == Directions.Down || directionLastMoved == Directions.Down_Left || directionLastMoved == Directions.Down_Right)
                newY = other.BoundingBox.TopLeft.Y - BoundingBox.Height;
            if (directionLastMoved == Directions.Left || directionLastMoved == Directions.Up_Left || directionLastMoved == Directions.Down_Left)
                newX = other.BoundingBox.BottomRight.X;
            if (directionLastMoved == Directions.Right || directionLastMoved == Directions.Up_Right || directionLastMoved == Directions.Down_Right)
                newX = other.BoundingBox.TopLeft.X - BoundingBox.Width;
            Vector2 newPos = new Vector2(newX, newY);
            float ratio = DistanceTraveledThisFrame / (DistanceTraveledThisFrame + other.DistanceTraveledThisFrame);
            if(other.StaticCollider || ratio.Equals(float.NaN))
                Position = newPos;
            else
            {
                Vector2 delta = (newPos - Position) * ratio;
                Position += delta;
            }
            UpdateCollider();
        }
    }
    protected override void Draw(GameTime gameTime)
    {
        base.Draw(gameTime);
        UpdateCollider();
    }
    protected virtual void Update(GameTime gameTime)
    {
        lastPosition = Position;
    }
    protected void UpdateCollider()
    {
        Rectangle currentFrameBox = this.animator.CurrentAnimation.CurrentFrame(0).Frame;
        BoundingBox = new Box(Position, currentFrameBox.Width * Scale, currentFrameBox.Height * Scale);
    }

In the Game1 class there is a List. Every update the list is being iterated over and CheckCollision is called for every two colliders:

protected override void Update(GameTime gameTime)
    {
        // Allows the game to exit
        if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
            this.Exit();

        // TODO: Add your update logic here
        foreach (IBoundingBoxCollider collider in colliderList)
        {
            if (collider.StaticCollider)
                continue;
            foreach (IBoundingBoxCollider other in colliderList)
            {
                if (collider != other)
                    collider.CheckCollision(other);
            }
        }
        if (InputEvent != null)
            InputEvent(gameTime, Keyboard.GetState(), Mouse.GetState());
        if (UpdateEvent != null)
            UpdateEvent(gameTime);
        base.Update(gameTime);
    }

EDIT1

Tried the solution by Monset, replacing the Box struct with the class Monset made, with a few naming changes:

public class Box : Rectangle
{
    #region data
    public Vector2 Position;
    public float Width, Height;
    public Rectangle GetRectangle
    { get { return new Rectangle((int)Position.X, (int)Position.Y, (int)Width, (int)Height); } }
    #endregion
    #region c'tor
    public Box(Vector2 position, float width, float height)
    {
        this.Position = position;
        this.Width = width;
        this.Height = height;
    }
    #endregion
}

It gives me cannot derive from sealed type 'Microsoft.Xna.Framework.Rectangle'

user1461837
  • 91
  • 2
  • 11

1 Answers1

0

Instead of AABB, use Rectangle class. This class has predefined functions like Intersects and Contains, and it is still a rectangle, and you can still change its position, width and height. If you want do define a Rectangle by two points, create a new class (this is a c#/pseudo code):

public class BoundingBox: Rectangle
{
    ...
    public Point TopLeft
    { get{ this.X = value.X; this.Y = value.Y; } }
    public Point BottomRight
    { get{ this.Width = value.X - this.X; this.Height = value.Y - this.Y; } }
    ...
}

If you still don't want to do it the easy way, the way someone who built XNA intended, try googling "how to check collision between two rectangles", and implement those formulas in your Intersects function. I don't recommend this, because you'll waste time with something that you already have (Rectangle). Take this last paragraph as my expirience with this, because I tried what you're trying now, and got to the point where I saw that my custom BoundingBox works pretty much the same as the Rectangle class that's included in XNA.

EDIT 1:
As asked in comment, for perserving position using decimal numbers (float), you could store 3 variables (position, width and height) in float and create a function, or a variable that will represent those variables in Rectangle. Doing this will perserve more information (because everything is stored in decimal numbers) and give you functionality that Rectangle class provides. That would look something like this (not tested):

public class BoundingBox: Rectangle
{
    // PUBLIC
    public Vector2 Position;
    public float Width, Height,
    public Rectangle Box
    { get { return new Rectangle((int)Position.X, (int)Position.Y, (int)Width, (int)Height); } }

    // or, if you don't understand get/set, create function
    public Rectange Box()
    { return new Rectangle((int)Position.X, (int)Position.Y, (int)Width, (int)Height); } 
}

If you're a beginner, please do not worry about CPU or RAM here. This will use more RAM space and CPU time, but you'll get to worry about that when your game starts lagging. For now, practice-practice-practice. If you're not a beginner, you know what you're doing.

Monset
  • 648
  • 5
  • 25
  • That was my first thought, but Rectangle's x and y are ints, and positions along the axes are represented with floats. Would that solve the problem I'm having? – user1461837 May 27 '16 at 15:37
  • Well, if this is one of your first projects, I recommend using Rectangle class both for collisions and for calculating positions. Get used to it's functionality. But, if you know that you will need decimal numbers in the future, you could do something like EDIT 1 – Monset May 27 '16 at 18:00
  • Please do consider checking my answer if it helped you. It helps me, and other people who open your question to know wether it helped you or not. – Monset May 28 '16 at 07:42