《守望先锋》架构设计和网络同步

《守望先锋》2017 GDC系列的分享前几年给了我很多帮助,尤其是kevinan大神的翻译更让我受益良多,如今我再想温习一下相关技术却发现很多网络上的文章图片都已经坏掉了,故在此收集网络资源发布重置版,当成备份。

重制版内容:新增多级标题,方便分块阅读,部分图片已由本人重置,一些必要的地方我录制了Gif图,方便观看。

全系列链接:《守望先锋》GDC2017技术分享精粹重制版总目录

原视频链接:https://www.youtube.com/watch?v=W3aieHjyNvw&t=2886s&ab_channel=GDC

ECS

ECS概述

哈喽,大家好,这次的分享是关于《守望先锋》(译注:下文统一简称为Overwatch)游戏架构设计和网络部分。老规矩,手机调成静音;离开时记得填写调查问卷;换下半藏,赶紧推车!(众笑)

我是Tim Ford,是暴雪公司Overwatch开发团队老大。自从2013年夏季项目启动以来就在这个团队了。在那之前,我在《Titan》项目组,不过这次分享跟Titan没有半毛钱关系。(众笑)

这次分享的一些技术,是用来降低不停增长的代码库的复杂度(译注,代码复杂度的概念需要读者自行查阅)。为了达到这个目的我们遵循了一套严谨的架构。最后会通过讨论网络同步(netcode)这个本质很复杂的问题,来说明具体如何管理复杂性。

Overwatch是一个近未来世界观的在线团队英雄射击游戏,它的主要是特点是英雄的多样性, 每个英雄都有自己的独门绝技。

Overwatch使用了一个叫做“实体组件系统”的架构,接下来我会简称它为ECS。

使用了一个叫做“实体组件系统”的架构,接下来我会简称它为ECS。

ECS不同于一些现成引擎中很流行的那种组件模型,而且与90年代后期到21世纪早期的经典Actor模式区别更大。我们团队对这些架构都有多年的经验,所以我们选择用ECS有点是“这山望着那山高”的意味。不过我们事先制作了一个原型,所以这个决定并不是一时冲动。

开发了3年多以后,我们才发现,原来ECS架构可以管理快速增长的代码复杂性。虽然我很乐意分享ECS的优点,但是要知道,我今天所讲的一切其实都是事后诸葛亮 。

ECS架构概述

ECS架构看起来就是这样子的。先有个World,它是系统(译注,这里的系统指的是ECS中的S,不是一般意义上的系统,为了方便阅读,下文统称System)和实体(Entity)的集合。而实体就是一个ID,这个ID对应了组件(Component)的集合。组件用来存储游戏状态并且没有任何的行为(Behavior)。System有行为但是没有状态。

这听起来可能挺让人惊讶的,因为组件没有函数而System没有任何字段。

ECS引擎用到的System和组件

图的左手边是以轮询顺序排列的System列表,右边是不同实体拥有的组件。在左边选择不同的System以后,就像弹钢琴一样,所有对应的组件会在右边高亮显示,我们管这叫组件元组(译注,元组tuple,从后文来看,主要作用就是可以调用Sibling函数来获取同一个元组内的组件,有点虚拟分组的意思)。

System遍历检查所有元组,并在其状态(State)上执行一些操作(也就是行为Behavior)。记住组件不包含任何函数,它的状态都是裸存储的。

绝大多数的重要System都关注了不止一个组件,如你所见,这里的Transform组件就被很多System用到。

来自原型引擎里的一个System轮询(tick)的例子

这个是物理System的轮询函数,非常直截了当,就是一个内部物理引擎的定时更新。物理引擎可能是Box2d或者是Domino(暴雪自有物理引擎)。执行完物理世界的模拟以后,就遍历元组集合。用DynamicPhysicsComponent组件里保存的proxy来取到底层的物理表示,并把它复制给Transform组件和Contact组件(译注:碰撞组件,后文会大量用到)。

System不知道实体到底是什么,它只关心组件集合的小切片(slice,译注:可以理解为特定子集合),然后在这个切片上执行一组行为。有些实体有多达30个组件,而有些只有2、3个,System不关心数量,它只关心执行操作行为的组件的子集。

像这个原型引擎里的例子,(指着上图7中)这个是玩家角色实体,可以做出很多很酷的行为,右边这些是玩家能够发射的子弹实体。

每个System在运行时,不知道也不关心这些实体是什么,它们只是在实体相关组件的子集上执行操作而已。

