多线程2D重力计算


24

我正在开发一个太空探索游戏,目前我已经开始研究重力(在XNA的C#中)。

重力仍然需要调整,但是在我能够做到这一点之前,我需要在物理计算中解决一些性能问题。

这使用了100个对象,通常不进行物理计算就可以渲染其中的1000个对象,而FPS则超过300 FPS(这是我的FPS上限),但是超过10个左右的对象会将游戏(及其运行的单线程)带入在进行物理计算时会屈膝。

我检查了我的线程使用情况,发现第一个线程正在杀死所有工作,因此我认为只需要对另一个线程进行物理计算即可。但是,当我尝试在另一个线程上运行Gravity.cs类的Update方法时,即使Gravity的Update方法中没有任何内容,游戏仍然会降低到2 FPS。

Gravity.cs

public void Update()
    {
        foreach (KeyValuePair<string, Entity> e in entityEngine.Entities)
        {
            Vector2 Force = new Vector2();

            foreach (KeyValuePair<string, Entity> e2 in entityEngine.Entities)
            {
                if (e2.Key != e.Key)
                {
                    float distance = Vector2.Distance(entityEngine.Entities[e.Key].Position, entityEngine.Entities[e2.Key].Position);
                    if (distance > (entityEngine.Entities[e.Key].Texture.Width / 2 + entityEngine.Entities[e2.Key].Texture.Width / 2))
                    {
                        double angle = Math.Atan2(entityEngine.Entities[e2.Key].Position.Y - entityEngine.Entities[e.Key].Position.Y, entityEngine.Entities[e2.Key].Position.X - entityEngine.Entities[e.Key].Position.X);

                        float mult = 0.1f *
                            (entityEngine.Entities[e.Key].Mass * entityEngine.Entities[e2.Key].Mass) / distance * distance;

                        Vector2 VecForce = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle));
                        VecForce.Normalize();

                        Force = Vector2.Add(Force, VecForce * mult);
                    }
                }
            }

            entityEngine.Entities[e.Key].Position += Force;
        }

    }

是的,我知道。这是一个嵌套的foreach循环,但我不知道还有其他方法可以进行引力计算,而且这似乎行得通,它非常密集,以至于需要自己的线程。(即使有人知道执行这些计算的超级有效方法,我仍然想知道我应该如何在多个线程上进行计算)

EntityEngine.cs(管理Gravity.cs的实例)

public class EntityEngine
{
    public Dictionary<string, Entity> Entities = new Dictionary<string, Entity>();
    public Gravity gravity;
    private Thread T;


    public EntityEngine()
    {
        gravity = new Gravity(this);
    }


    public void Update()
    {
        foreach (KeyValuePair<string, Entity> e in Entities)
        {
            Entities[e.Key].Update();
        }

        T = new Thread(new ThreadStart(gravity.Update));
        T.IsBackground = true;
        T.Start();
    }

}

EntityEngine在Game1.cs中创建,其Update()方法在Game1.cs中调用。

每当游戏更新时,我都需要在Gravity.cs中进行物理计算,以使计算不会将游戏的速度降低到极低的(0-2)FPS。

我将如何使该线程工作?(如果有人提出任何关于改进行星重力系统的建议,则欢迎)

我也不是在寻找为什么不应该使用线程或不正确使用线程的危险的教训,我正在寻找一个直接的答案。我已经花了一个小时来研究这个问题,但我了解或帮助的结果很少。我并不是要粗鲁无礼,但是作为编程新手总是很难获得一个有意义的直接答案,我通常宁愿得到一个如此复杂的答案,如果我理解了它,很容易就能解决我的问题,或者有人说为什么我不应该做我想做的事情,并且没有提供其他选择(很有帮助)。

感谢您的帮助!

编辑:阅读完我得到的答案后,我看到你们真的在乎,而不仅仅是试图给出可能有用的答案。我想用一块石头杀死两只鸟(提高性能并学习多线程的一些基础知识),但是似乎大多数问题出在我的计算上,而且线程多麻烦,而不是提高性能值得。谢谢大家,在我完成学业后,我将再次阅读您的答案,并尝试您的解决方案,再次感谢!


