Danger Penguin
Danger Penguin

Reputation: 1

2D Continous Collision Detection in Monogame/Xna Framework

I'm working on a custom physics engine for a potential game - based in Monogame/the XNA framework. While the physics itself works pretty well, I'm running into an issue with collision. When the player comes out of a jump, they can often end up inside the floor. See image below. I did a couple hours of research on my own and found out that what I probably need is continous collision detection (CCD) similar to how something like Unity might implement it, but all the questions I've found here or other places haven't really worked, and neither has any of my solutions, so I'm asking the strangers on the internet who are smarter than me.

Here's game 1:

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using MonoGame.Extended;
using AmethystDawn.Utilities;

namespace AmethystDawn
{
    internal class Game1 : Game
    {
        Texture2D spritesheet;
        Texture2D spritesheetFlipped;
        Texture2D activeSpritesheet;
        Texture2D platform;
        float timer; // millisecond timer
        int threshold;
        Rectangle[] sourceRectangles;
        byte previousAnimationIndex;
        byte currentAnimationIndex;
        RectangleF playerCollider;
        RectangleF groundCollider;

        PhysicsCalculator physics = new();

        Vector2 PlayerPos = new Vector2(0, 0);

        private GraphicsDeviceManager graphics;
        private SpriteBatch sprites;
        private SpriteFont font;

        public Game1() : base()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
            IsFixedTimeStep = true;
            IsMouseVisible = true;
            IsFixedTimeStep = false;
        }

        protected override void Initialize()
        {
            base.Initialize();
        }

        protected override void LoadContent()
        {
            graphics.PreferredBackBufferWidth = GraphicsDevice.DisplayMode.Width;
            graphics.PreferredBackBufferHeight = GraphicsDevice.DisplayMode.Height;
            graphics.IsFullScreen = true;
            graphics.HardwareModeSwitch = false;
            graphics.ApplyChanges();
            sprites = new SpriteBatch(GraphicsDevice);
            font = Content.Load<SpriteFont>("Fonts/november");
            spritesheet = Content.Load<Texture2D>("Sprites/Player/player spritesheet");
            spritesheetFlipped = Content.Load<Texture2D>("Sprites/Player/player spritesheet flipped");
            platform = Content.Load<Texture2D>("Sprites/platform");
            activeSpritesheet = spritesheet;
            timer = 0;
            threshold = 100;
            sourceRectangles = new Rectangle[4];
            sourceRectangles[0] = new Rectangle(0, 0, 32, 40);
            sourceRectangles[1] = new Rectangle(34, 0, 28, 40);
            sourceRectangles[2] = new Rectangle(66, 0, 28, 40);
            sourceRectangles[3] = new Rectangle(96, 0, 32, 40);
            previousAnimationIndex = 2;
            currentAnimationIndex = 1;
            base.LoadContent();
        }

        protected override void UnloadContent()
        {
            base.UnloadContent();
        }

        protected override void Update(GameTime gameTime)
        {
            if (timer > threshold) // check if the timer has exceeded the threshold
            {
                if (currentAnimationIndex == 1) // if sprite is in the middle sprite of the animation
                {
                    if (previousAnimationIndex == 0) // if the previous animation was the left-side sprite, then the next animation should be the right-side sprite
                    {
                        currentAnimationIndex = 2;
                    }
                    else
                    {
                        currentAnimationIndex = 0; // if not, then the next animation should be the left-side sprite
                    }
                    previousAnimationIndex = currentAnimationIndex;
                }
                else
                {
                    currentAnimationIndex = 1; // if not in the middle sprite of the animation, return to the middle sprite
                }
                timer = 0;
            }
            else
            {
                // if the timer has not reached the threshold, then add the milliseconds that have past since the last Update() to the timer
                timer += (float)gameTime.ElapsedGameTime.TotalMilliseconds;
            }

            base.Update(gameTime);
        }

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            sprites.Begin();

