【译】内容效率: 游戏数据服务器

发表于2016-02-25
评论2 2.3k浏览

  大家好!我是比尔“Lt伦道夫”克拉克,是英雄联盟 的一个游戏工程师。在工程中的许多Rioters注重直接传递使人畏惧的内容给玩家——我最近中意的一些例子中包括最新冠军、Jhin和支持项目重写。我的团队,在另一方面努力使这个过程更快且更容易。

  我们有一个明确的目标:允许此游戏项目中的Rioters能对于任何给定的LOL补丁,创造两倍的内容。这说着容易,却是一个具有挑战性的任务。

  今天,我将论述我们所奠定的用来推动它加速的基础:拳头公司游戏数据服务器(GDS)。虽然这将是一个技术性的文章,我也会将注释保持在一个相当高的水准。如果你是一名与跨越多个系统的数据打交道的工程师,我相信这将会使你特别感兴趣。


1、游戏数据

  让我们以一些背景知识开始。LOL运行中,有两种类型的游戏数据:键值对成为属性数据(例如黑道惠普奖金是300),还有不公开的对象是二进制数据(例如纹理、动画和声音)。在这篇帖子中,我将只讨论属性数据并且将二进制数据作为将来一个有潜力的帖子主题。

  对于所有LOL的历史中,属性数据一直包括一些松散的文件,它们在一个叫做DATA的大水桶文件夹中震荡。在此文件夹内,我们主要把数据保存在.ini文件(是的,Windows下的.ini文件格式)。它如图所示:


并不极美的界面


  显然我举这个例子是要强调一些当我们编辑.ini文件试会遇到的常见问题。这远不是用户友好的界面。当编辑一个生疏的文本会极其容易陷入困境——一些字段缺少重要的上下文,还有一些字段是重复出现的。为了进一步举例说明设计者每天必须处理的那堆困惑,这里有977法,它针对(当然可以忽略)“MissileEffect=AnnieBasicAttack_mis.troy,” 并且每个成功的人都从早期的LOL开发:“Death=Cardmaster_Death.wav.”引用了一个愉快的领域。

当前我们的数据系统的一些关键问题包括:

  · 用记事本++ 来编辑属性数据

  · 没有明确定义存在域

  · 没有类型安全

  · 当多人同时打开文件时的合并冲突问题

  · 繁琐的并发版本(活动的或内部平衡的)

  · 文件之间的松散联系;只是短名称和默认搜索路径


2、游戏数据服务器

  我们专门设计游戏数据服务器作为一个系统来解决这些问题。它的基础是RiotGameDataServer.exe ——一个运行在每位开发者后台的小程序。它在任务栏显示的是一个Riot 拳头,它的任务是传送属性数据到电脑上的其余程序。


GDS at the ready


  GDS抽象了所有其他工具的文件和数据管理,所以这些工具可以专注于传递所需的查看和编辑体验。我觉得这和一个操作系统抽象出的窗口创建十分相似,所以一个开发者可以注意应该出现在窗口的内容。与GDS交流的工具包括许多内部开发的工具,以及第三方标准例如Maya和Photoshop。它们都通过基于JSON的远程过程调用接口与GDS交流。

  关于RPC接口的灵巧性,我们可以使用一个叫做Swagger的标准(会列出所有可用函数)很轻易的吐出一个文档页面。这里是GDS揭露的一些函数子集:


Swagger文档


  GDS属性数据适当的存活在一个叫PROPERTIES 的文件夹中。这个文件夹最终会包括所有LOL的属性数据。当一个工具需要识别是什么让黑刀变成万神殿最喜欢的武器的细节,它将会发送一个HTTP请求到localhost:1300中,即GDS的监听处。若这个请求是写成“get?path=Items/BlackCleaver”,GDS就会在PROPERTIES/Items/BlackCleaver.json中查找文件。其响应如图所示:


Slightly prettier


  若一个工具想要改变黑刀所能给予的生命值,它将发送另一个请求到localhost(或127.0.0.1)端口号1300,这次发送“set&path=PROPERTIES/Items/BlackCleaver.FlatHPMod&value=1000”。然后GDS将在资源控制处查找文件(必然的),编辑它的值,并通过页面返回成功或失败信息。这样一来,任何我们做的工具都可以在不必考虑数据格式、文件操作、或其他复杂顾虑的情况下很轻易地编辑属性数据。

  这使得我们很容易的去创造一个工具如Riot编辑器,如下所示,就解决问题了#1:记事本++编辑属性数据。