现在[您上面概述的更新线程系统]会做什么(起作用)?顺便说一句,我会在游戏周期内尽快开始-例如在更新实体之前。
ThorinII

2
嵌套循环内部的Trig调用可能是最大的成功。如果您找到消除它们的方法,那将大大减少kO(n^2)问题的产生。
RBarryYoung 2013年

1
确实,完全没有必要进行Trig调用:您首先从一个矢量计算一个角度,然后使用该角度生成指向给定方向的另一个矢量。然后,您可以对该向量进行归一化,但是由于sin² + cos² ≡ 1它已经被归一化了!您可能只是使用了将两个感兴趣的对象连接起来的原始向量,并对其进行了标准化。无需任何触发电话。
左转

不推荐使用XNA吗?
jcora 2013年

@yannbane该问题不会对讨论增加任何帮助。不,XNA的状态不适合任何不推荐使用的定义。
塞斯·巴丁

Answers:


36

您这里拥有的是经典的O(n²)算法。问题的根本原因与线程无关,而与算法复杂度高有关。

如果您以前从未遇到过“ Big O”表示法,则基本上是指对n个元素进行操作所需的操作数(这是超级简化的解释)。您的100个元素正在执行循环的内部10000次

在游戏开发中,通常要避免使用O(n²)算法,除非您的数据量很少(最好是固定或上限)并且算法非常快。

如果每个实体都在影响其他每个实体,则您必然需要O(n²)算法。但是,似乎只有少数实体实际上在进行交互(由于if (distance < ...))-因此,您可以通过使用“ 空间分区 ” 来显着减少操作数量。

因为这是一个相当详细的主题,并且在某些方面是特定于游戏的,所以我建议您提出一个新问题以获取更多详细信息。让我们继续前进...


代码的主要性能问题之一很简单。这太慢了

foreach (KeyValuePair<string, Entity> e in Entities)
{
    Entities[e.Key].Update();
}

您正在按字符串对字典进行查找,每次迭代(在其他循环中多次),以查找已有的对象!

您可以这样做:

foreach (KeyValuePair<string, Entity> e in Entities)
{
    e.Value.Update();
}

或者,您可以这样做:(我个人比较喜欢,两者的速度应该差不多)

foreach (Entity e in Entities.Values)
{
    e.Update();
}

按字符串查找字典非常慢。直接迭代将明显更快。

虽然,您实际上需要多久按名称查找一次项目?与必须遍历所有对象的频率相比?如果仅很少进行查找命名,请考虑将您的实体存储在中List(为它们提供Name成员)。

您实际拥有的代码比较琐碎。我没有分析它,但是我敢打赌,您的大部分执行时间都将花在重复的字典查找上。仅通过解决此问题,您的代码很可能“足够快”。

编辑:下一个最大的问题可能是调用Atan2,然后立即将其转换为与Sin和的向量Cos!只需直接使用向量即可。


最后,让我们解决线程问题以及代码中的主要问题:

首先也是最明显的一点:不要在每一帧都创建一个新线程!线程对象非常“繁重”。最简单的解决方案是改为使用ThreadPool

当然,这不是那么简单。让我们继续进行第二个问题:不要一次接触两个线程上的数据!(不添加适当的线程安全基础结构。)

您基本上是在以最恐怖的方式踩踏内存。这里没有线程安全。gravity.Update您启动的多个“ ”线程中的任何一个都可能在意外的时间覆盖另一个线程中正在使用的数据。同时,您的主线程无疑也将涉及所有这些数据结构。如果此代码产生了难以复制的内存访问冲突,我不会感到惊讶。

使类似该线程的事物变得安全是困难的,并且可能会增加大量的性能开销,因此通常不值得付出这些努力。


但是,正如您所问的(不是这样)很好,无论如何如何做,让我们谈谈...

通常,我建议您先练习一些简单的事情,其中​​您的线程基本上是“即发即弃”。播放音频,向磁盘中写入内容等。当您必须将结果反馈回主线程时,事情变得很复杂。

基本上有三种方法可以解决您的问题:

1)围绕线程使用的所有数据设置锁。在C#中,使用该lock语句可以使它变得相当简单。

