UE3服务器脚本逻辑热更新方法

发表于2016-04-11
评论1 9.9k浏览

摘要

  本文介绍了枪神纪运营过程中使用的服务器脚本热更新的操作方法,附带阐述了UE3的服务器和客户端的脚本对象通信时用到的对象索引机制。通过热更新服务器脚本文件,能够达到服务器无需停机,客户端无需更新即可完成脚本逻辑的更新,是一种自适应的灰度的更新方法,实际中运营中可操作性较高,对在线几乎无影响。


背景

  对于进入运营阶段的UE3端游项目来说,核心逻辑往往是放在服务器上执行的,如果此时服务器逻辑出现了重大的bug,修复代价最小的方式就是服务器热加载新的脚本包,客户端无需更新,且并不被踢出现有单局,枪神纪和逆战项目组都使用了CGI的机制去拉起DS(Dedicated Server),意味着每一个单局都是一个进程,该进程的资源,配置,执行文件在拉起后已经形成一个整体,因此在服务器上动态替换已有的配置,资源或者执行文件,并不会导致现有单局崩溃,同时,当前的更新会反映在下一次启动Dedicated Server的时候。因此,项目组所使用的CGI拉起DS的方式天然具有热加载的基础。

  然而,要实现服务器上的脚本文件的动态更新,则需要首先理解客户端上和服务器上的脚本文件的通信机制。


UE3网络对象索引机制

  UE3的服务器和客户端通过PackageMap去建立一个Index到Object的映射表,每一个网络同步Object都有一个Index。当服务器告诉客户端某一个Object需要同步,或者执行某一个RPC函数的时候,往往都是直接使用了这个Object的Index来标识当前的Object。下图阐述了PacakgeMap的数据结构的核心思想:


  通常来说,客户端和服务器使用的脚本资源中的Object Index是同步的,因为他们往往是一份脚本文件(例如逆战),或者是同一份文件生成的两个平台的脚本文件(例如枪神纪,客户端使用PC资源,服务器使用DS资源)。但无论如何,他们的对应的脚本包中的对象是一一对应的。

  Object的Index的计算方式是基于加载包的顺序,每个包中的同步Object序号加上当前包的ObjectBase之后计算得到的。例如当前包,它的ObjectBase是N,那么第NObject的Index的值就是当前包的ObjectBase+N。如下图所示:



  而当前包的ObjectBase的计算,则是上一个包的最后一个对象的Index+1。如下图所示,Package B包里的Object Base就等于当前A包最后一个Index+1=3:



  而要完成一一对应,还有一个不可或缺的条件,就是服务器和客户端的包的加载顺序要是一致的,这个步骤是在LoadMap的过程中执行的,在加载每一个包的过程中,服务器都需要告诉客户端下一个要加载包的信息,而且当两边对应的包都加载完成后,他们就需要计算一下上面的PackageMap (Compute函数),来保证他们在加载包后生成的PackageMap是完整对应起来的。



  通过上面的加载次序保证,如果包中内容一致的话,那么就能通过ID同步服务器和客户端双方的Index。这也是通常的情况下的状态。


UE3脚本编译知识点

  这里只介绍和本文相关的一些知识点,详细的脚本编译底层实现,请参考KM文章 Unreal Script 揭秘 (http://km.oa.com/group/15976/attachments/attachment_view/72886)。如果想通过自己动手去弄明白UnrealScript编译与存储,那么推荐git EliotLib(https://github.com/EliotVU/Unreal-Library ) 库结合UE_Explorer(http://eliotvu.com/portfolio/view/21/ue-explorer)使用,能够快速的掌握Unreal Script脚本的底层。

  实际上,如果需要修改脚本代码,很容易就会产生新的NetObject,例如,下面的修改,我们仅仅是在脚本中,新定义了一个局部变量,让人吃惊的是,局部变量也作为一个NetObject导出了。



  通过UE_Explorer,我们发现导出的NetObject多了一个。即使,导出的这个Object并不会影响同步。



  正式因为多了这一个Object,如果只更新服务器,不更新客户端的话,那么会导致服务器上的后续包的NetIndex和客户端上的NetIndex全部错位,导致逻辑混乱。而为了解决这个问题,UE3提供了一种Conform to编译的机制。


UE3 Conform to编译机制

  如果我们在编译脚本的时候,把原有的脚本文件夹重命名为ScriptOriginal,那么下次编译脚本的时候,就会默认启动Conform To编译。这段逻辑由下面的代码实现:



  可以看到,脚本编译的时候,会自动搜寻目录下的ScriptOriginal文件夹,并尝试载入旧的脚本包。而在后面包的Save的过程中,将会使用旧包的Object次序,对新生成的脚本包的Object对象进行排序。



  通过上面的步骤,我们就能保证,即使是中间生成的新的Object,那么这些新生成的Object也是在包的末尾,而之前的Object的次序得到了保证。如下图,我们可以通过UE_Explorer看到新生成的包的Object列表。



  我们看到刚刚的UTP的临时变量虽然被包导出了,但是被放在包的最后端。同时,包里记录了Generation的信息,即上一个Generation是16113个Object,当前的包多了一个Object。



  而这个Gerneration信息是用在服务器和客户端脚本文件比对时,虽然其中一个包的NetIndex多了一个,但是会被妥善处理(),即在PacakgeMap中,无法被通用的NetIndex索引到。将会达到类似下面的效果。



  如上图所示,Object-B-3是新生成的Object,但是客户端上面没有他的信息,因此,Object-B-3将会失去网络同步的基础。大部分情况下,这样做是没有问题的,因为对于局部逻辑的修改所产生的Object本身就无需同步,只需要同步函数执行的结果即可。但是,如果如果这个Object是一个需要同步的Object,例如,我们可能新添加了一个同步变量,或者新添加了一个RPC函数,这样的修改都是会失效。因此,UE3在执行Conformto编译的时候,做了如下的检查(参考ValidateConformCompatibility):

1. ClassObject分析

2. 载入ClassObject之后,分析两个ClassObject的每一个Field

2.1 如果是一个变量属性,那么他们的NetFlag一定要是一致的

2.2 如果是一个函数,那么他们的NetFlag一定要是一致的


UE3 Conformto实践

  实践操作中,我们只使用Conformto解决服务器上的逻辑Bug,虽然,从理论上来说,我们也是可以停机更新客户端的,然而,如果两侧都不停机更新,会造成巨大的风险。例如,这次更新导致了不可预想的错误,此时如果客户端更新了,那么版本回退就会很棘手,挽救措施也异常难处理。而针对服务器的更新,即使出错了,那么也是一个灰度的过程,我们能够快速的反应,并再次更新服务器就可完成,无需版本回退。

所以,原则Number 1: 只使用Conform to编译方式处理服务器上的逻辑问题。

  而一旦进入Conformto操作阶段,我们就需要备份原来的Cook之前的脚本,用于做为Conformto的对象。

所以,原则Number 2: 备份当前外网的Cook之前的脚本文件夹,并将其重命名为ScriptOriginal

  然后,编译新的脚本,在编译完成之后,需要使用UE_Explorer确认生成的Object的次序。

所以,原则Number 3: 通过UE_Explorer确保新生成的Object在最后,从而确保NetIndex的一致性,GUID的一致性。

  最后,Cook新的脚本,然后只把更改的脚本文件发给服务器进行内网测试。

所以,原则Number 4: 再次通过 通过UE_Explorer确保Cook后的脚本包中的新生成的Object在最后,从而确保NetIndex的一致性,GUID的一致性。

 

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引