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
我正在为潜在游戏开发自定义物理引擎 - 基于 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