C#XNA:优化碰撞检测?

时间:2010-02-26 18:11:59

标签: c# optimization xna

我正在研究一个简单的碰撞检测演示,它只包含一堆在窗口中弹跳的物体。 (目标是在不丢帧的情况下查看游戏可以同时处理多少个对象。)

存在重力,因此物体要么移动,要么与墙壁碰撞。

天真的解决方案是O(n ^ 2):

foreach Collidable c1:
      foreach Collidable c2:
             checkCollision(c1, c2);

这很糟糕。所以我设置了CollisionCell个对象,它们维护有关屏幕一部分的信息。这个想法是每个Collidable只需要检查其单元格中的其他对象。使用60像素×60像素的电池,这几乎可以提高10倍,但我想进一步推进。

剖析器显示,代码将50%的时间花在每个单元用于获取其内容的函数中。这是:

    // all the objects in this cell
    public ICollection<GameObject> Containing
    {
        get
        {
            ICollection<GameObject> containing = new HashSet<GameObject>();

            foreach (GameObject obj in engine.GameObjects) {
                // 20% of processor time spent in this conditional
                if (obj.Position.X >= bounds.X &&
                    obj.Position.X < bounds.X + bounds.Width &&
                    obj.Position.Y >= bounds.Y &&
                    obj.Position.Y < bounds.Y + bounds.Height) {

                    containing.Add(obj);
                }
            }

            return containing;
        }
    }

在该计划的20%的时间内用于该条件。

以下是调用上述函数的地方:

    // Get a list of lists of cell contents
        List<List<GameObject>> cellContentsSet = cellManager.getCellContents();

        // foreach item, only check items in the same cell
        foreach (List<GameObject> cellMembers in cellContentsSet) {
            foreach (GameObject item in cellMembers) {
                 // process collisions
            }
        }


//...

    // Gets a list of list of cell contents (each sub list = 1 cell)
    internal List<List<GameObject>> getCellContents() {
        List<List<GameObject>> result = new List<List<GameObject>>();
        foreach (CollisionCell cell in cellSet) {
            result.Add(new List<GameObject>(cell.Containing.ToArray()));
        }
        return result;
    }

现在,我必须遍历每个细胞 - 甚至是空细胞。也许这可以通过某种方式得到改善,但我不知道如何在不以某种方式查看它的情况下验证单元格是否为空。 (也许我可以在某些物理引擎中实现类似睡眠对象的东西,如果某个对象在一段时间内仍然会进入睡眠状态并且不包含在每帧的计算中。)

