图片:在开发周期结束时,您的游戏爬行,但您没有看到探查器中的任何明显的热点。罪魁祸首?随机内存访问模式和常量缓存未命中。为了尝试提高性能,您尝试并行化代码的部分,但它需要英勇的工作,并且到底,由于您必须添加的所有同步,您几乎没有速度速度。要将其关闭,代码非常复杂,修复错误会产生更多问题,并立即丢弃添加新功能的想法。听起来有点熟?
这种情况非常准确地描述了我过去10年所涉及的每场比赛。原因不是我们正在使用的编程语言,也不是开发工具,也没有缺乏纪律。在我的经验中,它是面向对象的编程(OOP)和围绕它的文化,这在很大程度上归咎于这些问题。 OOP可能会阻碍你的项目而不是帮助它!
OOP在目前的游戏开发文化中被加入,在考虑比赛时很难在物体上思考。毕竟,我们一直在创建多年来代表车辆,玩家和国家机器的课程。什么是替代品?程序编程?功能语言?异国情调的编程语言?
以数据为导向的设计是一种方法,可以解决解决所有这些问题的程序设计。程序编程侧重于过程调用作为其主要元素,OOP主要与对象交易。请注意,这两种方法的主要焦点是代码:在一个案例中的普通程序(或函数),并在另一个内部状态相关联的分组代码。面向数据的设计将编程从对象的编程视角转移到数据本身:数据的类型,如何在内存中布置,以及如何在游戏中读取和处理。
根据定义编程是关于转换数据:它是创建一系列机器指令的行为,描述了如何处理输入数据并创建一些特定的输出数据。游戏只不过是一个以交互式率有效的程序,因此我们主要在那个数据上集中注意力而不是操纵它的代码是有意义的?
我想清理潜在的混乱和压力,以至于数据导向的设计并不意味着某些东西是数据驱动的。数据驱动的游戏通常是一个游戏,该游戏在代码之外暴露大量功能,并允许数据确定游戏的行为。这是对数据面向设计的正交概念,并且可以与任何类型的编程方法一起使用。
如果我们从数据的角度查看程序,理想的数据是什么样的?这取决于数据以及它是如何使用的。通常,理想数据是我们可以与最少量的努力一起使用的格式。在最佳情况下,格式将与输出相同,因此处理仅限于刚刚复制该数据。非常经常,我们理想的数据布局将是我们可以顺序地处理的大块连续性的均匀数据块。在任何情况下,目标是最大限度地减少转换量,并尽可能最大限度地释放您的数据,在资产建设过程中脱离此理想格式。
由于以数据为导向的设计首先将数据置于数据,我们可以围绕理想的数据格式构建整个程序。我们将永远无法使其完全理想(同样的方式,代码几乎没有逐字oop),但这是牢记的主要目标。一旦我们实现了这一点,列在列开始时提到的大多数问题都倾向于融化(更多关于下一部分的信息)。
当我们考虑对象时,我们立即想到树木继承树,遏制树或消息传递树,我们的数据自然地排列。因此,当我们对对象执行操作时,它通常会导致该对象依次进一步访问树中的其他对象。在执行相同操作的一组对象上迭代,在每个对象处生成级联,完全不同的操作(参见图1A)。
为了实现最佳的数据布局,将每个对象分解为不同的组件,以及在内存中的同一类型的组组件,无论它们来自哪些对象,都会有助于它们。该组织导致大块的同类数据块,允许我们顺序处理数据(见图1B)。一种关键原因,为什么数据导向设计如此强大,是因为它在大群对象上工作得很好。 OOP,根据定义,在一个对象上工作。退后一分钟,想想你工作的最后一场比赛:你的代码中有多少个地方你只有一个东西?一个敌人?一辆车?一个路径文件节点?一个子弹?一个颗粒?绝不!哪里有一个,有很多。 OOP忽略了它并以隔离的每个对象处理。相反,我们可以轻松地为我们和硬件制作东西,并组织我们的数据来处理具有许多相同类型的常见情况。
这听起来像是一种奇怪的方法吗?你猜怎么着?您可能已经在代码的某些部分中执行此操作:粒子系统!以数据为导向的设计将我们的整个CodeBase转化为巨大的粒子系统。也许是游戏程序员更熟悉的这种方法的名称将是粒子驱动的编程。
关于数据的第一和架构程序基于这促成了大量优势。
这些天,我们需要处理多个核心的事实。任何尝试使用一些OOP代码和并行化的人都可以证明易于困难,容易出错,并且可能不是很高效。通常,您最终添加了许多同步原语来防止从多个线程中并发访问数据,通常很多线程最终idling,相当于等待其他线程完成。结果,性能改善可能是非常强大的。
当我们应用数据设计时,并行化变得更简单:我们有输入数据,一个小功能来处理它,以及一些输出数据。我们可以轻松地采取类似的东西,并在多个线程中拆分,它们之间的同步最小。我们甚至可以进一步获取并在具有本地存储器的处理器上运行该代码(如单元处理器上的spus),而无需以不同的方式执行任何操作。
除了使用多个核心外,还有一个以实现现代硬件在现代硬件中实现良好性能的键,其深度指令管道和具有多个级别高速缓存的慢速存储器系统,具有缓存友好的内存访问。面向数据的设计导致非常有效地使用指令高速缓存,因为同一代码一遍又一遍地执行。此外,如果我们在大型连续块中铺设数据,我们可以顺序处理数据,近乎完美的数据缓存使用和卓越的性能。可能的优化。当我们考虑对象或函数时,我们倾向于在功能甚至算法水平时陷入优化;重新排序某些函数调用,更改排序方法,甚至用程序集重新编写一些C代码。
这种优化肯定是有益的,但通过首先思考数据,我们可以更进一步回来并制作更大,更重要的优化。请记住,所有游戏都可以将一些数据(资产,输入,状态)转换为其他数据(图形命令,新游戏状态)。通过牢记数据流动,我们可以基于如何转换数据的方式进行更高级别,更智能的决策,以及如何使用它。通过更传统的OOP方法实现,这种优化可能是非常困难和耗时的。
到目前为止,所面向数据设计的所有优点都是基于性能:高速缓存利用率,优化和并行化。毫无疑问,作为游戏程序员,表现是我们的一个极为重要的目标。技术之间通常有冲突,从而提高了有助于可读性和易于发展的性能和技术。例如,在汇编语言中重写一些代码可能会导致性能提升,但通常会使代码更加难以读取和维护。
幸运的是,面向数据的设计有利于性能和易于发展。当您专门编写代码以转换数据时,您最终可以使用小功能,在代码的其他部分上具有很少的依赖性。 CodeBase最终非常“平坦”,具有许多叶片功能,没有许多依赖项。这种模块化级别和缺乏依赖性使得更容易理解,更换和更新代码。
数据导向设计的最后一个主要优点是易于测试。正如我们在6月和8月内在产品栏中所看到的,写入单元测试以检查对象交互并不琐碎。您需要立即设置模拟和测试事物。坦率地说,这有点痛苦。另一方面,在直接与数据交换时,写入单元测试不能更容易:创建一些输入数据,调用变换函数,并检查输出数据是我们期望的。没有别的东西。这实际上是一个巨大的优势,并使代码非常易于测试,无论您是否正在进行测试驱动的开发或代码后的写入单元测试。
面向数据的设计不是游戏开发中所有问题的银弹。它确实有助于大量编写高性能代码,并使节目更加可读,更易于维护,但它确实有了几个缺点。
有针对性设计的主要问题是它与大多数程序员在学校习惯或学习的不同之处。它需要将我们的精神模型转化为90度,并改变了我们如何考虑它。在它成为第二个性质之前,它需要一些练习。
此外,因为它是一种不同的方法,它可能具有挑战性地与现有代码接口,以更多OOP或程序方式编写。在隔离中写一个函数很难,但只要您可以将数据面向设计的设计应用于整个子系统,您应该能够获得很多好处。
足够的理论和概述。您如何实际上启用数据设计设计?首先,只需选择代码中的特定区域:导航,动画,碰撞或其他东西。后来,当大多数游戏引擎都以数据为中心时,您可以从帧的开始直到结束时一直担心数据流。
下一步是清楚地识别系统所需的数据输入,以及它需要生成的数据类型。现在可以在OOP条款中思考它,只是为了帮助我们识别数据。例如,在动画系统中,一些输入数据是骨架,基本姿势,动画数据和当前状态。结果不是“代码播放动画”,但是当前播放的动画生成的数据。在这种情况下,我们的输出将是一组新的姿势和更新状态。
重要的是要进一步迈出一步并基于它的使用方式对输入数据进行分类。它是只读,读写还是只写?该分类将帮助指导关于存储它的位置的设计决策,以及何时根据程序的其他部分的依赖项来处理它。
此时,请停止思考单个操作所需的数据,并根据将其应用到几十或数百个条目方面进行思考。我们不再有一个骨架,一个基本姿势和当前状态,而是我们在每个块中具有许多实例的每个类型的块。
非常仔细地仔细考虑如何在转换过程中使用数据从输入到输出。您可能会意识到您需要在结构中扫描特定字段以执行数据的传递,然后您需要使用结果进行另一个传递。在这种情况下,将初始字段拆分为可以独立处理的单独存储器块可能更有意义,允许更好的高速缓存利用率和潜在的并行化。或者也许您需要向量化代码的某些部分,这需要从不同位置获取数据以将其放入相同的向量寄存器中。在这种情况下,可以连续地存储数据,使得可以直接应用矢量操作,而没有任何额外的变换。
现在,您应该对您的数据非常了解。编写代码转换它会更简单。这就像通过填写空白来编写代码。与相同的OOP代码所在的相比,您甚至会感到惊喜地意识到代码比在第一位置更简单和更小。
如果您思考过去一年的大部分主题,您会发现他们都领导了这种类型的设计。现在是时候要注意数据如何对齐(2008年12月和2009年12月),以便将数据直接烘烤到您可以有效使用的输入格式(OCT和2008年11月),或者使用数据之间的非指针引用块,因此它们可以很容易地重新安置(9月2009年9月)。
这是否意味着OOP是无用的,你永远不应该在你的程序中应用它?我不准备好这么做。当对象的思考时,当每个对象(图形设备,日志管理器等中的一个中只有一个时,虽然在这种情况下,但您可能会用简单的C样式函数和文件级静态数据写入。即使在这种情况下,这些对象仍然是在转换数据中设计的。
我仍然发现自己使用OOP的另一个情况是GUI系统。也许是因为你正在使用已经以面向对象方式设计的系统,或者可能是因为性能和复杂性并不是与GUI代码的重要因素。在任何情况下,我更喜欢亮起的GUI API,并尽可能多地使用遏制(Cocoa和Cocoatouch是良好的例子)。这是可以编写的数据导向的GUI系统,以便与之合作,但我还没有见过。
最后,如果这是你喜欢考虑游戏的方式,那么没有什么能阻止你的物体的心理图片。这只是敌人实体不会在内存中的相同物理位置。相反,它将被分成较小的子组件,每个子组件,每个形成类似组件的较大数据表的一部分。
以数据为导向的设计有点偏离传统的编程方法,而是始终思考数据以及如何转换,您将能够在性能和易于发展方面获得巨大的利益。
感谢Mike Acton和Jim Tilander多年来挑战我的想法,并为他们的反馈提供了对本文的反馈。
图片:在开发周期结束时,您的游戏爬行,但您没有看到探查器中的任何明显的热点。罪魁祸首?随机内存访问模式和常量缓存未命中。为了尝试提高性能,您尝试并行化代码的部分,但它需要英勇的工作,并且到底,由于您必须添加的所有同步,您几乎没有速度速度。要将其关闭,代码非常复杂,修复错误会产生更多问题,并立即丢弃添加新功能的想法。听起来有点熟?
这种情况非常准确地描述了我过去10年所涉及的每场比赛。原因不是我们正在使用的编程语言,也不是开发工具,也没有缺乏纪律。在我的经验中,它是面向对象的编程(OOP)和围绕它的文化,这在很大程度上归咎于这些问题。 OOP可能会阻碍你的项目而不是帮助它!
OOP在目前的游戏开发文化中被加入,在考虑比赛时很难在物体上思考。毕竟,我们一直在创建多年来代表车辆,玩家和国家机器的课程。什么是替代品?程序编程?功能语言?异国情调的编程语言?
以数据为导向的设计是一种方法,可以解决解决所有这些问题的程序设计。程序编程侧重于过程调用作为其主要元素,OOP主要与对象交易。请注意,这两种方法的主要焦点是代码:在一个案例中的普通程序(或函数),并在另一个内部状态相关联的分组代码。面向数据的设计将编程从对象的编程视角转移到数据本身:数据的类型,如何在内存中布置,以及如何在游戏中读取和处理。
根据定义编程是关于转换数据:它是创建一系列机器指令的行为,描述了如何处理输入数据并创建一些特定的输出数据。游戏只不过是一个以交互式率有效的程序,因此我们主要在那个数据上集中注意力而不是操纵它的代码是有意义的?
我想清理潜在的混乱和压力,以至于数据导向的设计并不意味着某些东西是数据驱动的。数据驱动的游戏通常是一个游戏,该游戏在代码之外暴露大量功能,并允许数据确定游戏的行为。这是对数据面向设计的正交概念,并且可以与任何类型的编程方法一起使用。
如果我们从数据的角度查看程序,理想的数据是什么样的?这取决于数据以及它是如何使用的。通常,理想数据是我们可以与最少量的努力一起使用的格式。在最佳情况下,格式将与输出相同,因此处理仅限于刚刚复制该数据。非常经常,我们理想的数据布局将是我们可以顺序地处理的大块连续性的均匀数据块。在任何情况下,目标是最大限度地减少转换量,并尽可能最大限度地释放您的数据,在资产建设过程中脱离此理想格式。
由于以数据为导向的设计首先将数据置于数据,我们可以围绕理想的数据格式构建整个程序。我们将永远无法使其完全理想(同样的方式,代码几乎没有逐字oop),但这是牢记的主要目标。一旦我们实现了这一点,列在列开始时提到的大多数问题都倾向于融化(更多关于下一部分的信息)。
当我们考虑对象时,我们立即想到树木继承树,遏制树或消息传递树,我们的数据自然地排列。因此,当我们对对象执行操作时,它通常会导致该对象依次进一步访问树中的其他对象。在执行相同操作的一组对象上迭代,在每个对象处生成级联,完全不同的操作(参见图1A)。
为了实现最佳的数据布局,将每个对象分解为不同的组件,以及在内存中的同一类型的组组件,无论它们来自哪些对象,都会有助于它们。该组织导致大块的同类数据块,允许我们顺序处理数据(见图1B)。一种关键原因,为什么数据导向设计如此强大,是因为它在大群对象上工作得很好。 OOP,根据定义,在一个对象上工作。退后一分钟,想想你工作的最后一场比赛:你的代码中有多少个地方你只有一个东西?一个敌人?一辆车?一个散文赛
......