            sprites.Draw(platform, new Vector2(0, GraphicsDevice.Viewport.Height - platform.Height - 50), Color.White);
            groundCollider = new RectangleF(0, GraphicsDevice.Viewport.Height - platform.Height - 50, platform.Width, platform.Height);

            var kstate = Keyboard.GetState();

            playerCollider = new(PlayerPos.X, PlayerPos.Y, sourceRectangles[currentAnimationIndex].Width, sourceRectangles[currentAnimationIndex].Height);
            if (IsColliding(groundCollider, playerCollider))
            {
                physics.UpdatePhysicValues(false);
                /*if (PlayerPos.Y + playerCollider.Height + 100 > groundCollider.Y)
                {
                    PlayerPos.Y = groundCollider.Y - groundCollider.Height;
                }*/
                if (kstate.IsKeyDown(Keys.Space))
                {
                    physics.Jump(3f);
                }
            }
            else
            {
                physics.UpdatePhysicValues(true);
                if (kstate.IsKeyDown(Keys.Space))
                {
                    physics.MidairJump(3f);
                }
                else
                {
                    physics.LockJump();
                }
            }

            if (kstate.IsKeyDown(Keys.A))
            {
                physics.ApplyWalkingForce(new Vector2(-1, 0), 0.5f);
                activeSpritesheet = spritesheetFlipped;
                sourceRectangles[0] = new Rectangle(0, 0, 32, 40);
                sourceRectangles[1] = new Rectangle(34, 0, 28, 40);
                sourceRectangles[2] = new Rectangle(66, 0, 28, 40);
                sourceRectangles[3] = new Rectangle(96, 0, 32, 40);
            }
            else if (kstate.IsKeyDown(Keys.D))
            {
                physics.ApplyWalkingForce(new Vector2(1, 0), 0.5f);
                activeSpritesheet = spritesheet;
                sourceRectangles[0] = new Rectangle(96, 0, 32, 40);
                sourceRectangles[1] = new Rectangle(66, 0, 28, 40);
                sourceRectangles[2] = new Rectangle(34, 0, 28, 40);
                sourceRectangles[3] = new Rectangle(0, 0, 32, 40);
            }
            else
            {

            }

            if (kstate.IsKeyDown(Keys.S) && !IsColliding(groundCollider, playerCollider))
            {
                physics.ApplyExtraGravity(1f);
            }

            if (kstate.IsKeyDown(Keys.R))
            {
                PlayerPos = new Vector2(0, 0);
            }

            PlayerPos = physics.position(PlayerPos);

            // is player on the bounds of the screen
            if (PlayerPos.X < 0)
            {
                PlayerPos.X = 0;
                physics.HitWall();
            }
            else if (PlayerPos.X > GraphicsDevice.Viewport.Width - 32)
            {
                PlayerPos.X = GraphicsDevice.Viewport.Width - 32;
                physics.HitWall();
            }
            sprites.Draw(activeSpritesheet, PlayerPos, sourceRectangles[currentAnimationIndex], Color.White, 0f, new Vector2(0, 0), 1f, SpriteEffects.None, 0f);
            sprites.End();


            base.Draw(gameTime);
        }

        private bool IsColliding(RectangleF rect1, RectangleF rect2)
        {
            return rect1.Intersects(rect2);
        }
    }
}

And here's the physics calculator:

using System.Diagnostics;
using Microsoft.Xna.Framework;

namespace AmethystDawn.Utilities
{
    internal class PhysicsCalculator
    {
        private float directionalForce;
        private Vector2 direction;
        private const float directionalForceMax = 10f;
        private float walkingForce;
        private const float walkingForceMax = 0.5f;
        private float gravityForce;
        private const float gravityForceMax = 25f;
        private float jumpForce;
        private const float jumpForceMax = 5f;
        private int framesInAir;
        private const int framesInAirMax = 90;
        
