2D Platformer AABB碰撞问题


51

http://dl.dropbox.com/u/3724424/Programming/Gifs/game6.gif

我的AABB冲突解决方法有问题。


我先解析X轴,然后解析Y轴,以解决AABB相交问题。这样做是为了防止此错误:http : //i.stack.imgur.com/NLg4j.png


当物体移入播放器且必须水平推动播放器时,当前方法可以正常工作。如您在.gif文件中所见,水平尖峰正确地推动了播放器。


但是,当垂直峰值移动到播放器中时,X轴仍将首先解析。这使得“将钉鞋用作升降机”是不可能的。

当玩家进入垂直尖峰(受重力影响而掉入)时,他被推向Y轴,因为开始时X轴上没有重叠。


我尝试过的方法是此链接的第一个答案中描述的方法:2D矩形对象碰撞检测

但是,尖峰和移动的对象是通过更改位置而不是速度来移动的,直到调用Update()方法之前,我才计算它们的下一个预测位置。不用说此解决方案也不起作用。:(


我需要以上述两种情况都可以正常工作的方式解决AABB碰撞。

这是我当前的碰撞源代码:http : //pastebin.com/MiCi3nA1

如果有人可以研究这个问题,我将非常感激,因为这个错误从一开始就一直存在于引擎中,并且我一直在努力寻找一个好的解决方案,但没有成功。这严重使我整夜都在看碰撞代码,并阻止我进入“有趣的部分”并对游戏逻辑进行编码:(


我尝试实现与XNA AppHub平台演示中相同的碰撞系统(通过复制粘贴大部分内容)。但是,“跳跃”错误发生在我的游戏中,而AppHub演示中没有发生。[跳虫:http : //i.stack.imgur.com/NLg4j.png ]

要跳,我检查播放器是否为“ onGround”,然后将-5添加到Velocity.Y。

由于玩家的Velocity.X高于Velocity.Y(请参见图中的第四个面板),因此onGround在不应该设置为true时设置为true,从而使玩家跳入半空中。

我相信这在AppHub演示中不会发生,因为播放器的Velocity.X永远不会高于Velocity.Y,但我可能会误会。

我先在X轴上解析,然后在Y轴上解析,从而解决了这个问题。但是,如上所述,这加剧了与尖峰的碰撞。


5
实际上没有错。我使用的方法先在X轴上推,然后在Y轴上推。这就是为什么即使在垂直峰值时玩家也被水平推的原因。我需要找到一种避免“跳跃问题”的解决方案,并将播放器推入浅轴(穿透力最小的轴)上,但是我找不到任何解决方法。
维托里奥·罗密欧

1
您可以检测到玩家正在碰到障碍物的哪一面,并解决该问题
Ioachim

4
@Johnathan Hobbs,阅读问题。Vee确切地知道他的代码在做什么,但是不知道如何解决某个问题。在这种情况下,单步执行代码将无济于事。
AttackingHobo

4
@Maik @Jonathan:程序没有什么“出错”-他确切地知道算法在什么地方以及为什么不执行他想要的操作。他只是不知道如何更改算法以完成他想要的事情。因此调试器在这种情况下没有用。
BlueRaja-Danny Pflughoeft 2011年

1
@AttackingHobo @BlueRaja我同意,我已经删除了我的评论。那就是我只是对正在发生的事情作出结论。我真的很抱歉,让您不得不解释这一点,并误导了至少一个人。老实说,您可以指望我在下一次留下任何答复之前,能够真正正确地吸收问题。
doppelgreener 2011年

Answers:


9

好的,我弄清楚了XNA AppHub Platformer演示为什么没有“跳转”错误:该演示从顶部到底部测试了碰撞块。当面对“墙”时,玩家可能会重叠多个图块。解决顺序很重要,因为解决一个冲突也可以解决其他冲突(但方向不同)。onGround仅当通过在y轴上向上推动播放器解决了碰撞时才设置该属性。如果先前的分辨率将播放器向下和/或水平推下,则不会出现该分辨率。

通过更改此行,我能够重现XNA演示中的“跳转”错误:

for (int y = topTile; y <= bottomTile; ++y)

对此:

for (int y = bottomTile; y >= topTile; --y)

(我还调整了一些与物理相关的常数,但这无关紧要。)

bodiesToCheck解决游戏中的冲突之前,也许先在y轴上排序会解决“跳跃”错误。我建议按照XNA演示和Trevor的建议在“浅”穿透轴上解决冲突。还要注意,XNA演示播放器的高度是可碰撞块的两倍,使多重碰撞的可能性更大。


这听起来很有趣……您认为在检测到碰撞之前通过Y坐标对所有事物进行排序可以解决我所有的问题吗?有收获吗?
罗密欧

Aaaand我找到了“渔获”。使用此方法可以纠正跳跃的错误,但是如果我在达到上限后将Velocity.Y设置为0,则会再次发生。
罗密欧

@Vee:Position = nextPosition立即在for循环内设置,否则onGround仍会发生不必要的碰撞分辨率(设置)。击中天花板时,应垂直向下(永远不要向上)推播放器,因此onGround切勿设置。XNA演示就是这样做的,我无法在此处重现“天花板”错误。
Leftium

pastebin.com/TLrQPeBU-这是我的代码:实际上,我在Position本身上工作,而没有使用“ nextPosition”变量。它应该像在XNA演示中一样工作,但是如果我保留将Velocity.Y设置为0的行(第57行)的注释,则会发生跳跃错误。如果我将其卸下,则跳入天花板时播放器将保持漂浮状态。这个可以解决吗?
罗密欧

@Vee:您的代码未显示onGround设置方式,因此我无法调查为什么错误地允许了跳跃。XNA演示更新了isOnGround内部的类似属性HandleCollisions()。另外,在设置Velocity.Y为0后,为什么重力又没有开始将播放器向下移动?我的猜测是该onGround属性设置不正确。看一下XNA演示如何更新previousBottomisOnGroundIsOnGround)。
Leftium 2011年

9

对您而言,最简单的解决方案是在解决任何冲突之前针对世界上的每个对象检查两个碰撞方向,然后解决两个由此产生的“复合碰撞”中较小的一个。这意味着您将以最小的量进行解析,而不是始终先解析x或始终先解析y。

您的代码如下所示:

// This loop repeats, until our object has been fully pushed outside of all
// collision objects
while ( StillCollidingWithSomething(object) )
{
  float xDistanceToResolve = XDistanceToMoveToResolveCollisions( object );
  float yDistanceToResolve = YDistanceToMoveToResolveCollisions( object );
  bool xIsColliding = (xDistanceToResolve != 0.f);

  // if we aren't colliding on x (not possible for normal solid collision 
  // shapes, but can happen for unidirectional collision objects, such as 
  // platforms which can be jumped up through, but support the player from 
  // above), or if a correction along y would simply require a smaller move 
  // than one along x, then resolve our collision by moving along y.

  if ( !xIsColliding || fabs( yDistanceToResolve ) < fabs( xDistanceToResolve ) )
  {
    object->Move( 0.f, yDistanceToResolve );
  }
  else // otherwise, resolve the collision by moving along x
  {
    object->Move( xDistanceToResolve, 0.f );
  }
}

重大修订:通过阅读其他答案的评论,我认为我终于注意到一个未阐明的假设,这将导致此方法行不通(并且这解释了为什么我无法理解某些问题-但并非全部-人们看到了这种方法)。详细地说,这是一些伪代码,更明确地显示了我之前引用的功能应该实际执行的操作:

bool StillCollidingWithSomething( MovingObject object )
{
  // loop over every collision object in the world.  (Implementation detail:
  // don't test 'object' against itself!)
  for( int i = 0; i < collisionObjectCount; i++ )
  {
    // if the moving object overlaps any collision object in the world, then
    // it's colliding
    if ( Overlaps( collisionObject[i], object ) )
      return true;
  }
  return false;
}

float XDistanceToMoveToResolveCollisions( MovingObject object )
{
  // check how far we'd have to move left or right to stop colliding with anything
  // return whichever move is smaller
  float moveOutLeft = FindDistanceToEmptySpaceAlongNegativeX(object->GetPosition());
  float moveOutRight = FindDistanceToEmptySpaceAlongX(object->GetPosition());
  float bestMoveOut = min( fabs(moveOutLeft), fabs(moveOutRight) );

  return minimumMove;
}

float FindDistanceToEmptySpaceAlongX( Vector2D position )
{
  Vector2D cursor = position;
  bool colliding = true;
  // until we stop colliding...
  while ( colliding )
  {
    colliding = false;
    // loop over all collision objects...
    for( int i = 0; i < collisionObjectCount; i++ )
    {
      // and if we hit an object...
      if ( Overlaps( collisionObject[i], cursor ) )
      {
        // move outside of the object, and repeat.
        cursor.x = collisionObject[i].rightSide;
        colliding = true;

        // break back to the 'while' loop, to re-test collisions with
        // our new cursor position
        break;
      }
    }
  }
  // return how far we had to move, to reach empty space
  return cursor.x - position.x;  
}

这不是“每个对象对”测试;它无法通过分别针对世界地图的每个图块测试和解析运动对象而起作用(该方法永远无法可靠地起作用,并且随着图块大小的减小,其灾难性方式将日益失败)。相反,它是同时针对世界地图中的每个对象测试移动的对象,然后根据针对整个世界地图的碰撞进行解析。

这样,您可以确保(例如)一堵墙内的单个墙砖永远不会在两个相邻砖之间使玩家弹跳,从而导致玩家被困在它们之间“不存在”的空间中;碰撞分辨率距离始终是一直计算到世界上的空白,不仅仅是到单个图块的边界,该图块上可能再有另一个实心图块。


1
i.stack.imgur.com/NLg4j.png-如果使用上述方法,则会发生这种情况。
维托里奥·罗密欧

1
也许我没有正确理解您的代码,但让我在图中说明问题:由于播放器在X轴上的速度更快,因此X的穿透力大于Y的穿透力。碰撞选择Y轴(因为穿透力较小),然后垂直推动播放器。如果播放器足够慢,以至于Y的穿透力始终大于X的穿透力,则不会发生这种情况。
罗密欧

1
@Vee在此,您指的是图表的哪个窗格,表示使用此方法有不正确的行为?我假设窗格5中的玩家在X上的穿透力明显小于在Y上的穿透力,这将导致沿X轴推出-这是正确的行为。仅当您基于速度(如图像中)而不是基于实际穿透力(如我建议的那样)选择分辨率的轴时,才会出现不良行为。
Trevor Powell

1
@Trevor:啊,我明白你在说什么。在这种情况下,您可以简单地做到if(fabs( yDistanceToResolve ) < fabs( xDistanceToResolve ) || xDistanceToResolve == 0) { object->Move( 0.f, yDistanceToResolve ); } else { object->Move( xDistanceToResolve, 0.f ); }
BlueRaja-Danny Pflughoeft

1
@BlueRaja很好。正如您所建议的,我已经对答案进行了编辑,以减少使用条件语句。谢谢!
Trevor Powell

6

编辑

我已经改善了

看来我更改的关键是对交叉点进行分类。

交叉点为:

  • 天花板颠簸
  • 地面撞击(从地板上推出)
  • 半空墙碰撞
  • 地面撞墙

然后按顺序解决它们

将地面碰撞定义为玩家至少在瓷砖上的1/4路

因此这是地面碰撞,玩家(蓝色)将坐在瓷砖上方(黑色地面碰撞

但这不是地面碰撞,玩家着地时会在瓷砖的右侧“滑动” 不是gonig成为地面碰撞玩家会滑倒

通过这种方法,玩家将不再被困在墙壁的侧面


我尝试了您的方法(请参阅我的实现:pastebin.com/HarnNnnp),但是我不知道将播放器的Velocity设置为0的位置。如果encrY <= 0,我尝试将其设置为0,但这会使播放器在空中停止同时沿着墙壁滑动,也让他反复跳墙。
罗密欧

但是,当播放器与尖峰交互时(如.gif中所示),这可以正常工作。如果您能帮助我解决跳墙(也卡在墙上)的问题,我将不胜感激。请记住,我的墙是由几个AABB瓷砖制成的。
罗密欧

很抱歉再次发表评论,但是在将代码复制粘贴到一个全新的项目中之后,将sp更改为5并以墙体形状生成了图块,就会发生跳跃错误(请参阅此图:i.stack.imgur.com /NLg4j.png)–
罗密欧

好的,现在尝试。
bobobobo

+1用于工作代码示例(尽管缺少水平移动的“峰值”块:)
Leftium 2011年

3

前注:
我的引擎使用碰撞检测类,仅当对象移动时并且仅在当前占用的网格正方形中实时检查与所有对象的碰撞。

我的解决方案:

当我在2d平台游戏中遇到类似问题时,我做了如下事情:

-(引擎迅速检测到可能发生的碰撞)
-(游戏通知引擎检查特定物体的确切碰撞)[返回物体的向量*]
-(物体观察穿透深度以及相对于其他物体先前位置的先前位置,确定要从哪一侧滑出)
-(对象移动,并将其速度与对象速度平均(如果对象正在移动))


“(对象观察穿透深度以及相对于其他对象先前位置的先前位置,以确定要滑出的那一侧)”这是我感兴趣的部分。您能否详细说明?在.gif和图表所示的情况下,它是否成功?
罗密欧

我还没有找到(除了高速度以外),此方法不工作的情况。
ultifinitus

听起来不错,但您能否实际解释如何解决渗透问题?您是否总是在最小轴上求解?您检查碰撞的一面吗?
罗密欧

实际上,不可以,较小的轴并不是一种好的(主要的)加工方式。恕我直言。我通常根据哪一侧在另一对象的一侧之前进行解析,这是最常见的。当失败时,我将根据速度和深度进行检查。
ultifinitus 2011年

2

在电子游戏中,我编程了一种方法,就是要有一个功能来判断您的位置是否有效,即。bool ValidPos(int x,int y,int wi,int he);

该函数对照游戏中的所有几何图形检查边界框(x,y,wi,he),如果存在任何交集,则返回false。

如果要移动,请向右说,确定玩家的位置,在x上加4并检查该位置是否有效,如果无效,请使用+ 3,+ 2等进行检查,直到正确为止。

如果还需要重力,则需要一个变量,只要不落地就可以增长(命中点:ValidPos(x,y + 1,wi,he)== true,y在这里向下为正)。如果您可以移动该距离(即ValidPos(x,y + gravity,wi,he)返回true),则说明您正在跌倒,这有时在您跌倒时无法控制角色时很有用。

现在,您的问题是您的世界中有移动的物体,因此首先需要检查您的旧位置是否仍然有效!

如果不是,则需要找到一个位置。如果游戏中物体的移动速度不能超过每转一圈说2个像素,则需要检查位置(x,y-1)是否有效,然后是(x,y-2),然后是(x + 1,y),依此类推等等。应该检查(x-2,y-2)到(x + 2,y + 2)之间的整个空间。如果没有有效的职位,则意味着您已被“压垮”。

高温超导

瓦尔蒙德


在Game Maker时代,我曾经使用这种方法。我可以想到一些方法来通过更好的搜索来加快/改进它的速度(例如,对您所行驶的空间进行二分搜索,尽管取决于您所走的距离和几何形状,它可能会更慢),但是这种方法的主要缺点是而不是对所有可能发生的冲突进行一次检查,而您要做的只是检查位置变化。
杰夫,

“对您所做的任何更改,您都在做检查”对不起,但这到底是什么意思?顺便说一句,您当然会使用空间分区技术(BSP,Octree,quadtree,Tilemap等)来加快游戏的速度,但是无论如何(如果地图很大),您总是需要这样做,但是问题不在于此,而是关于用于(正确)移动播放器的算法。
瓦尔蒙德2011年

@Valmond我认为@Jeff表示,如果玩家向左移动10个像素,则最多可以进行10种不同的碰撞检测。
乔纳森·康奈尔

如果那是他的意思,那么他就是对的,而且应该是那样的(谁能告诉我们是否在+6而不是+7处停下来了?)。电脑运行速度很快,我在S40上使用了它(约200MHz,不是那么快的kvm),它的运行就像一个魅力。您总是可以尝试优化初始算法,但这总是会给您像OP那样的极端情况。
Valmond 2011年

这就是我的意思
Jeff

2

在开始回答之前,我有几个问题。首先,在您被困在墙上的原始bug中,左边的那些瓷砖是单个的瓷砖,而不是一个大的瓷砖吗?如果是的话,玩家是否会陷入困境?如果对这两个问题的回答都是肯定的,请确保您的新职位有效。这意味着您必须检查告诉玩家移动的位置是否发生碰撞。因此,请按照以下说明解决最小位移,然后仅在他能够移到那里。:P这几乎在鼻子底下:这实际上会引入另一个错误,我称之为“角落情况”。本质上来说,就拐角而言(例如,.gif中出现水平尖峰的左下角,但是如果没有尖峰)将无法解决冲突,因为它会认为您生成的任何分辨率都不会导致有效位置。要解决此问题,只需简单地保存冲突是否已解决以及所有最小穿透分辨率的列表即可。之后,如果冲突尚未解决,请遍历您生成的每个分辨率,并跟踪最大X和最大Y分辨率(最大不必来自同一分辨率)。然后解决这些最大值上的冲突。这似乎可以解决您的所有问题以及我遇到的问题。

    List<Vector2> collisions = new List<Vector2>();
        bool resolved = false;
        foreach (Platform p in testPlat)
        {
            Vector2 dif = p.resolveCollision(player.getCollisionMask());

            RectangleF newPos = player.getCollisionMask();

            newPos.X -= dif.X;
            newPos.Y -= dif.Y;


            if (!PlatformCollision(newPos)) //This checks if there's a collision (ie if we're entering an invalid space)
            {
                if (dif.X != 0)
                    player.velocity.X = 0; //Do whatever you want here, I like to stop my player on collisions
                if (dif.Y != 0)
                    player.velocity.Y = 0;

                player.MoveY(-dif.Y);
                player.MoveX(-dif.X);
                resolved = true;
            }
            collisions.Add(dif);
        }

        if (!resolved)
        {
            Vector2 max = Vector2.Zero;

            foreach (Vector2 v in collisions)
            {
                if (Math.Abs(v.X) > Math.Abs(max.X))
                {
                    max.X = v.X;
                }
                if (Math.Abs(v.Y) > Math.Abs(max.Y))
                {
                    max.Y = v.Y;
                }
            }

            player.MoveY(-max.Y);

            if (max.Y != 0)
                player.velocity.Y = 0;
            player.MoveX(-max.X);

            if (max.X != 0)
                player.velocity.X = 0;
        }

另一个问题是,您显示的是单个或单个图块的峰值?如果它们是单独的薄瓷砖,则可能需要对水平和垂直瓷砖使用与下面介绍的方法不同的方法。但是,如果它们是整块瓷砖,则应该可以使用。


好吧,基本上,这就是@Trevor Powell所描述的。由于仅使用AABB,因此您要做的就是找出一个矩形能穿透另一个矩形的程度。这将为您提供X轴和Y轴上的数量。从两者中选择最小值,然后沿该轴移动碰撞对象该数量。这就是解决AABB碰撞所需的全部。在这样的碰撞中,您将永远不需要沿着一个以上的轴移动,因此,您永远不必为先移动哪个轴而感到困惑,因为您只会移动最小的轴。

Metanet软件在此处提供了有关该方法的经典教程。它也可以变成其他形状。

这是我制作的XNA函数,用于查找两个矩形的重叠矢量:

    public Point resolveCollision(Rectangle otherRect)
    {
        if (!isCollision(otherRect))
        {
            return Point.Zero;
        }

        int minOtherX = otherRect.X;
        int maxOtherX = otherRect.X + otherRect.Width;

        int minMyX = collisionMask.X;
        int maxMyX = collisionMask.X + collisionMask.Width;

        int minOtherY = otherRect.Y;
        int maxOtherY = otherRect.Y + otherRect.Height;

        int minMyY = collisionMask.Y;
        int maxMyY = collisionMask.Y + collisionMask.Height;

        int dx, dy;

        if (maxOtherX - minMyX < maxMyX - minOtherX)
        {
            dx = (maxOtherX - minMyX);
        }
        else
        {
            dx = -(maxMyX - minOtherX);
        }

        if (maxOtherY - minMyY < maxMyY - minOtherY)
        {
            dy = (maxOtherY - minMyY);
        }
        else
        {
            dy = -(maxMyY - minOtherY);
        }

        if (Math.Abs(dx) < Math.Abs(dy))
            return new Point(dx, 0);
        else
            return new Point(0, dy);
    }

(我希望它很简单,因为我敢肯定有更好的方法来实现它...)

isCollision(Rectangle)实际上只是对XNA的Rectangle.Intersects(Rectangle)的调用。

我已经在移动平台上进行了测试,它似乎工作正常。我将再进行一些与您的.gif类似的测试,以确保结果正常并进行报告。



我对此表示怀疑,但我认为它不适用于瘦平台。我想不出没有理由。另外,如果您需要资源,我可以为您上传XNA项目
Jeff

是的,我只是对此进行了更多测试,这又回到了卡在墙上的同样问题。这是因为两块瓷砖彼此对齐,它会告诉您由于较低的一块而向上移动,然后向上移动(对于您的情况而言是正确的),直到较高的一块,如果您的y速度足够慢,则有效地抵消了重力运动。我必须为此寻求解决方案,我似乎记得以前曾遇到过这个问题。
杰夫,

好的,我想出了一种非常巧妙的方法来解决您的第一个问题。解决方案包括找到每个AABB的最小穿透力,仅对最大X的ABB进行解析,然后对最大Y的第二遍进行解析。当您被压碎时看起来很糟糕,但可以提升工作量并且可以推动您横向移动,原来的粘性问题就消失了。它是hackey,我不知道它将如何转换为其他形状。另一种可能是焊接接触的几何体,但我什
Jeff

墙壁是由16x16的瓷砖制成。尖峰是16x16的图块。如果我在最小轴上进行解析,则会发生跳跃错误(请参见图表)。您的“极度hacky”解决方案是否可以处理.gif中显示的情况?
罗密欧

是的,我的作品。你的角色尺寸是多少?
杰夫

1

这很简单。

重写尖峰。这是尖峰的错。

您的碰撞应该以离散的有形单位发生。据我所知,问题是:

1. Player is outside spikes
2. Spikes move into intersection position with the player
3. Player resolves incorrectly

问题出在第二步,而不是第三步!

如果要使事物感觉稳定,则不应让项目彼此滑入。一旦您处于交叉路口,如果您失去位置,该问题将变得更难解决。

理想情况下,长钉应检查玩家的存在,并且在移动时,应在必要时推动他。

一个简单的方法来实现这一点的球员有moveXmoveY它理解景观功能,并通过一定的增量推玩家或者只要它们可以在不碰撞障碍物

通常,它们将由事件循环调用。但是,它们也可以被对象调用以推动播放器。

function spikes going up:

    move spikes up

    if intersecting player:
        d = player bottom edge + spikes top edge

        player.moveY(-d)
    end if

end function

显然,玩家仍然需要如果反应到尖峰进入他们

总体规则是,在任何对象移动之后,它应在没有相交的位置结束。这样,就不可能得到您所看到的故障。


moveY无法移除相交点的极端情况下(因为正在挡住另一堵墙),您可以再次检查相交点,然后尝试moveX或干掉他。
克里斯·伯特·布朗
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.