我可以做些什么来优化它? (另外,我是C#的新手 - 还有其他明显的风格错误吗?)

当游戏开始滞后时,对象往往被打得相当紧密,所以没有那么多动作在进行。也许我可以以某种方式利用这一点,编写一个函数来查看,给定一个对象的当前速度,它是否可能在下一次调用Update()之前离开当前单元格

更新1 我决定维护上次更新时发现在单元格中的对象列表,并先检查它们是否仍在单元格中。另外,我保留area变量的CollisionCell,当细胞被填充时,我可以停止查看。这是我的实现,它使整个演示更慢:

    // all the objects in this cell
    private ICollection<GameObject> prevContaining;
    private ICollection<GameObject> containing;
    internal ICollection<GameObject> Containing {
        get {
            return containing;
        }
    }

    /**
     * To ensure that `containing` and `prevContaining` are up to date, this MUST be called once per Update() loop in which it is used.
     * What is a good way to enforce this?
     */ 
    public void updateContaining()
    {
        ICollection<GameObject> result = new HashSet<GameObject>();
        uint area = checked((uint) bounds.Width * (uint) bounds.Height); // the area of this cell

        // first, try to fill up this cell with objects that were in it previously
        ICollection<GameObject>[] toSearch = new ICollection<GameObject>[] { prevContaining, engine.GameObjects };
        foreach (ICollection<GameObject> potentiallyContained in toSearch) {
            if (area > 0) { // redundant, but faster?
                foreach (GameObject obj in potentiallyContained) {
                    if (obj.Position.X >= bounds.X &&
                        obj.Position.X < bounds.X + bounds.Width &&
                        obj.Position.Y >= bounds.Y &&
                        obj.Position.Y < bounds.Y + bounds.Height) {

                        result.Add(obj);
                        area -= checked((uint) Math.Pow(obj.Radius, 2)); // assuming objects are square
                        if (area <= 0) {
                            break;
                        }
                    }
                }
            }
        }
        prevContaining = containing;
        containing = result;
   }

更新2 我放弃了最后一种方法。现在我正在尝试维护一个可碰撞的池(orphans),并在找到包含它们的单元格时从它们中删除对象:

    internal List<List<GameObject>> getCellContents() {
        List<GameObject> orphans = new List<GameObject>(engine.GameObjects);
        List<List<GameObject>> result = new List<List<GameObject>>();
        foreach (CollisionCell cell in cellSet) {
            cell.updateContaining(ref orphans); // this call will alter orphans!
            result.Add(new List<GameObject>(cell.Containing)); 
            if (orphans.Count == 0) {
                break;
            }
        }
        return result;
    }

    // `orphans` is a list of GameObjects that do not yet have a cell
    public void updateContaining(ref List<GameObject> orphans) {
        ICollection<GameObject> result = new HashSet<GameObject>();

        for (int i = 0; i < orphans.Count; i++) {
            // 20% of processor time spent in this conditional
            if (orphans[i].Position.X >= bounds.X &&
                orphans[i].Position.X < bounds.X + bounds.Width &&
                orphans[i].Position.Y >= bounds.Y &&
                orphans[i].Position.Y < bounds.Y + bounds.Height) {

                result.Add(orphans[i]);
                orphans.RemoveAt(i);
            }
        }

        containing = result;
    }

这只会带来微小的改善,而不是我正在寻找的2倍或3倍。

更新3 我再次放弃了上述方法,并决定让每个对象维护其当前的单元格:

    private CollisionCell currCell;
    internal CollisionCell CurrCell {
        get {
            return currCell;
        }
        set {
            currCell = value;
        }
    }

此值已更新:

    // Run 1 cycle of this object
    public virtual void Run()
    {
        position += velocity;
        parent.CellManager.updateContainingCell(this);
    }

CellManager代码:

private IDictionary<Vector2, CollisionCell> cellCoords = new Dictionary<Vector2, CollisionCell>();
    internal void updateContainingCell(GameObject gameObject) {
        CollisionCell currCell = findContainingCell(gameObject);
        gameObject.CurrCell = currCell;
        if (currCell != null) {
            currCell.Containing.Add(gameObject);
        }
    }

    // null if no such cell exists
    private CollisionCell findContainingCell(GameObject gameObject) {

        if (gameObject.Position.X > GameEngine.GameWidth
            || gameObject.Position.X < 0
            || gameObject.Position.Y > GameEngine.GameHeight
            || gameObject.Position.Y < 0) {
            return null;
        }

        // we'll need to be able to access these outside of the loops
        uint minWidth = 0;
        uint minHeight = 0;

        for (minWidth = 0; minWidth + cellWidth < gameObject.Position.X; minWidth += cellWidth) ;
        for (minHeight = 0; minHeight + cellHeight < gameObject.Position.Y; minHeight += cellHeight) ;

        CollisionCell currCell = cellCoords[new Vector2(minWidth, minHeight)];

        // Make sure `currCell` actually contains gameObject
        Debug.Assert(gameObject.Position.X >= currCell.Bounds.X && gameObject.Position.X <= currCell.Bounds.Width + currCell.Bounds.X,
            String.Format("{0} should be between lower bound {1} and upper bound {2}", gameObject.Position.X, currCell.Bounds.X, currCell.Bounds.X + currCell.Bounds.Width));
        Debug.Assert(gameObject.Position.Y >= currCell.Bounds.Y && gameObject.Position.Y <= currCell.Bounds.Height + currCell.Bounds.Y,
            String.Format("{0} should be between lower bound {1} and upper bound {2}", gameObject.Position.Y, currCell.Bounds.Y, currCell.Bounds.Y + currCell.Bounds.Height));

        return currCell;
    }

我认为这会让它变得更好 - 现在我只需要遍历可碰撞物,而不是所有可碰撞物*细胞。相反,游戏现在非常缓慢,只用其上述方法提供了十分之一的性能。

分析器指示现在不同的方法是主要的热点,并且获取对象的邻居的时间非常短。那个方法从以前没有变化,所以也许我比以前更加称呼它...

8 个答案:

答案 0 :(得分:12)

它将50%的时间花在该功能上,因为你经常调用该功能。优化一个函数只会产生性能的逐步改进。

或者,只需调用函数!

您已经通过设置空间分区方案(查找Quadtrees以查看更高级的技术形式)开始了这条路径。

第二种方法是将N * N循环分解为增量形式并使用 CPU预算

您可以为在帧时间内(在更新期间)需要操作的每个模块分配CPU预算。碰撞是这些模块之一,AI可能是另一个。

假设您希望以60 fps的速度运行游戏。这意味着您在帧之间刻录的CPU时间约为1/60秒= 0.0167秒。不,我们可以在模块之间拆分0.0167秒。让我们给出碰撞 30%的预算: 0.005秒

现在你的碰撞算法知道它只能花费0.005秒工作。因此,如果它耗尽时间,它将需要推迟一些任务 - 您将使算法增量。实现这一目标的代码可以简单如下:

const double CollisionBudget = 0.005;

Collision[] _allPossibleCollisions;
int _lastCheckedCollision;

void HandleCollisions() {

    var startTime = HighPerformanceCounter.Now;

    if (_allPossibleCollisions == null || 
        _lastCheckedCollision >= _allPossibleCollisions.Length) {

        // Start a new series
        _allPossibleCollisions = GenerateAllPossibleCollisions();
        _lastCheckedCollision = 0;
    }

    for (var i=_lastCheckedCollision; i<_allPossibleCollisions.Length; i++) {
        // Don't go over the budget
        if (HighPerformanceCount.Now - startTime > CollisionBudget) {
            break;
        }
        _lastCheckedCollision = i;

        if (CheckCollision(_allPossibleCollisions[i])) {
            HandleCollision(_allPossibleCollisions[i]);
        }
    }
}

现在,碰撞代码的速度并不重要,它将尽可能快地完成,而不会影响用户的感知性能

好处包括:

  • 该算法设计为耗尽时间,它只是在下一帧恢复,因此您不必担心这种特殊的边缘情况。
  • 随着高级/耗时算法数量的增加,CPU预算变得越来越重要。想想AI。因此,尽早实施这样的系统是一个好主意。
  • 人体响应时间小于30 Hz,帧环路以60 Hz运行。这使得算法可以完成30帧的工作,因此可以完成它的工作。
  • 这样做可以提供稳定数据无关帧速率。
  • 它仍然受益于碰撞算法本身的性能优化。
  • 碰撞算法旨在追踪发生碰撞的“子帧”。也就是说,你永远永远不会碰到碰撞只是,因为它发生了 - 认为你这样做是骗你自己。

答案 1 :(得分:8)

我可以在这里帮忙;我把自己的碰撞检测作为一个实验。我想我现在可以告诉你,如果不改变算法,你将无法获得所需的性能。当然,天真的方式很好,但只有在折叠之前才适用于这么多项目。你需要的是Sweep and prune。基本思路是这样的(来自我的碰撞检测库项目):

using System.Collections.Generic;
using AtomPhysics.Interfaces;

namespace AtomPhysics.Collisions
{
    public class SweepAndPruneBroadPhase : IBroadPhaseCollider
    {
        private INarrowPhaseCollider _narrowPhase;
        private AtomPhysicsSim _sim;
        private List<Extent> _xAxisExtents = new List<Extent>();
        private List<Extent> _yAxisExtents = new List<Extent>();
        private Extent e1;

        public SweepAndPruneBroadPhase(INarrowPhaseCollider narrowPhase)
        {
            _narrowPhase = narrowPhase;
        }

        public AtomPhysicsSim Sim
        {
            get { return _sim; }
            set { _sim = null; }
        }
        public INarrowPhaseCollider NarrowPhase
        {
            get { return _narrowPhase; }
            set { _narrowPhase = value; }
        }
        public bool NeedsNotification { get { return true; } }


        public void Add(Nucleus nucleus)
        {
            Extent xStartExtent = new Extent(nucleus, ExtentType.Start);
            Extent xEndExtent = new Extent(nucleus, ExtentType.End);
            _xAxisExtents.Add(xStartExtent);
            _xAxisExtents.Add(xEndExtent);
            Extent yStartExtent = new Extent(nucleus, ExtentType.Start);
            Extent yEndExtent = new Extent(nucleus, ExtentType.End);
            _yAxisExtents.Add(yStartExtent);
            _yAxisExtents.Add(yEndExtent);
        }
        public void Remove(Nucleus nucleus)
        {
            foreach (Extent e in _xAxisExtents)
            {
                if (e.Nucleus == nucleus)
                {
                    _xAxisExtents.Remove(e);
                }
            }
            foreach (Extent e in _yAxisExtents)
            {
                if (e.Nucleus == nucleus)
                {
                    _yAxisExtents.Remove(e);
                }
            }
        }

        public void Update()
        {
            _xAxisExtents.InsertionSort(comparisonMethodX);
            _yAxisExtents.InsertionSort(comparisonMethodY);
            for (int i = 0; i < _xAxisExtents.Count; i++)
            {
                e1 = _xAxisExtents[i];
                if (e1.Type == ExtentType.Start)
                {
                    HashSet<Extent> potentialCollisionsX = new HashSet<Extent>();
                    for (int j = i + 1; j < _xAxisExtents.Count && _xAxisExtents[j].Nucleus.ID != e1.Nucleus.ID; j++)
                    {
                        potentialCollisionsX.Add(_xAxisExtents[j]);
                    }
                    HashSet<Extent> potentialCollisionsY = new HashSet<Extent>();
                    for (int j = i + 1; j < _yAxisExtents.Count && _yAxisExtents[j].Nucleus.ID != e1.Nucleus.ID; j++)
                    {
                        potentialCollisionsY.Add(_yAxisExtents[j]);
                    }

                    List<Extent> probableCollisions = new List<Extent>();
                    foreach (Extent e in potentialCollisionsX)
                    {
                        if (potentialCollisionsY.Contains(e) && !probableCollisions.Contains(e) && e.Nucleus.ID != e1.Nucleus.ID)
                        {
                            probableCollisions.Add(e);
                        }
                    }
                    foreach (Extent e2 in probableCollisions)
                    {
                        if (e1.Nucleus.DNCList.Contains(e2.Nucleus) || e2.Nucleus.DNCList.Contains(e1.Nucleus))
                            continue;
                        NarrowPhase.DoCollision(e1.Nucleus, e2.Nucleus);
                    }
                }
            }
        }

        private bool comparisonMethodX(Extent e1, Extent e2)
        {
            float e1PositionX = e1.Nucleus.NonLinearSpace != null ? e1.Nucleus.NonLinearPosition.X : e1.Nucleus.Position.X;
            float e2PositionX = e2.Nucleus.NonLinearSpace != null ? e2.Nucleus.NonLinearPosition.X : e2.Nucleus.Position.X;
            e1PositionX += (e1.Type == ExtentType.Start) ? -e1.Nucleus.Radius : e1.Nucleus.Radius;
            e2PositionX += (e2.Type == ExtentType.Start) ? -e2.Nucleus.Radius : e2.Nucleus.Radius;
            return e1PositionX < e2PositionX;
        }
        private bool comparisonMethodY(Extent e1, Extent e2)
        {
            float e1PositionY = e1.Nucleus.NonLinearSpace != null ? e1.Nucleus.NonLinearPosition.Y : e1.Nucleus.Position.Y;
            float e2PositionY = e2.Nucleus.NonLinearSpace != null ? e2.Nucleus.NonLinearPosition.Y : e2.Nucleus.Position.Y;
            e1PositionY += (e1.Type == ExtentType.Start) ? -e1.Nucleus.Radius : e1.Nucleus.Radius;
            e2PositionY += (e2.Type == ExtentType.Start) ? -e2.Nucleus.Radius : e2.Nucleus.Radius;
            return e1PositionY < e2PositionY;
        }
        private enum ExtentType { Start, End }
        private sealed class Extent
        {
            private ExtentType _type;
            public ExtentType Type
            {
                get
                {
                    return _type;
                }
                set
                {
                    _type = value;
                    _hashcode = 23;
                    _hashcode *= 17 + Nucleus.GetHashCode();
                }
            }
            private Nucleus _nucleus;
            public Nucleus Nucleus
            {
                get
                {
                    return _nucleus;
                }
                set
                {
                    _nucleus = value;
                    _hashcode = 23;
                    _hashcode *= 17 + Nucleus.GetHashCode();
                }
            }

            private int _hashcode;

            public Extent(Nucleus nucleus, ExtentType type)
            {
                Nucleus = nucleus;
                Type = type;
                _hashcode = 23;
                _hashcode *= 17 + Nucleus.GetHashCode();
            }

            public override bool Equals(object obj)
            {
                return Equals(obj as Extent);
            }
            public bool Equals(Extent extent)
            {
                if (this.Nucleus == extent.Nucleus)
                {
                    return true;
                }
                return false;
            }
            public override int GetHashCode()
            {
                return _hashcode;
            }
        }
    }
}

这里是执行插入排序的代码(或多或少直接翻译伪代码here):

/// <summary>
/// Performs an insertion sort on the list.
/// </summary>
/// <typeparam name="T">The type of the list supplied.</typeparam>
/// <param name="list">the list to sort.</param>
/// <param name="comparison">the method for comparison of two elements.</param>
/// <returns></returns>
public static void InsertionSort<T>(this IList<T> list, Func<T, T, bool> comparison)
{
    for (int i = 2; i < list.Count; i++)
    {
        for (int j = i; j > 1 && comparison(list[j], list[j - 1]); j--)
        {
            T tempItem = list[j];
            list.RemoveAt(j);
            list.Insert(j - 1, tempItem);
        }
    }
}
IIRC,我能够获得极大的性能提升,特别是在处理大量碰撞体时。您需要根据代码调整它,但这是扫描和修剪背后的基本前提。

我想提醒你的另一件事是你应该使用一个分析器,就像Red Gate所做的那样。有一个免费试用,应该持续足够长的时间。

答案 2 :(得分:5)

看起来您正在遍历所有游戏对象,只是为了查看单元格中包含的对象。似乎更好的方法是存储每个单元格的单元格中的游戏对象列表。如果你这样做并且每个对象都知道它所在的单元格,那么在单元格之间移动对象应该很容易。这似乎会带来最大的性能提升。

这是另一个用于确定对象所在单元格的优化提示: 如果您已经确定了一个对象所在的单元格并且知道基于对象的速度它不会更改当前帧的单元格,则无需重新运行确定该对象所在单元格的逻辑。可以通过创建包含对象所在的所有单元格的边界框来快速检查。然后,您可以创建一个边界框,该边界框是对象的大小+当前帧的对象的速度。如果单元格边界框包含对象+速度边界框,则无需进一步检查。如果对象没有移动,那就更容易了,你可以使用对象边界框。

如果有意义,或者google / bing搜索“Quad Tree”,或者如果您不介意使用开源代码,请告诉我,请查看这个令人敬畏的物理库:http://www.codeplex.com/FarseerPhysics

答案 3 :(得分:1)

我和你在同一条船上。我正在努力创造一个头顶射手,需要将效率提升到最大值,这样我就可以立刻在屏幕上投入大量的子弹和敌人。

我将所有可碰撞对象都放在带有编号索引的数组中。这提供了利用观察的机会:如果您为每个项目完全迭代列表,那么您将重复工作。那是(并注意,我正在编写变量名称,以便更容易吐出一些伪代码)

 if (objs[49].Intersects(objs[51]))

相当于:

 if (objs[51].Intersects(objs[49]))

因此,如果您使用带编号的索引,则可以通过不重复工作来节省一些时间。这样做:

for (int i1 = 0; i1 < collidables.Count; i1++)
{
    //By setting i2 = i1 + 1 you ensure an obj isn't checking collision with itself, and that objects already checked against i1 aren't checked again. For instance, collidables[4] doesn't need to check against collidables[0] again since this was checked earlier.
    for (int i2 = i1 + 1; i2 < collidables.Count; i2++)
    {
        //Check collisions here
    }
}

另外,我会让每个单元格都有一个计数或一个标志来确定你是否需要检查冲突。如果设置了某个标志,或者计数小于2,则不需要检查冲突。

答案 4 :(得分:1)

只是抬头:有些人认为更加狡猾;这是一个很棒的2D物理库,可以与XNA一起使用。如果你是XNA的3D物理引擎市场,我在XNA项目中使用bulletxbullet的c#端口)效果很好。

注意:我与子弹或子弹项目没有任何关系。

答案 5 :(得分:0)

一个想法可能是使用边界圆。基本上,当创建Collidable时,跟踪它的中心点并计算包含整个对象的半径/直径。然后,您可以使用类似的东西进行首次消除

int r = C1.BoundingRadius + C2.BoundingRadius;

if( Math.Abs(C1.X - C2.X) > r && Math.Abs(C1.Y - C2.Y) > r )
/// Skip further checks...

对于大多数对象,这会将比较减少到两个,但这会让你获得多少我不确定...个人资料!

答案 6 :(得分:0)

可以采取一些措施来加快这个过程......但据我所知,你检查简单矩形碰撞的方法就好了。

但我会替换支票

if (obj.Position.X ....)

使用

if (obj.Bounds.IntersercsWith(this.Bounds))

我也会替换

result.Add(new List<GameObject>(cell.Containing.ToArray()));

有关

result.Add(new List<GameObject>(cell.Containing));

由于Containing属性返回ICollection<T>并且继承了IEnumerable<T>构造函数接受的List<T>

方法ToArray()只是迭代到返回数组的列表,并且在创建新列表时再次完成此过程。

答案 7 :(得分:0)

我知道这篇帖子很老但我会说明确的答案是完全错误的......

他的代码包含一个致命的错误,并且不会给性能提升,这将需要性能!

起初有点意思......

他的代码已创建,因此您必须在Draw方法中调用此代码,但这是碰撞检测的错误位置。在你的绘制方法中,你应该只绘制其他东西!

但是你不能在Update中调用HandleCollisions(),因为Update会获得比Draw更多的调用。

如果你想调用HandleCollisions(),你的代码必须如下所示......这段代码可以防止你的碰撞检测每帧运行一次以上。

private bool check = false;

protected override Update(GameTime gameTime)
{
    if(!check)
    {    
        check = true;
        HandleCollisions();
    }
}

protected override Draw(GameTime gameTime)
{
    check = false;
}

现在让我们看一下HandleCollisions()的错误。

示例:我们有500个对象,我们会在不优化检测的情况下检查每个可能的碰撞。

对于500对象,我们应该进行249500次碰撞检查(499X500,因为我们不想检查对象是否与其自身发生冲突)

但是使用Frank的代码,我们将失去99.998%的爆炸(仅完成500次碰撞检查)。 &LT;&LT;这将增加表演!

为什么呢?因为_lastCheckedCollision永远不会与allPossibleCollisions.Length相同或更大......并且因为那样你只会检查最后一个索引499

for (var i=_lastCheckedCollision; i<_allPossibleCollisions.Length; i++)
    _lastCheckedCollision = i;
//<< This could not be the same as _allPossibleCollisions.Length,
//because i have to be lower as _allPossibleCollisions.Length

你必须替换这个

if (_allPossibleCollisions == null || 
    _lastCheckedCollision >= _allPossibleCollisions.Length)

用这个

if (_allPossibleCollisions == null || 
    _lastCheckedCollision >= _allPossibleCollisions.Length - 1) {

所以你的整个代码都可以被替换。

private bool check = false;

protected override Update(GameTime gameTime)
{
    if(!check)
    {    
        check = true;
        _allPossibleCollisions = GenerateAllPossibleCollisions();
        for(int i=0; i < _allPossibleCollisions.Length; i++)
        {
            if (CheckCollision(_allPossibleCollisions[i]))
            {
                //Collision!
            }
        }
    }
}

protected override Draw(GameTime gameTime)
{
    check = false;
}

...这应该比你的代码快很多......而且它有效:D ......

RCIX答案应该标记为正确,因为弗兰克的答案是错误的。