​TGDC | 一个游戏程序员的坚持 —— 论向量化编程

腾讯游戏学院 2020-12-22 32.3k
2020年12月7日,由腾讯游戏学院举办的第四届腾讯游戏开发者大会(Tencent Game Developers Conference,简称TGDC)于线上举行。来自重庆帕斯亚科技的CTO谢怡欣先生,分享了他对于向量化编程的一些看法。以下是分享视频和文字实录:


大家好,我是来自重庆帕斯亚科技的谢怡欣。

首先,我想向大家简单介绍一下我的经历。之前我在加拿大温哥华工作了几年,是Offworld Industries的一个高级程序员,参与制作了一款射击游戏《Squad》。我还做过一个手游,叫《Lionheart Tactics》。现在我在重庆帕斯亚科技担任技术负责,帕斯亚科技是2011年成立的,我们专注于高创意度的沙盒独立游戏。我先后参与开发了《星球探险家》、《波西亚时光》和《超级巴基球》这些作品。


在演讲开始之前,我想先给大家看一个视频。大概在2016年的时候,我在Youtube上看到的这个视频,来自于一个叫Mike Acton的程序员。他在视频里面就讲述了一个他称之为面向数据编程的概念。当时我看到那个视频是非常的不自在,因为2016年的话,应该是属于我刚熟悉面向对象编程的阶段。当时他说的那些东西,基本上就是说你学的那些东西,基本都是“垃圾”,都没什么意义。这确实很难让别人接受。但是我在潜意识当中,却又觉得这个人说的话,某些观点是挺有意义的。

我接下来要跟大家分享的视频,就是他做技术分享之后的问答环节。我们可以看到一个比较年长的业界人士,对他分享观点的一些质问。结果Mike Acton就直接把话给他怼回去了。请先看一下这个视频:


确实态度不算特别的友好。一个很偶然的机会,我和一个做HR的小伙伴,就聊到了这个事情。我和她一起看了这个视频,她就提出一个观点,就是这个穿橘黄色衣服的Mike Acton,他的姿态是比较有攻击性的。就是他很喜欢把手举到肩膀以上,这样的话实际上是给观众一个信息,就是我说的东西,或者是我的地位比你们要高。可能这也是我当时听到他说的话,觉得难以接受的原因之一。当然,如果说有机会Mike Acton能看到我今天的演讲的话,我想说我肯定不会有任何的不敬,我非常欣赏和佩服您的才华和知识。

这里有另外一个链接,这个链接就是很早以前微软一个写Windows的程序员他的一个抱怨。他就是讲Windows为什么比其他的操作系统慢。他说因为Windows程序实际上是由于商业化开发,然后迭代了很多很多次,真正写了很多代码的那些程序员,可能早就被类似亚马逊和谷歌这些公司挖走了。剩下的一些程序员,都是相对来说没那么多经验,改代码也不知道从何下手的。


从项目管理的角度说,实际上做代码清理是不产生任何价值的。如果是增加新的功能,那就是增加它的价值。但如果只是清理代码,可能就不会被你的领导所器重。

然后说一下面向对象编程。在我看来,根据程序员自身的水平,他对于这种编程的理解是相差比较大的。就算是很高级很有经验的程序员,他对面向对象编程的一些设计模式,也会有一些细微的差别。在经过多次修改之后,肯定也都会产生刚才那个程序员抱怨的那种大片大片的死代码。但是你又不是很敢删,我相信很多在座的程序员都有过这样的经历。

此外,面向对象编程实际上对缓存是很不友好的,但是这一部分资料网上有很多,我就不再赘述了。还有一个点是,我不知道大家有没有发现,现在主流的游戏软件、游戏程序或者说应用程序,都只是用了一到两个线程,很少有多线程能得到充分利用的。现在大多数的中高端硬件,都是支持十个以上的线程,而向量化编程的话,实际上是可以充分利用这些计算资源的。

接下来,我想说一下为什么需要了解向量化编程。在我看来,向量化编程实际上是提高程序员的内力。内力是什么东西?就比如说张无忌他的内功深厚,他学了九阳神功之后,感觉他学其他的武功都很快,基本上就是信手拈来。如果你作为一个程序员,有很强的内功的话,那你要学那些比如说游戏客户端、服务器、全栈工程师,包括多线程编程什么的,都会变得很容易。我觉得至少从我一段时间的学习经历来看的话,我觉得真的是有这种效果的。


还有一点,简单说一下。向量化编程实际上是简化了多线程加锁的逻辑,基本上是没有什么锁的。或者说,锁这个概念已经从框架层就给你模糊掉了,你是基本用不到这种东西的,所以说是一个很好的简化。

向量化编程提高了代码的可读性。大家可以想一下,比如说你有一个函数,函数里面有很多很多行,实际上每一个行都是一个节点,然后每一个节点如果说是调用另外一个函数,它实际上在那个地方,就是一个分支。它就是可能分到另外一个深度的指数下面去了。那个东西又可能调用其他的函数,就分得更细一点。面向对象编程实际上是非常复杂的,基本上是比较鼓励这种分支。