通常,您会创建(并保留!)new object专门用于锁定的数据,以保护某些数据集(出于安全原因,通常仅在编写公共API时才会出现这种情况-但样式完全一样)。然后,您必须访问其保护的数据的任何地方锁定您的锁定对象!

当然,如果某个事物因为正在使用而被某个线程“锁定”,而另一个线程试图访问它,则第二个线程将被迫等待,直到第一个线程完成。因此,除非您仔细选择可以并行完成的任务,否则您基本上将获得单线程性能(或更糟)。

因此,就您的情况而言,除非您可以设计游戏以使其他一些并行运行的代码不会影响您的实体集合,否则这样做是没有意义的。

2)将数据复制到线程中,进行处理,然后在完成后再次将结果取出。

确切的实现方式取决于您在做什么。但这显然会涉及潜在的昂贵的复制操作(或两个),在许多情况下,复制操作会比仅执行单线程操作要慢。

而且,当然,您仍然需要在后台执行其他工作,否则您的主线程将坐在那里等待您的其他线程完成,以便可以将数据复制回去!

3)使用线程安全的数据结构。

它们比单线程对象慢很多,并且比简单锁定通常更难使用。除非您小心使用它们,否则它们仍然会出现锁定问题(将性能降低到单个线程)。


最后,由于这是基于框架的模拟,因此您需要让主线程等待其他线程提供其结果,以便可以渲染框架并继续进行模拟。要在此处进行完整的解释确实太长了,但是基本上您将需要学习如何使用Monitor.WaitMonitor.Pulse这是一篇文章,可以让您开始


我知道我没有提供任何具体实现细节(最后一点除外)或任何这些方法的代码。首先,将涉及很多内容。其次,它们都不适用于您自己的代码-您需要着眼于整个体系结构并增加线程。

线程化不会神奇地提高您那里的代码的效率,它只能让您同时执行其他操作!


8
如果可以的话,+ 10。也许您可以将最后一句话作为引言放在顶部,因为它在这里总结了核心问题。如果您没有其他要做的事情,则在另一个线程上运行代码不会神奇地加快渲染速度。渲染器可能会等待线程完成,但是如果渲染线程不知道(怎么知道?),它将绘制一个不一致的游戏状态,而某些实体物理仍有待更新。
LearnCocos2D 2013年

我完全相信线程不是我所需要的,谢谢您的冗长和专业的信息!至于性能改进,我做了您(和其他人)建议的更改,但是当处理> 60个对象时,我仍然表现不佳。我认为最好将另一个问题更多地关注于N-Body仿真效率。不过,您会得到我的答复。谢谢!
邮递员

1
不客气,很高兴为您提供帮助:)当您发布新问题时,请在此处放置链接,以便我以及其他关注的人看到它。
Andrew Russell

@Postman虽然我完全同意这个答案的说法,但我认为它完全错过了这样一个事实,即这基本上是利用线程的PERFECT算法。他们在GPU上做这些事情是有原因的,这是因为如果将写入操作移至第二步,这是一种微不足道的并行算法。无需锁定或复制或线程安全的数据结构。一个简单的Parallel.ForEach及其完成没有问题。
耐嚼口香糖

@ChewyGumball非常有效的一点!而且,尽管Postman必须将他的算法分为两阶段,但无论如何应该说它应该是两阶段的。但是,值得指出的是,这Parallel并非没有开销,因此绝对值得分析-特别是对于如此小的数据集和(应该是)相对快速的代码段。而且,当然,在这种情况下,降低算法复杂度仍然可以说是更好的方法,而不是简单地对它进行并行处理。
Andrew Russell

22

好的,乍一看,您应该尝试一些事情。首先,您应该尝试减少冲突检查,可以通过使用某种空间结构(例如,四叉树)来实现。这将使您减少第二个foreach计数,因为您将只查询关闭第一个的实体。

关于线程:尽量不要在每次更新时都创建线程。这种开销可能会使您的速度下降比加速速度更快。而是尝试创建单个碰撞线程,然后让它为您完成工作。我没有具体的复制粘贴此代码方法,但是有一些有关线程同步和C#的后台工作程序的文章。

