最近一段时间以来,本人参与了公司下一代游戏编辑器的开发,从而有机会针对编辑器设计做一些简单的思考——如何设计更好的抽象,从而达到在客户端,服务端,以及游戏编辑器中复用尽可能多的代码?如何能够尽可能的缩短游戏设计师(策划)及美术设计师(3D/2D场景美术)的工作流程?市面上优秀的引擎往往都附带有所见即所得编辑器,这样的编辑器应当如何设计?网络游戏编辑器又有哪些可以从中借鉴和学习之处?

将尚不是很成熟的思考结果总结成本文。本人水平所限,许多错漏之处难免考虑不周全,如果有同学对本文所述的问题有任何想法,亦或是有其他文章与此相关,欢迎一起交流。

客户端逻辑的复用

把游戏客户端进行拆解,可以将其看成一个拥有输入、输出及内部逻辑循环的系统。玩家通过鼠标键盘发送消息给客户端,服务器通过网络接口发送消息给客户端,然后客户端于每一帧把当前对应的表现绘制到屏幕上。

客户端输入包括:Windows消息(键盘,鼠标,windows的其他消息,快捷键,定时器,摇杆等),网络消息(通过服务器或者其他玩家发送来的消息);输出包括:屏幕显示,网络消息(发送给服务器的响应或请求)。

事实上一个游戏编辑器也可以简单的看成类似于上述的系统,其输入包括:Windows消息(键盘,鼠标,windows的其他消息,菜单快捷键等,定时器等);输出则包括了屏幕显示,正常情况下编辑器不需要接受或发出任何网络消息(这里指与服务器进行逻辑上的通信,而非指通过版本控制系统进行数据的同步管理)。

实际上编辑器与客户端程序实际上都拥有相似的输入接口(Windows消息),只是同样的消息引发了不同的逻辑——在这里我们可以对输入接口做以抽象,并使用不同的实现来分别实现其逻辑,从而达到在编辑器中拥有热切换编辑状态和游戏状态逻辑的能力。(许多知名的游戏引擎都拥有类似上述的可在编辑器中切换编辑/游戏状态的功能,比如Crysis的SandBox编辑器,再如RunicGames的TorchLight编辑器等)。

具体来说,通过设计一个类似如下的接口(只考虑位于单个窗口中操作的情况):

class Operation
{
public:
    virtual bool OnLButtonDown(POINT pt, UINT uFlags) = 0;
    virtual bool OnLButtonUp(POINT pt, UINT uFlags) = 0;
    virtual bool OnMouseMove(POINT pt, UINT uFlags) = 0;
    virtual bool OnKeyDown(UINT nChar, UINT nRepCounts, UINT nFlags) = 0;
};

(此处仅起示意作用,就不将全部的接口都列出了)

实际程序中,我们在消息处理线程中将收到的Windows消息交给该接口,该接口背后的实现是编辑器逻辑呢?(左键按下是在选取场景中的某个物体,拖动其位置等)还是客户端逻辑呢?(左键按下是在尝试点击某个NPC或者怪物,并引发下一步动作,攻击之?对话之?)我们的前端程序并不关心接口背后的实现逻辑为何种。这样我们就可以达到动态替换逻辑的目的——在编辑器中一键切换运行模式。

这样做(所见即所玩)有什么好处?好处很多——如果不能在编辑器中立即看到游戏效果,那么游戏编辑人员(国内通常为策划人员,国外的游戏开发者中颇多为关卡设计师)必须首先在编辑器中将各类物体,对象,事件安放好,然后导出数据,再在游戏客户端中找到对应场景,并一一测试在编辑器中设置的对象。这种做法相比于在编辑器中立时可见(所编辑即所玩)的设计,其缺陷在于增加了中间环节,增加了出错可能性,当出现了与预想结果不同的现象时,定位问题所在需要花费更多时间,从而降低了游戏开发效率。此外,由于游戏开发设计的需要,往往编辑器编辑的许多数据并不是游戏最终直接使用的数据,因此存在编辑器导出数据的环节,编辑器需要将编辑好的数据导出,客户端程序需要将导出后数据进行加载,导出和加载都可能存在错误(比如编辑器中新增了功能(版本增加,文件结构改变),客户端必须增加或修改对应代码才能支持,这些环节的出错,都会降低游戏开发效率)。

