22

还有一个关于什么是面向数据的设计的问题,还有一篇经常被引用的文章(我已经读了五六遍了)。我理解这个的一般概念,特别是在处理例如 3d 模型时,您希望将所有顶点保持在一起,而不是用法线污染您的脸等。

但是,除了最琐碎的情况(3d 模型、粒子、BSP 树等)之外,我确实很难想象面向数据的设计是如何工作的。有没有什么好的例子真正包含面向数据的设计并展示了这在实践中是如何工作的?如果需要,我可以浏览大型代码库。

我特别感兴趣的是“有一个就有很多”的口头禅,我似乎无法与这里的其他人联系起来。是的,敌人总是不止一个,但是,你仍然需要单独更新每个敌人,因为他们现在的移动方式不同了,是吗?上述问题的已接受答案中的“球”示例也是如此(实际上,我在对该答案的评论中提出了这个问题,但尚未得到答复)。仅仅是渲染只需要位置而不需要速度,而游戏模拟需要两者,而不需要材料吗?还是我错过了什么?也许我已经理解了它,这是一个比我想象的要简单得多的概念。

任何指针将不胜感激!

4

2 回答 2

54

那么,国防部到底是什么?显然,这与性能有关,但不仅如此。它还关乎精心设计的可读、易理解甚至可重用的代码。现在面向对象的设计就是设计代码和数据以适应封装的虚拟“对象”。每个对象都是一个单独的实体,其中包含对象可能具有的属性的变量以及对自身或世界上其他对象执行操作的方法。OO 设计的优点是很容易将代码建模为对象,因为我们周围的整个(真实)世界似乎都以相同的方式工作。具有可以相互交互的属性的对象。

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

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

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

所以回到我们的子弹。我们只保留子弹管理器,而不是为每个子弹创建一个类。每个子弹都有一个位置和一个速度。每个子弹的位置都需要更新。每个子弹都必须进行碰撞检查,并且所有击中某物的子弹都需要采取相应的措施。所以只要看看这个描述,我就能以更好的方式设计整个系统。让我们将所有子弹的位置放在一个数组/向量中。让我们将所有子弹的速度放在一个数组/向量中。现在让我们开始遍历这两个数组,并用相应的速度更新每个位置值。现在,加载到数据缓存中的所有数据都是我们将要使用的数据。我们甚至可以放一个智能的预加载命令来预先加载一些数组数据,以便数据' 当我们到达它时,它在缓存中。接下来,碰撞检查。我不会在这里详细介绍,但您可以想象一个接一个地更新所有项目符号会有什么帮助。另请注意,如果发生冲突,我们不会调用新函数或做任何事情。我们只保留一个包含所有发生碰撞的子弹的向量,当碰撞检查完成后,我们可以一个接一个地更新所有这些。看看我们是如何通过不同的数据布局从大量内存访问变为几乎没有的?您是否还注意到我们的代码和数据,即使不再以 OO 方式设计,仍然易于理解和易于重用?不会调用新函数或做任何事情。我们只保留一个包含所有发生碰撞的子弹的向量,当碰撞检查完成后,我们可以一个接一个地更新所有这些。看看我们是如何通过不同的数据布局从大量内存访问变为几乎没有的?您是否还注意到我们的代码和数据,即使不再以 OO 方式设计,仍然易于理解和易于重用?不会调用新函数或做任何事情。我们只保留一个包含所有发生碰撞的子弹的向量,当碰撞检查完成后,我们可以一个接一个地更新所有这些。看看我们是如何通过不同的数据布局从大量内存访问变为几乎没有的?您是否还注意到我们的代码和数据,即使不再以 OO 方式设计,仍然易于理解和易于重用?

所以回到“有一个有很多”的问题。在设计 OO 代码时,您会考虑一个对象,原型/类。子弹有速度,子弹有位置,子弹会以它的速度移动每一帧,子弹可以击中某物,等等。当你想到这一点时,你会想到一个类,它有速度、位置和移动子弹并检查碰撞的更新功能。但是,当您有多个对象时,您需要考虑所有对象。子弹有位置、速度。有些子弹可能会发生碰撞。你看到我们不再考虑单个对象了吗?我们正在考虑所有这些,并且现在设计代码的方式大不相同。

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

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

于 2012-03-26T13:43:56.213 回答
2

我阅读了您链接到的问题和文章。

我读过一本关于数据驱动设计的书。

我和你几乎在同一条船上。

我对 Noel 文章的理解是,您以典型的面向对象的方式设计游戏。您有在这些类上工作的类和方法。

完成设计后,您会问自己以下问题:

如何将我设计的所有数据排列在一个巨大的 blob 中?

把它想象成把你的整个设计写成一种功能方法,有很多从属方法。它让我想起了我年轻时庞大的 500,000 行 Cobol 程序。

现在,您可能不会将整个游戏编写为一个巨大的函数式方法。真的,在文章中,Noel 是在谈论游戏的渲染部分。将其视为游戏引擎(一个巨大的功能方法)和驱动游戏引擎的代码(OOP 代码)。

我特别感兴趣的是“有一个就有很多”的口头禅,我似乎无法与这里的其他人联系起来。是的,敌人总是不止一个,但是,你仍然需要单独更新每个敌人,因为他们现在的移动方式不同了,是吗?

你在考虑对象。尝试从功能的角度思考。

每个敌人的更新都是一个循环的迭代。

重要的是敌人数据被结构化为在一个内存位置,而不是分散在敌人对象实例中。

于 2010-08-06T17:45:29.893 回答