另一点是,在foreach循环中您不需要执行此操作,entityEngine.Entities[e.Key].Texture因为您已经在foreach标头中访问了dict。相反,您可以只写e.Texture。我真的不知道这会带来什么影响,只是想让你知道;)

最后一件事:目前,您正在仔细检查每个实体,因为它在第一个和第二个foreach循环中被查询。

2个实体A和B的示例:

pick A in first foreach loop
   pick A in second foreach loop
      skip A because keys are the same
   pick B in second foreach loop
      collision stuff
pick B in first foreach loop
   pick A in second foreach loop
      collision stuff
   pick B in second foreach loop
      skip B because keys are the same

尽管这是一种可能的方法,但也许您可以一圈处理A和B,跳过一半的碰撞检查

希望这可以帮助您入门=)

PS:即使您说您不想听,也请:尝试将碰撞检测保留在同一线程中,并使其足够快。线程化似乎是一个好主意,但是随之而来的是需要像地狱一样进行同步。如果碰撞检查的速度比更新速度慢(将其穿线的原因),则会出现故障和错误,因为碰撞将在船舶已经移动之后触发,反之亦然。我不想阻止您,这只是个人经历。

EDIT1:与QuadTree教程(Java)的链接:http ://gamedev.tutsplus.com/tutorials/implementation/quick-tip-use-quadtrees-to-detect-likely-collisions-in-2d-space/


10
使用四叉树/八叉树进行重力模拟的好处是,您不仅可以忽略远处的粒子,还可以存储树的每个分支中所有粒子的总质量和质心,并使用此值来计算平均重力效应该分支中所有粒子的总数位于其他远距离粒子上。这被称为Barnes-Hut算法这是专业人士使用的算法
Ilmari Karonen

10

老实说,您应该做的第一件事就是切换到更好的算法。

即使在最佳情况下,并行化仿真也只能以等于系统上可用CPU数×每个CPU内核数×系统中每个内核可用线程数的速度来加速仿真-例如,现代PC的速度在4到16之间。(移动你的代码到GPU可以产生很多更令人印象深刻的并行化的因素,在额外的开发复杂性的成本和更低的每线程基准计算速度。)随着O(N²)算法,比如你的示例代码,这将让你使用2到4倍于当前数量的粒子。

相反,切换到更高效的算法可以轻松加快仿真速度,例如,提高100到10000的因子(纯估计数字)。使用空间细分的良好n体模拟算法的时间复杂度大致为O(n log n),它是“几乎线性的”,因此您可以预期几乎相同的增加粒子数的因素可以处理。而且,那仍然只使用一个线程,因此在那之上仍然有并行化的空间。

无论如何,正如其他答案所指出的那样,有效模拟大量交互粒子的一般技巧是将它们组织成四叉树(2D)或八叉树(3D)。特别是,为了模拟重力,您要使用的基本算法是Barnes–Hut模拟算法,其中存储了四边形/八角形每个单元中包含的所有粒子的总质量(和质量中心),并且使用它来近似估算该单元中粒子对其他远距离粒子的平均重力效应。

您可以通过Googling找到有关Barnes-Hut算法的大量描述和教程,但这是一个入门的好简单方法,同时还描述了用于GPU模拟星系碰撞的高级实现


6

另一个与线程无关的优化答案。对于那个很抱歉。

您正在计算每对的Distance()。这涉及到求平方根,这很慢。它还涉及多个对象查找以获取实际大小。

您可以改用DistanceSquared()函数对此进行优化。预先计算任何两个对象可以交互的最大距离,将其平方,然后与DistanceSquared()比较。当且仅当距离的平方在最大范围内时,然后取平方根并将其与实际物体的尺寸进行比较。

编辑:此优化主要用于当您测试碰撞时,我现在注意到这实际上并不是您在做什么(尽管您肯定会在某些时候这样做)。但是,如果所有粒子的大小/质量都相似,它可能仍然适用于您的情况。


是的 此解决方案可能很好(仅可忽略不计的精度损失),但是当对象的质量相差很大时会遇到麻烦。如果某些对象的质量非常大而某些对象的质量非常小,则合理的最大距离会更高。例如,地球重力对小尘粒的影响对于地球而言可以忽略不计,但对于尘埃颗粒而言(对于相当大的距离)而言可以忽略不计。但是实际上,两个距离相同的尘埃不会互相影响很大。
SDwarfs