良好的设计架构,通过把逻辑封装在一套预定义好的接口背后,使得同一套客户端/服务端逻辑,可以被复用于编辑器中,也可以复用于客户端/服务端程序中(逻辑代码的复用存在两个级别——代码级与二进制级,如果没有平台差异,那么在合理的设计中,我们理应做到二进制级的复用——即通过一个设计好的二进制动态运行库交给不同的进程加载;某些逻辑可能只能在源码级进行复用——比如服务器端逻辑;当然倘若采用脚本语言编写逻辑,我们可以忽略这里的差别)。当我们切换编辑器模式,程序从编辑状态转入游戏状态时,接口背后的实现被替换为客户端逻辑,最终达到在编辑器中伪造出客户端操作及逻辑的目的。

除了获得所见即所玩的好处,这种将输入接口抽象的做法,还有一个额外的好处——录像重播功能。将游戏视为输入输出系统的设计,给了我们将所有的输入事件按照时间顺序记录(通过输入设备的鼠标键盘事件,系统/定时器消息,网络消息等)。如果将所有的输入事件,按照时间记录保存下来,后再按照时间顺序作为输入重新调用对应的接口,我们就获得了整个游戏过程的重演。星际争霸,以及魔兽争霸等游戏中都有录像功能,可以将游戏过程记录下来,并重新播放,应当是基于类似的技术。

更进一步,由于网络游戏中的绝大部分逻辑是运行在服务器上的,例如与NPC交互时有什么反应,NPC会说什么话,会提供什么可选任务,会提供什么服务,NPC会如何行动,怪物会如何行动,怪物被击杀主角获得什么奖励,主角有什么技能,可以做什么事情(移动,施放技能,击杀怪物,拾取物品等)。诸如此类的逻辑实际上并非客户端逻辑,而是服务器逻辑,如何在编辑器中模拟这些东西呢?

服务端逻辑的复用

不同于单机游戏——游戏逻辑运行于本机上,网络游戏往往多数逻辑都运行在服务器上。这就造成游戏开发中的困扰——编辑好的场景,NPC,怪物无法立即看到实际状况,游戏设计师们往往并不知道该NPC是否一切正常。设计师需要将编辑器中制作的场景导出给服务器端程序,服务端可能需要重启一次服务器,加载这部分数据,然后设计者再从客户端启动游戏,连接服务器并测试新加入的内容。(这其中的流程包括,编辑器导出数据给服务端,服务端重启,服务端读取新数据,启动客户端并连接服务器:每一个步骤都可能存在其他因素引入的错误,降低开发效率;类似的道理,当出现表现出来的状况与预想不同时,游戏设计师难以定位问题所在)。

由于本人没有编写过服务端代码,对网络游戏服务端的架构并不是很熟悉,因此以下的分析仅从个人理想化的角度出发,简单谈谈服务端逻辑在编辑器中的整合。服务端运行的游戏逻辑,主要包括玩家逻辑(客户端发送的让玩家做某某事的请求,其他玩家的状态广播,玩家状态改变,玩家数据改变),怪物逻辑(怪物的行为,技能,AI,掉落以及击杀奖励等),技能逻辑(技能的时间,伤害,状态等),可达地点判定(什么地方可以到达,什么地方不能走)。服务端逻辑也可以简单的看成一个大循环,一方面通过网络收到的消息和内部触发机制创造事件,另一方面不断的循环处理事件(期间可能要读数据库,读服务器数据等),并最终做以输出——通过网络发送消息给玩家,或者写数据库。

将服务端进行拆解:我们得到一个读取(从数据文件/网络/数据库),逻辑循环(处理事件),输出(写文件/写数据库/发送网络消息)的模型。其中数据文件——往往是由游戏设计师在场景中编辑的数据,或者技能数据,或者游戏里的其他模板(物品,人物,怪,NPC等)数据,数据库往往保存了玩家的数据,人物技能/人物等级/属性/物品等。

思考一下,我们发现,服务器逻辑所需要的游戏数据文件,各类数据模板,定时器,触发器等正是编辑器所拥有的数据。服务端所需要的网络访问,数据库访问,则可以通过Proxy模式伪造一个网络层和数据库层,让服务器逻辑运行在非真实服务器的环境下(虽然服务器逻辑会以为自己运行于服务器环境中)。这就给我们在编辑器环境中运行服务器逻辑提供了可能性。

