关于什么是面向数据的设计已经one more question,并且经常提到一个article(我已经读过它5到6次)。我理解这个的一般概念,特别是在处理例如3d模型时,你想要将所有顶点保持在一起,而不是用法线污染你的面孔等。
然而,我确实很难想象数据导向设计如何适用于除最简单的案例(3d模型,粒子,BSP树等)以外的任何其他设计。是否有任何好的例子真正包含数据导向设计并展示了它在实践中如何运作?如果需要,我可以通过大型代码库。
我特别感兴趣的是“哪里有一个很多”的咒语,我似乎无法与其他人联系。是的,总有不止一个敌人,但是,你仍然需要单独更新每个敌人,因为他们现在不会以同样的方式移动它们吗?对于上述问题的接受答案中的'球'示例也是如此(我实际上在对该答案的评论中询问了这一点,但尚未得到答复)。仅仅是渲染只需要位置,而不是速度,而游戏模拟需要两者,而不是材料?或者我错过了什么?也许我已经理解了它,这是一个比我想象的更直接的概念。
任何指针都会非常感激!
答案 0 :(得分:42)
那么,DOD到底是什么?显然,它是关于性能的,但不仅如此。它还涉及设计良好的代码,这些代码易读,易于理解,甚至可重复使用。 现在面向对象的设计就是设计代码和数据以适应封装的虚拟对象"。每个对象都是一个单独的实体,包含对象可能具有的属性的变量以及对自身或世界上的其他对象采取操作的方法。 OO设计的优势在于,您可以轻松地将代码建模到对象中,因为我们周围的整个(真实)世界似乎以相同的方式工作。具有可以相互交互的属性的对象。
现在的问题是计算机中的cpu以完全不同的方式工作。当你让它一次又一次地做同样的事情时,它最有效。这是为什么?因为一个叫做缓存的小东西。在现代计算机上访问RAM可能需要100或200个CPU周期(并且CPU必须等待所有时间!),这太长了。因此,CPU上的这一小部分内存可以非常快速地访问,缓存内存。问题是它只有几MB的顶部。因此,每当您需要缓存中的数据时,您仍需要很长时间才能使用RAM。这不仅仅是数据的方式,代码也是如此。尝试执行不在指令高速缓存中的函数将导致从RAM加载代码时停止。
回到OO编程。对象很大,但大多数函数只需要一小部分数据,因此我们通过加载不必要的数据来浪费缓存。方法调用其他方法调用其他方法,打破指令缓存。不过,我们经常一遍又一遍地做很多相同的事情。让我们从游戏中拿一颗子弹。在一个天真的实现中,每个子弹可以是一个单独的对象。可能有一个子弹管理器类。它调用了第一个子弹的更新功能。它使用方向/速度更新3D位置。这会导致来自对象的许多其他数据被加载到缓存中。接下来,我们调用World Manager类来检查与其他对象的冲突。这会将许多其他内容加载到缓存中,甚至可能导致原始项目符号管理器类中的代码从指令缓存中删除。现在我们回到子弹更新,没有碰撞,所以我们回到子弹管理器。它可能需要再次加载一些代码。接下来,子弹#2更新。这会将大量数据加载到缓存中,调用世界......等等。在这种情况下,我们有两个加载代码的档位,让我们说两个加载数据的档位。至少有400个周期浪费了1个子弹,我们还没有考虑到其他东西的子弹。现在一个CPU运行在3+ GHz以上,所以我们不会注意到一颗子弹,但如果我们有100颗子弹呢?还是更多?
所以这就是那里有很多故事的地方。是的,在某些情况下,您只能获得对象,经理类,文件访问等。但更常见的是,有很多类似的案例。天真,甚至不天真的面向对象设计将导致许多问题。所以进入面向数据的设计。 DOD的关键是围绕数据建模代码,而不是像OO设计那样反过来。这从设计的第一阶段开始。您不首先设计您的OO代码然后进行优化。首先列出并检查您的数据,并思考如何修改它(我稍后将介绍一个实际的例子)。一旦您知道代码将如何修改数据,您就可以以尽可能高效的方式对其进行处理。现在你可能认为这只会导致代码和数据到处都是可怕的,但只有你设计得很糟糕的情况才会出现这种情况(糟糕的设计与OO编程一样容易)。如果您设计得很好,代码和数据可以围绕特定功能巧妙地设计,从而产生非常易读且甚至可重复使用的代码。
回到我们的子弹。我们只保留子弹管理器,而不是为每个子弹创建一个类。每个子弹都有一个位置和一个速度。每个子弹的位置都需要更新。每个子弹都必须进行碰撞检查,所有击中某些东西的子弹都需要采取相应的措施。因此,只要看一下这个描述,我就能以更好的方式设计整个系统。让我们将所有项目符号的位置放在数组/向量中。让我们将所有项目符号的速度放在数组/向量中。现在让我们开始迭代这两个数组并用它的相应速度更新每个位置值。现在,加载到数据缓存中的所有数据都是我们将要使用的数据。我们甚至可以预先设置一个智能预加载命令来预先加载一些数组数据,以便在我们到达时将数据放入缓存中。接下来,碰撞检查。我不会在这里详细介绍,但你可以想象如何在彼此之后更新所有子弹可以提供帮助。另请注意,如果发生碰撞,我们将不会调用新功能或执行任何操作。我们只保留一个带有碰撞的所有子弹的向量,当完成碰撞检查时,我们可以相互更新所有这些。通过以不同的方式放置我们的数据,看看我们如何从大量内存访问到几乎没有内存访问?您是否也注意到我们的代码和数据(即使不是以OO方式设计)仍然易于理解且易于重用?
所以回到"那里有很多"。在设计OO代码时,您会考虑一个对象,即原型/类。子弹有一个速度,一个子弹有一个位置,一个子弹会按每个帧移动它的速度,一个子弹可以击中一些东西等等。当你想到这个时,你会想到一个有速度的类,位置,以及更新子弹和检查碰撞的更新功能。但是,当您有多个对象时,您需要考虑所有这些对象。子弹有位置,速度。有些子弹可能会发生碰撞。您是否看到我们不再考虑单个对象?我们正在考虑所有这些问题,并且现在正在以不同的方式设计代码。
我希望这有助于回答你问题的第二部分。它不是关于你是否需要更新每个敌人,而是关于更新它们的最有效方式。虽然使用DOD设计你的敌人可能无助于获得很多性能,但围绕这些原则设计整个游戏(仅在适用的情况下!)可能会带来很多性能提升!
所以在问题的第一部分,这是国防部的其他例子。对不起,我在那里没那么多。有一个非常好的例子,我前段时间遇到过这个问题,Bjoern Knafla编写了一个关于行为树数据导向设计的系列文章:http://bjoernknafla.com/data-oriented-behavior-tree-overview你可能想要从4系列中的第一个开始,链接在文章本身。 尽管有一个老问题,但希望这仍然有帮助。或者也许其他一些SO用户会遇到这个问题并从这个答案中得到一些用处。
答案 1 :(得分:1)
我阅读了您链接的问题和文章。
我读了一本关于数据驱动设计的书。
我和你在同一条船上。
我理解诺埃尔的文章的方式是你以典型的面向对象的方式设计你的游戏。您可以使用适用于类的类和方法。
完成设计后,您会问自己以下问题:
如何在一个巨大的blob中安排我设计的所有数据?
从将整个设计编写为一种功能方法的角度来考虑它,并使用许多从属方法。这让我想起了我年轻时的50万线科博尔节目。
现在,你可能不会把整个游戏都写成一个巨大的功能方法。真的,在文章中,Noel谈论的是游戏的渲染部分。可以把它想象成一个游戏引擎(一个巨大的功能方法)和驱动游戏引擎的代码(OOP代码)。
我特别感兴趣的是“哪里有一个很多”的咒语,我似乎无法与其他人联系。是的,总有不止一个敌人,但你仍然需要单独更新每个敌人,因为他们现在的行动方式不同吗?
你在考虑对象。尝试考虑功能性。
每次敌人更新都是循环的迭代。
重要的是,敌方数据的结构是在一个内存位置,而不是分散在敌人的对象实例中。