Overwatch里的(ECS架构的)实现,就是这样子的。

EntityAdmin是个World,存储了一个所有System的集合,和一个所有实体的哈希表。表键是实体的ID。ID是个32位无符号整形数,用来在实体管理器(Entity Array)上唯一标识这个实体。另一方面,每个实体也都存了这个实体ID和资源句柄(resource handle),后者是个可选字段,指向了实体对应的Asset资源(译注:这需要依赖暴雪的另一套专门的Asset管理系统),资源定义了实体。

组件Component是个基类,有几百个子类。每个子类组件都含有在System上执行Behavior时所需的成员变量。在这里多态唯一的用处就是重载Create和析构(Destructor)之类的生命周期管理函数。而其他能被继承组件类实例直接使用的,就只有一些用来方便地访问内部状态的helper函数了。但这些helper函数不是行为(译注:这里强调是为了遵循前面提到的原则:组件没有行为),只是简单的访问器。

EntityAdmin的结尾部分会调用所有System的Update。每个System都会做一些工作。上图9就是我们的使用方式,我们没有在固定的元组组件集合上执行操作,而是选择了一些基础组件来遍历,然后再由相应的行为去调用其他兄弟组件。所以你可以看到这里的操作只针对那些含有Derp和Herp组件的实体的元组执行。

Overwatch客户端的System和组件列表

这里有大概46不同的System和103个组件。这一页的炫酷动画是用来吸引你们看的(众笑)。

然后是服务器

你可以看到有些System执行需要很多组件,而有些System仅仅需要几个。理想情况下,我们尽量确保每个System都依赖很多组件去运行。把他们当成纯函数(译注,pure function,无副作用的函数),而不改变(mutating)它们的状态,就可以做到这一点。我们的确有少量的System需要改变组件状态,这种情况下它们必须自己管理复杂性。

下面是个真实的System代码

这个System是用来管理玩家连接的,它负责我们所有游戏服务器上的强制下线(译注,AFK, Away From Keyboard,表示长时间没操作而被认为离线)功能。

这个System遍历所有的Connection组件(译注:这里不太合适直接翻译成“连接”),Connection组件用来管理服务器上的玩家网络连接,是挂在代表玩家的实体上的。它可以是正在进行比赛的玩家、观战者或者其他玩家控制的角色。System不知道也不关心这些细节,它的职责就是强制下线。

每一个Connection组件的元组包含了输入流(InputStream)和Stats组件(译注:看起来是用来统计战斗信息的)。我们从输入流组件读入你的操作,来确保你必须做点什么事情,例如键盘按键;并从Stats组件读取你在某种程度上对游戏的贡献。

你只要做这些操作就会不停重置AFK定时器,否则的话,我们就会通过存储在Connection组件上的网络连接句柄发消息给你的客户端,踢你下线。

System上运行的实体必须拥有完整的元组才能使得这些行为能够正常工作。像我们游戏里的机器人实体就没有Connection组件和输入流组件,只有一个Stats组件,所以它就不会受到强制下线功能的影响。System的行为依赖于完整集合的“切片”。坦率来说,我们也确实没必要浪费资源去让强制机器人下线。

为什么不能直接用传统面向对象编程模型?

上面System的更新行为会带来了一个疑问:为什么不能使用传统的面向对象编程(OOP)的组件模型呢?例如在Connection组件里重载Update函数,不停地跟踪检测AFK?

答案是,因为Connection组件会同时被多个行为所使用,包括:AFK检查;能接收网络广播消息的已连接玩家列表;存储包括玩家名称在内的状态;存储玩家已解锁成就之类的状态。所以(如果用传统OOP方式的话)具体哪个行为应该放在组件的Update中调用?其余部分又应该放在哪里?

传统OOP中,一个类既是行为又是数据,但是Connection组件不是行为,它就只是状态。Connection完全不符合OOP中的对象的概念,它在不同的System中、不同的时机下,意味着完全不同的事情。

行为和状态分离的优势

想象一下你家前院盛开的樱桃树吧,从主观上讲,这些树对于你、你们小区业委会主席、园丁、一只鸟、房产税官员和白蚁而言都是完全不同的。从描述这些树的状态上,不同的观察者会看见不同的行为。树是一个被不同的观察者区别对待的主体(subject)。

类比来说,玩家实体,或者更准确地说,Connection组件,就是一个被不同System区别对待的主体

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值