C# XNA: оптимизация обнаружения столкновений?

я работаю над простой демонстрацией для обнаружения столкновений, которая содержит только кучу объектов, подпрыгивающих в окне. (Цель состоит в том, чтобы увидеть, сколько объектов игра может обрабатывать сразу без падения кадров.)

существует гравитация, поэтому объекты либо движутся, либо сталкиваются со стеной.

наивным решением было O (n^2):

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

это очень плохо. Поэтому я установил CollisionCell объекты, которые поддерживают информацию о a часть экрана. Идея в том, что каждый Collidable нужно только проверить другие объекты в своей ячейке. С 60 px клетками 60 px это дает почти 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 я отказался от последнего подхода. Теперь я пытаюсь поддерживать пул 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;
    }

это дает только незначительное улучшение, а не 2x или 3x, которые я ищу.

обновление 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;
    }

я думал, что это сделает его лучше-теперь мне нужно только перебирать collidables, а не все collidables * ячейки. Вместо этого игра теперь ужасно медленная, обеспечивая только 1/10 ее производительности с моими вышеуказанными подходами.

профилировщик указывает, что теперь основным горячим местом является другой метод, и время для получения соседей для объекта тривиально короткое. Этот метод не изменился с тех пор, так что, возможно, я называю его больше, чем раньше...

7 ответов


он проводит 50% своего времени в этой функции, потому что вы часто вызываете эту функцию. Оптимизация этой одной функции приведет только к постепенному повышению производительности.

альтернативно, просто вызовите функцию less!

вы уже начали этот путь, настроив схему пространственного разбиения (lookup деревья квадрантов чтобы увидеть более продвинутую форму вашей техники).

второй подход состоит в том, чтобы разбить цикл N*N на инкрементная форма и использовать бюджет процессора.

вы можете выделить бюджет процессора для каждого из модулей, которые хотят действия во время кадров (во время обновлений). Столкновение-один из этих модулей, AI может быть другим.

предположим, вы хотите запустить свою игру на 60 кадров в секунду. Это означает, что у вас есть около 1/60 s = 0.0167 S времени процессора для записи между кадрами. Нет, мы можем разделить эти 0.0167 s между нашими модулями. Давайте дадим столкновение 30% бюджета: 0.005 s.

теперь ваш алгоритм столкновения знает, что он может тратить только 0.005 s работает. Поэтому, если у него закончится время, ему нужно будет отложить некоторые задачи на потом - вы сделаете алгоритм инкрементным. Код для достижения этого может быть таким же простым, как:

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]);
        }
    }
}

нет, сейчас это не имеет значения, как быстро код столкновения, это будет сделано так быстро, как это возможно не влияя на производительность пользователя.

преимущества:

  • алгоритм предназначен для истечения времени, он просто возобновляется на следующем кадре, поэтому вам не нужно беспокоиться об этом конкретном случае edge.
  • бюджетирование ЦП становится все более и более важным по мере увеличения количества продвинутых / трудоемких алгоритмов. Думаю ИИ. Так что это хорошая идея, чтобы реализовать такую систему рано.
  • человеческое время на ответ чем 30 Hz, ваша петля рамки бежит на 60 Hz. Что дает алгоритму 30 кадров для завершения его работы, поэтому нормально, что он не заканчивает свою работу.
  • это дает стабильный, "данные" -независимая частота кадров.
  • он по-прежнему выигрывает от оптимизации производительности алгоритма столкновения сам.
  • алгоритмы столкновения предназначены для отслеживания "подрамника", в котором произошли столкновения. То есть, вы никогда не будет будьте настолько удачливы, чтобы поймать столкновение!--45-->просто как это бывает-думать, что вы это делаете, значит лгать себе.

Я могу помочь здесь; я написал свое собственное обнаружение столкновений в качестве эксперимента. Я думаю, что могу сказать вам прямо сейчас, что вы не получите требуемую производительность без изменения алгоритмов. Конечно, наивный способ хорош, но работает только для стольких предметов, прежде чем рухнуть. Что вам нужно, так это подметать и подрезать. Основная идея выглядит так (из моего проекта библиотеки обнаружения столкновений):

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;
            }
        }
    }
}

и вот код, который выполняет сортировку вставки (более или менее прямой перевод псевдокода здесь):

/// <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, я смог получить чрезвычайно большое увеличение производительности с этим, особенно при работе с большим количеством сталкивающихся тел. Вам нужно будет адаптировать его для вашего кода, но это основная предпосылка за sweep и prune.

еще я хочу напомнить вам, что вы должны использовать профилировщик, как тот, который сделан Красные Ворота. Есть бесплатная пробная версия, которая должна длиться долго достаточно.


похоже, что вы перебираете все игровые объекты, чтобы увидеть, какие объекты содержатся в ячейке. Похоже, лучшим подходом было бы сохранить список игровых объектов, которые находятся в ячейке для каждой ячейки. Если вы это сделаете, и каждый объект знает, в каких ячейках он находится, перемещение объектов между ячейками должно быть легким. Похоже, это даст наибольший прирост производительности.

вот еще один совет по оптимизации для определения того, в каких ячейках находится объект: Если вы уже определили, в какой ячейке(ячейках) находится объект, и знают, что на основе скорости объектов он не будет изменять ячейки для текущего кадра, нет необходимости повторно запускать логику, которая определяет, в каких ячейках находится объект. Можно выполнить быструю проверку, создав ограничивающее поле, содержащее все ячейки, в которых находится объект. Затем можно создать ограничивающую рамку, размер объекта + скорость объекта для текущего кадра. Если ограничивающая ячейка содержит объект + граничной скорости, без дополнительных проверок должны быть сделаны. Если объект не движется, это еще проще, и вы можете просто использовать ограничивающую рамку объекта.

Дайте мне знать, если это имеет смысл, или поиск google / bing для "Quad Tree", или если вы не против использования открытого исходного кода, проверьте эту удивительную библиотеку физики:http://www.codeplex.com/FarseerPhysics


Я в той же лодке, как вы. Я пытаюсь создать надземный шутер и должен максимально повысить эффективность, чтобы у меня было сразу много пуль и врагов на экране.

Я бы получил все мои collidable объекты в массиве с нумерованным индексом. Это дает возможность воспользоваться наблюдением: если вы полностью повторите список для каждого элемента, вы будете дублировать усилия. То есть (и обратите внимание, я составляю имена переменных, чтобы облегчить выплюнь какой-нибудь псевдокод)

 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, то нет необходимости проверять наличие коллизий.


просто хедз-ап: некоторые люди предлагают farseer; который является большой 2D-физической библиотекой для использования с XNA. Если вы находитесь на рынке для 3D-физического движка для XNA, я использовал bulletx (порт c#пуля) в проектах XNA с большим эффектом.

Примечание: у меня нет принадлежности к проектам bullet или bulletx.


идея может заключаться в использовании ограничивающего круга. В принципе, когда создается 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...

это отбрасывает сравнения с двумя для большинства объектов, но насколько это поможет вам, я не уверен...профиль!


есть несколько вещей, которые можно сделать, чтобы ускорить процесс... но, насколько я вижу, ваш метод проверки простого прямоугольного столкновения просто прекрасен.

но я бы заменил чек

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));

как содержащее свойство возвращает ICollection<T> и что наследует IEnumerable<T> это принято List<T> конструктор.

и метод ToArray() просто повторяет список, возвращая массив, и th