向量化编程实际上它也是一个树形结构,但是相对来说要平坦很多,就是树的复杂程度要简化很多,也比较线性化。还有一点是,作为程序员,你学面向对象编程也就学几年,我觉得应该差不多掌握以后,就可以考虑去学习一些新的技术和新的研究方向了。我觉得向量化编程就是一个不错的选择。

说到向量化编程,就不得不借助ECS框架。ECS在网上实际上是有非常多的很成熟的教程的。就是说为什么它的速度快,这些缓存、数据对齐这一系列东西,在这里我就不再赘述了。我就做个很简单的介绍,然后再加上我自己的一些理解。


ECS就是Entity Component System的一个缩写。Entity就是数字,是一个索引。Component就是组件,就是纯数据的东西。当然这里实际上在我们的实际开发当中,Component上面也可以带一些简单的方法,但是那个方法只是管自己的逻辑,就不会涉及到和其他数据类型的交互。肯定就是说Component也不会含有指针或者是任何复杂的那种数据类型。它可以是数组,可以是Entity,也可以是引用到另外的一个Entity,这个是没问题的,因为Entity也是数字对吧。System就是系统,它是对指定Component结合的Entity进行数据变换。

在这里我想说一下,它跟传统的Object Oriented Programming差别没有想象中的那么大,从概念上几乎是一样的。Entity对应那边就是Object,Component对应那边可能就是Object上面的一个属性。你像比如说一个英雄,在ECS的话,英雄可能就是Entity,他的那些属性,就是那些Component。在面向对象编程的话,英雄就是Object,他的那些属性,比如说他的Class里面,可能有其他的一些字段。那么从概念上面来讲,这个基本上是一对一的。区别就是在于面向对象变成里面的那些方法,实际上是和它的类是写到一块的。在那个方法里面,基本上是想怎么来就怎么来。就是你想访问什么样的数据,你就访问什么样的数据,没有什么规定。ECS里面的System的话,对数据的访问是非常严格的。这也就是可能会劝退很多程序员的一个点。


我想再引入一个维度,就是从频率这个维度来看。游戏逻辑的编程频率维度可能分低频率和高频率。低频率时间基本上可以把它归纳成在一帧里面,就能够开始并且结束的。就是完成它所有的数据转换的一个事件,就比如说开始播一个动画,结束播一个动画; 怪物的产生,或者是说死亡;或者是说按了一个什么键,这些都是低频事件。高频事件,就是一个持续的连续的行为。比如说一个角色,他在一直不停地动,说着是说不停地在播一个动画,他需要每一帧都去维护。这个东西肯定也可以从函数的入口来判断,比如说是Update,一般Update就是比较高频,做按键的检测或者鼠标的检测,都是在Update里面。那么检测实际上也是个高频操作。但是至于检测到按键之后做的那些事情,那个就是低频事件。

我想再说明一点,就是从频率这个维度来讲,向量化编程可能是比较初级的。这是我在摸索过程当中,寻找出来的一条路径。我不排除有其他更好更高效的维度,我就想引入相对来说比较简单的编程实例。这个例子就是在游戏当中,比如说你有NPC,他可能每一帧都要去检测他的视野范围里面有没有其他阵营里的人。如果有,可能这个NPC就需要做一些反应。这段代码是伪代码,简化了很多的一个版本。我只是想让大家能够看一看就好了。在Update里面,就是做一个物理上面的查询,Get OverlapSphere,把自己坦克的位置放进去,然后把自己的视野半径放进去,最后看Collider,就是有没有碰撞体。要是有碰撞体的话,在它上面去再去拿一个看它有没有Tank的这个Component。如果说有,再生成特效,生成飞行道具,播一些音效这些之类的东西,这一段代码大家可以看一下。


实际上,Physics.GetOverlapSphere,它实际上是一个很高频的操作。就是我不管你这个坦克,只要是活着的,只要在那里没有做其他事情,它就会执行这个代码。在Colliders Length大于0那一段代码里面,它实际上是一个低频代码,你真正遇到敌人了,你才会触发的逻辑,就是这样的一个高频和低频的分段。

然后就是向量化编程的一个实例,这个我引用了Unity dots最新出的Date-Oriented...TechStack的一些API。肯定也不是很完整,如果大家真的要去用的话,可能也要去参考一下他们官方网站上面的一些文档,这里我大概有那个意思就行了。


第一段就是EntityManager GetCompoentArray,就是把所有的坦克,比如说你有100个坦克、1000个坦克,把所有坦克的位置信息,放到一个数组里面。第二句就是把它的视野半径放到一个数组里面。第三个就是说把它周围有没有东西这个状态,放到一个数组里面。然后那个Entities for Each就是dots,就是ECS很标准化的向量化的一个操作。就是把所有的坦克,它的位置还有它的视野做一个查询。使用C++比较多的小伙伴就会发现,这个Physics.GetOverlapSphere实际上是一个const,是一个常称之为常量函数的东西。它这个函数是不会改变任何状态的,这种函数实际上是在向量化编程里面是非常友好的。因为它不涉及到racing condition,就是不会产生那种比如说一个线程在读一个内存,或者是说同时另一个线程又在往那个内存或者说往那个变量里面写东西的那种情况。这种对于多线程是非常友好的。


