面向数据编程
面向数据编程是一种强大的编程方法,能最大限度发挥性能。
面向数据编程是一种强大的编程方法,能显著提升性能。它专注于将 数据 作为核心元素,其他一切都围绕它组织,以便访问或修改这些数据。这种方法也非常适合多人游戏,因为它让需要在玩家之间同步的数据更容易、更快地访问。
面向数据编程鼓励你把场景中的一切都看作需要在各个系统中被复制和变异的数据。这种方法的主要好处在于优化从内存中读取数据的速度,而这往往是运行现代应用和游戏时的主要瓶颈。
由于这种惊人的性能提升,过去几年里,游戏制作行业的许多领域都在转向采用这种方法。
Decentraland 的 SDK 在 JavaScript 中执行场景,而这种语言的一个缺点是无法控制内存分配。不过,运行 Decentraland 的引擎使用的是 C#,而遵循面向数据的原则对它非常有利。SDK 和引擎之间会不断互相发送消息。为了让这种通信尽可能高效,最好让双方的数据结构尽可能相似,以避免不得不不断重组这些数据。
它的样子
面向数据编程不同于面向对象编程,后者是许多开发者目前熟悉的方法。在面向对象编程中,代码的结构围绕试图复制现实世界构造的抽象——对象——来组织。每个对象都可以同时保存数据和功能。采用这种方法的应用在概念上往往很容易规划,但运行起来也更低效。
在面向数据编程中,数据不是围绕对象组织的,而是围绕优化访问便利性来组织。数据所代表的现实世界构造不会参与到那些会改变该数据的不同流程中。
Decentraland SDK 所构建的实体组件系统(ECS)模型与面向数据编程方法非常兼容。每个组件都是结构化数据集合的一部分。组件通过引用属于某个实体,但数据不是围绕实体组织的,而是作为相似组件的集合来组织的。例如,所有 Transform 场景中的组件都是平等的。其中一个变换组件可能属于你主建筑的模型,另一个可能属于桌子上的一个玻璃物体,而它是桌子的子对象。然后,场景中的系统会逐个处理这些 Transform 组件列表,不做任何区分。所有 Transform 组件都具有相同的字段,并经过相同的检查。

想象一个场景里有十几扇门,它们要么开着,要么关着。你可以把所有这些门的状态表示为一个简单的“isOpen”组件,其中包含一个布尔值。如果“isOpen”为 true,门就应该是开的;如果为 false,门就应该是关的。如果玩家点击一扇门,它就应该切换状态,而其他玩家也应该看到它切换。在你的场景处理门状态变化并与其他玩家同步时,场景其实并不关心“isOpen”代表什么。整组组件只是需要与其他玩家同步的一组布尔值。然后,你场景中的一个单独系统可以负责定期将每个“isOpen”的状态与对应门的旋转同步起来。
面向数据编程并不一定更难,但它是一种需要学习和采用的不同方法。不习惯这种方法的开发者可能需要一些时间来熟悉它,我们鼓励你去探索并亲手尝试示例,以便更好地把握。
为什么它有效
要理解为什么面向数据编程会带来如此大的差异,我们需要看看硬件。
当处理器需要从内存中提取数据时,它会把一整块数据加载到缓存中,其中也包括那些恰好写在我们想要的值旁边的内存中的数据。你的代码越能利用已经在缓存中的数据,运行速度就越快。
想象机器的内存就是一个字面意义上的仓库,数据存放在许多堆叠起来的箱子里。每当我们需要某些数据时,就得叫一辆叉车去取出那个数据所在的箱子,并把它搬到前台供检查。前台一次只能放下几个箱子,所以你不能把太多数据留在附近。
叉车要开到仓库后方把箱子运回来需要很长时间。如果你想要的这些箱子里的数据是分散的,那就意味着要让叉车跑很多趟。大多数时候,你会站在前台旁无聊地打发时间,等待你请求的下一个箱子到来。
如果你巧妙地把这些箱子堆放好,并将很可能同时需要的数据打包在一起,就能避免很多这类浪费时间。如果你把数据分组得很巧妙,你通常会发现接下来需要的东西已经在前台的某个箱子里了。你可以直接开始使用,而不用麻烦叉车操作员。
例如,如果你的场景需要获取一扇门的状态来检查它是开还是关,硬件读取的不只是描述这扇门状态的那个布尔值,而是会读取大量可能相关也可能无关的其他数据。
假设你的场景中有一个系统,需要每一帧都更新十几扇门的开/关状态。如果你的代码按照面向对象编程方式组织,那么不同相关信息片段是如何分组的就无法预料。也许我们仓库里的一个“箱子”同时装着门 A 的“isOpen”状态、门 A 的材质,以及它在打开时播放的音频。你可能需要为那十几扇门中的每一扇都跑一趟去取一个新的数据“箱子”。当然,这整个过程还需要在每一帧重复一次。于是,即便门都没有改变状态,每秒也会有 360 次(30 x 12)比喻性的往返仓库后方的行程。
另一方面,如果你的代码遵循面向数据的方法,那么这 12 个布尔值很可能都在同一个箱子里。这是因为这些布尔值都属于同一个数组,而该数组一次性被记录到内存中。你并不会显式地组织这些数据如何适配内存,所以在最坏情况下,数组可能会被拆到两个箱子里。但即使在最坏的情况下,2 次往返也比 12 次好得多。
每一帧检查每扇门的状态听起来像很多工作,但如果所有数据都已经在内存缓存中,实际上会非常快。如果你的数据组织得井井有条,你的场景就可以对大量实体运行这类流程,同时仍然运行得非常快。
最后更新于