实践中面向数据的设计?

fal*_*tro 16 data-oriented-design

还有一个问题是关于什么是面向数据的设计,并且有一篇文章经常被提及(我已经读了5到6次).我理解这个的一般概念,特别是在处理例如3d模型时,你想要将所有顶点保持在一起,而不是用法线污染你的脸等.

但是,我确实很难想象数据导向设计如何适用于除最简单的案例(3d模型,粒子,BSP树等)以外的任何其他设计.是否有任何好的例子真正包含数据导向设计并展示了它在实践中如何运作?如果需要,我可以通过大型代码库.

我特别感兴趣的是"哪里有一个很多"的咒语,我似乎无法与其他人联系.是的,总有不止一个敌人,但是,你仍然需要单独更新每个敌人,因为他们现在不会以同样的方式移动它们吗?这同样适用于在接受的答案上述问题的"balls',例如(其实我到这个问题的答案评论问这一点,但还没有得到答复还).仅仅是渲染只需要位置,而不是速度,而游戏模拟需要两者,而不是材料?或者我错过了什么?也许我已经理解了它,这是一个比我想象的更直接的概念.

任何指针将非常感谢!

Mar*_*art 45

好吧,所以我意识到这个问题很老了,但我仍然认为值得回复,因为不幸的是勒布朗克先生,并且我在充分尊重的情况下说这一点,有很多国防部错误.事实上,句子"在你完成设计之后,你会问自己以下问题:我如何安排我在一个巨大的blob中设计的所有数据?" 与DOD试图做的相反,它几乎是模仿的,尽管答案的其余部分更重要.不过没有不尊重,Le Blanc先生显然是这个社区非常知识渊博和乐于助人的成员.

那么,DOD到底是什么?显然,这是关于性能的,但不仅如此.它也是关于设计良好的代码,可读,易于理解,甚至可重复使用.现在面向对象的设计就是设计代码和数据以适应封装的虚拟"对象".每个对象都是一个单独的实体,包含对象可能具有的属性的变量以及对自身或世界上的其他对象采取操作的方法.OO设计的优势在于,您可以轻松地将代码建模到对象中,因为我们周围的整个(真实)世界似乎以相同的方式工作.具有可以相互交互的属性的对象.

现在的问题是你的计算机中的CPU以完全不同的方式工作.当你让它一次又一次地做同样的事情时,它最有效.这是为什么?因为一个叫做缓存的小东西.在现代计算机上访问RAM可能需要100或200个CPU周期(并且CPU必须等待所有时间!),这太长了.因此,CPU上的这一小部分内存可以非常快速地访问,缓存内存.问题是它只有几MB的顶部.因此,每当您需要不在缓存中的数据时,您仍需要很长时间才能使用RAM.这不仅仅是数据的方式,代码也是如此.尝试执行不在指令高速缓存中的函数将在从RAM加载代码时导致停顿.

回到OO编程.对象很大,但大多数函数只需要一小部分数据,所以我们通过加载不必要的数据来浪费缓存.方法调用调用其他方法的其他方法,打破指令缓存.不过,我们经常一遍又一遍地做很多相同的事情.我们以游戏中的子弹为例.在一个天真的实现中,每个子弹可以是一个单独的对象.可能有一个子弹管理器类.它调用了第一个项目符号的更新功能.它使用方向/速度更新3D位置.这会导致来自对象的许多其他数据被加载到缓存中.接下来,我们调用World Manager类来检查与其他对象的冲突.这会将许多其他内容加载到缓存中,甚至可能导致原始项目符号管理器类中的代码从指令缓存中删除.现在我们回到子弹更新,没有碰撞,所以我们回到子弹管理器.它可能需要再次加载一些代码.接下来,子弹#2更新.这会将大量数据加载到缓存中,调用世界......等等.在这种情况下,我们有两个加载代码的档位,让我们说两个加载数据的档位.对于1个子弹来说,这至少浪费了400个周期,并且我们还没有考虑到其他因素的子弹.现在CPU运行在3+ GHz以上,所以我们不会注意到一颗子弹,但是如果我们有100颗子弹怎么办?还是更多?

