jjzjj

C# XNA : Optimizing Collision Detection?

coder 2024-05-19 原文

我正在开发一个简单的碰撞检测演示,其中仅包含一堆在窗口中弹跳的对象。 (目标是查看游戏一次可以处理多少个对象而不会丢帧。)

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

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

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

这很糟糕。因此,我设置了 CollisionCell 对象,它维护有关屏幕一部分的信息。这个想法是每个 Collidable 只需要检查其单元格中的其他对象。对于 60 像素 x 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;
    }

现在,我必须遍历每个单元格 - 甚至是空单元格。也许这可以以某种方式改进,但我不确定如何在不以某种方式查看单元格的情况下验证单元格是否为空。 (也许我可以在一些物理引擎中实现类似于 sleep 对象的东西,如果一个对象将静止一段时间,它就会进入休眠状态,并且不会包含在每一帧的计算中。)

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

当游戏开始滞后时,物体往往会挤得相当紧,因此不会有太多运动。也许我可以以某种方式利用这一点,编写一个函数来查看给定对象的当前速度,它是否可以在下一次调用 Update()

之前离开其当前单元格

UPDATE 1 我决定维护一个列表,列出上次更新时发现在单元格中的对象,并首先检查它们是否仍在单元格中。此外,我维护了 CollisionCell 变量的一个 area,当单元格被填满时我可以停止查看。这是我的实现,它使整个演示变慢了很多:

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

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

    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 倍。

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

我认为这会让它变得更好——现在我只需要遍历可碰撞对象,而不是所有可碰撞对象 * 单元格。取而代之的是,游戏现在慢得可怕,使用我的上述方法只能提供其性能的 1/10。

探查器表明现在主要的热点是一种不同的方法,并且为对象获取邻居的时间非常短。该方法与以前相比没有变化,所以我可能比以前更常调用它...

最佳答案

它在该函数上花费了 50% 的时间,因为您经常调用该函数。优化一个功能只会对性能产生增量改进。

或者,只需少调用函数!

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

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

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

假设您希望以 60 fps 的速度运行游戏。这意味着您有大约 1/60 s = 0.0167 s 的 CPU 时间在帧之间刻录。不,我们可以在我们的模块之间拆分那些 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 预算变得越来越重要。想想人工智能。因此,尽早实现此类系统是个好主意。
  • 人类响应时间小于 30 赫兹,您的帧循环以 60 赫兹运行。这给了算法 30 帧来完成它的工作,所以它没有完成它的工作是可以的。
  • 这样做可以提供稳定数据独立的帧速率。
  • 它仍然受益于碰撞算法本身的性能优化。
  • 碰撞算法旨在追踪发生碰撞的“子帧”。也就是说,您永远不会幸运地 catch 碰撞,恰好发生碰撞 - 认为您这样做是在自欺欺人。

关于C# XNA : Optimizing Collision Detection?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/2343789/

