C#XNA:优化碰撞检测?

Nic*_*ner 16 c# optimization xna

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

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

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

foreach Collidable c1:
      foreach Collidable c2:
             checkCollision(c1, c2);
Run Code Online (Sandbox Code Playgroud)

这很糟糕.所以我设置了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;
        }
    }
Run Code Online (Sandbox Code Playgroud)

其中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;
    }
Run Code Online (Sandbox Code Playgroud)

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

我该怎么做才能优化这个?(另外,我是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;
   }
Run Code Online (Sandbox Code Playgroud)

更新2我放弃了最后一种方法.现在我正在尝试维护一个collidables(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;
    }
Run Code Online (Sandbox Code Playgroud)

这只会产生微小的改善,而不是我正在寻找的2倍或3倍.

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

    private CollisionCell currCell;
    internal CollisionCell CurrCell {
        get {
            return currCell;
        }
        set {
            currCell = value;
        }
    }
Run Code Online (Sandbox Code Playgroud)

此值得到更新:

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

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;
    }
Run Code Online (Sandbox Code Playgroud)

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

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

Fra*_*ger 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]);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

好处包括:

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


RCI*_*CIX 8

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

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;
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

和这里的,做插入排序(更多或更少的直接的伪代码的编译代码在这里):

/// <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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

IIRC,我能够获得极大的性能提升,特别是在处理大量碰撞体时.您需要根据代码调整它,但这是扫描和修剪背后的基本前提.

我要提醒你的另一件事是你应该使用一个分析器,就像红门制作的那样.有一个免费试用,应该持续足够长的时间.


jjx*_*tra 5

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

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

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