        public void UpdatePhysicValues(bool falling)
        {
            if (directionalForce > 0)
            {
                directionalForce -= 0.5f;
            }

            if (walkingForce > 0)
            {
                walkingForce -= 0.02f;
            }
            else
            {
                walkingForce = 0;
            }

            if (gravityForce > jumpForce)
            {
                if (falling && !(gravityForce > gravityForceMax))
                {
                    gravityForce += 0.2f;
                }
                else if (!falling)
                {
                    gravityForce = 0;
                    direction.Y = 0;
                    framesInAir = 0;
                }
            }
            else
            {
                jumpForce -= 0.3f;
            }

            FixDirection();
        }

        public void ApplyDirectionalForce(Vector2 directionHit, float forceToApply)
        {
            direction += directionHit;
            directionalForce += forceToApply;
            if (directionalForce > directionalForceMax) directionalForce = directionalForceMax;
        }

        public void ApplyWalkingForce(Vector2 directionWalked, float forceToApply)
        {
            direction += directionWalked;
            walkingForce += forceToApply;
            if (walkingForce > walkingForceMax) walkingForce = walkingForceMax;
        }

        public void Jump(float force)
        {
            direction += new Vector2(0, -1);
            jumpForce += force;
            if (jumpForce > jumpForceMax) jumpForce = jumpForceMax;
        }

        public void MidairJump(float force)
        {
            framesInAir++;
            if (framesInAir > framesInAirMax) return;
            jumpForce += force;
            if (jumpForce > jumpForceMax) jumpForce = jumpForceMax;
        }

        public void LockJump()
        {
            framesInAir = framesInAirMax;
        }

        public void ApplyExtraGravity(float amount)
        {
            gravityForce += amount;
        }

        public Vector2 position(Vector2 currentPosition)
        {
            currentPosition += new Vector2(0, gravityForce);
            currentPosition += new Vector2(direction.X * directionalForce, direction.Y * directionalForce);
            currentPosition += new Vector2(direction.X * walkingForce, direction.Y * walkingForce);
            currentPosition += new Vector2(0, direction.Y * jumpForce);
            return currentPosition;
        }

        public void HitWall()
        {
            direction.X = 0;
        }

        private void CorrectGravity()
        {

        }

        private void FixDirection()
        {
            if (direction.X > 20) direction.X = 20;
            if (direction.Y > 20) direction.Y = 20;
            if (direction.X < -20) direction.X = -20;
            if (direction.Y < -15) direction.Y = -15;

            if (walkingForce <= 0 && directionalForce <= 0) direction.X = 0;
        }
    }
}

And the image:

See image of that here.

Upvotes: 0

Views: 1631

Answers (1)

Steven
Steven

Reputation: 2123

I remember watching a tutorial that explained the way of handling continious collision, but that was done in GameMaker Studio 2. I'll try to translate it to XNA.

In short: You need to check for collision ahead of you with calculating the collision with the current speed beforehand, then let the player approach the solid object 1 pixel at a time through a while loop, and once it hits, then set the velocity in that direction to 0.

Original GMS2 code:

if place_meeting(x+hspeed_, y, o_solid) {
    while !place_meeting(x+sign(hspeed_), y, o_solid) {
        x += sign(hspeed_);
    }
    hspeed_ = 0;
}
x += hspeed_;
 

translated to XNA (dummy code as quick example):

private bool IsColliding(RectangleF rect1, RectangleF rect2, int vspeed)
{
    if (rect1.Intersects(new Rectangle(rect2.x, rect2.y + vspeed, rect2.Width, rect2.Height))
    {
        while (!rect1.Intersects(new Rectangle(rect2.x, rect2.y+Sign(vspeed), rect2.Width, rect2.Height)
        {
            rect1 += Sign(vspeed) //moves towards the collision 1 pixel at a time
        }
        return true;
    }
    return false;
}

//Sign is a build-in function in GMS2 that only returns 1, 0 or -1, depending if the number is positive, 0 or negative
private int Sign(value)
{
    return (value > 0) ? 1 : (value < 0) ? -1 : 0;
}

Source: https://youtu.be/zqtT_9eWIkM?list=PL9FzW-m48fn3Ya8QUTsqU-SU6-UGEqhx6

Upvotes: 1

Related Questions