有关C# XNA : Optimizing Collision Detection?的更多相关文章

  1. c# - 在 XNA 中加载 XML 时只获取最后一个值 - 2

    我正在制作一个小型XNA游戏,为了更改我有“门户”的map,我已将有关这些门户的信息存储在一个XML文件中。319Map002139Map001319Map001在Game1.cs文件中,我将“portal”声明为变量Portals[]portal;然后在loadContent区域将XML文件加载到门户中。portal=Content.Load("Portals/Test");不幸的是,这是我的问题发生的地方,因为当我尝试使用portal[].position对于[]中的任何数字,我都会得到相同的值,这是我列表中的最后一个值。我在visualbasics中运行它时查看了所有值,它们似乎

  2. xna - OmitXmlDeclaration 不适用于 XmlSerializer - 2

    我一直在为基于图block的游戏开发map编辑器。我想加载map并将其保存到xml文件。发现了xmlSerializer类并想尝试一下而不是构建我自己的解析器。这并不难弄清楚。我现在遇到的一个问题是我无法摆脱xml声明。似乎根本没有应用XmlSettings。整晚都在乱搞,没有任何进展,非常感谢帮助。这是代码:publicstaticvoidSerializeMap(Arraymap){XmlWriterSettingsxmlSettings=newXmlWriterSettings();xmlSettings.OmitXmlDeclaration=true;xmlSettings.C

  3. c# - XNA:加载和读取 XML 文件的最佳方式? - 2

    我很难完成这个看似简单的任务。我想像加载艺术Assets一样轻松地加载XML文件:content=newContentManager(Services);content.RootDirectory="Content";Texture2dbackground=content.Load("images\\ice");我不知道该怎么做。这tutorial似乎很有帮助,但如何获得StorageDevice实例?我现在确实有一些东西在工作,但感觉很糟糕:publicIDictionaryGet(stringtypeName){IDictionaryresult=newDictionary();x

  4. c# - XNA 4.0 中的自定义光标 - 2

    所以我一直在关注这个答案:AddingacustomcursorinXNA/C#?...让自定义鼠标光标在XNA上工作。我已经完成了与解决方案类似的所有操作,没有出现错误,但仍然没有自定义光标(它仍然显示Windows默认光标)。我不确定真正要做什么...我在Game1.cs文件的底部创建了getCursorPos方法,在Game1类的开头包含以下声明:privateMouseStatemouseState;privateintcursorX;privateintcursorY;LoadContent中的代码给我一个错误:cursorTex=content.Load("cursor.p

  5. c# - XNA 窗口缩放性能 - 2

    在我的XNA游戏中,我针对1920x1080分辨率对整个游戏进行编程和设计,然后缩放比例和信箱以适应正在运行的系统(XBox或PC)。这是一个很好的解决方案,因为它让我永远只担心一个解决方案。但是,我现在想知道随着游戏变得更加复杂,这是否会在未来再次困扰我。因为我必须在每次绘制时缩放所有内容(我只使用缩放因子缩放SpriteBatch.Begin()一次,完成所有绘制,然后调用End()),这会对性能产生不利影响吗?我知道当本地设置为720p时,XBox已经为XNA游戏做到了这一点(我实际上是在XBox上运行时,它只是获得了适当的缩放因子)......所以我无法想象它太糟糕了,即使是P

  6. windows - 适用于 Windows Phone 的 XNA 游戏开发 - 2

    我正在尝试参与XNA游戏开发(很可能是WindowsPhone)。我有相当多的编程经验,并且制作了很多用于练习的小型XNA游戏。此时,在我接手更大的项目之前,我想确保我了解基本的设计和流程概念。您用来学习游戏设计良好实践和概念的最佳资源/书籍是什么?或者,如果您过去从事过项目或目前正在从事项目,那么您希望自己知道或以不同方式做的事情有哪些? 最佳答案 我找到了LearningXNA成为一本伟大的书。我有3.0,但4.0现在可用,其中也有一个关于WindowsPhone7的部分。我无法想象它们有多大不同,因为它是同一作者。虽然听起来很

  7. c# - PC 上 XNA 中的信箱和缩放 - 2

    有没有一种方法可以基本上以1080p(或720p)作为我的默认分辨率开发我的XNA游戏,然后根据设置的分辨率将游戏中的所有内容缩放到适当的大小,而无需设置缩放因子在每个SpriteDraw()方法中?我的想法是,我可以根据1080p的分辨率开发所有图形、配置坐标等,但是对于XBOX,只需将分辨率设置为720p并缩小(以便XBOX将所有内容视为720因此可以处理开发人员文档中提到的所有分辨率),并通过自动为非16:9分辨率的View设置信箱来在PC规模上达到任何需要的分辨率或纵横比。我已经设置了我的游戏,这样spritebatch.begin()和end()就会在最高层被调用,围绕所有其

  8. windows - Xna 文本在字符之间有额外的空间 - 2

    我正在制作一款新游戏,我想将自己的纹理用于文本。我遇到了这个名为SpriteFont2.0的酷工具,它就像一个WYSIWYG编辑器,用于在XNA中为SpriteFont文件制作纹理文件。我做了这个字体:但我的问题是,当我将它放入XNA时,字母之间有很多额外的空间。例如,这应该说“这是一个测试”,但结果是这样的:我的纹理文件有问题吗,或者我的编程需要做些什么。我刚刚在SpriteBatch对象中使用了第一个DrawString方法。我也将纹理文件的内容导入更改为Sprite字体纹理。我不知道为什么文字之间的字符间距如此之大。 最佳答案

  9. windows - 你能在 xbox 360 的 XNA 项目中有 WPF 引用吗 - 2

    我正在Windows和Xbox360中编写一个XNA项目,它的Windows端有一个控制台,我将其编写为WPF应用程序。我想知道的是,如果我将它留在我的库代码中并引用WPF,该dll是否仍然可以在360上运行? 最佳答案 没有。您只能在XBOX360上使用.NETCompactFramework。这不包括WPF。事实上,您仅限于XBOX360的CompactFramework实现,它是基于.NET2.0CompactFramework构建的。这意味着任何.NET3.0/3.5特定类都将不起作用。MSDN列出了supportednam

  10. c# - XNA 在不增加分辨率的情况下调整窗口大小 - 2

    我想在较大的窗口上创建一个低分辨率的游戏。(例如960x540大小的窗口上的96x54分辨率)。我该怎么做?有没有办法独立于首选的后台缓冲区宽度和高度来调整窗口大小?或者我应该只保留我绘制的低分辨率渲染目标,并在完成最近的纹理采样调整后将其作为全屏四边形绘制在我的窗口上?提前致谢小花 最佳答案 我倾向于选择“渲染到纹理”解决方案,这样我就可以在不失真的情况下实现全屏显示。我用来实现此目的的类通常类似于:classVirtualScreen{publicreadonlyintVirtualWidth;publicreadonlyint

随机推荐