问题描述
我正在为平台游戏创建 MonoGame 2D 引擎框架,但在创建碰撞响应系统时遇到了问题。尽管我已经让 SAT 检测 起作用了,但响应会沿着静态物体边缘的实际方向而不是其正常方向传播。反转法线的轴对我不起作用,也没有任何作用,它只会产生涉及身体离开屏幕的故障。
由于我正在尝试制作平台游戏,因此我只希望将静态身体的法线视为响应方向。例如,如果静态物体是一个盒子,我只希望移动物体在 90 度法线上移动。
这是问题的实际操作视频:https://www.youtube.com/watch?v=-wyXfZkxis0
以及“碰撞”模块的源代码,其中包含所有相关的几何计算(底部的平移向量算法):
using System;
using Microsoft.Xna.Framework;
namespace Crossfrog.ferrum.Engine.Modules
{
public static class Collision
{
public static bool RectsCollide(Rectangle rect1,Rectangle rect2)
{
return
rect1.X <= rect2.X + rect2.Width &&
rect1.Y <= rect2.Y + rect2.Height &&
rect1.X + rect1.Width >= rect2.X &&
rect1.Y + rect1.Height >= rect2.Y;
}
private static float DotProduct(Vector2 v1,Vector2 v2)
{
return (v1.X * v2.X) + (v1.Y * v2.Y);
}
private static Vector2 normalBetween(Vector2 v1,Vector2 v2)
{
return new Vector2(-(v1.Y - v2.Y),v1.X - v2.X);
}
private struct ProjectionLine
{
public float Start;
public float End;
}
private static ProjectionLine ProjectLine(Vector2[] points,Vector2 normal)
{
var projectionLine = new ProjectionLine() { Start = float.MaxValue,End = float.MinValue };
foreach (var p in points)
{
var projectionScale = DotProduct(p,normal);
projectionLine.Start = Math.Min(projectionScale,projectionLine.Start);
projectionLine.End = Math.Max(projectionScale,projectionLine.End);
}
return projectionLine;
}
private static bool CheckOverlapSAT(Vector2[] shape1,Vector2[] shape2)
{
for (int i = 0; i < shape1.Length; i++)
{
var vertex = shape1[i];
var nextVertex = shape1[(i + 1) % shape1.Length];
var edgenormal = normalBetween(vertex,nextVertex);
var firstProjection = ProjectLine(shape1,edgenormal);
var secondProjection = ProjectLine(shape2,edgenormal);
if (!(firstProjection.Start <= secondProjection.End && firstProjection.End >= secondProjection.Start))
return false;
}
return true;
}
public static bool ConvexpolysCollide(Vector2[] shape1,Vector2[] shape2)
{
return CheckOverlapSAT(shape1,shape2) && CheckOverlapSAT(shape2,shape1);
}
private static float? CollisionResponseAcrossLine(ProjectionLine line1,ProjectionLine line2)
{
if (line1.Start <= line2.Start && line1.End > line2.Start)
return line2.Start - line1.End;
else if (line2.Start <= line1.Start && line2.End > line1.Start)
return line2.End - line1.Start;
return null;
}
public static Vector2 MTVBetween(Vector2[] mover,Vector2[] collider)
{
if (!ConvexpolysCollide(mover,collider))
return Vector2.Zero;
float minResponseMagnitude = float.MaxValue;
var responsenormal = Vector2.Zero;
for (int c = 0; c < collider.Length; c++)
{
var cPoint = collider[c];
var cNextPoint = collider[(c + 1) % collider.Length];
var cEdgenormal = normalBetween(cPoint,cNextPoint);
var cProjected = ProjectLine(collider,cEdgenormal);
var mProjected = ProjectLine(mover,cEdgenormal);
var responseMagnitude = CollisionResponseAcrossLine(cProjected,mProjected);
if (responseMagnitude != null && responseMagnitude < minResponseMagnitude)
{
minResponseMagnitude = (float)responseMagnitude;
responsenormal = cEdgenormal;
}
}
var normalLength = responsenormal.Length();
responsenormal /= normalLength;
minResponseMagnitude /= normalLength;
var mtv = responsenormal * minResponseMagnitude;
return mtv;
}
}
}
解决方法
您的代码几乎是正确的,只需按照以下步骤操作即可。
- 在 NormalBetween() 中标准化你的法线。否则,您的投影值会失真,不应通过比较来获得正确的轴。
import library
from library.core.core_module1 import core_function
from library.feature.feature_module1 import feature_function
- 在 CollisionResponseAcrossLine() 中使用与 CheckOverlapSAT() 中相同的碰撞表达式。或者仅对两者使用一种检测方法。
return Vector2.Normalize(new Vector2(-(v1.Y - v2.Y),v1.X - v2.X));
- 比较 MTVBetween() 中的绝对幅度。当它们指向法线的另一个方向时,计算出的幅度可能为负。
if (line1.Start <= line2.Start && line1.End >= line2.Start) // use the >= operator
return line2.Start - line1.End;
else if (line2.Start <= line1.Start && line2.End >= line1.Start) // use the >= operator
return line2.End - line1.Start;
return null;
- 不再需要以下代码,因为我们已经在 1 中对向量进行了归一化。
if (responseMagnitude != null && Math.Abs(responseMagnitude.Value) < Math.Abs(minResponseMagnitude))
这应该能让您的示例发挥作用。但是当您尝试使用两个具有不同分隔轴的多边形时,它不起作用,因为在碰撞响应代码中,您只检查静态碰撞器的轴。还应检查动子的轴,就像您在碰撞检测方法 CheckOverlapSAT() 中所做的一样。
在 MTVBetween() 中调用 CheckOverlapSAT() 方法似乎是多余的,您也可以在任何 responseMagnitude 为 null 时中断 MTVBetween() 方法。
最后但并非最不重要的一点,请考虑将 CollisionResponseAcrossLine() 代码替换为以下内容:
//var normalLength = responseNormal.Length();
//responseNormal /= normalLength;
//minResponseMagnitude /= normalLength;
这也说明了玩家在障碍物内的情况。它比较到双方的距离并选择较小的一个。以前,在这种情况下,玩家总是会走到同一个边缘。
如果您想要仅支持 AABB 的代码,那么您可以走更简单的路线,而无需依赖 SAT。但我猜你也想支持多边形。