<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Beyond Earth</title><link>http://teassy000.github.io/docs/translations/%E9%9D%A2%E5%90%91%E6%95%B0%E6%8D%AE%E8%AE%BE%E8%AE%A1/</link><description>Recent content on Beyond Earth</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><atom:link href="http://teassy000.github.io/docs/translations/%E9%9D%A2%E5%90%91%E6%95%B0%E6%8D%AE%E8%AE%BE%E8%AE%A1/index.xml" rel="self" type="application/rss+xml"/><item><title/><link>http://teassy000.github.io/docs/translations/%E9%9D%A2%E5%90%91%E6%95%B0%E6%8D%AE%E8%AE%BE%E8%AE%A1/01_%E9%9D%A2%E5%90%91%E6%95%B0%E6%8D%AE%E8%AE%BE%E8%AE%A1/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://teassy000.github.io/docs/translations/%E9%9D%A2%E5%90%91%E6%95%B0%E6%8D%AE%E8%AE%BE%E8%AE%A1/01_%E9%9D%A2%E5%90%91%E6%95%B0%E6%8D%AE%E8%AE%BE%E8%AE%A1/</guid><description>面向数据设计 # 面向数据设计，已经以各种形式存在了几十年，但直到 2009 年 9 月，才在 Noel Llopis 的同名文章 1 首次这样出现。一直以来，关于它算不算是种编程范式，争议不断。有人认为它可以与面向对象、过程式、函数式编程等其他范式混用。某种意义上的确如此，面向数据设计确实能同其他范式共同发挥作用，但也不否认，它是种更广义的编程方式。Lisp 程序员知道，函数式编程可以与面向对象共存，C 程序员很清楚面向对象能与过程式编程共存。这里，我们先将这些争议搁置，直接断言：面向数据设计是新的重要工具；它能够与其他工具共存。2
2009 年正合适。革命性的硬件业已成熟；潜力巨大的计算机被无视硬件的编程范式束缚；开发者编码的方式能让许多引擎程序员落泪。但时代变了。现在许多移动端和桌面端解决方案，似乎不太需要面向数据设计，并不是机器擅长处理低效实现，而是游戏设计要求不高，也不复杂。但现在手游的开发趋势似乎正向 AAA 级迈进，又一次，产生了对复杂情况管理的需求，以期最大程度发挥硬件。
如今，我们被多核设备围绕着，口袋里这个也不例外。学习如何较少依赖串行开发软件就变得尤为重要。面向数据的程序员能获得诸多好处，包括但不限于：摆脱对象信息传递、获得即时响应等。编程时，坚实依赖对数据流的认识，为将来进入 GPGPU 和其他计算方法做好准备。由此，落地诸多游戏构想。面向数据设计的需求只会增长。抽象和串行思维将制约你的竞争对手，而那些接受面向数据方法的人会茁壮成长。
一切围绕数据 # 数据即一切。数据是创造用户体验时需要变换的；是打开文档时加载的；是屏幕上的图形；手柄按钮的脉冲；是扬声器震动空气产生的波；是角色升级的路线；也是对手向玩家开枪的诱因。数据是炸药引信的时长；是撞到尖刺掉宝的数量；是游戏结束时绚丽场景中每个粒子即时的位置和速度：经由源码，编译到汇编指令，再解码为机器指令，操纵磁盘读取内容，再一步步最终呈现到眼前。
不存在没有数据的应用。没有图像，Photoshop 无从谈起。没有画笔、图层、笔压，它什么也不是。没有字符、字体、分页符，Microsoft Word 也没有意义。没有事件，FL Studio 毫无价值。没有源码，Visual Studio 只能算作花瓶。过去所有的程序，都是基于输入数据，输出数据。数据的形式有时极其复杂，有时简单到无需文档，但所有应用程序都接收、产出数据。如果没有可识别的数据，顶多只算是玩具、Demo。
指令也是数据。指令会占用内存，使用带宽，并且可以被变换，加载，保存，构建。对于开发者，自然不会认为指令是数据3，但在旧的、保护性较差的硬件上，它们的区别很小。尽管大多数当代硬件，会保护为可执行文件预留的内存，避免其被损害、被修改，但这一相对较新的发明仍未成熟。改进的哈佛架构对内存中的数据和指令同等依赖4。因此，指令仍是数据，它们也是要变换的对象。我们接受指令并将其转化为行动。指令的数量、大小、频率都很重要。我们控制、筛选、使用哪些指令来解决问题的行为，即是优化。知道了数据是什么，便能决定如何处理数据。了解指令，便有了理论支撑，能决定哪些指令是必要的，哪些是冗余的，哪些可以用低成本方案替代。
现在，我们有了面向数据的开发方法的论证基础，但还遗漏了一个主要因素。所有这些数据和及其变换，从字符串，到图像，再到指令，都必须在某样东西上运行。这个东西有时相当抽象，如，未知硬件上运行的虚拟机；有时又很具体，比如已知 CPU、GPU、内存容量、带宽的自己的电脑。所有情况下，数据又不仅仅是数据，而是存在于某个硬件上的数据，而且必须经由同一硬件变换。本质上讲，面向数据设计是变换结构良好的数据，设计软件的方法，其中结构良好的标准是由数据的目标硬件，对其执行变换的模式与类型共同决定。有时，数据并不是很明确，硬件可能也捉摸不透。但大多数情况下，良好的硬件评判能力几乎对每一个软件项目都有所帮助。
如果应用程序的最终结果是数据，且所有的输入都可以表示为数据，并且了解所有的数据变换都没有凭空发生，那就可以基于此原则建立一个软件开发的方法论：理解数据，了解机器对特定数量、频率、统计量的数据执行变换时都发生了什么。在此基础上，就可以草拟一个关于如何使方法论面向数据的声明。
数据不是问题域 # 原则一：数据不是问题域。
对于有些人，面向数据设计似乎处于大多数其他编程范式的对立面，因为它不太容易让问题域进到软件源码中。它不鼓励将对象概念映射到用户的语境(Context)。因为数据刻意地，自始至终没有意义。重视抽象的范式会假装计算机和它的数据不存在，将字节、CPU管道、其他硬件特征等概念抽象出去，取而代之的是：引入问题模型。他们常把有观点的模型引入代码，或者把世界模型作为问题的语境。就是说，要么围绕预期解决方案的属性，要么围绕问题域的描述来构建代码。
赋予数据意义就能创造信息。意义并非数据固有。只有一个数字 4，几乎没意义，但如果说 4 英里，或 4 个鸡蛋，就有了意义。假设有 3 个数字，作为一个三元组意义不大，但如果将它们命名为 $x , y , z$，就可以赋予其位置的意义。有一份游戏中的位置列表，在没有语境的情况下也没什么意义。面向对象设计可能会把位置作为对象的一部分，通过类的名称和相邻的数据（也已经命名），就可以推断出数据的含义。如果没有已命名的语境数据与之关联，位置可以被赋予其他意义。虽然某种程度上，把数字放在语境当中是好的，但同时也阻碍了把位置作为三个数字的集合来思考，然而这一点对程序员思考如何解决的真正问题时，至关重要。
举例来讲，当把数据放在对象的深层，到后来又忘了它的存在。想想诸多已发售或尚在制作中的游戏，本可以使用一个 2D 或 3D 网格(grid)系统处理数据布局。不知为何，开发人员将地图上的每一个引用都实例化了。这还不是个例，在已经发售的游戏中，这种以对象为中心的方法摧残硬件的案例并不少见：相较于由真正的网格驱动，有数百个对象直接放置在世界空间的网格坐标上。可能程序员看到一个网格，看到所需的元素数量，就会对是否要为它分配一块内存而犹豫不决。一个简单的 $256 \times 256$ 的 tileMap 需要 $65,536$ 个 tile 。面向对象程序员可能会觉得 6 万多个对象相当耗费。对他们来说，只在必要的时才为 tile 分配对象可能更有意义，甚至到了在编辑器中真的有 $65000$ 个人工创建的 tile 的地步。但正由于它们是人工放置的，必要性就被确定了，于是就变成了不得不处理的确切问题。</description></item><item><title/><link>http://teassy000.github.io/docs/translations/%E9%9D%A2%E5%90%91%E6%95%B0%E6%8D%AE%E8%AE%BE%E8%AE%A1/02_%E5%85%B3%E7%B3%BB%E5%9E%8B%E6%95%B0%E6%8D%AE%E5%BA%93/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://teassy000.github.io/docs/translations/%E9%9D%A2%E5%90%91%E6%95%B0%E6%8D%AE%E8%AE%BE%E8%AE%A1/02_%E5%85%B3%E7%B3%BB%E5%9E%8B%E6%95%B0%E6%8D%AE%E5%BA%93/</guid><description>关系型数据库 # 为了能更好地布局数据，我们可以将现有的结构变换为线性的。将面向数据的方法应用于现有的代码和数据布局时，问题通常来自于，会隐藏，封装数据的编程范式中固有的状态复杂度。这些范式隐藏了内部状态，所以通常不会触及到，但当需要修改数据布局时，就会遇到阻碍。并不是因为它们不够抽象，无法做到在不影响用户代码正确性的前提下改变底层结构，而是因为数据结构被连接起来并赋予意义。这类耦合会很难消除。
在这一章中，我们将介绍关系模型、关系数据库技术、标准化的一些相关内容。它们是将高度复杂的数据结构和关系，变换为干净的、线性可存储数据条目集合的很好的例子。
当然做面向数据设计也不需要把数据变换到数据库风格，但很多时候，在简单的数组上工作会方便许多。本章将通过示例，告诉读者如何从有复杂连接的对象网络迁移到更简单的数组推理关系模型。
复杂状态 # 大多数软件中的数据，总有一些复杂或相互联系的特质。而在游戏开发中，为了保障玩家的沉浸感，需要在游戏内处理多种不同资源，在不同阶段实现听觉、视觉、甚至是触觉反馈。对于许多在面向对象设计中成长的程序员，把可用的结构类型减少到只有简单的数组似乎有些难以想象。从使用对象、类、模板、封装数据的方法，到一个只能访问线性容器的世界，是非常困难的。
Edgar F. Codd 在 A Relational Model of Data for Large Shared Data Bank1 中提出了关系模型，用于处理同数据交互的代理的，当下以及未来的需求。一个为插入、更新、删除和查询操作构建数据的解决方案。他认为这一建议在保证能良好利用数据的同时，不再那么依赖对数据布局的深刻理解；同时还能降低引入内部不一致的概率。
关系模型能够提供一个框架。在 Further Normalization of the Data Base Relational Model2中，Edgar F. Codd又提出了我们至今仍在用的标准化基本术语，能系统地将最为复杂的、相互关联的状态信息，减少到只有唯一独立元组的线性列表中。
寻找计算框架 # 数据库能以结构化的方式存储高度复杂的数据，并提供一种语言来变换数据和生成报表。SQL 语言由 IBM 的 Donald D. Chamberlin 和 Raymond F. Boyce 于 20 世纪 70 年代发明，它在能够存储可计算的数据的同时，也可以按照关系模型的形式维护数据。可游戏里没有简单的可计算数据，有的只是类和对象。都是些枪、剑、汽车、宝石、日常活动、纹理、声音、成就。看起来，数据库技术在使用面向对象时不会有什么帮助。
游戏中的数据关系可能非常复杂，乍一看似乎并不能整齐地放入数据库的行中。CD 收藏则很适合放进数据库，专辑可以整齐地排列在一个表中。但是，对于许多游戏对象来说却不适用。对于没有经验的人来说，很难用正确格式的表列来描述一个关卡文件。在正确的列项描述赛车游戏里的汽车可能也不简单。要为每个车轮设置一列吗？要为每个碰撞基元设置一列？还是只为碰撞网格设置一列？
好吧，整齐划一的数据库思维或许并不适用于游戏。但其实，只是因为数据还未标准化。这里我们会逐步执行这些标准化步骤，用以说明如何从网络模型或层级模型变换为需要的形式。我们会从一个关卡文件开始，随后读者会发现这些有几十年历史的技术，是如何拓展视野，帮助我们了解游戏数据到底在做什么的。
用不了多久，我们就会发现，数据库技术已经考虑过所有需要的操作了，只不过存储数据的方式将其掩藏了起来。任何数据结构都是在性能、可读性、可维护性、面向未来、可扩展性、可复用性之间做权衡。例如，常见的最灵活的数据库是文件系统。它有一个两列的表。一列是作为主键的文件路径，另一列是作为数据的字符串。这种简单的数据库简直就是完美的面向未来的系统。文件中可以存储任何东西。表越复杂，就越不具备未来性，也越不容易维护，但性能和可读性就越高。例如，文件没有自己的文档，但只需要数据库的模式(Schema)，就足以理解一个设计得足够好的数据库。这也是为什么，游戏甚至看起来都没有数据库。它们复杂到了，为了性能，已经忘了自己只是种数据变换。这种可变尺度的复杂度也影响了可扩展性，这就是为什么有人转向 NoSQL 数据库，以及文档存储这类数据归档。这些系统更像是一个文件系统，文件按名字访问，而且对结构的限制较少。同时也有利于横向扩展，如果不需要在不同机器上的多个表中保持数据一致，增加硬件就会更容易些。或许有一天，内存与最近的物理 CPU 紧密相连时，或内存芯片本身处理能力更强时，又或者桌面设备中运行 100 个 SoC 比一片 CPU 更高效时，从上层迁移到文档存储可能对应用程序会更好。但至少现在，对于本地硬件上的任务，还看不出这种处理模式究竟能带来什么好处。
这里我们不打算深入研究如何利用大型数据基元最底层的细节，如网格、纹理、声音等。现在，只把这些原始资产（声音、纹理、顶点缓冲区等）看作是如同整数、浮点数、字符串和布尔值一样基元。这样做是因为关系模型要求在处理数据时要有原子性。什么是原子性，什么不是，至今仍在争论，还没有一个绝对的答案。但是对于开发供人消费的软件，颗粒度可以植根于从人类感知的角度考虑数据。现有的 API 根据使用方式，会以不同的方式呈现字符串。例如，人类可读的字符串（通常是 UTF8）和用于调试的 ASCII 字符串之间的区别。如果能意识到所有这些东西都是资源，把它们拆分成小块，就会失去原本具有辨识度的特征。那么把声音、纹理、网格添加到这上面就顺理成章了。例如，半个句子比整个句子的作用要小得多。而且由于拆分破坏了完整性，一个句子的片断显然不能在保持其意义的前提下，连同另一个不同句子的随机片断重复使用。即使是字幕也是沿着有意义的边界做分割的。正是这种对有意义的边界的需求，给出了我们对于面向人类的软件开发中，原子性的定义。为此，在处理数据、进行标准化处理时，尽量停留在名词的层面，即可命名的片段。一整首歌可以是一个原子，时钟的一次滴答声也可以是一个原子，一整页的文字是一个原子，玩家的账号也是一个原子。</description></item><item><title/><link>http://teassy000.github.io/docs/translations/%E9%9D%A2%E5%90%91%E6%95%B0%E6%8D%AE%E8%AE%BE%E8%AE%A1/03_%E5%AD%98%E5%9C%A8%E6%80%A7%E5%A4%84%E7%90%86/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>http://teassy000.github.io/docs/translations/%E9%9D%A2%E5%90%91%E6%95%B0%E6%8D%AE%E8%AE%BE%E8%AE%A1/03_%E5%AD%98%E5%9C%A8%E6%80%A7%E5%A4%84%E7%90%86/</guid><description>存在性处理 # 如果苹果已经售罄，还要砍价吗？
存在性处理旨在消除：&amp;ldquo;是否要处理数据&amp;rdquo; 这一类冗余查询。大多数软件中，为了确保对象在工作开始前有效，会先检查其是否为空。那如果能始终保证指针不为空呢？如果可以确保其始终有效，并且一定会处理呢？
本章中，我们会展示一种可以用于面向数据的，运行时多态技术。当然，它不是唯一的面向数据设计友好型的运行时多态。但却是作者找到的第一个解决方案，并且很适合其他游戏开发技术，如：组件和计算着色器。
复杂度 # 学习软件工程时，我们可能会看到，其中有提到过循环复杂度 (或条件复杂度)。这是一个用数字表示，用于分析大型软件项目程序复杂度的指标。循环复杂度只涉及到流程控制。在这里，该公式表示为为 $(1 + 被分析系统中存在的条件数)$。因此对于任何系统，都从 $1$ 开始。对于每个 if, while, for, do-while 都 $+1$。另外 switch 语句中除 default 外，每个 case 也要 $+1$。
现在，仔细考虑虚函数调用的原理，即在函数指针表中查找，并进入类方法的分支。显然，虚函数调用实际同 switch 语句一样复杂。虚函数调用中，想要统计流程控制的数目会比较困难。要知道复杂度度，就必须知道满足的方法数目。因此必须计算对父类虚函数的 override 数目。如果方法是纯虚函数，那复杂度可以 $-1$。然而，有时看不到全部的运行时代码，如动态加载库，潜在的代码分支数目就会增加一个未知量。允许接入第三方库的系统，有必要接受这种不可见或模糊的复杂度，但需要一定程度的信任，这表示任意环节都没法彻底测试。
这种复杂度通常称为控制流复杂度。软件中还有一种固有的复杂度，就是状态复杂度。在 Out of the Tar Pit1 一文中有结论：最能提升软件复杂度的是状态。这篇论文提出了一个方案，以图最大限度减少所谓的意外状态，即：不直接解决问题，但在软件完成工作需要的状态。该方案还尝试废弃掉那些仅仅为了支持某种编程风格而引入的状态。
必要的控制是：实现设计时，一个功能必须在某些条件满足时才发生。例如：按下跳跃键时跳跃；存档数据变脏或计时器结束时，在检查点自动保存。
意外的控制，从使用者的角度看，对于程序是非必要的，但可能也是关乎程序能否工作的基础功能。这种控制的复杂度一般分为两种形式。第一种是结构性的，如支持某种编程范式、提供性能改进、驱动一种算法等。第二种，则是防御性编程或用于辅助开发者的，如引用计数、垃圾回收。这些技术在使用时会去确定数据是否存在，也会检查边界，因而也会增加复杂度。实践中，可以在使用容器和其他结构时看到它们，控制流会以边界检查确保数据没有以超出范围。垃圾回收也会增加复杂度。许多语言中，都难以确保回收会何时，怎样触发。也就意味很难推断对象的生命周期。使用这些语言时，人们倾向于在开发初期忽略内存分配，因此在临近交付时，可能很难修复内存泄漏。非托管语言的垃圾回收处理起来要容易些，因为引用计数更易查询，但也是因为非托管语言通常预先分配的频率较低。
调试 # 在高复杂度的程序中，会遇到哪些问题？分析系统的复杂度有助于了解其测试难度，反过来也有助于了解其调试难度。有些问题可以归类为处于意外状态，但也无法更进一步了。其他的可以归类为进入坏的状态：由于对无效数据做出反应而表现出意外的行为。不过，还有一些问题可以归类为性能问题，而非正确性问题。某种程度上，这些问题虽然被大量的学术文献忽视，但在实践中代价很高，而且通常来自于复杂的状态依赖关系。
例如，由缓存等优化技术引入的复杂度，是状态复杂度问题。CPU 的缓存处于一种不知道的状态，且在工作中没能预期到，就会导致性能不佳或不稳定。
许多时候，调试中的困难来自于：没有完全了解所有的流程控制点，假设已经采取了一条措施，而实际上并没有。程序按我们的要求去做，而非遵照我们的意思，它们就会进入一个预料外状态。
使用虚拟调用的运行时多态，会大大增加这种情况发生的可能性。因为不确定我们是否已经完全知道代码所有不同的分支，除非使用日志记录代码，或者用调试器来查看它在运行时的走向。
为什么要用 if # 真实的游戏开发案例中，显式的流程控制语句常常属于非必要集。实行防御性编程的地方，许多流程控制语句只是为了防止崩溃。阻止越界访问，保护为 NULL 的指针，防御其他会使程序终止的特殊情况。好在，GitHub 上有很多高质量的 C++ 源码，与这种趋势背道而驰。它们更倾向于使用引用类型，或尽可能使用值类型。游戏开发中，另一种常见的流控制是循环。虽然这种情况很多，但大多数编译器都能识别它们，并做出很好的优化，而且在去除不必要的条件检查方面做得很好。最后一种不重要但常见的流程控制来自多态调用，它对于实现一些游戏逻辑很有用，但主要是为了满足面向对象编写游戏的方法中，部分执行的 &amp;ldquo;更少代码，更多用途&amp;rdquo; 开发模式。
本质上，游戏设计中的流控制，不太会在性能分析文件中以分支控制出现，因为所有支持性代码会运行得更频繁。因此可能会忽视每个条件对软件性能的影响。用条件实现 AI，处理角色移动，决定何时加载关卡的代码，会在充满循环和树状遍历的系统中调用；或对访问中的数组边界检查，返回数据，产生布尔值，最终驱动 if 落入其中一个分支。也就是说，当代码库其他部分都很慢时，就很难验证为其中一项任务编写快速的代码。很难讲又增加了哪些额外成本。
如果觉得值得考虑一下清除控制流，就得先了解哪些控制流操作是可以清除的。如果从防御性编程开始着手，可以用数组的集合来表示工作数据集。这样就可以保证数据中没有 NULL。仅这一步，就可以清除许多流程控制语句。这样并不会摆脱循环，但只要是运行纯函数式变换的数据的循环，就不必担心副作用，并且反而会更容易推理。</description></item></channel></rss>