所以这就是那里有很多故事的地方.是的,在某些情况下,您只能使用对象,经理类,文件访问等.但更常见的是,有很多类似的情况.天真,甚至不天真的面向对象设计将导致许多问题.所以进入面向数据的设计.DOD的关键是围绕数据建模代码,而不是像OO设计那样反过来.这从设计的第一阶段开始.您不首先设计您的OO代码然后进行优化.首先列出并检查您的数据并思考如何修改它(稍后我会得到一个实际的例子).一旦您知道代码将如何修改数据,您就可以以尽可能高效的方式对其进行处理.现在你可能认为这只会导致代码和数据到处都是可怕的,但只有你设计得很糟糕的情况才会出现这种情况(糟糕的设计与OO编程一样容易).如果您设计得很好,代码和数据可以围绕特定功能巧妙地设计,从而产生非常易读且甚至可重复使用的代码.

所以回到我们的子弹.我们只保留子弹管理器,而不是为每个子弹创建一个类.每个子弹都有一个位置和一个速度.每个子弹的位置都需要更新.每个子弹都必须进行碰撞检查,所有击中某些东西的子弹都需要采取相应的措施.因此,只要看一下这个描述,我就能以更好的方式设计整个系统.让我们将所有项目符号的位置放在数组/向量中.让我们把所有子弹的速度放在一个数组/向量中.现在让我们开始迭代这两个数组并用它的相应速度更新每个位置值.现在,加载到数据缓存中的所有数据都是我们将要使用的数据.我们甚至可以预先设置一个智能预加载命令来预先加载一些数组数据,这样当我们到达时,数据就会在缓存中.接下来,碰撞检查.我不会在这里详述,但你可以想象如何在彼此之后更新所有子弹可以提供帮助.另请注意,如果发生碰撞,我们不会调用新函数或执行任何操作.我们只保留一个带有碰撞的所有子弹的向量,当完成碰撞检查时,我们可以相互更新所有这些.通过以不同的方式放置我们的数据,看看我们如何从大量内存访问到几乎没有内存访问?你是否也注意到我们的代码和数据,即使不是以OO方式设计,仍然易于理解和易于重用?

所以回到"哪里有一个有很多".在设计OO代码时,您会考虑一个对象,即原型/类.子弹有一个速度,一个子弹有一个位置,一个子弹会按每个帧的速度移动一个子弹,一个子弹可以击中一些东西等等.当你想到这个时,你会想到一个类,有速度,位置和一个更新功能,移动子弹并检查碰撞.但是,当您有多个对象时,您需要考虑所有这些对象.子弹有位置,速度.有些子弹可能会发生碰撞.您是否看到我们不再考虑单个对象?我们正在考虑所有这些并且现在正在以不同的方式设计代码.

我希望这有助于回答你问题的第二部分.这不是关于你是否需要更新每个敌人,而是关于更新它们的最有效方法.虽然使用DOD设计你的敌人可能无助于获得很多性能,围绕这些原则设计整个游戏(仅在适用的情况下!)可能会带来很多性能提升!

所以在问题的第一部分,这是国防部的其他例子.对不起,我在那里没那么多.有一个非常好的例子,我前段时间遇到过这个问题,Bjoern Knafla撰写了一篇关于行为树数据导向设计的系列文章:http://bjoernknafla.com/data-oriented-behavior-tree-overview 你可能想要从4系列中的第一个开始,链接在文章本身.尽管有一个老问题,但希望这仍然有帮助.或者也许其他一些SO用户会遇到这个问题并从这个答案中得到一些用处.

  • 很棒的解释.有一件事我没有得到.每个属性(位置,运动......)都有一个向量,它们是如何连接的?想象一下子弹碰撞,我怎么知道我必须从所有向量中删除哪个元素? (2认同)
  • 它们可以通过id连接.因此,不是指向子弹对象的指针,而是具有BulletManager类可以转换为正确索引的id.因此,假设你要销毁/删除id为#9001的项目符号,你用该id调用BulletManager的删除函数,BulletManager在其索引中查找#9001并发现它指向数组索引42.然后,它可以在其所有数据中数组(在我们的例子中是位置和速度)将索引42处的数据替换为最后一个索引处的数据,更新id表以反映指向最后一个索引的id现在指向索引42并且#9001消失了 (2认同)
  • 因此,当您创建新项目符号时,可以使用初始位置/速度作为参数调用BulletManager的create函数.它找到最新的未使用的数组索引(比方说98)并存储速度和位置.它还会生成一个新的未使用的ID,比方说#10080.然后它更新id表以反映id为#10080的新项目符号位于索引98处,并将10080 id返回给调用函数,以便它可以使用该id与项目符号交互,如果将来需要的话. (2认同)
  • +1是为了使内容精巧和耐心;一个很好的答案。 (2认同)