Monogame/Xna 框架中的二维连续碰撞检测

2D Continous Collision Detection in Monogame/Xna Framework

我正在为潜在游戏开发自定义物理引擎 - 基于 Monogame/the XNA 框架。虽然物理本身运行良好,但我 运行 遇到了碰撞问题。当玩家从跳跃中跳出来时,他们通常会落在地板内。 See image below. 我自己做了几个小时的研究,发现我可能需要的是连续碰撞检测 (CCD),类似于 Unity 之类的东西可能实现它的方式,但我在这里找到的所有问题或其他地方都没有真正奏效,我的解决方案也没有,所以我在网上问问比我聪明的陌生人

这是游戏 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);
        }
    }
}

这是物理计算器:

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;
        }
    }
}

和图像:

我记得看过一个解释如何处理连续碰撞的教程,但那是在 GameMaker Studio 2 中完成的。 我会尝试将它翻译成 XNA。

简而言之:你需要通过预先计算当前速度的碰撞来检查前方是否有碰撞,然后让玩家通过while循环一次1个像素地接近固体物体,一旦碰撞,然后将那个方向的速度设置为0.

原始 GMS2 代码:

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

翻译成 XNA(虚拟代码作为快速示例):

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;
}

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