下面那个HITS,相当于是把它返回的结果就放到那个里面。如果说你有十个盒子十个线程,然后这里面有1000个坦克,每一个线程可能分到的就是100个坦克,那么他们就分配1000个数组。第一个线程可能就是填充HITS数组从0到99的位置。以此类推,Schedule就是做这个事情的。最后那个Complete Dependency就是它主线程上面的一个阻塞。它的意思就是说等所有线程的工作全部都做完了以后,我们的主线程然后再开始往下面走。接下来就是和刚才的那种顺序化写法是一模一样的。就是我现在有这个结果,我怎么去做响应,比如说create effect,就是产生特效,创造飞行道具,或者是说播音效也好,这些东西就跟之前的实际上是一样的。大家可以发现这个东西实际上就是在向量化一个高频的操作,然后低频的操作还是按照之前传统的那种顺序化的写法写出来。


这里我想再做一个比喻。因为我平时有时候也玩一下乐高,有一次我在拼这个起重机的时候,我就发现一个比较有趣的事情。这是当时我拼的起重机的底盘,底盘有四个轮子,要支撑这四个轮子,就需要用到三个触角的那种零件,左边右边各有一个,背后也有两个,一共四个。拼装这个零件的话,就是下面的这个步骤。大家看到从158-165一共八个步骤,172-179,又是八个步骤。你会发现,这个八步和那个八步实际上是一模一样的,只是方向不一样而已。


当时我就在想,它只需要重复四遍做四个轮子而已,那如果说现在要重复一百遍的话,你会用什么样的一个流程去做呢?你是会按部就班地从零件包里找那两个零件,然后按照说明书的步骤一步一步拼起来吗?那你光是找零件的动作,就需要重复8x100=800次,相当的耗费时间。但是也有另外一个办法,比如你先做158,再做172,然后再回过头做158,再做172,你一次性找100个158的那两个零件,找100对就行了。同理,159也是找100对那两个零件。用这个流程的话,找零件这个步骤你只会重复8次,其他时间你都在做很高效的拼接。


这个例子实际上是可以体现向量化编程的一个很核心的思想,就是顺序执行和向量化执行的差别。遇到这种数量级比较大的,100次或者说更多次的这种操作,你可以想办法把它向量化,然后再对于那种低频的操作,还是以顺序化的方式去书写。


最后我再讲一下向量化变成在实际应用当中的优劣势。优势还是挺明显的,刚刚也大概提了一下,它的代码调用数的深度,低于面向对象编程,能够产生争议的点比较少,代码管理的成本也会低一些。System代码基本上不需要时间去读,你只要把数据结构定义好,你这个数据是用来干什么的就行。System代码实际上就是把数据A变成数据B,就是一个数组,数组A变成数组B,就是一个非常简单非常透明的操作。

劣势,确实这个技术的起点会比较高,在写System代码的时候,需要把所有的数据的读写关系,是只读还是只写,还是又读又写,这些东西要把它摸索得很清楚,你才可以写出比较好的System的代码。在这方面确实门槛是比面向对象编程是要高一些的。然后算法从单线程改成多线程,难度确实是比较高。在这个点上我想给大家一个建议,一开始不要想把所有的算法,所有的在顺序化,或者说面向对象编程的那种思维,想出来的那种算法,都把它改成多线程。我觉得这是一个难度比较大的问题。可能从项目管理上面来说,可以先就用单线程写一下就好了。如果说这个东西真的在最后产品测试的时候发现花的时间太多,我们需要优化,然后在那个时候,再考虑怎么把它的高频的那些操作向量化。

还有最后一点,如果说用ECS这套框架,写顺序化执行的代码,它的boilerplate会比较多。如果说是面向对象编程,你有一个实例,你点一下,自动就把它的属性这些成员变量就给你点出来了。在ECS就用Get Component data,如果说对数据有改动,还会再用Set Component data,把它赋值赋回去,大概就是这个样子。

然后下面是我在自己学习向量化编程的时候,自己摸索做的一个展示。这是一个比较典型的塔防的一个DEMO。你们可能会发现有的时候这些小虫子会消失,实际上它们是被炮塔攻击了,只是没有添加特效。大家可以看到比较多的虫子,它们是沿着这个地形去走的。我特别花时间做了一个爬墙的逻辑,就是每个虫子实际上都做了两个射线查询,来判断自己是不是在墙上。当然有些岩石是没有做碰撞体的,所以说它可能就是从岩石上面就穿过去了。当时也是时间比较赶,所以说也没有做太多的那种细节的打磨。


在这个场景里面差不多有七八千到一万个虫子,每一帧都做了射线查询,以及它的周围有哪些虫子,避免虫子与虫子之间有穿插的现象,当时也是在开发环境里面维持了有三十帧的样子。

今天我的演讲就到此结束,谢谢大家!

来源:腾讯游戏学院
原文:https://mp.weixin.qq.com/s/PrU0o44PGYverwrQbJcchg

相关推荐