最后,要想达到服务端代码复用,那么第一就是要求逻辑代码本身不具有任何的平台相关性——调用了LinuxAPI或者WindowsAPI都是应该避免的——理想化的做法是使用java/python之类的跨平台语言。(在同做服务端程序的同事的交流中,了解到服务端程序运行的瓶颈往往并不在于CPU计算,而在于网络IO上,因此采用非平台相关语言,未必会增加多少运行上的效率损失,反而带来了代码复用的便利性,换句话说,就算我们不采用平台无关语言编写服务端逻辑,比如采用c或者c++编写服务端逻辑,只要这部分逻辑代码不沾染任何平台相关API即可。)这么做的目的是为了服务端逻辑的跨平台复用。

设计中尚存在的问题

前面谈到的目标是好的,但事实上还存在许多尚且不确定的地方。接下来简单分析一下上述做法还有哪些问题尚未考虑周全:

1. 数据格式不统一的问题。客户端逻辑需要读取数据(美术制作的模型,凸包,场景地形等等),服务器逻辑也需要读取数据(策划设置的触发器,技能,NPC,怪等等)。这些数据虽然都是由编辑器提供的,但是基于诸多理由,编辑阶段的数据与实际运行阶段的数据完全可能并不相同。如何在运行时动态的将数据转化为客户端及服务器认可的数据是一个问题。特别的,如果采用光照图之类的技术进行渲染,编辑器在导出数据的时候,还要进行费时的光照图计算;类似的还有通过图的计算,也是颇为耗费时间的工作。如何让编辑器中热启动的服务器和客户端能够在足够短的时间里获得准备好的数据,是一个需要仔细考虑的问题。

谈到游戏数据的管理(创造,编辑,保存,读取),完全可以另列一文单独讨论,此前曾经拜读了MiloYip老大的《从头开始思考游戏数据管理系统》一文,其中提及了颇多关于游戏数据管理的思考,获益良多。在此对有兴趣的同学做以推荐。

2. 客户端逻辑并不简单只有输入逻辑,事实上还有玩家逻辑,动画播放,资源动态加载与释放,界面逻辑等等,都是复杂的模块。这些逻辑如何进行复用,接口需要如何设计,尚缺乏详尽的思考。

3. 由于本人对服务端逻辑的结构尚缺乏足够的了解,服务器逻辑的复用或存在许多未考虑的问题。尽管之前提过可以通过Proxy模式为服务器逻辑提供一个虚拟的运行环境,但是相信实际的困难依旧有许多。

简要设计图

我将上述的想法做了简单的汇总,下图是一个简要的设计概览:

编辑器,服务端,客户端概念设计

上图假定服务器端运行在Linux下,客户端以及编辑器运行于Windows平台。

整体架构依赖于三个抽象接口,分别为服务端编辑器所依赖的数据库接口,客户端编辑器共同依赖的网络接口和用户输入接口。围绕这三个抽象接口,我们将许多耦合进行分解,使得服务器逻辑,客户端逻辑被独立成单独的模块,不仅可以用于服务端程序或者客户端程序当中,还可以复用于编辑器中,从而使得编辑器拥有了伪造一个服务端和客户端的能力,通过这种手段达到所编辑即所玩的目的。

(更进一步,通过将编辑器中的FakeNetwork层替换为RealNetwork网络层,可以让编辑器直接连接到实际运行的服务器上,我们便得以在编辑器中直接启动一个客户端程序)

基于上述架构进行开发时,开发人员在许多地方避免了重复代码的编写(比如服务器端逻辑决定了NPC的行为,则编辑器编写者无需在编辑器中编写任何伪造NPC行为的代码,我们可以直接获得真实的服务器端NPC的表现——通过源于实际的服务端逻辑编写者编写的服务端逻辑代码;类似的,客户端逻辑决定了界面,消息响应等,既可用于实际的客户端,也可用于编辑器中模拟客户端的行为)

后记

本文实际上是针对网络游戏编辑器设计的一个尚未成熟的思路。倘若按此思路进行实做,还有不少需要考虑、充实的细节,以及需要克服的困难。但从缩短游戏开发流程,提高代码复用率的角度,本人以为文中所述的方向应当是日后网络游戏开发的趋势。