现在我们已经取得的进展


3、属性标记

  对于任何给定的类型,确定哪些字段是实际存在的很重要,这样我们就能知道用户可以编辑什么。要做到这一点,我们维持一套连锁宏和神奇的模板,让我们能在引擎代码中直接标记类型。如图所示:


Mildly modified with macros


  注意宏: PROPERTY_CLASS,PROPERTY_START, PROPERTY, 和 PROPERTY_END. 这些主要是负责两个主要的任务:

· 告诉定义类存在哪些出口,并且它们的哪些域是可编辑的。

· 告诉属性加载系统内存偏移,以将属性值推入运行时。

  PROPERTY 宏自动的通过专门的简单模板函数推断类型。我们可以引用复杂的类型,例如BoundingVolume,倘若它们已经有了它们自己的子属性标记了的话。并且我们也可以跳过域,如mRuntimeNumber,意味着它们将不会暴露给GDS。

下面是GDS将使用的已生成的JSON定义:

Aww yiss


此属性标记就解决了问题#2 和 #3:分别为 没有明确定义存在域、没有类型安全。


4、层

  除了GDS抽象掉的文件和数据管理等工具,它还提供给了Riot 开发者一件很酷的东西,在技术上成为“层”。一层表示一个可以开启或关闭的功能,并且我们可以创建一个新的层对应于一个新的冠军、一个新的皮肤、一个游戏模式或一个主要的重新平衡。然后,无论创造者创造的功能是什么内容,它们都可以让GDS做,打个比方,APItemRework 层是“活性的”层。

  随后,GDS标记任何文件所做的任何改变到APItemRework层的一部分。在磁盘中,这样的文件像RabadonsDeathcap.json,紧接着它,另一个文件叫做RabadonsDeathcap.APItemRework.json 。在第二层文件,GDS仅标记每个域所改变的delta 值。先前或后续的值将稍后被保存以调和合并冲突。这里是这两个文件的分屏浏览图:


左: 基础数据; 右:层 deltas


  由于我们捕捉的只是个别有变化的域,所以我们不再需要担心多个Rioters 同时的编辑同一个文件,除非我们恰好改变同一个域。并且如果同个域有改变,我们保存先前后续即可找出冲突。这个好处会避免我们运送出一个bug:在创建DJ Sona 时,团队不小心将它的数据回退到一个老的版本。

现在我们已经解决了问题#4:当多人同时打开文件时的合并冲突问题。

  层让我们捕捉到所有与特定功能相关的变化。实际上这些功能特性,我们给出了一个概念叫做“游戏版本”。一个游戏版本通过开启一系列的层来定义一个游戏的完整版本。每个游戏版本仅保存在一个简单的JSON层名称列表中。在任何时候,我们保留几个主要的游戏版本:

Alpha: 我们正在内部测试的一系列功能并且准备更新到测试服。

Beta测试版:一系列当前已经在测试服的功能,像Jhin。值得注意的是,Beta测试版继承了已发布的功能列表,所以它会有最近的更新,例如可用的季前赛。

Release发行版: 一系列已经发布在外网服务器的功能,如闪亮的新补丁6.3 。

  有一件很酷的事情是,从一个游戏版本移动功能到另一个上面,仅需要在我们的层管理窗口做一个简单的拖放操作即可,以至于当我们改变了什么可用的地方时,我们不再需要从这里拖动成百的文件到那里。

  这让我们轻松解决了问题#5:繁琐的并发版本(活动的或内部平衡的)通过全文件重写。


5、概要

  希望这片文章让你感受到我们是如何帮助使英雄联盟开发更有效可行的。对于细心的读者,你可能注意到我没有去钻研问题#6:文件之间的松散联系;只是短名称和默认搜索路径。我省略了它的解决方法,因为这个问题比预期中的要艰巨——分发的冗余、避免不必要的代码回调、剧增的数据端口、斑块大小、还有更多——这值得开启他自己专属的博客条目。

  请务必通过评论让我们知道你想知道的地方,因为我们会改变我们对于定义在英雄联盟中的游戏数据的争论方式。


时空裂痕再见! 

比尔克拉克发帖

翻译出处:https://engineering.riotgames.com/news/game-data-server

原文作者未做版权声明,视为共享知识产权进入公共领域,自动获得授权。

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