其实这是一个很好的观点。我误认为这是碰撞测试,但实际上却是相反的:如果粒子不接触,它们会相互影响。
阿利斯泰尔·巴克斯顿2013年

3

我对线程了解不多,但是您的循环似乎很耗时,因此也许可以从中进行更改

i = 0; i < count; i++
  j = 0; j < count; j++

  object_i += force(object_j);

对此

i = 0; i < count-1; i++
  j = i+1; j < count; j++

  object_i += force(object_j);
  object_j += force(object_i);

有帮助


1
为什么会有帮助?

1
因为前两个循环进行了10000次迭代,但是第二个循环仅进行了4 950次迭代。
2013年

1

如果您在使用10个模拟对象时已经遇到如此巨大的问题,则需要优化代码!您的嵌套循环只会导致10 * 10的迭代,其中10个迭代被跳过(相同的对象),从而导致内部循环进行90次迭代。如果仅实现2 FPS,则意味着您的性能非常差,每秒只能实现180次内部循环迭代。

我建议您执行以下操作:

  1. 准备/基准测试:要确定该例程是问题所在,请编写一个小的基准例程。它应执行Update()多次重力方法(例如1000次)并测量其时间。如果要通过100个对象达到30 FPS,则应模拟100个对象并测量30次执行的时间。应该少于1秒。需要使用这样的基准来进行合理的优化。否则,您可能会遇到相反的情况,并使代码的运行速度变慢,因为您只是认为它必须更快。所以我真的鼓励您这样做!

  2. 优化:尽管您无法对O(N²)努力问题做很多事情(意味着:计算时间随着模拟对象的数量N呈平方增加),但是您可以改进代码本身。

    a)您在代码中使用了大量“关联数组”(字典)查找。这些太慢了!例如entityEngine.Entities[e.Key].Position。你不能只用e.Value.Position吗?这样可以节省一次查找。您可以在整个内部循环中的任何位置执行此操作,以访问e和e2引用的对象的属性。b)在循环内的循环中创建一个新的Vector 。如果您的准确度并不需要真正精确,则可以尝试使用查找表。为此,您可以将值缩放到定义的范围,将其四舍五入为整数值,然后在预先计算的结果表中查找它。如果您需要帮助,请询问。d)您经常使用。您可以预先计算并将结果存储为或-如果它始终是偶数正整数值-您可以使用移位操作new Vector2( .... )。所有“新”调用都隐含了一些内存分配(以及后来的释放)。这些甚至比查找“字典”要慢得多。如果您仅临时需要此Vector,则可以在循环外分配它,并通过将其值重新初始化为新值而不是创建新对象来重新使用它。c)你使用了大量的三角函数(如atan2cos.Texture.Width / 2.Texture.HalfWidth>> 1 由两个。

一次只执行一项更改,并通过基准测试来衡量更改,以了解更改如何影响您的运行时!也许一件事是好事,而另一个想法是坏事(即使我确实在上面提出了它们!)...

我认为这些优化比尝试使用多个线程获得更好的性能要好得多!您将很难协调线程,因此它们不会覆盖其他值。同样,它们在访问相似的存储区域时也会发生冲突。如果您使用4个CPU /线程来完成这项工作,则帧速率可能只会提高2到3。


0

您可以在没有对象创建行的情况下对其进行重做吗?

Vector2 Force =新的Vector2();

Vector2 VecForce =新的Vector2((float)Math.Cos(angle),(float)Math.Sin(angle));

如果您可以将力值放置到实体中而不是每次都创建两个新对象,则可能有助于提高性能。


4
Vector2XNA中的值类型。它没有GC开销,并且构建开销可以忽略不计。这不是问题的根源。
Andrew Russell

@安德鲁·罗素(Andrew Russell):我不确定,但是如果您使用“ new Vector2”,情况是否真的如此?如果使用不带“ new”的Vector2(....),则可能会有所不同。
SDwarfs

1
@StefanK。在C#中,您无法做到这一点。需要新的。您在考虑C ++吗?
MrKWatkins
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.