云风的BLOG:游戏开发Archives

最近去了一趟南宁马山县。攀岩一周,身体很累,心里很舒服。这周除了爬石头,什么都没想,脑子全部放空了。

“一款好游戏,胜过两款伟大游戏”……这世上最容易做的就是“多”,如果我们不小心,就可能会把三四款游戏都塞进一个游戏里。有时候,决定什么内容不该加到游戏里,比决定什么内容该加进入更加总要。——《席德梅尔的回忆录》

这几天,读了本书《席德梅尔的回忆录》:《文明》是在陪产假中诞生的,它一开始更像是《铁路大亨》的延续,一个全球规模的《模拟城市》,一个实时模拟游戏。它开发了很久都没有找到正确的方向,一度项目被搁置。而重新继续这个项目后,经过了搁置期的思考,才试着将其改为回合制游戏。

回合制走不通,游戏原型转为了即时制。席德之前就有一款成功的即时制游戏《葛底斯堡战役》。但这个《恐龙争霸》却因为恐龙题材难以嫁接足够多的远程武器以至于无法平衡。

然后,这款恐龙游戏又演化成了口袋妖怪,或是更接近恐龙万智牌。平平无奇的“借鉴”让这款游戏毫无新意。卡牌形式很好玩,但是“这些卡牌的互动方式与《万智牌》太像了。如果你能加入自己的想法,那借鉴一点创意是可以的,但我从来不觉得恐龙游戏有足够多的新元素可自证清白。”席德忍受不了这一点,最终彻底放弃了这个项目。

“首先,设计师创建了一些游戏机制。然后,他们把这些游戏机制用一些具有代表性的虚构元素包装起来。在游戏过程中,这些机制之间会产生一系列事件。这些事件会触动玩家潜意识中的触发器,从而激发出情感。最后,这些情感交织到一起,变成了一种综合的体验。”——TynanSylvester《DesigningGames:AGuidetoEnginerringExperiences》

我非常认同Rimworld作者Tynan对电子游戏的定义:游戏是一种制造体验的人工系统。游戏用一种工程手段制造体验,目的是激发人类的情感。在《体验引擎》的书中论述,追踪情感的真正源头非常困难,因为情感的触发由大脑的潜意识处理,自动表达的。即使不知道为什么会产生某种情感,我们的理性还是会想当然的为之安排一个原因。这些想当然的原因往往是错的。这种现象被称为情感错位,因为情感错位的存在,想要了解游戏如何影响我们是十分困难的事情。

我最近把游戏开发工作中的具体实现停了下来。因为我意识到,游戏核心固然是设计一些机制,程序实现可以把这些机制做出来并加以测试,但游戏机制只是手段而不是目的。我对游戏设计的理解还不够,所以还需要继续以设计游戏的角度去挖掘游戏深层次的东西。以游戏爱好者的角度去玩那些好评如潮的游戏体会游戏带来的乐趣是不够的,还需要多玩一些毁誉参半但制作者有自己想法的作品。当然,还需要回避一些仅仅是把已有游戏换一个虚构层做出来的仿冒品。

成为好的Designer之前,必须做一个更好的Gamer。我相信自己比之前是一个更好游戏玩家。因为相比之前,我可以更快的学习游戏规则,忽略游戏的表象,直接去感知作者想表达的东西。玩一些steam上只有几个评价且好评率不高的游戏,即使是半成品,对我来说也不算是太难的事了。具体游戏的评价,我大多直接写在steam上,而这里,我想记录一些最近想到的比较形而上的总结。

游戏行业从业20多年,一直在做底层开发,即使是帮助其他团队写上层游戏逻辑,也都是实现某些特定的功能模块。直到最近,我想单独开发游戏,才好好想想架子该怎么搭。

从最初的原始demo开始,由于缺乏经验,写着写着经常有失控的感觉。好在一个人做,重构的心理负担不大。想改的时候,停下来花上两三天全部重写也不太所谓。最近总算顺畅了一点,感觉需要整理一下思路,所以写一篇blog记录一下。

任何复杂的软件问题,都可以通过拆分为若干子问题减少复杂度。

我认为,游戏的上层逻辑,即gameplay部分,主要分为三块:数据模型、外在表现和人机交互。

这样,我们就可以方便的对它进行整体或局部的测试。必要时还可以更换游戏引擎,甚至从文本roguelike换到3D表现都不会受影响。

“外在表现”当然是指游戏的画面表现、声音声效等等,通常这由游戏引擎实现,但还会有大量的代码存在于gameplay的实现中。这一块代码也会维护大量的状态,但不会涉及数据持久化。简单的判断方法是:如果游戏保存进度时,所有这里的所有状态都可以舍弃,下次读取进度后,这些状态又能被重建,而玩家不会感觉丢失了任何数据。

“人机交互”是游戏软件必要的部分,如果没有“人”的存在,游戏也就没有意义了。这块分为两个部分:图形界面用于展示游戏的数据给人看,同时接收一些来至界面的间接输入;控制器(包括并不限于手柄鼠标键盘触摸屏等)对游戏的直接控制。

对于联网游戏,还应包括第四大块:“网络输入”。这篇blog仅讨论非联网游戏。

在矮人要塞like的游戏中,都有一套基于工人的任务分发系统。玩家通常不能像RTS中那样直接操作工人去工作,而是对要做的事情下达任务,等着工人自主去完成。

由于任务数量通常远多于工人数量,这个任务分发系统中大多配有优先级设置,可以让诸多任务有条不紊的进行。调整优先级变成玩家主动操控的渠道。初玩这类游戏,会有点不习惯:感觉难以在微观层面直接做自己像做的事情。像捡块石头放进指定仓库这件事,无法像玩RTS游戏那样,先点选工人,再针对石头发出拾取指令……但习惯之后,恐怕又回不去了。比如我在玩Ratopia时,就对操控鼠王直接干活烦躁不已。

以我游戏时长最长的缺氧和边缘世界相比较,同样是提供玩家主动操控的能力:Rimworld可以给工人的任务队列直接下达指令(这更接近RTS的玩法),而ONI则是通过给单个任务本身排优先级实现的。ONI设计了警报级任务,可以越过一切优先级设定,强制立刻完成。虽然ONI也保留了指挥单个小人移动到指定位置,但实际游戏中几乎没什么用。

对于拾取物品,Rimworld可以封禁、解禁单个物品,而ONI没有这个设计。ONI的工人几乎不会主动把地上的东西搬入仓库,除非下达清扫指令。

我比较喜欢ONI的系统,打算用这个规则打底设计自己的游戏。下面是设计的草稿:

像异星工厂、缺氧、边缘世界都有大量的mod。可以通过大改游戏机制,把本体游戏改造成完全体验不同的游戏。这些好玩的mod几乎都是一个人完成的。所以我觉得,固然游戏的外层玩法决定了整体体验,但其实开发的总工作量并不大。而且,一旦玩法不满意,也容易修改。

我的游戏开发计划是先完成一些底层基础系统,再考虑完整游戏的全貌。没有上层玩法支撑,光有底层系统玩起来一定寡淡无味,但我认为它们是设计和开发中最重要的一环。

在进一步实现demo之前,我设计了一下飞船的建造系统。

在设计飞船建造的游戏时,我想做一个空气流动的模拟系统。这里有一个初步的方案,不知道好不好,先记录一下:

宅在家里一个月了。一直在想,如果不考虑迎合市场,不顾及销量,到底应该做一个怎样的游戏才能让自己在制作过程中得到满足。

过年前曾经参加过一次老同事聚餐。组织者说,这屋子坐的都是做游戏的老人了,程序、策划、美术全齐了,还都是不差钱的主。大家要不要凑个局,想想做个啥游戏出来?接下来是一阵沉默,直到有声音说,“我没什么想法”,饭桌上的人纷纷点头,转移了话题。

可见,想清楚做什么很难。单独一人的状态也很难得,没有太多的外界干扰,不为了做事而做,可以慢慢来。

首先,我想做一款游戏,这毋庸置疑。玩游戏是我这些年最大的爱好。光在steam上这些年就花掉了上万小时,switch上也有几千小时。我能在制作游戏的过程中获得我要的东西。

其次,做一款游戏的目的不是为了收入。我对物质生活要求极低,不需要花钱满足欲望。除非需要雇人一起做游戏,不然制作游戏的开销只是自己家庭的日常开销,而我这些年的积蓄已够过完余生。我喜欢的游戏都不需要太复杂的美术资产,这方面并不需要额外的投入。

另一方面,我也不需要用游戏讨好玩家来获得成就感,不需要用一个产品来证明自己,这些成就感的体验都已有过,不是我想追求的东西。

所以,我所需要的是制作过程带来的持续体验,让自己觉得自己在做一件有意义的事。我所喜欢和擅长的其实是:认清问题,解决它们。

我提了一个问题:如果上一帧某个像素被渲染过,而若有办法知道当前帧不需要重复渲染这个像素,那么减少重复渲染就能减少总的能耗。这个方法要成立,必须让检查某个像素是否需要重复渲染的成本比直接渲染它要低一个数量级。之所以现存的商业引擎都不在这个问题上发力,主要是因为它们并没有优先考虑怎么给移动设备省电,而要做到这样的先决条件(可以廉价的找到不需要重新渲染的像素),需要引擎本身的结构去配合。

提起游戏引擎,特别是商业通用游戏引擎,比如Unreal或是Unity,给人的第一印象会是它们的可视化编辑器。而在实际开发中,在不同引擎下做游戏开发,影响最大的是引擎层的API以及这些API带来的模式。

而对于使用自家专有引擎开发出来的游戏,却少见有特别的编辑器。比如以Mod丰富见长的P社游戏,新系列都使用一个名叫Clausewitz的引擎,玩家们在之上创造了大量的Mod,却不见有特别的编辑器。Mod作者多在文本上工作,在游戏本身中调试。游戏程序本身就充当了编辑器:或许只比游戏功能多一个控制台而已。在这类引擎上开发,工作模式还是基于命令行。

我们的游戏引擎是基于虚拟文件系统,可以通过网络把开发机上的目录映射到手机上。这对开发非常方便,开发者只需要在自己的开发机上修改资源,立刻就能反应到手机上。

但当游戏发行(也就是我们正在准备的工作),我们还是需要把所有资源打包,并当版本更新时,一次性的下载更新补丁更好。

最典型的是RPG类游戏的人物属性面板。通常需要在面板上显示3D人物模型。通常还可以旋转这些模型,让玩家360度观看。我们目前的游戏类似Factorio,没有Avatar,但点开建筑的信息面板时,也需要把建筑的3D模型动态展现出来。

最初,我们没去细想3D渲染怎么和已有的RmlUI结合在一起,直接把模型渲染在UI层之上。相当于在UI模块外开了个后门。UI上只需要把位置空出来,等UI渲染完后,再叠加3D模型上去。但这样做的坏处是很明显的:3D模型无法和UI窗口有一致的层次结构。

最近,我希望在UI上增加更多3d模型。它们仅仅是用来取代原来的2D图片。从UI角度看,这些就应该是图片,只不过这些图片并不是文件系统中的图片文件,而是运行时由3d渲染模块生成的。如果继续沿用目前的图片方案,我们就多出一些开发期处理这些预渲染图片的维护成本。但是,如果直接使用已有方法的话,那个看起来临时的解决方案又有点不堪重负。

上篇谈了一下我们游戏引擎的虚拟文件系统(vfs)。我觉得这个系统中,游戏资产的管理部分还是个满有意思的设计,值得写一下。

VFS的设计动机是方便把开发机磁盘上的数据同步到运行设备(通常是手机)中。传统游戏引擎的做法通常是建一个叫做资产仓库的东西,在开发期间不断添加维护这个仓库。需要把游戏部署在运行设备时,再打包上传过去。因为传统游戏引擎在开发期间一般直接在开发机上运行,所以打包上传(从开发机转移游戏资产)并不频繁。

而我们的游戏引擎特别为手机游戏开发而设计,我们不可能直接在手机设备上开发,所以开发机一是和运行机分离的。为了提高开发效率,所以我们设计了VFS系统。可以通过网络同步资源仓库。

目前我们游戏用的引擎早在2018年就开始了。因为一开始,它就定位为一个主要用于手机平台的游戏引擎,要方便手机开发。因为我们不太可能直接在手机设备上编写代码、制作资源,所以开发机一定是和游戏运行环境分离的。从一开始,我们就设计了一个虚拟文件系统,它可以通过网络,把开发机上的文件系统映射到手机设备上,同时兼有版本管理的功能。这样,才可以做到在开发期间,开发机上所做的大多数修改,都能立刻反映到手机上。

最近做了这方面的重构工作。其中的难点在于:特效模块是第三方的,并不完全贴合我们的引擎设计,而短期内又没有重新实现或改造的计划。

我们的手机游戏引擎一直在跟随着游戏项目的进程不断优化。一开始是因为游戏引擎在手机上帧数达不到要求。得益于ECS框架,我们把初期用Lua快速开发出来的几个核心system用C重写后,得到了质的飞跃。

从xcode的调试信息看,在游戏场景丰富时,大约会占用280%的cpu。换句话说,如果我们采用的是单线程架构,在不删减特性的前提下,做到流畅是相当困难的。

我们的游戏引擎是在PC上开发的,但希望专门用于手机。虽然我希望日常游戏开发都在真机上进行,引擎为之也做了很多的努力。但实际并不算顺利。主要还是引擎以及游戏的核心C部分还在频繁改动,在真机上调试毕竟不如PC上方便。好在最近已经慢慢稳定下来,开发中使用真机的机会越来越多,也就更多的考虑在手机上的交互问题。

我意识到的第一个问题就是:在PC上模拟的版本是无法体验手机上的交互感受的,想把手机上的交互做好,就必须时刻在手机上体验。

我们引擎原本的交互界面都是基于鼠标消息,触摸屏消息只是对鼠标消息的模拟。这样是不对的。意识到问题后,我们首先把鼠标消息从引擎中去掉,把触摸屏手势消息改为firstclass,而鼠标消息则反过来用手势消息模拟。

基本手势其实有四个:点击、长按、拖动、缩放。其中在不同场合下,拖动可以被清扫取代,但两者不可共存。

今天解决了一个问题,觉得很有趣。本来想发到推特,发现一两句话说不清。所以写一篇blog记录一下。

我们正在开发的游戏中,会用一个id来表示一个游戏对象到底是什么。比如,“铁片”是1,“煤”是2,“采矿机”是3……这样,在运行时,C代码可以根据对象的类型方便的查询对象的属性。而对象的属性则是用Lua配置好,在运行期不变的。例如每燃烧一个单位的“煤”,可以产生100KJ的热量;一箱“铁片”有100个。

最简单的做法是,在程序运行时,用一个自增ID分配给每个物品类型。但这会导致ID极不稳定,考虑到ID需要持久化,在开发过程中,持久化的存档也变得很不稳定。所以我选择了用物品的名称以及一些内在的参数(例如版本号)一起计算hash值当作ID使用。

今天是年前最后一天工作,我想对最近做的一些事情做一些记录。

我们在使用自研引擎开发游戏时,遇到了不少和预期设计有距离的问题,针对问题再反思了原有的设计并做出改进。我认为、凭空设计一个通用游戏引擎是不可能的,必须结合实际项目,做许多针对性的实现。但同时应不断反思,避免过多的技术债。在最近一年,我参与的引擎具体编码工作很少,更多的是站在一个反思者位置,监督代码和设计的演变。

我们的引擎主体框架是基于Lua的,受益于Lua的动态性,可以很方便的把各个模块粘合在一起。但是、Lua和C/C++相比,又有两个数量级的性能差异,对于渲染代码而言,若将和GPU沟通的API完全放在Lua的binding层,对于对象数量巨大的场合,很容易出现性能问题。我估计、在手机环境这个“巨大”差不多在10K这个数量级吧,而PC环境还能支撑到100K左右。

一般来说,如果10K是场景可渲染对象的数目的话,那么对于很多游戏场景还是适用的。但如果Lua调用的图形API放在太底层,手机上(以Apple的A9芯片为下线)一个流畅的场景却很难支撑到10K对象。这是因为,每个可渲染对象需要一系列参数传到图形API层,设置VB/IB渲染状态,尤其是大量的uniform,这些API差不多会在10个调用左右,它们会吃掉一个数量级,最终Lua层能流畅处理的数量大约只剩下1K左右了。

我们的虚拟文件系统有两个工作模式。一种模式主要用于编辑器和开发环境,所有的文件名和路径都基于原生文件系统,我们叫它编辑器模式;另一种模式被称为运行时模式,主要用于运行时环境。文件均用类似git仓库的形式,用文件内容的hash值作文件名,储存在一颗Merkletree上,并通过一个专门的fileserver为运行环境提供资源的更新服务。

在编辑器模式下,并不是直接映射原生文件系统的整棵目录树的,而是增加了一个mount配置,可以把很多不同的目录装配在一起。当初这么设计是希望提供一定的灵活度,方便游戏项目可以把引擎提供的基础功能和资源组装进来。

我们现在制作的游戏以手机优先,交互都是围绕着触摸屏来做。如果开发的时候总是在PC上用鼠标测试,很多交互问题都容易被忽视掉。所以,我们自研的引擎花了很大气力在手机设备上即改即所得。在开发时推荐开发人员直接在手机上修改、调试。这样更容易发掘出平时用鼠标操作难以察觉的问题。

和Factorio体验的最大不同在于,我们不使用传送带做主要物流手段,而是依靠公路网和货车。这种形式并不新,我在20多年前的凯撒3(以及同系列的法老王、宙斯、龙之崛起等)中就玩到过。采用这种物流方式的游戏一直不曾间断,这几年我玩过放逐之城、缺氧、环世界、海狸浮生记也是这种。

这类游戏的共性是需要靠城市资源供养一批工人,然后这些工人干活来供养城市,其中一个重要的工作就是物流。一般受资源产能影响,不会养太多工人,不然很难产生正循环。所以物流所占工作比例并不大。大部分工人的工作(在生产建筑中)从事生产。

我们正在制作的游戏中,交通和物流是基于公路网的。公路网其实是以路口为顶点,路为边构成的(无向)图。因为我们有大量的车辆行驶在这个路网中,所以,我需要一个空间高效的方法储存这些车辆的路径。

在大部分情况下,我们的车辆都选择唯一的最短路径,也就是说如果从A路口到B路口存在一条制定好的路径的话,所有的车都会走这条路。基于这一点,我们可以对多条路径合并储存。

我选择用当前路口id和目的地路口id做key,把下一站路口id作为value,保存在一张hash表中。这样,先用dijkstra算法求出起点到终点的最短路线后,再把每一段路线用该结构保存下来即可。

当车辆到达某个路口,只需要用当前路口和它自己的目的地就可以索引到应该往哪个方向开。这个结构很适合保存在Luatable中,用元表触发尚未计算过的路径。而且这样一个路径表只是一个cache,随时可以清理重建。

我们在很早以前就放弃了原来设计的DSL,只使用数学库的部分功能。趁这次重构,我打算把这些已经废弃的部分彻底删掉,并重新设计底层的数据结构,让它能更好的适应其核心特性。

就目前的使用经验来看,几乎所有游戏中的Entity都是从Prefab实例化得来的。一个Prefab会实例化出n个Entity,但这n个Entity的生命期管理却很麻烦。

最自然的想法是:所有的Entity都必须是场景树上的节点(拥有场景组件),当我们删除一个场景节点时,它所有的子孙都一起移除。

在使用过程中,我陆续发现了这个特性的其它用途。

过去的某个游戏项目中,曾经遇到这么一个需求:游戏中需要为玩家生成一个头像,这个头像是由很多部分构成的。有玩家的肖像、名字、等级、修饰用的图标等等。这个头像可能同时在屏幕的很多位置显示。

如果场景是一个简单的树结构,那么,每个出现头像的地方,都是一个构成头像元素的子树。这样,场景中就可能出现完全一样的多份子树。固然我们可以用一个通用接口构造这个子树出来,但是,一旦这个玩家的属性发生变化,就需要针对所有场景中出现的同类子树进行修改。这需要为每个玩家额外维护一个列表,引用所有头像在场景树中出现的位置。这无疑增加了数据结构的复杂性。

游戏的业务逻辑到画面呈现的过程和GUI系统在结构上有相似之处,又有一些不同点。

在软件设计时,我们通常倾向与把数据和展示分离。在GUI系统中,数据通常被称为Model,展示被称为View。典型的MVC(及其衍生品MVP/MVVM等)模式就是建立在此基础上。业务逻辑修改Model,经过展示模块,把Model映射到View中呈现给用户。

我倾向于把游戏软件切分为gameplay和view两个完全分离的模块,各自有独立的数据结构和设计。gameplay应该可以完全独立于view运转,而让图形引擎关心且只关心view部分。在制作游戏软件时,如何解决好gameplay的信息如何在view模块中展示出来,就成了必须考虑的设计点。

最近这个项目,里面有大量的机械动画:采矿机、抽水泵、发电机、组装机、机械臂、等等。

我发现,我们自研的引擎的动画模块其实是不够用的。

我们在设计引擎的动画模块时,是按过去经常做的MMORPG里的需求来设计的。主要是控制Avatar的动作:走、跳、跑、攻击、等等。在从制作软件导入动画数据后,引擎需要做的加工主要是动画和动画之间的融合。例如,从走路过渡到跑步。还有在动画中加入一些运行时的控制:例如转头盯着物体、调整脚掌贴合地面。这些是用IK模块来实现的。

我们最近在开发的类似异星工厂的游戏中,一个重要的物流子系统就是流体系统。我个人觉得,它是所有子系统中最难实现的一个。

一个好的流体系统非常难兼顾高效和拟真。在Factorio的仿制品戴森球计划上线时,我饶有兴趣的想看看它是怎么做流体管道的,但让我失望的是,它几乎把流体系统砍掉了。

我在Factorio中非常喜欢流体系统的伴生玩法。所以,在我们的游戏中,即使我没有做传送带,也要做一个流体子系统出来。我们的设计直接参考了前面提到的几篇blog,并加入了一些我自己的想法。

最近在做一个类似异星工厂的以自动化生产为主题的游戏。虽然我们的设计中,虽然我们的物流系统和异星工厂有许多差异(暂时没有设计传送带),但是我们也有类似的机械臂系统。

机械臂以一个固定的方向,让物品短距离移动,从一端转移到另一端。它用来把物品在物流网络和生产机械之间转移。我们在实现这个系统时,修改了几次方案,我觉得有点意思,值得记录一下。

最近几年有很多基于球体地图的游戏,包括今年颇有好评的戴森球计划。

大多数在球面打格子分割地图的游戏,会采用六边形网格。但是,只靠正六边形网格无法铺满整个球面。必然会留下12个五边形。因为用正六边形铺满球面时,其实是在尝试用正20面体逼近球体。如果我们将正20面体做平面展开,每个三角形面中划分多个正三角形,每6个三角形合成一个正六边形,但中间的12个交接点必然是5个三角形缝合。只要处理好这个五边形,尝试用六边形网格做游戏是个合适的选择。

据戴森球计划的开发日志所述,开发团队也曾考虑过六边形网格方案,后来觉得不合适又回归四边形。我猜想多少和戴森球计划参照的游戏原型异星工厂是四边形网格有点关系。我也觉得在四边形网格上设计这种传送带游戏更舒适。不过,相比目前戴森球计划中用的方案:从赤道到两极,沿着纬线圈一圈圈减少网格数量;我更喜欢网格能完全对齐的方案。

这套脚本语言还是以配置数据为主,但也提供了很多逻辑控制手段,很值得学习。

最近在用自研引擎开发项目时,发现了一些问题。在解决问题的同时,也逐步对之前的设计做了一些调整。一开始只是一些小修复,慢慢的发展成了大规模的代码重构。

粒子系统中,势必会引入多种材质。要么按材质分为不同的管理器对象,要么把所有粒子片放在一个管理器下,但增加材质的属性。

如果是前者,即使粒子的其它属性都有共性,也无法一起处理;而后者,则涉及材质分类的问题。

我们不大可能在渲染阶段无视粒子的材质属性,每个粒子片都单独向渲染器提交材质。因为无论是面片粒子,还是模型粒子,都应该批量提交粒子的空间结构数据,然后一次渲染。如果粒子是面片,那么就应该把一组粒子的顶点信息组织在同一个顶点buffer中;如果粒子是模型,就应该把每个个体的空间矩阵组织在InstanceBuffer中。

如果材质属性只是一个id或材质对象指针,作为一个属性关联在粒子对象上的话,不同材质的粒子是无序的,怎样的数据结构可以方便管理呢?

TL;DR在花了一整个晚上用C++完成了这一块的功能后,我陷入了自我怀疑中。到底花这么多精力做这么一小块功能有意义么?强调类型安全无非是为了减少与之关联的代码的缺陷,提高质量;但代码不那么浅显易懂却降低了质量。

先来回顾一下设计:在这个粒子系统中,我期望把粒子对象的不同属性分开管理。

这是因为,在处理单个属性时,往往并不关心别的属性。比如,我们在递减生命期,处理生命期结束的对象时,关心的仅仅是生命期这个属性;在处理粒子受到的重力或其它力的影响时,我们只关心当前的加速度和速度;在计算粒子的空间位置时,只关心上一次的位置和瞬间速度;而在渲染时候,无论是生命期、加速度、速度,这些均不关心。

当数据按属性聚合,代码在批量处理数据时,连续内存对cache友好,即使属性只有一个字节,也不会因为对齐问题浪费内存。同一属性的数据尺寸完全相同,处理起来更简单。而且粒子对象相互不受影响,我们只是把同一个操作作用在很多组数据上,次序不敏感。非常适合并行处理。

更重要的是,不同类型的粒子需要自由的根据需要组合属性和行为。有的粒子有物理信息参与刚体碰撞运算,有的则只需要显示不需要这个信息;有的粒子有颜色信息,有的不需要有;有的粒子是一个面片,有的却是一个模型,拥有不同的材质。这导致粒子对象包含的信息量是不同的。及时拥有同一属性,作用在上面的行为也可能不同:例如同样是物理形状信息,可能用于刚体碰撞,改变运动轨迹,也可能只是为了触发一下碰撞事件。

在传统的面向对象的方式中,常用多态(C++的虚函数)来实现,或者有大量的ifelseswitchcase。

如果能按组件和行为聚合,那么就能减少大量的分支。每个粒子的功能组合(打开某个特性关闭某个特性)也方便在运行时决定,而不用生成大量的静态类。

策略竞争类的游戏一定要设计多种结束条件。RFTG的基础版设计了建造出12+张卡结束和分完12n个VP结束。基础策略就可区分为快速打出一堆小分卡,还是构造一个VP引擎。我最近玩得比较多的五扩(XenoInvasion)还增加了击败(或被击溃)Xeno结束。

这样可以使得创造出strategictension。

今天想谈谈游戏引擎中Culling模块。

当场景中的可渲染对象很多,而当前会被渲染的对象相较甚少的时候,我们通常会启用一个culling的过程。Culling会想办法剔除一些当前不必渲染的对象,避免这些对象提交到GPU,让GPU承担剔除的过程。这样可以减少CPU到GPU的带宽。

最基本的Culling是用相机的视锥体和对象做一个相交测试,如果对象和视锥体不相交,则可判定它不必渲染;复杂的Culling还包括遮挡测试,如果一个对象完全被墙体挡住,那么也不必渲染。这篇只谈前者。

很容易得知,Culling算法的复杂度上限为O(n)。即,我们把场景中的每个对象逐一和视锥体做一次相交判断即可。这里的n为场景中元素的个数。

当n特别大的时候,通过巧妙地设计数据结构,我们则有可能把复杂度降低。但如何做到呢?

在上一篇blog中,我谈到了UI模块。而UI模块中绕不开的一个问题就是怎么实现文字渲染。

和西方文字不同,汉字的数量多达数万。想把所有文字的字模一次性烘培到贴图上未尝不可,但略显浪费。如果游戏只是用有限几种字体倒也不失一种简单明了的方法。但如果使用字体丰富,而多数字体只使用几个汉字,那么就不太妥当了。

在游戏(包括引擎)开发的过程中,谈及UI模块,通常所指有二:

这两者很多时候都是共用的一个模块,比如之前的Unity就直接把引擎开发用的UI模块扔给开发者开发游戏使用。但很快,开发者就发现,适合工具开发的UI模块,在做游戏时就不那么顺手了。所以就有了第三方UI插件的流行,以至于最后又倒逼Unity官方重新制作游戏UI模块。

开发工具面临的需求和游戏场景面临的需求很不一样:

开发工具需要的时候更好的将内部数据以可视化方式呈现出来,供用户浏览和修改,以适应数据驱动的开发。UI的呈现需要的一致性,风格统一有利于减少学习成本,同时需要清晰的表达复杂的数据结构。有时还需要将内部数据的变化过程同步的动态呈现,给开发者更直观的感受。

游戏UI是游戏过程的情感体验的一部分,外观和交互需要根据游戏设计专门化。它往往并不需要表达游戏内部复杂的数据结构,而是将那些数据以额外面对玩家的形式展现出来。玩家通过界面下达的指令也并非直接对数据的修改,而是以指令流的形式传递过去。另外,HUD也是很大的一个部分,和UI对话框在设计上也有很大的不同。

他们两者之间在技术上的共性其实很小,针对这些共性的技术实现可能也只有几百到上千行代码的规模,远少于差异部分需要的代码量。我比较倾向于把这两个东西分开实现。

我一直在思考的问题是:为什么一定要用树结构组织可渲染对象?树结构到底带来了什么好处?

最直接的好处是,减少矩阵运算的次数。因为,渲染层最终需要对象在整个世界中的位置,而每个被渲染的部件本身却是逐级组合起来的(为了减少数据重复,我们不能因为一个部件换了个位置,就复制一次),部件只会记录相对整体的一个局部空间变换。如果我们平坦的保存没有可渲染部件,势必在计算它最终被渲染到屏幕时的世界矩阵的时候,需要连乘一长串局部矩阵。而组织成树结构,以一定的次序计算,可以大大减少最终矩阵乘法的数量。

但这一点好处,我认为还没有触及本质。表达空间位置的矩阵,仅仅是可渲染对象的一个属性而已。

层次结构的本质是让属性可以用继承的方式优化储存,并方便批量修改。对于每种属性,会定义一种对应的继承方法。

Unity推广了预制件Prefab这个概念,在此之前,UnrealEngine里有个类似的东西叫做蓝图Blueprint。当然它们不完全是一种东西。我现在自己设计游戏引擎,更喜欢Unity中的Prefab,但我认为Blueprint这个名字其实更贴切一些。

当我们说制作一个Prefab的时候,其实制作的是一个预制件的模板。引擎运行时对应的那些数据,其实按照这个模板生产出来的。所以,工具制作出来的Prefab其实是一个template,所以,它本质上就是一张蓝图。但因为Unity深入人心,所以我还是打算基于把这个东西叫预制件。

对于ECS系统来说,预制件是一组数据,通常保存在文件中,可以以该资源文件的内容为模板,来构造一组Entity。注意:预制件作为资源文件时,和贴图、模型数据等这些资源文件是不同的。贴图之类的资源,映射到内存后,就是一个数据块或一个引擎中的handle,可以被共享使用。但预制件本身只是一个模板,它用于生产数据,本身不是数据。从这个角度讲,如果把预制件文件当作资源纳入资源管理模块统一管理的话,预制件资源对应的是一个function(生成器)而不是table(数据集)。

最近在重构引擎的场景管理模块。主要动机之一,还是觉得现在已经实现的东西(由于需求不断增加)太复杂了,以至于老在修补其中的bug。

经过了几天的思考后,我决定把场景管理中的一部分挪出来,单独作为一个模块。那就是对层次结构的排序。

具体是这样的:

问题的具体描述是这样的:

我们的引擎每帧会将场景中的对象依次提交到一个渲染队列中,每个可渲染物件,除了自身的网格、材质外,还有它自身的包围盒(通常是AABB),以及它在世界空间中的矩阵。

我们有一套资源系统,场景中的对象会引用资源系统中的对象,这些资源对象是一个不变量,会被多个场景对象所引用。而资源对象又可以是一个树结构,比如一个模型就可以由若干子模型所构成。提交到最终渲染队列中的是不可再拆分的子模型的信息。

也就是说,在场景管理的层次,对象的数量是远少于提交到渲染队列中的对象数量的。这就是为什么我们渲染每次重建渲染队列,而没有将每帧提交给渲染队列的列表持久化为一个链表并作增减维护的原因。

问题出在提交给渲染队列的每个物件的包围盒上。

游戏里的场景对象,通常以树结构保存。这是因为,每个对象的空间状态,通常都受上一级的某个对象影响。

从管理角度讲,每个对象最好都能知道它可以影响其它哪些对象;且必须知道它被哪个对象影响。所以,这会用到一个典型的树结构。尤其在做编辑器时,树结构还会直接呈现在编辑界面上。不过,我认为在运行时,从父对象遍历到子对象的需求并不是必要的,需要时可以额外记录。从数据上考虑,父亲记住孩子和孩子记住父亲,是重复了同一种关系信息。如果不需要记住孩子的兄弟次序,那么在核心数据结构中,我们只需要让孩子记住父亲就足够了。

去掉冗余信息可以简化数据结构、减少维护成本、避免犯错误。尤其对于ECS架构,我希望所有对象都是平坦的,在场景对象组件上,一个parentid可以最少的构造出场景的层次结构出来。

最近我们开发中的游戏引擎在修理资源管理模块中的bug时,我提出了一些想法,希望可以简化资源对象的生命期管理。

其实这个模块已经被重构过几次了。我想理一下它的发展轨迹。

最开始,我们不想太考虑资源的生命期问题,全部都不释放。当然,谁都明白,这种策略只适合做demo,不可能用在产品中。

另一个促使我们认真考虑资源管理模块的设计的原因是,当我们从demo过渡到现实世界的大游戏场景时,过多的资源量触发了bgfx的一个内部限制:如果你在一个渲染帧内调用了过多资源api(例如创建新的buffertexture等),会超出bgfx的多线程渲染内部的一个消息管道上限,直接让程序崩溃。

所以我们不得不比计划提前实现资源的异步加载模块,它属于资源管理模块的一部分,所以也就顺理成章的考虑整个资源管理模块的设计。

法线贴图上的法线向量虽然有三个分量,但是它们是归一化的。而且,切线空间上的法线贴图的第三个向量Z总是正值,所以我们只需要用两个分量就可以保存下法线信息,在运行时再计算出第三个向量。

显卡硬件一开始并不支持的双通道压缩贴图,所以最早使用这个算法的Id是用dxt5模拟的,使用Dxt5的RGB中的G保存一个通道,再用A保存另一个通道。后来的EAC_RG11则直接支持了双通道压缩贴图。

ASTC虽然没有特别支持双通道贴图,但是它的encoder可以把信息权重放在两个通道上,这样就可以在同样的bpp下,让双通道信息的误差更小。

bgfx自带的贴图压缩工具没有支持这样的压缩参数,我最近的工作就是完善它。

我们公司的一些Unity项目,当cacheserver的数据上涨到几百G后,经常遇到问题。最近一次是nodejs内存使用太多导致进程挂掉。

最近两个月,结合过去的经验,我们对最初设计的框架做了较大的调整。这主要是源于对框架要解决的事情的更深入的理解,以及在实践过程中针对典型场景总结出来的模式。

在此之前,我们一直在直接使用lua描述数据;但最近随着数据类型系统的完善,同事建议设计一种专有数据格式会更好。希望专用格式手写和阅读起来能比lua方便,对diff更友好,还能更贴近我们的类型系统,同时解析也能更高效一些。lua的解析器虽然已经效率很高,但是在描述复杂数据结构时,它其实是先生成的构造数据结构的字节码,然后再通常虚拟机运行字节码才构造出最终的数据结构。这样的两步工作会比一趟扫描解析构造要慢一些且消耗更多的内存。

现有的流行数据格式都有一些我们不太喜欢的缺点:

所以ECS框架改变了数据组织方式,把同类数据聚合在一起,并用专门的业务处理流程只针对特定数据进行处理。这就是C和S的概念:Component就是对象的一个方面aspect的数据集,而System就是针对特定一个或几个aspect处理方法。

那么,Entity是什么呢?

我认为Entity主要解决了三个问题。

因为职业原因,我又琢磨了一下这类游戏的实现方法。在网上搜不到太多直接资料,所以这篇blog更多的是对自己的想法的记录。这次主要还是想理解一下游戏中是如何处理武器和怪物之间的击打判定的。

今天想写写这个话题是因为上周我们一个MOBA项目抱怨skynet的定时器精度只有10ms(100Hz),无法满足他们项目“帧同步”的需求。他们表示他们的项目需要服务器精确的按66ms的周期向客户端推送数据,而skynet只能以60ms或70ms的周期触发事件,这影响了游戏的“手感”。

讨论下来,我认为,这是对所谓“帧同步”算法有什么误解。我们客户端运行时不应该依赖服务器的准时推送消息才能得到(手感)正确的结果。虽然在skynet下你可以写个服务代替底层提供的timer来更准确的按15Hz发出心跳消息,但我觉得服务器依赖时钟的精确是游戏设计上的错误,提供“手感”完全应该是客户端程序的责任。

这篇blog就来写写基于lockstep的网络游戏同步方案到底是怎么回事。

这个虚拟文件系统是用lua编写的,这就有了一个小问题:lua代码本身也是放在虚拟文件系统中的,那么就需要解决自举。这些代码很有可能需要从网络更新(网络文件系统模块),而网络模块也是lua编写的,代码同样放在这套文件系统内。

这篇blog我想谈谈自举是怎样完成的。

我打算就我们在开发客户端引擎框架时最近遇到的两个问题写两篇Blog,这里先谈第一个问题。

我们的框架技术选型是用Lua做开发。和很多C++开发背景(现有大部分的游戏客户端引擎使用C++开发)的人的认知不同,我们并不把Lua作为一个嵌入式脚本来看待,而是把它当成一种通用语言来设计整个引擎框架。

选择Lua有很大成分是因为我的个人偏好,另一部分原因是Lua有优秀的和C/C++代码交互的能力。可以方便地把性能热点模块,在设计上做出良好的抽象后,用C/C++编写高性能的模块,交给Lua调用。

但和Javascript不同,我们在做原生App时,和操作系统打交道的部分还是得用操作系统的语言,C/C++/ObjectiveC/Java等等。Lua虚拟机还是要通过一个C模块实现嵌入到App中去,这个工作得我们自己来完成。

随后交给开发组的一个同学实现,这半年来,一直在使用。最近做了引擎一个小版本的内部验收,我感觉这块东西还有比较大的改进余地。因为资源文件系统目前和开发期资源在线更新部分现在掺杂在一起,而网络更新部分似乎还有些bug,偶尔会卡住。我觉得定位bug成本较高,不如把这块重新实现一遍,顺便把新的改进想法加进去。

现代2d游戏的图形地层绝大多数也是基于3dapi实现的。为了提高性能,通常要把若干图元(sprite)装箱在整张贴图中。这个装箱过程可以是在线下完成,也可以是在运行期来做。

TexturePacker就是做这件事的优秀商业工具。不过我认为把它放在开发工具链中还有一些不足。图元的装箱和根据装箱结果合成贴图是两件事情,如果我们是手工操作,合在一起完成当然方便;但如果是在自动化流程中,分开独立完成更好。因为迭代开发过程中,每次重新打包资源,都只会修改少部分图元,且图元的大小未必会改变。如果大小不变,就不必重新做装箱运算。

如果部分修改图元,则合成贴图的过程有可能能减少运算过程。通常我们需要对最终的贴图做一次压缩,生成类似ETC的压缩贴图类型,这是极消耗cpu的。而ETC压缩格式是基于4x4的区块独立压缩,只要保证图元尺寸是4的倍数、就可以先压缩,再合成。这样,没有修改过的图元就可以不必重新运算,直接从文件cache中读回。

有些时候不合成、仅保存装箱结果更适用。可以在运行时根据altas数据把分离的图元装载在贴图中。分开打包独立的图元资源更适合游戏更新。

第二,在提高装箱利用率上面TexturePacker做了很多的努力。很多sprite的边角会有大量的空白,仅仅按轴对齐的四边形包围盒裁剪还是浪费太大。它的最新版本支持了多边形装箱、即尽可能的把边角都裁剪下来。这种做法的代价是增加了运行时的多边形数量(对2d游戏来说,通常不太重要),但让装箱边角余料可能多填一些小sprite进去。

但我认为其实还可以找到更多方法。

这篇blog就想谈谈最近我在为公司新的2d项目完善ejoy2d的工具链,编写装箱工具时,做的一些工作。

上次说到,我们的引擎打算在PC上开发,设备上直接调试。如果是按传统的开发方式:运行前将app打包上载然后再运行,肯定是无法满足开发需要的。所以必须建立一套资源的同步机制。

目前,我们已经实现了基本的资源文件系统,大致是这样工作的:

所有的资源文件,包括程序代码本身(基于Lua),都是放在开发PC上的。开发环境会缓存每个文件的md5值,文件系统将用此md5值为标准,默认md5相同的文件,其内容也是一致的,暂不考虑md5冲突的问题。

在设备上,用设备的本地文件系统做一个cache,cache中分为两个区间,一是资源文件区,按所有资源文件的md5值为文件名(按md5的16进制码的前三字节散列在不同子目录中,防止单个目录文件数量过多)保存。二是目录结构区,每个子目录用一个递增数字id做文件名,内容则是可读文件名以及其内容对应的md5值或子目录编号。其中id0是资源根目录。

然后是我给bgfx的debugtext做了中文的支持。bgfx专门做了一个层,模拟VGA的textmode。一开始我学着过去dos模式下做中文系统的方式增加了双字节支持,采用在模拟出来的videomemory的attributebyte上设置几个特殊标记来表示接下来的两个slot是连起来的一个unicode字符。

这种让一个汉子占据两个videomemoryslot的方式在过去非常常见,不过弊端也很明显:容易产生半个汉字乱码问题,解决方案看起来比较trick。而且把汉字作为一种特殊字符来处理,而不是彻底解决unicode大字符集的解决方案感觉也很不美观。

接下来我花了一点气力实现一个更加通用的unicode方案:

至少要支持多个codepage,并把原本就支持的dos字符集当作cp437,而可以由使用者执行添加新的codepage,比如汉字所用的cp936。再将不同的codepage统一转换为unicode。我为多codepage编写了一个简单的查找cache,字形贴图管理的新模块。cp936里用到15点阵汉字字形是从文泉驿黑体中导出的。另外为了更好的支持unicode,把virtualvideomemory里保存codepoint的单字节扩展为3字节,可以把单个汉字放在一个slot里。不过,为了排版正确,还是需要在debugprint的api中检测到汉字就在每个汉字后保留一个空格,让单个汉子占两个英文字符的位置。

今年国庆节的时候,偶然间我又翻到这个仓库,居然作者一直在更新。坚持了五年,一直在维护这么个小玩意,让我对这个项目多了点信心。节后我饶有兴趣的研究了一下它的代码。

现在我觉得,这个库的设计思想非常对我的胃口,核心部分几乎没有多余的东西:数据计算、平台API支持、数据持久化格式支持、等等都没有放在核心部分。它仅仅只做了一件事:把不同平台的图形API:DirectX、OpenGL等等整合为一套统一的接口,方便在此基础上开发跨平台的3d图形程序。不同平台的3dapi的差异,正是3d游戏开发中最脏最累的活了。

经过了解,卡顿主要是在压力测试中表现出的网络消息流量过大造成的,又没有找到合适的方案减少流量。

昨天,我们自己的一个MMORPG项目中发生一个小bug,就这个bug我们做了一些讨论。我想借这个问题展开,在一个抽象层面谈谈我觉得MMORPG的网络同步应该怎样做。

这些游戏基于同一套引擎,所以数据文件格式也是共通的。P社开放了Mod,允许玩家来修改游戏,所以数据文件都是明文文本存放在文件系统中,这给了我们一个极好的学习机会:对于游戏从业者,我很有兴趣看看成熟引擎是如何管理游戏数据和游戏逻辑的。

据我所接触到的国内游戏公司,包括我们自己公司在内,游戏数据大都是基于excel这种二维表来表达的。我把它称为csv模式。这种模式的特点是,基础数据结构基于若干张二维表,每张表有不确定的行数,但每行有固定了列数。用它做基础数据结构的缺陷是很明显的,比如它很难表达树状层级结构。这往往就依赖做一个中间层,规范一些使用格式,在其上模拟出复杂数据结构。

另一种在软件行业广泛使用的基础数据结构是json/xml模式。json比xml要简单。它的特点就是定义了两种基础的复合结构,字典和数组,允许结构嵌套。基于这种模式管理游戏数据的我也见过一些。不过对于策划来说,编辑树结构的数据终究不如excel拉表方便。查看起来也没有特别好的可视化工具,所以感觉用的人要少一些。

最开始,我以为P社的数据文件是偏向于后一种json模式。但实际研究下来又觉得有很大的不同。今天我尝试用lpeg写了一个简单的parser试图把它读进luavm,写完parser后突然醒悟过来,其实它就是基于的嵌套list,不正是lisp吗?想明白这点后,有种醍醐灌顶的感觉,的确lisp模式要比json模式简洁的多,并不比csv模式复杂。但表达能力却强于它们两者,的确是一个更好的数据组织方案。

EntityComponentSystem(ECS)是一个gameplay层面的框架,它是建立在渲染引擎、物理引擎之上的,主要解决的问题是如何建立一个模型来处理游戏对象(GameObject)的更新操作。

传统的很多游戏引擎是基于面向对象来设计的,游戏中的东西都是对象,每个对象有一个叫做Update的方法,框架遍历所有的对象,依次调用其Update方法。有些引擎甚至定义了多种Update方法,在同一帧的不同时机去调用。

我觉得守望先锋之所以要设计一个新的框架来解决这个问题,是因为他们面对的问题复杂度可能到了一个更高的程度:比如如何用预测技术做更准确的网络同步。网络同步只关心很少的对象属性,没必要在设计同步模块时牵扯过多不必要的东西。为了准确,需要让客户端和服务器跑同一套代码,而服务器并不需要做显示,所以要比较容易的去掉显示系统;客户端和服务器也不完全是同样的逻辑,需要共享一部分系统,而在另一部分上根据分别实现……

昨天和人闲扯,谈到了MMORPG客户端的网络消息应该基于怎样的模型。依稀记得很早写过我的观点,但是blog上却找不到。那么今天补上这么一篇吧。

我认为,MMO类游戏,服务器扮演的角色是虚拟的世界,一切的状态变化都是在游戏服务器仲裁和演化的。而客户端的角色本质上是一个状态呈现器,把玩家视角看到的虚拟世界的状态,通过网络消息呈现出来。所以、在设计客户端的网络消息分发框架时,应围绕这个职责来设计。

我们公司有两个项目的客户端在使用git做项目管理,三个项目使用svn管理。程序员比较爱git,但是为什么svn还存在?主要是在做客户端开发时,策划和美术人员始终迈不那道坎。即使已经在用git的项目,策划们还是反应用起来比svn跟容易犯错误,遇到自己无法解决的问题也更多。

我是非常想在公司全部推广使用git做项目管理的。所以需要仔细考察到底是什么东西阻止了策划(及美术)们掌握这个工具。

思考一番后,我认为一套完整的培训机制还是得建立起来。完全靠自学和口口相传是不靠谱的。尤其是在有svn基础的时候,非程序开发人员接受git其实比程序开发人员要困难的多。如果你硬要把svn的概念全部适配到git上,其实就是在把git当svn在用,不仅获得不了好处,反而增加了很多困扰。

而实用主义者,在没有外力的情况下,只会看到表面。不可能系统的从原理上系统理解git到底解决了什么问题、每步操作背后到底做了什么,如果出现了问题,问题是怎么引起的。我们知道,在用git的时候,由于分支和提交都比svn方便,分布式的结构也会更容易导致版本演化图变得异常复杂。当它乱成一团乱麻的时候,任何新的合并操作都会比之前遇到更多麻烦。如果使用者心里有清晰的概念,时刻保持演化关系简单,他遇到的问题自然会少。而你遇到问题乱解决一通,只满足于把现在的问题搞定,那么下次就会面临更大的灾难。

我们公司的一个MMORPG项目最近在内存方面碰到了红线,昨天开会讨论了一下。我提出了一个改进方案,写篇blog记录一下。

问题是这样的。在当下的手机及平板硬件设备条件下,操作系统留给应用的可用内存并不多,大约只有500M左右。

根据实际测试,游戏想跑在当前主流高端手机上必须把自己的内存占用峰值控制在400M内存以下,350M会是一个合理的值,而这个值是远远低于10年前的PC游戏标准的。而我们的项目却是一个写实类型的拥有大场景的MMORPG。

网游里有很多抽卡、开箱子之类的赌性玩法,在最开始,游戏设计者实现的时候,仅仅给这些抽取概率简单的设置了一个值。比如抽卡抽出橙卡的概率是10%,那么就是说,玩家每次抽一张卡,有90%的可能是白卡,10%的可能是橙卡。

连抽20张10%概率橙卡一张都抽不到的机会多不?一张抽不中的概率是0.9,20张都抽不中的概率是0.9^20=12.2%。这可不算小数字啊。平均8个大R就会碰到一次,一下子赶跑了1/8的金主,这个责任小策划可担当不起。

所以、一般网游都会用各种规则来避免玩家出现连抽不中的尴尬。例如,我忘记是谁发明的10连抽规则:如果你购买一个大包连抽10次,我在规则上就保证里面一定至少有一张橙卡。实现它应该并不困难,按常规概率生成10张的卡包,如果里面没有橙卡,那么我加一张即可。

很多使用Unity3D开发的项目,都不太喜欢C#这门开发语言,对于游戏开发很多人还是更喜欢Lua一些。而Lua作为一门嵌入式语言,嵌入别的宿主中正是它说擅长的事。这些年,我见过许多人都做过U3D的Lua嵌入方案。比如我公司的阿楠同学用纯C#实现了一个Lua5.2(用于在U3Dweb控件中嵌入Lua语言的UniLua);还有uluasluawluapluaxlua...数不胜数。我猜测,a-z这26个字母早就用完了。

上面提到的项目的作者不少是我很熟悉的朋友,我们公司现在的U3D游戏也由同事自己实现了一套差不多的东西。所以我曾了解过这些方案。但我一直觉得这些方案要么做的过于繁琐,要么有些细节上不太完备,总是手痒想按自己的想法搞搞看。

我认为简单且完备的Mono/Lua交互方案是这样的:

U3D的打包流程,谁用谁知道。

由于输出ios包必须在xcode环境,跑在Mac系统上,所以为了定期版本打包,我们采购了配置比较高的垃圾桶来做。一台大约要三万RMB左右。

但我觉得这个方案的性价比太低了。

经过简单的考察,我发现,打包流程中最慢的环节是贴图压缩。在不同的平台,需要把原始贴图文件压缩成对应平台的压缩贴图格式:ios平台对应的是PVR压缩格式;Android平台对应的是ETC压缩格式,等等。

u3d自己也意识到压缩贴图太慢,所以官方给出了一个CacheServer方案。

在制作2d游戏时,通常我们需要把大量小图素合并到一整张大贴图上。这可以使用装箱算法(BinPacking)完成。当然,很多人很懒,那么就用别人做好的工具,比如TexturePacker。

但是在实际开发中,TexturePacker有几个严重的缺陷,我个人还是建议自己来做合图的工具,完善工具链。

缺陷2:如果要求最终合并的图是pvr或etc压缩贴图,那么还需要最后再对目标图做一次压缩。通常这个压缩过程是比较慢的。

这个缺陷2在开发期需要反复打包资源时,对开发效率影响尤其大。但其实如果自己好好设计工具链,是完全可以避免的。

下面就谈谈应该怎么处理ETC这类压缩贴图的合并问题。

我们来看看这些类型的游戏和放置类游戏的不同点。

放置类游戏大部分是玩家个人和服务器在玩,不涉及第三方的干扰。所以,只要操作序列一致,那么结果就一致。

MMORPGMOBA动作游戏这些,是多人在玩。如果我们能同步所有玩家的操作,让所有玩家的操作序列在一条线上,那么也一定可以保证结果一致。这点,是上篇blog的结论。

最近想试着做一款类似ShopHeroes的放置类网络游戏。总结一下此类游戏的客户端服务器同步问题。

传统放置类游戏,比如小黑屋,是单机运行,不需要和服务器同步的。但出于防止作弊的目的(作弊会使玩家迅速失去游戏乐趣)或者希望加上多人玩法,我们通常希望把游戏进程放在服务器上管理,这样就有了网络同步问题。

加上了服务器后,我们依然想保持玩家单机游戏的流畅体验,这里该怎么做,还是有许多门道的。

上一篇谈到了ShopHeroes的经济系统,这一篇想谈谈它的公会系统。

和很多其它网络游戏不同,ShopHeroes的公会系统并不是一个可以被剥离的系统,它和整个游戏,包括其经济系统是密不可分的。我觉得这个系统是为了完成其游戏的根本设计目的:引导玩家差异化发展从而制造出市场需求,而设计出来的东西。

在游戏中,不存在未加入公会的玩家。玩家在新手教学阶段就会被强制引导到一个叫做外城的环境,也就是游戏中的公会。你必须选择加入一个已有公会,或是自己创建一个单人公会,否则游戏无法进行下去。

在游戏的前几十小时,一个人玩其实也没有任何障碍。你可以把公会建筑看成是个人资产的一部分,该升级升级。但在上一篇提到过,游戏中用来升级的金币最终会成为紧缺资源,总有一天你的收入会承担不起。当然,作为一个固执的RMB战士,你还是可以一个人玩下去的,城市升级可以用钻石替代。而且很贴心的设计成非线性对应关系。正如上一篇提到的,金币随等级不同(生产能力成指数上升),其价值也不同。所以如果你想投资20K金币,可以用80钻替代;而一次投资500K的话,就只需要300钻而不是2000钻了。

相比一个人用钻石经营一座城市,更经济的方法是加入一个公会,大家共同建设一座城市;对于个人来说只有好处,没有任何坏处,何乐而不为呢。

最近两周在玩一个叫做ShopHeroes的游戏。知道这款游戏是因为它前两个月在Steam上线了PC版,玩了一下觉得有点意思。由于其UI一眼看上去就是手机风格,便在appstore上搜索了一下,改到ios上玩。

游戏设计的很好,对我这种资深游戏玩家有莫大的吸引力(对于快餐手游氛围下进入的新玩家可能因为系统过于复杂而玩不进去)。它设计了无数个斯金纳箱,营造出文明的那种onemoreturn的心理感受,让人欲罢不能。具体不展开讲,有兴趣的同学自己玩玩试试。

而ShopHeroes似乎真正做到了这点。从结果上看,在市场上的中高等级玩家之间流通的物品全部是用钻石结算的。而游戏本身,玩家提交求购或售卖单的时候,都可以自由选择用钻石(现实货币)或金币(游戏货币)结算。虽然我是一个免费游戏玩家,但我尝试过用(免费获得的)钻石提单到市场,均快速成交,能感受的到市场的活跃。在这些钻石交易里,系统是要收取25%的交易税的。

我思考了好几天,游戏是怎么做到:引导玩家(同时包括RMB战士和免费玩家)进行钻石交易从而收税,这一点的呢?

按局打的纯PVP机制的游戏,面临最大的问题将是,在一个玩家想找人对战的时候,找不到对手匹配。

如果游戏的在线玩家达不到一定人数,那么这个问题会恶化:等不到人和你一起玩、放弃等待、新的玩家更找不到对手。

像皇室战争、王者荣耀、炉石传说这些火爆的pvp游戏都属于迈过了线的作品,玩家不太愁等不到人一起玩,提升了游戏体验,聚集了更多的玩家。而当玩家群有限时,同类产品就很难竞争,只要在线用户掉到一定限度以下,很可能导致(无非找到对手)体验下降,更多玩家流失。

那么,有没有办法解决初期玩家过少的问题呢?

直观的想法就是没人玩AI凑。可AI并不是真人,和AI在公平规则下对战乐趣会少很多,且高水品AI开发起来也非常困难。最关键的是,一旦玩家乐于和AI对战(无论是因为对战本身的乐趣,还是可以刷分刷掉落),你会进一步失去在线用户。

最近一个月,玩群星(Stellaris)有点着魔。不同于P社之前我最喜欢的维多利亚2,这个上手更舒服。是我玩过的把大战略和4X结合的最好的游戏了。我很欣赏P社这种尽力降低玩家门槛的做法,让大战略和4x游戏不那么高冷,普通玩家也能很快领略其中的乐趣。

这次P社的引擎革新后,汉化变得很容易了。之前,大多数人和我一样使用的3dm版汉化mod;但这个mod翻译的时候,译者并没有怎么玩游戏,所以很多地方用词不当,还有一些仓促翻译导致的错别字。我实在受不了老式的闷头汉化的模式了,发现问题反馈到修正的周期太长,所以就自己维护了一个汉化mod。

游戏文本的版本更新,可以直接体现在diff里。路人发现有错别字也可以顺手提个pr。而我自己一般是在玩游戏时瞟见翻译的不合理的地方,立刻打开文本编辑器校对一下。也正因为如此,游戏里出现的event都特别仔细的阅读;群星的开发者真是脑洞大开啊,游戏里的event涵盖了几乎我所有阅读过的科幻小说,看过的科幻电影的梗。光这一点就值回了票价。

在群星发布前,我还十分担心,如果没有了维多利亚里那种厚重的历史感,一个科幻题材的战略游戏该如何给玩家代入感;没想到它是通过这个手法来完美的解决了这个问题。

在上一篇blog里,我谈到游戏服务器其实只需要使用req/resp模式就够了。有同学表示不太理解,认为服务器主动推送,或者说pub/sub的消息模式必不可少。

在聊天中我解释了很多,是时候记录一下了。

从本质上来说,如果你只是想把一系列消息发送到客户端,req/resp请求回应模式和pub/sub发布订阅模式并没有什么不同。你可以把req/resp理解成一次性的,只订阅一条(或有限条)消息;如果需要维持这类消息的推送,需要客户端在收到消息后再次发起订阅。

这和timer定时器系统一样,订阅一个定期触发的定时器这种功能并非必要。如果你需要一个每分钟触发一次的定时器,完全可以在触发一次定时操作后,立刻发起下一次定时任务请求。它在功能上是等价的。

潜在的实时性影响是:如果规定必须在收到回应消息后,才允许发起下一次请求;那么一个事件发生后推送到客户端的延迟可能增加了最多2倍。即,如果你刚刚回应了一条消息,这个时候又发生了新的事件;客户端需要在收到前一个事件后再提起新请求,然后你才可以把后发生的事件通过新请求的回应包发过去。

降低这个实时性影响的方法也很简单,协议上允许在客户端发起请求后未收到回应之前再次发起请求即可。对应同类消息许多少并发的请求,要根据这类消息的实时性要求有多高来决定。无限提高允许的并发量没有太大意义,毕竟物理上的延迟不可能完全消除。

这两天在一个游戏设计论坛潜水,读了很多帖子,感觉挺有收获的。

尤其是有一贴,楼主贴了一长篇他的idea,应当是想了很久的,算是比较完整,只差动手了。按他的说法,想先在论坛上收集一些反馈。这个论坛聚集了许多核心玩家,和相当数量的游戏开发者,按道理说,他找对地方了。

可是,在下面的回帖中,某人对此作了严厉的批评。

我问了一下设计人员一些细节,感觉现在的规则设计是很有问题的。下面记录一下我的想法,和一些改进建议。

排行榜奖励问题不只在我们这样特定的游戏中有,现在几乎所有的pay2win游戏都会涉及到。我们最初的规则是,每周为排名前列的公会按名次发放一定的钻石奖励,鼓励大公会竞争,促进消费。

后来,有玩家反应,如果公会无法排在前列,就没有动力竞争了。所以又加了补充规则,如果公会排名又提升,也有相应的奖励。

过年回武汉家中,只有一台2000块的一体机可以用,自然是跑不动3d游戏的。我挑了一款InvisibleInc玩,居然很流畅。

这是款XCOMlike的策略游戏,场景是isometric的。但是用QE两个键可以旋转场景,虽然静止下来只有四个方向,但是旋转过程却以动画方式呈现。这让我一度认为它的场景是基于3d模型制作的。但令人惊讶的是,在低配置的一体机上却非常流畅。相比较,我最近在玩的XCOM2却在GTX550显卡上也有卡顿。

我仔细观察后,发现其实InvisibleInc的场景里的物件都只是2D图片。甚至只有正面和背面两张图,通过左右镜像得到了四个视角。它的旋转速度很快,并加了一定的模糊效果,欺骗了人眼。其实只有地板和墙壁是真正旋转的,其它物件只有坐标在做旋转,而图片本身在旋转过程中并没有变化。

也就是说,它的引擎其实是基于2D制作的,模拟出了3d才有的视觉效果。

一般游戏会把所需要资源数据打包成一个大文件,游戏运行时可以像访问普通文件一样,访问包内文件。如何打包如何更新资源包,是设计重点。

现在有很多资源包直接使用通用打包(压缩)格式,比如zip。也有自行设计的,多半是为了一些特殊需求。比如资源间有引用关系等。如果资源数量过多,通常还会对原始资源文件名做一次hash索引,加快包内文件检索效率。像暴雪的mpq格式,还有unity3d的assetbundle格式都是这样的。

一旦资源打包,原始文件名信息就不再需要了。应用程序可以在运行时通过文件名的hash值索引到包内文件。(所以第三方的mpq解包工具需要提供一份额外的文件名列表)

这两天都在忙一个事情,把我们的一个新游戏提交到steam绿光计划上。

去年底开始,公司里就有两个人开始在忙一个小项目。最初的一个月,他们只是做了一个原型,本来打算演化成一个手机游戏,作为2015年公司的新项目去推广的。这个原型只有一个简单的战斗动画,提出想法的同学是一个格斗游戏迷,他只是想简化格斗游戏里的操作,提取出一些核心乐趣带给大众玩家。

当然,他同时也是一名优秀的美术设计人员,所以原型做的非常绚丽,一下子就吸引了公司里很多人。

对于这款游戏做出来后如何盈利,最初的两个开发人员(一个美术和一个程序)一点想法都没有。他们没有沉浸过任何网络性质的手游,不懂怎么挖坑赚大R的钱。只知道打磨那些demo中自己认为有趣的地方。所以对于这个项目是否立项,作为公司的决策人是很为难的。

公司的程序群里,有同学提出,能不能把算法提取出来,用来做游戏资源图片的压缩。针对卡通类的图片,可以用很小的文件保存,在加载或安装时放大。我觉得有点意思,就做了一些研究。

最近想在游戏中加一点简单的环境光,因为游戏中使用的都是2d图片,那么最廉价的方法应该是给图片加上法线图了。

好在我们游戏的原始图素都是用3d建模,然后再用平面修整的。基本几何结构信息可以从模型提取出来。当然,我们并不真的需要自己写程序去从模型中计算出最终渲染图的法线信息。所有渲染软件都可以输出它。

比如3dsmax,如果你把渲染结果输出成.rla文件,那么就可以勾选normalzbuffer等额外的通道输出。

记得我读大学时写过一个rla文件解析程序,当时是为了提取里面的Z通道。这都过了十多年了,果然完全找不回当年写的代码了,也忘记曾经怎么实现的,所以就从头来搞。

温度图(Temperature)一般根据纬度以及当地的海拔计算出来,也会影响当地的动植物。

地质的排水情况(Drainage)可以影响当地能否形成湖泊、湿地和河流。同样影响了当地的土质(进一步影响植被)。

另外还有,火山的分布情况(Volcanism),以及野生物分布图(Savagery)会改变当地的矿产和动物分布。

Unity3D的assetbundle的格式并没有公开。但为了做更好的差异更新,我们还是希望了解其打包格式。这样可以制作专门的差异比较合并工具,会比直接做二进制差异比较效果好的多。因为可以把assetbundle内的数据拆分为独立单元,只对变更的单元做差异比较即可。

注:陌陌争霸的数据库部分我没有参与具体设计,只是参与了一些讨论和提出一些意见。在出现问题的时候,也都是由肥龙、晓靖、Aply同学判断研究解决的。所以我对Redis的判断大多也从他们的讨论中听来,加上自己的一些猜测,并没有去仔细阅读Redis文档和阅读Redis代码。虽然我们最终都解决了问题,但本文中说描述的技术细节还是很有可能与事实相悖,请阅读的同学自行甄别。

我们将数据中心分为32个库,按玩家ID分开。不同的玩家之间数据是完全独立的。在设计时,我坚决反对了从一个单点访问数据中心的做法,坚持每个游戏服务器节点都要多每个数据仓库直接连接。因为在这里制造一个单点毫无必要。

根据我们事前对游戏数据量的估算,前期我们只需要把32个数据仓库部署到4台物理机上即可,每台机器上启动8个Redis进程。一开始我们使用64G内存的机器,后来增加到了96G内存。实测每个Redis服务会占到4~5G内存,看起来是绰绰有余的。

由于我们仅仅是从文档上了解的Redis数据落地机制,不清楚会踏上什么坑,为了保险起见,还配备了4台物理机做为从机,对主机进行数据同步备份。

Redis支持两种BGSAVE的策略,一种是快照方式,在发起落地指令时,fork出一个进程把整个内存dump到硬盘上;另一种唤作AOF方式,把所有对数据库的写操作记录下来。我们的游戏不适合用AOF方式,因为我们的写入操作实在的太频繁了,且数据量巨大。

经过一些简单的了解,我发现国内很多游戏开发者都不约而同的采用了mongodb,这是为什么呢?我的看法是这样的:

游戏的需求多变,很难在一开始就把数据结构设计清楚。而游戏领域的许多程序员的技术背景又和其他领域不同。在设计游戏服务器前,他们更多的是在设计游戏的客户端:画面、键盘鼠标交互、UI才是他们花精力最多的地方。对该怎么使用数据库没有太多了解。这个时候,出现了mongodb这样的NOSQL数据库。mongodb是基于文档的,不需要你设计数据表,和动态语言更容易结合。看起来很美好,你只需要把随便一个结构的数据对象往数据库里一塞,然后就祈祷数据库系统会为你搞定其它的事情。如果数据库干的不错,性能不够,那是数据库的责任,和我无关。看到那些评测数据又表明mongodb的性能非常棒,似乎没有什么可担心的了。

其实无论什么系统,在对性能有要求的环境下,完全当黑盒用都是不行的。

游戏更是如此。上篇我就谈过,我们绝对不可能把游戏里数据的变化全部扔到数据库中去做。传统数据库并非为游戏设计的。

比如,你把一群玩家的坐标同步到数据库,能够把具体某个玩家附近玩家列表查询出来么?mongodb倒是提供了geo类型,可以用near或within指令查询得到附近用户。可他能满足10Hz的更新频率么?

我们可以把玩家的buf公式一一送入数据库,然后修改一些属性值,就可以查询到通过buf运算得到的结果么?

这类问题有很多,即使你能找到方法让数据库为你工作,那么性能也是堪忧的。当我们能在特定的数据库服务内一一去解决她们,最终数据库就是一个游戏服务器了。

到2013年夏天,感觉应该开始动手做服务器部分了。晓靖在斗罗大陆的端游项目中积累了不少服务器开发的经验,也是除我之外,对skynet最为熟悉的人;如果这个试验项目只配备一个程序来开发服务器的话,没有更好的人选了。

从那个时候起,我们开始考虑服务器的结构,其中也包括了数据库的选型和构架。

skynet有自己的IO模型,如果要足够高效,最好是能用skynet提供的socket库自己写DB的driver。因为redis的协议最简洁,所以最先我只给skynet制作了redis的driver。而我们代理的游戏狂刃的开发方使用的是MongoDB,为了运营方便,我们的平台也使用它做了不少东西,我便制作给skynet制作了初步的mongodbdriver。到服务器开始开发时,我们有了两个选择。

去赌场参观过的同学应该都见过那种押大小的骰子游戏。庄家投掷三枚骰子,把骰盅盖住,玩家可以押大或小。开盅后,如果发现三个数字之和大于等于11就算大,小于等于10就算小。如果你猜对了,庄家就1赔1算给你筹码;否则输掉筹码。另外,还可以以不同赔率压数字,或压三个相同。

如果把这个游戏搬到网络上如何呢?(注意:网上赌博在很多国家是被禁止的,这里只做技术分析而已)

如何让玩家相信庄家没有作弊,真的产生了随机的骰子呢?

COC的地图有40x40格,边界还有两格不可以放建筑的格子。所以整个地图是44x44格的。但在做坐标判定时,基于格子是远远不够的。COC官方版本很可能是基于像素级的精度的,和本文所述方法不一致,所以本文仅提出一种可行的数据结构,

所谓类似COC这样的斜视角引擎,在英文社区中叫作IsometricTilesetEngine。它在早期计算资源匮乏的年代用的很多,后来内存不是问题了后,就很少有人用了。由于没有Z-Buffer,这种引擎的绘制次序其实很讲究。比如下图:

我们的第一个手游差不多做完了,预计在明年1月初推广,目前内测的情况非常不错,我们也可以考虑开始下一步在手游领域立新项目了。

上个项目做的太匆忙,今年4月份才开始。因为决定做一个2d游戏,我觉得在2d游戏引擎方面我有超过15年的经验,使用一个流行的开源引擎,比如大家都在用的Cocos2d-X还不如自己写。这样对引擎的可控性更强,可以方便的定制自己需要的功能,并在性能上做针对性的优化。手机设备的硬件性能远不如PC机,即使程序性能足够,我们也要考虑硬件的能耗,让电池用的更久一点,让设备不那么放烫。优化引擎也是游戏程序员的乐趣之一。

当然它还很不完整,有兴趣的同学可以跟踪这个项目,我和我的同事会逐步完善它。同时欢迎其他同学推送Pull-request。下面对ejoy2d做一个简单的介绍:

我们的游戏中需要对渲染字体做勾边处理,有种简单的方法是将字体多画几遍,向各个方向偏移一两个像素用黑色各画一遍,然后再用需要的颜色画一遍覆盖上去。这个方法的缺点是一个字就要画多次,影响渲染效率。

前几年有人发明了另一种方法,google一下SignedDistanceFieldFontRendering就可以找到大量的资料。大体原理是把字体数据预处理一遍,把每个像素离笔画的距离用灰度的形式记录在贴图上,然后写一个专门的shader来渲染字体。好处是字体可以缩放而不产生锯齿,也比较容易缺点边界做勾边处理。缺点是字模数据需要离线预处理。

但是,勾过边的字模信息中同时包含了轮廓信息和字模主体信息,看起来似乎很难用单通道记录整个字模数据了。这给染色也带来了麻烦。

汉字的显示,是基于3dapi的图形引擎必须处理的问题。和西方文字不同,汉字的字形很难全部放在一张贴图上,尤其是游戏中有大小不同的字体的需求更是如此。即使放下,也很浪费内存或显存。如果不想申请很大的贴图来存放汉字字形,图形引擎往往需要做动态字形贴图的处理。

我们的需求本质上是对一张贴图的区块进行管理。每个汉字都占据其中的一小块。当贴图填满时,最久没有用过的汉字块可以被淘汰掉,让新的汉字覆盖上去。同样的字体的最大高度是相同的,可以排列在一行,但宽度可以不同。横向排列时,少许的空洞的允许的。

我设计了如下接口:

最近在玩一款iOS游戏,叫做ClashofClans。这款游戏让我发现,手机/平板的网络游戏和传统网络游戏、网页游戏,除了UI方面,设计也是可以有很大的不同的。

COC开创了一种:在线创造内容,离线后供其他玩家娱乐的模式。这是以往的游戏所不具备的。

ps.在官方主页上,pixellight是基于OpenGL的,但实际上,它将渲染层剥离的很好。如果你取的是源代码,而不是下载的SDK的话,会发现它也支持了Direct3D。另,从2013年开始,这个项目将License改为了MIT,而不是之前的LGPL。对于商业游戏开发来说,GPL的确不是个很好的选择。

今天收到人民邮电出版的杨海玲同学寄来的几本书,首先感谢一下。看来短期内是没有那么多精力全部去读了,所以先随便翻翻感兴趣的章节。

在游戏人工智能编程中举了个实际的例子来说明这个问题:在一个FPS游戏中,NPC有多种武器可供选择,这些武器的威力、射程等有所差异;策划决定根据NPC和玩家的距离以及NPC武器弹药的余量,两个因素来考虑NPC当下应该选择哪一种武器对抗玩家。这个例子不错,不过我有另一个自己的例子:在MMO中,NPC在追击玩家时可能会考虑几个因素:离开他的出生点的距离,以及玩家的实力,或是自己的HP量等等。下面我用自己的例子来说明问题。

先甩开模糊逻辑是什么不谈,我们在做AI定制的时候会遇到怎样的很难决策的问题呢?

策划往往会定义规则:当NPC距离出生点很远时,他们停止攻击玩家并返回出生点。

这里有一个问题,即使是最新手的策划也看得出来,这个规则描述是不严谨的。到底什么叫“很远”。所以通常,需要加一个定义,追击半径,变成“当NPC距离出生点超过追击半径时,他们停止攻击玩家并返回出生点”。然后,在策划表格里估计就会多出一个表项:追击半径40米之内的东西。

顺便吐槽:把设计这个规则(追击条件),和填写这个数字(40米追击半径)的工作分开,分别称做系统策划和数值策划,我怎么看都是件极不靠谱的事情。

在网络游戏中倒卖货物是一大乐趣(如果经济系统做的不坏的话)。我见到许多wow玩家以此为乐,在国产游戏中,以梦幻西游为首,开店摆摊也让人乐此不疲。我最近对此有一些想法,和我们公司的策划交流以后,发现几句话很难说清楚。

大部分人第一反应都是增强版的拍卖场,比如增加求购系统,或是更方便的拍卖寄售系统,各种其它的拍卖场增强。

对于稀有品,我认为现在wow的拍卖场已经可以解决大部分问题,是不需要替换的。但一般商品,我认为应以增加流通,方便需求类玩家为主。最简单的方案是让系统根据商品流通速度来自动调节价格,系统统一销售和收购。但我们也知道光靠这种计划经济是很难得到一个合理的市场环境的,也少了许多玩家参与的乐趣。

所以我才有了下面这些想法:

这个技术可以给玩家在MMO中带来许多以往在单机游戏中才能体验的融入感。在wow之前,许多游戏策划都曾经想过让玩家可以通过完成剧情改变游戏的环境,但苦于找不到好的表现方式。这或许是因为大部分游戏策划都在以现实世界为参考去考虑技术能够实现的可能性,而在这个微创新年代,没有摹本可以参考就没有了思路。但我有些奇怪的是,在wow把相位技术展现给世界已经超过4年了,为啥山寨大国还没有全力跟进呢?

莫非是因为这里面还真有什么技术难点?

我们的大部分设计是围绕单个进程进行的,所有的数据在进程内都可以方便的共享。只需要这些数据结构是线程安全的即可。

但最终,我们不会在单台机器上运营整个游戏服务器,所以还是要考虑玩家在不同物理机器间移动的问题。

虽然我们还没有开始进行跨机方面的开发,但是不少服务已经要开始考虑这个问题了。目前、每个玩家接入游戏服务器并认证完毕后,都会有一个lua虚拟机伴随他。也就是我称之为agent的东西。

agent和场景服务map间会有高频率的互动,所以,我们必须要求agent和玩家所在map在同一个进程内。当玩家跳转到不在同一进程内的map上时,需要把agent迁移到对应的进程内。

迁移agent并不算难做:只需要把agent的数据持久化,然后在新的进程内启动起来即可。难点在于,新的agent的通讯handle变化了。为了性能考虑,我并不想实现一套跨机器的唯一id系统。那么,就需要解决一个问题:如果handle发生变化,如何通知持有这个handle的服务模块。

这项工作,终于如期完成了。

半个多月前在白板上留下的工作计划还没擦掉。我列出了12点需要改进或重写的地方,考虑到内容较多,又去掉了3项。在大家的通力合作下,完成的很顺利。

昨天的测试结果基本让人满意,在同一台机器上,200个机器人的混战CPU占用率平均仅130%左右,而机器人client边数据包延迟只有1秒,完全可以实用。这离我们的设计目标(500人同战场流畅战斗)还有一些距离,但考虑到今年新配置两块IntelXeonE5-2620@2.00GHz的话,按其性能指标,应当再有至少一倍的性能提升。

最近在接手改进我们的怪物AI的底层模块。这部分策划是希望可以由他们来直接配置而不是提交需求文档让程序实现。

我们的前一个版本有较大的性能问题,光是空跑一些场景,没有玩家的情况下CPU符合都相当之高了。我觉得是允许策划的配置项过于细节,以及底层模块实现的方式不对,导致了大量无用的lua脚本空转导致的。

目前的AI脚本是每个挂在独立的NPC上,利用心跳(大约是0.5s一次),定期让NPC去思考现在应该去干些什么。这些干些什么的具体逻辑在很细节的层面都是要去运行一个策划配置的脚本在一个沙盒中运行的。在实际监测中,一个心跳的一段AI脚本居然会跑上万行lua代码,想不慢都难啊。

我决定看看星际争霸2的地图编辑器是如何工作的。

我没有玩过魔兽争霸3的编辑器,也没有玩过星际2的。但似乎,它们可以让用户自定义出各种形式的游戏来,并不局限在RTS一种类型中。我相信这个发展了超过十年的自定义地图的编辑模式,一定有很多成熟的业务抽象。

我只好自己把弄编辑器,在自己的使用中,推想暴雪解决问题的思路。短短两天的研究肯定会有许多错误和遗漏,也希望借公开的blog,有行家看到能够赐教。

我们的项目的第二里程碑在中秋前结束了。这次耗时三个月的开发周期,堆砌了大量的游戏特性,大部分任务都是在追赶进度中完成的,导致最终有很多问题。

所以,节后我们决定调整一个月,专门用来重构不满意的代码以及修理bug。好在这次我们的代码总量并不算太大。服务器部分,所有已有的C底层之外的代码,全部Lua代码,总量在五万行左右。这是一个非常可控的规模。即使全部重写,在已有的开发经验基础上,都不是什么接受不了的事情。所以我对改进这些代码很有信心。

有部分代码有较严重的性能问题,节前我给底层增加了一些统计和检测模块,发现部分逻辑模块,单条消息的处理居然超过了一万条Lua指令。比较严重的是AI和怪物管理模块,以及战斗的buff和属性计算。

这几个问题,接下来我会一个个着手解决。这两天在优化属性计算的部分。

最近在试图优化游戏服务器,提升承载能力。

以前这方面没有做到,是因为:从底层看来,仅仅是一个个的消息包的流动。而一条完整的协议响应却是有多个包进出的。当处理一个服务请求的过程中,向外做了一次远程调用,那么当前服务体就被切出了,直到回应的session响应才继续。在新的设计中,sessionid被暴露给底层模块可见,这就使监控变得可行。

另外,由于是完全用C语言编写而不再使用Erlang,我们可以使用成熟的profile工具。gprof使用起来非常方便,容易对性能热点定位。

到昨天,终于把全部代码基本移植到了新框架下,正常启动了起来。这项工作算是搞一段落。庆幸的是,我这个月的工作,并没有影响到其他人对游戏逻辑的开发。只是我单方面的同步不断新增的逻辑逻辑代码。

Skynet的重写,实际上在半个月前就已经完成。那时,已经可以用新的服务器承载原有的独立的用户认证系统了。那么后半个月的这些琐碎工作,其实都是在移植那些游戏逻辑代码。

在Skynet原始设计的时候,api是比较简洁的,原则上讲,是可以透明替换。但实际上,在使用中,增加了许多阴暗角落。一些接口层的小变动,增加的隐式特性,使得并不能百分百兼容。另外,原来的一些通讯协议和约定不算太合理,在重新制作时,我换掉了部分的方案,但需要编写一个兼容的链路层。

比如:以前,我们把通过tcp接入的client和server内部同进程内的服务等同处理。认为它们都是通过相同的二进制数据包协议通讯。但是,同进程内的服务间通讯明显是可以被优化的,他们可以通过C结构而不是被编码过的数据包交换信息,并可以做到由发起请求方分配内存,接受方释放内存,减少无谓的数据复制。在老的版本中,强行把两者统一了起来,失去了许多优化空间。在新版本里,我增加了较少的约定,修改了一点接口,就大幅度提升了进程内服务间信息交换的效率。

在这个过程中,可以剔除许多冗余代码,去掉一些我们曾经以为会用到,到实际废弃的模块。彻底解决一些历史变更引起的问题。过程很痛苦,但很值得。新写的代码各种类型检查更严格,就此发现了老的逻辑层代码中许多隐藏的bug。一些原有用erlang实现的模块,重新用lua实现了一遍,混合太多语言做开发,一些很疼的地方,经历过的人自然清楚。以后如非必要,尽量不用lua之外的语言往这个系统里增加组件了。

btw,新系统还没有经过压力测试。一些优化工作也没有展开。但初步看起来,还是卓有成效的。至少,改进了数据共享模块,以及提出许多冗余后,整个系统的内存占用量下降到原来的1/5不到。CPU占用率也有大幅度的下降。当然,这几乎不关C还是Erlang做开发的事,重点得益于经过半年的需求总结,以及我梳理了大部分模块后做的整体改进。

因为这个项目是继承的Erlang老版本的设计来重新用C编写的。再一些接口定义上也存在一些历史遗留问题.我需要尽量兼容老版本,这样才能把上层代码较容易的迁移过来。

最近的开发已经涉及具体业务流程了,搬迁了不少老代码过来。我不想污染放在外面的开源版本。所以在开发机上同时维护了两个分支,同时对应到github的公开仓库,以及我们项目的开发仓库。

btw,我想把自己的开发机上一个分支版本对应到办公室仓库的master分支,遇到了许多麻烦。应该是我对git的工作流不熟悉导致的。

先谈谈集群的设计。

最终,我们希望整个skynet系统可以部署到多台物理机上。这样,单进程的skynet节点是不够满足需求的。我希望skynet单节点是围绕单进程运作的,这样服务间才可以以接近零成本的交换数据。这样,进程和进程间(通常部署到不同的物理机上)通讯就做成一个比较外围的设置就好了。

为了定位方便,我希望整个系统里,所有服务节点都有唯一id。那么最简单的方案就是限制有限的机器数量、同时设置中心服务器来协调。我用32bit的id来标识skynet上的服务节点。其中高8位是机器标识,低24位是同一台机器上的服务节点id。我们用简单的判断算法就可以知道一个id是远程id还是本地id(只需要比较高8位就可以了)。

我设计了一台master中心服务器用来同步机器信息。把每个skynet进程上用于和其他机器通讯的部件称为Harbor。每个skynet进程有一个harborid为1到255(保留0给系统内部用)。在每个skynet进程启动时,向master机器汇报自己的harborid。一旦冲突,则禁止连入。

master服务其实就是一个简单的内存key-value数据库。数字key对应的value正是harbor的通讯地址。另外,支持了拥有全局名字的服务,也依靠master机器同步。比如,你可以从某台skynet节点注册一个叫DATABASE的服务节点,它只要将DATABASE和节点id的对应关系通知master机器,就可以依靠master机器同步给所有注册入网络的skynet节点。

master做的事情很简单,其实就是回应名字的查询,以及在更新名字后,同步给网络中所有的机器。

skynet节点,通过master,认识网络中所有其它skynet节点。它们相互一一建立单向通讯通道。也就是说,如果一共有100个skynet节点,在它们启动完毕后,会建立起1万条通讯通道。

最近工作展开后,我们一共有10名程序员在目前的项目上工作。我暂时没有和其他人有依赖关系的工作,最近一周在改进以前做的一些东西,在不修改接口的前提下,争取提供更高的性能,以及完成一些之前没完成的功能,为以后的扩展做准备。

最近值得一提的东西是:关于我们的共享储存的数据结构。

后来,我们采用了Erlang做底层的框架。在同一台机器上,只有一个系统进程。所以,这个东西可以不必实现的这么复杂。我抽了三天实现,重新实现了一个。这次不考虑跨进程的问题,只在同一进程的不同线程中,让独立的LuaState可以访问同一份结构化数据。至于结构化数据支持到怎样的数据类型,我认为和Lua原有的table类型大致一致就可以了。

起先,我想用读写锁来解决这个问题。方案想好了,一直没有实现。只是把读写锁的基本功能实现了。

这几天这个问题被重提出来。因为,前段我们都采用了鸵鸟政策,当问题不存在(事实上我们也没有发现实际中出现可观测到的问题)。

反正探讨了好几个解决方案,一开始都是围绕怎么加锁,锁的粒度有多大来展开的。甚至,我们把其中的一种方案都实现出来了,并写了压力测试程序测试。不过,这些方案都不太令人满意。大家担心锁的开销,以及逻辑代码编写者所需求关心的问题太多,导致有死锁的可能性。

昨天差一点决定用一个地图锁来解决这个问题,就是用牺牲同一个地图进程上,玩家间并行的可能性为代价的。这个方案也不无不可。但昨晚躺在床上一直睡不安稳。因为这样做,就失去了一开始我期望用并行方案来设计游戏服务器的初衷。如果这样,还不如全部退化到单地图单进程来编写程序。那么一定有方法是可以避开锁以及避免让写逻辑的程序员去关心数据共享的读写冲突问题的。

我们项目的第一个里程碑杯已经比较顺利的完成了。主要是完成最基本的用户注册登陆,场景漫游,走跑跳,远程进程法术等技能攻击,怪物及npc的简单行为,等等。尝试了几十个玩家与上百个npc的混战,还比较流畅。

中间小作休整,然后开始做第二里程碑的计划安排。这次预计要三个月,完成几乎所有的MMO必须的游戏功能了。

其中一个大的任务就是要完成背包、物品、装备、货币、掉落、交易这些相互关联的模块。

下面简单记录一下背包系统的设计实现思路:

物品交易和掉落系统不是一期里程碑的内容,手头事情不多,就还是先做设计了。

这次重新设计实现,做了少量的改动。主要是一些简化。设计原则是,交易服务器独立,简洁,健壮,容易回溯交易历史做数据恢复和分析。

交易服务,我认为是一个后备系统。在数据不出问题时,不大用理会它的存在。它仅仅是用来保证物品不被复制,确定每件物品可以有唯一的所有人。在出现争议的时候,可以作为仲裁机构。

交易认证服务,其实只关心一件事情。即,某个物品id归属于谁。它不关心物品id代表的是什么含义。当然,可以有另一个数据库记录这个关系。在物品的所有者改变时,交易服务来保证其原子性。除了玩家间的转换,还有大量的和系统间的交换。掉落物品其实是从系统改为玩家所有,而玩家销毁物品则是把所有者改为系统所有。

最近我在协助解决NPC(包括地图上的怪物)的行为控制以及AI的问题。

今天展开来谈谈我的方案。

我想把怪物的移动行为独立出来做,以减少AI的压力。也就是说,地图上所有的怪,在设定的时候,都可以设定他们的巡逻路径,或是仅仅站立不动。我希望在没有外力干扰的时候,处理这些行为对系统压力最小。

我不想让怪在没有任何玩家看见的时候就让它静止不动,因为这样可能会增加实现的复杂性,并在怪物行为较为复杂时,无法贯彻策划的意图。

最好的方法还是把之隔离,使其对系统的负荷受控。同时也可以通过分离,减小实现的复杂性。

这个子系统是这样的:

策划更喜欢通过excel表格来表达他心中的数值关系,而不是通过代码语言。我需要找到另一种更切合实际的方案来将策划的想法转换为可以运行的代码。

研究了一下策划历史项目上的excel表格后,我归纳了一下其需求。我们需要把问题分步解决,excel表格到程序可以利用的数据结构是一项工作,而从表达了数据之间联系的结构到代码又是另一项工作。

对于数值运算,有怎样的需求呢?

许多表格其实就是在不同的位置表达了这种公式推导关系:一个属性等价于另一些属性组成的表达式。而在运行时,根据人物的一些基础属性,可以通过一系列的公式推导,得到最终的一系列属性值。满足这个需求并不难,读出表格里的对应项,做简单的解析就可以了。(这里涉及到另一个问题,表格里的对应项在哪里,今天暂且不谈)

另一种表格称为查表。其实就是表达一种映射关系。如下表:

蜗牛同学打算改进skynet增加异步IO的支持。

我今天在考虑现有的API时候,对比原有的timer接口和打算新增加的异步IO接口,发现它们其实是同一类东西。即,都是一个异步事件。由客户准备好一类请求,绑定一个sessionid。当这个事件发生后,skynet将这个sessionid推送回来,通知这个事件已经发生。

在用户编写的代码的执行序上,异步IO和RPC调用一样,虽然底层通过消息驱动回调机制转了一大圈,但主干上的逻辑执行次序是连续的。

受历史影响,我之前在封装Timer的时候,受到历史经验的影响,简单的做了个lua内callback的封装。今天仔细考虑后发现,我们整个系统不应该存在任何显式的回调机制。正确的接口应该保持和异步IO一致:

这几天我的工作是设计未来游戏服务器的热更新系统。

我最近对之前所用到的一些方案,如修改luamodule的加载策略,增加一些间接层,来达到热更新代码的系统设计做了一些思考。感觉在处理热更新这个问题时,还不够严谨。经过两天的思考,我按我的构思实现了新系统的雏形。

在函数式编程语言中,热更新通常比较容易实现。erlang,lisp都把热升级做为核心特性之一。函数副作用越小的语言,越容易做热升级:你只需要简单的把新写的函数替换回去就好了。

对于纯粹的请求回应式的服务,做热升级也相对容易。比如大多数webserver,采用REST风格的协议设计的网站,重启服务器,通常对用户都是透明的。中间状态保存在数据库中,改写了代码,把旧的服务停掉,启动新版的服务,很少有出错的。

我想把我们的系统分步设计实现,先实现最简单的热更新功能,再逐步完善。如果一开始就指望系统的任何一个部分都可以不停机更新掉老版本的代码是不太现实的,考虑的太多,容易使系统变的过于复杂不可靠。

那么第一步,我们要实现的就仅仅是游戏逻辑有关的代码热更新。而不考虑更新服务器框架有关的模块。我想把这部分称为热修复,而不是热升级。主要用来解决运行时,不停机去修复一些bug;而不是在不停机的状态下,更新新版本。在这个阶段,也不考虑可以更新服务间的通讯协议,只考虑更处理这些协议的代码逻辑。

做以上限制后,热更新系统实现起来就非常简单了。

总结一下最近的工作,修改bug以及调整以前设计的细节是主要的。因为往往只有在实现时,需求和问题才会暴露出来。

下面谈谈对于下面工作的一些思路。主要是场景服务以及一些同步策略。

今天例会,梳理了工作计划后,发现要开始实现AOI模块了。

所谓AOI(AreaOfInterest),大致有两个用途。

一则是解决NPC的AI事件触发问题。游戏场景中有众多的NPC,比PC大致要多一个数量级。NPC的AI触发条件往往是和其它NPC或PC距离接近。如果没有AOI模块,每个NPC都需要遍历场景中其它对象,判断与之距离。这个检索量是非常巨大的(复杂度O(N*N))。一般我们会设计一个AOI模块,统一处理,并优化比较次数,当两个对象距离接近时,以消息的形式通知它们。

二则用于减少向PC发送的同步消息数量。把离PC较远的物体状态变化的消息过滤掉。PC身上可以带一个附近对象列表,由AOI消息来增减这个列表的内容。

AOI的传统实现方法大致有三种:

第一,也是最苯的方案。直接定期比较所有对象间的关系,发现能够触发AOI事件就发送消息。这种方案实现起来相当简洁,几乎不可能有bug,可以用来验证服务协议的正确性。在场景中对象不对的情况下其实也是不错的一个方案。如果我们独立出来的话,利用一个单独的核,其实可以定期处理相当大的对象数量。

第三,使用十字链表(3d空间则再增加一个链表维度)保存一系列线段,当线段移动时触发AOI事件。算法不展开解释,这个用的很多应该搜的到。优点是可以混用于不同半径的AOI区域。

首先是最重要的协议设计。以前我认为,AOI服务器应该支持对象的增删,可以在对象进入对方的AOI区域以及退出AOI区域时发出消息。

最近两周,陆续有些新同事到岗,或即将到岗。所以我不想过多的再某个需要实现的技术细节上沉浸进去了。至少要等计划的人员齐备,大家都有事情做了以后,个人再好领一块具体事情做。所以属于我个人的代码不多。我主要也就是维护一下前面我自己实现的模块,以及把之前写的一些代码交接给下面具体负责的同学。

至少要达到wow里的水准吧,在网络不稳定,延迟在200到2000ms波动时,玩家还要玩的比较舒服。

项目的进展比较缓慢、主要是解决一些琐碎的技术问题,客户端的比较多,服务器这边就是节前的一些bug修改和功能完善。大部分工作都不是我自己在做。由于感到人手不足,小规模私下的做了一点点招聘工作。也算物色到一两个同学可以过来一起干的。好久没做招聘工作了,都不知道怎么开始谈。唉,我们这里条件不算好,要求还多,都不好意思开口。

可写的东西其实也不少。今天挑一点来记录一下。

我的规划大约是这样的:

核心部分仅仅实现了结构化数据在内存中的表达。但储存在内存中的数据还不能直接使用。CAPI虽然实现好了,但性能比较低。我实现的是无锁的数据结构,采用单向链表保存。检索一项属性值都是O(n)的复杂度。这显然是不能在项目中直接使用的。为了提供性能,需要再做一层cache。在Lua虚拟机中,用hash表映射。使得读写数据的速度都降低到O(1)。因为我自己对Lua比较熟悉,所以这步Lua的薄封装还是我自己完成的。实测下来,和原生的Lua表访问差距不到一个数量级(3,4倍左右),是可以接受的范围。比常规IPC通讯要快的多,也没有异步通讯的负担。以后编写逻辑代码的时候稍微注意一点就好了。

需要额外开发的工作是,要定义一个数据描述的小语言。类似C语言的结构定义。在数据储存中,我没有实现无格式信息的字典类型。map的key都是在结构定义中预先定义好的,内存中存放的是编号。这一是因为实现简单,而是可以实现成无锁的数据结构。再就是数据结构也能严谨一些,减少typo(可以立刻检查到)。

今天很早就起床了,以至于到了办公室还不到11点。中饭前有一个多小时可以做各种杂事。

我把周末做的工作和蜗牛同步了一下信息,然后得到了几个新需求。主要就是还是需要在协议定义中加入protobuf中等价于service的东西。思索了一下,觉得有必要,就花了一个小时把特性加上。CBindingAPI方面还有一点疏漏的地方。大概是源于基于Erlang框架下的一些小困难。略微修改了下C接口协议就OK了。然后很happy的去食堂吃饭。

然后我暂时就可以转向Client方面的一些需求分析以及解决了。

以前见过许多项目,有的设计出繁杂的excel表格式,然后export给程序用;有的干脆让策划写程序代码;甚至有的做一堆漂亮UI的公式编辑器。我想最快也最方便达到效果的,莫过于设计一个最小需求集合的DSL,让策划认同其语法,然后使用DSL来编辑了。

开始这个话题前,离上篇开发笔记已经有一周多了。我是打算一直把开发笔记写下去的,而开发过程中一定不会一帆风顺,各种技术的抉择,放弃,都可能有反复。公开记录这个历程,即是对思路的持久化,又是一种自我督促。不轻易陷入到技术细节中而丢失了产品开发进度。而且有一天,当我们的项目完成了后,我可以对所有人说,看,我们的东西就是这样一步步做出来的。每个点滴都凝聚了叫得上名字的开发人员这么多个月的心血。

技术方案的争议在我们几个人内部是很激烈的。让自己的想法说服每个人是很困难的。有下面这个话题,是源于我们未来的服务器的数据流到底是怎样的。

我希望数据和逻辑可以分离,有物理上独立的点可以存取数据。并且有单独的agent实体为每个外部连接服务。这使得进程间通讯的代价变得很频繁。对于一个及时战斗的游戏,我们又希望对象实体之间的交互速度足够快。所以对于这个看似挺漂亮的方案,可能面临实现出来性能不达要求的结果。这也是争议的焦点之一。

核心问题在于,每个PC(玩家)以及有可能的话也包括NPC相互在不同的实体中(我没有有进程,因为不想被理解成OS的进程),他们在互动时,逻辑代码会读写别的对象的数据。最终有一个实体来保有和维护一个对象的所有数据,它提供一个RPC接口来操控数据固然是必须的。因为整个虚拟世界会搭建在多台物理机上,所以RPC是唯一的途径。这里可以理解成,每个实体是一个数据库,保存了实体的所有数据,开放一个RPC接口让外部来读写内部的这些数据。

但是,在高频的热点数据交互时,无论怎么优化协议和实现,可能都很难把性能提升到需要的水平。至少很难达到让这些数据都在一个进程中处理的性能。

这样,除了RPC接口,我希望再提供一个更直接的api采用共享状态的方式来操控数据。如果我们认为两个实体的数据交互很频繁,就可以想办法把这两个实体的运行流程迁移到同一台物理机上,让同时处理这两个对象的进程可以同时用共享内存的方式读写两者的数据,性能可以做到理论上的上限。

ok,这就涉及到了,如何让一块带结构的数据被多个进程共享访问的问题。结构化是其中的难点。

方案如下:

这周我开始做场景模块。因为所有PC在server端采用独立的agent的方式工作。他们分离开,就需要有一个模块来沟通他们。在一期目标中,就是一个简单的场景服务。用来同步每个agent看到的世界。

大批量的数据同步对性能需求比较高。因为在N个角色场景中,同步量是N*N的。虽说,先设计接口,实现的优化在第二步。但是接口设计又决定了实现可以怎样优化,所以需要比较谨慎。

比如,同步接口和异步接口就是不同的。

对于agent服务,是一个典型的包驱动模式。无论我们用Erlang框架,还是用ZeroMQ自己搭建的框架,agent都会不会有太多的不同。它将利用一个单一的输入点获取输入,根据这些输入产生输出。这个输入点是由框架提供的。框架把Agent感兴趣的包发给它。

我们会有许多agent,在没有它感兴趣的包到来时,agent处于挂起状态(而不是轮询输入点)。这样做可以大量节省cpu资源。怎样做的高效,就是框架的责任了,暂时我们不展开讨论框架的设计。

下面来看,一旦数据包进入agent应该怎样处理。

我一直对国内MMORPG制作把设计人员分为文案策划、系统策划、数值策划不以为然。文案拆分出去做倒还说得过去,这所谓系统策划和数值策划的拆分简直就是莫名其妙了。现代电子游戏从桌面游戏一路发展过来,怎样让玩家享受规则,一直是一个整体。如果一个人来设计一个游戏,那么脑子里必然要逐步形成这个游戏做出来是什么样子的,然后细化里面的细节,玩家在他设计的规则下怎么进行游戏。所谓数值设计,是这些细节里重要的一部分。

很难想像,一个设计人员来想游戏的玩法,然后说细节我不管了,有另一个人来负责就好了。然后再有一个人专心于填写excel表格,用加加减减模拟一些玩家行为。我看,国内的MMORPG产业之所以有向这样发展的趋势,更是因为提起MMORPG,游戏整体已经确定下来了,每出一个新产品,必然先找一个已有产品做原型的缘故。即,架子其实已经在那里了,只需要不同角度的修饰工罢了。

如果类比软件开发,我很难想像有一堆人专心做各个模块的接口定义(系统策划),另一堆人专心做接口背后的代码实现(数值策划)。据说这种软件开发模式倒真的存在,只是我没经历过而已。

这是我第一次实战使用Redis,没有什么经验。不过类似的设施几年前自己实现过,区别不大。经过这几年,有了Redis这个开源项目,就不需要重造轮子了。但其模式还是比较熟悉的。也就是说,是按我历史经验来使用Redis。

一期项目需要比较简单,不打算把数据拆分到不同的数据服务器上。但为日后的拆分需求做好设计准备。以后有需要,可以按Key的前缀把数据分到不同的位置。例如,account信息是最可能独立出去的,因为它和具体游戏无关。

折腾了好久,终于可以开始正式项目开发了。

之前的这段日子,我们陷落在公司的股权分配问题中,纠结于到底需要几个人到位才启动;更是反复讨论,到底应该做个怎样的游戏。林林总总,终于,在已经到位的几位同学的摩拳擦掌中,叮当决定自己挂帅开始干了。

就这么不到十个人,空旷的办公室,跟我们起先想像的情况不太一样。尤其是主策划还没有落定。我说,叮当,你好歹也是一资深游戏玩家,带了这么多年的游戏部,跟了这么多成功的项目,没吃过猪肉总见过猪跑吧,我就不相信你干着会比别人差。若不是我必须盯着程序实现这块,我都想自己做主策划了。不过有你干,我放心。

主策划的位置,咱们可以先空着,前期工作不能延。产品经理代理一下游戏主策划的位置也是创业公司必须的;正如我这挂牌的CTO,除了负责系统架构以外,也得兼个程序员做实现嘛。

经过两天对项目计划表的讨论后,我们今天就算正式开工了。

游戏是怎样的?半保密,不在这里多写,怕大家骂。再说了,这个东西现在说是怎样的,一两年后肯定有变化,多说无益,多解释无益。简单说呢,就是一个战斗系统类似魔兽世界,但系统核心玩法差异很大,更为轻松,更偏重PvP的MMORPG。为什么是这样一个东西,不想解释,反正不是拍脑袋想出来的。

既然是开发笔记,就写写现在在做些啥,打算怎样做下去。

由于在筹备做新的项目,所以关于网络游戏方面的讨论,在朋友圈子中多了许多。前几天和投资人吃饭,说起前几年盛大提出所谓免费游戏的策略转型,问道有没有可能在未来几年出现新的收费模式。从这个话题引开,饭桌上我跟叮当讨论了很久。

一旦换成了道具收费,玩家投入的金钱则不那么容易被估算了。这带来了更多的可变因素。游戏的生命期很可能被大幅度压缩而不被设计人员所知。杀鸡取卵这种事情不那么容易被意识到。

MMORPG中,场景信息同步是很基础而必不可少的服务。这部分很值得抽象出来,专门做成一个通用的服务程序。

此服务无非提供的是,向有需求的对象,同步场景中每个实体的状态信息。那么,我们分解需求,可以看到两点,一是提交状态,二是同步状态。

玩家、NPC、场景中的可变物品,其实都有可改变的状态。比如对象的位置坐标是最常见的状态。其它的状态还有玩家或NPC做的动作,玩家离线,上线,等等。

既然MMORPG都有千篇一律同质化的趋势,好歹我们技术人员也应该总结出点东西来,新项目开发可以用现成的模式。

一般来说,MMORPG服务器要解决的问题无非是,同步玩家的位置,状态,把这些信息广播出去(细分的话,有非战斗环境和战斗环境);需要建立一个聊天服务,供玩家文字交流;有一个信息发布渠道;有任务NPC和玩家一对一交流;玩家调整自己的装备(也可以看成是和一特定NPC交流)。

以上,我们可以看到几个基本需求是可以正交分解出来,并有不同的实现方式的。

同步玩家的状态,基本上是由玩家自己不断提交自己的最新状态,然后由服务器把这些信息广播给其他人。

聊天服务比较成熟,基本上就是玩家订阅聊天频道,并按权限向频道内发布信息。

信息发布可以看成是一个特殊的聊天频道。

任务NPC或是整理自己装备,都类似于向特定节点做请求等待回复。

今天,公司的同事在popo群中讨论svn管理美术资源的问题。当资源量大了以后,工作起来效率特别低。因为svn储存的是压缩过的文件diff,对于大文件,计算最终结果的时候比较消耗cpu。

我认为git会好一些,但是git不满足权限管理的需要。不过更换同质工具并没有解决本质问题。就这个,我想了一下。或者应该换个思路,从工具入手,可能会更适合3d游戏的开发。当然,前提是,要有足够的引擎开发和改造能力。对现有工具做适量改进。

我希望从模型编辑器入手。这个工具通常是资源处理环节的最底层,直接面向美术人员的生产工具(maxmaya等)。资源在这个工具中被转换到engine可用的状态,然后在其它开发工具中被加工。

若论熟悉,其实游戏服务器的架构和实现,我这些年考虑的更多,也做的更多。但这个东西不适合做成开源项目。因为受众太小(不是每个人都有机会和意愿去架设网络游戏服务器的),而且它不能单独运行看到结果。没有玩家的游戏服务器是没有意义的。

3dengine这些年也有在开发。但是公司不可能允许开源已有项目。如果想玩儿,从头做是唯一选择。而且重新开始有更多的乐趣,再造轮子原本就是程序员们的欢乐源泉之一。如果自娱是唯一目的的话,就不必过多考虑商业引擎必须考虑的问题。比如兼容更多的底层3DAPI,做全平台兼容,支持低端硬件等等。甚至于还要去考虑日后防外挂等无聊的问题。

今天突然想到的,先记下来。

想法是这样的:

当多核解决了CPU运算能力问题,当64bit系统解决了内存不足问题,IO问题依然让人困扰。

梦幻西游的服务器从更早的产品延续,已经跑了10年了。当初只求快速把项目做出来,用最简单的方法做出来,保证稳定可以用,自然遗留了无数问题。逻辑脚本中充斥随手可见的磁盘操作。

仔细分析了问题后,我发现,系统利用磁盘其实是有两种不同的用途。

一是备份需求,定期需要把数据持久化下来。因为服务器数量很多,硬件故障率几乎是每周一到两次。所以必须保证定期(不长于半小时)的数据备份。避免硬件故障的意外导致长期回档。

二是数据交换的需求。由于游戏逻辑写的很随意,以快速实现的新功能为主。许多脚本中随便读写文件,提供给逻辑需要来使用。这部分甚至许多是阻塞的。整个游戏逻辑进程串行执行逻辑,任何点的阻塞都会导致服务阻塞。这有很大的历史原因在里面,是不可能彻底修改其结构的。

根据系统调用的监测,我们发现,在定期写盘的高峰期段,逻辑进程中偶尔的读文件操作API可能用长达1到2秒读一个较小的文件。据我理解,导致这个现象的产生多是因为硬盘设备上的操作队列被阻塞。有大量未完成的磁盘IO工作在进行,导致系统延迟。

理论上,所以真正写到磁盘上的操作都应该被安排到优先级很低,保证所有的读操作可以以最快的速度完成。这样才能让反映速度变快。即,写盘操作,应该先刷新内存cache,然后把真正的写操作推迟到IO队列尾上。中间有任何的读操作都应该允许插队完成。由于我们的逻辑进程只有一个线程在跑逻辑,而所有的读文件操作都只发生在这个线程里。(其它工作进程都无读操作)整个系统对磁盘的读操作本身是不会竞争。而写操作则几乎是允许无限推迟的。

可惜,我们很难在os的底层控制IO操作队列的细节。

今天,想了一个变通的方案来解决这个问题:

在需要并行化处理数据的时候,采用消息队列通讯的方式来协作,比采用共享状态的方式要好的多。Erlang,Go都使用这一手段来让并行任务之间协同工作。

ZeroMQ并不是一个对socket的封装,不能用它去实现已有的网络协议。它有自己的模式,不同于更底层的点对点通讯模式。它有比tcp协议更高一级的协议。(当然ZeroMQ不一定基于TCP协议,它也可以用于进程间和进程内通讯。)它改变了通讯都基于一对一的连接这个假设。

ZeroMQ把通讯的需求看成四类。其中一类是一对一结对通讯,用来支持传统的TCPsocket模型,但并不推荐使用。常用的通讯模式只有三类。

继续前几天的话题。做梦幻西游服务器优化的事情。以往的代码,定期存盘的工作分两个步骤,把VM里的动态数据序列化,然后把序列化后的数据写盘。这两个步骤,序列化工作并没有独立在单独线程/进程里做,而是放在主线程的。IO部分则在一个独立进程中。

序列化任务是个繁琐的过程。非常耗时(相对于MMORPG这个需要对用户请求快速反应的环境)。当玩家同时在线人数升高时,一个简便的优化方法是把整个序列化任务分步完成,分摊到多个心跳内。这里虽然有一些数据一致性问题,但也有不同的手段解决。

但是,在线人数达到一定后,序列化过程依然会对系统性能造成较大影响。在做定期存盘时,玩家的输入反应速度明显变大。表现得是游戏服务器周期性的卡。为了缓解这一点,我希望改造系统,把序列化任务分离到独立进程去做。

方法倒是很简单,在定期存盘一刻,调用fork,然后在子进程中慢慢的做序列化工作。(可以考虑使用nice)做完后,再把数据交到IO进程写盘。不过鉴于我们前期设计的问题,具体实现中,我需要通过共享内存把序列化结果交还父进程,由父进程送去IO进程。

因为fork会产生一个内存快照,所以甚至没有数据一致性问题。这应该是一个网络游戏用到的常见模式。

可问题就出在于,经过历史变迁,我们的服务器已经使用了多线程,这使得fork子进程的做法变的不那么可靠,需要自己推敲一下。

网络游戏若要有支持一个稳固的经济系统,服务器底层必须有一个可靠的数据服务。要设计出精简的数据协议可不容易。它需要保证在发生异常(可能是硬件异常,也可能是软件异常)时,不会出现物品/货币丢失,复制的问题。

使用带事务的数据库是一个不错的选择,但对程序员的要求较高。程序员需要仔细考虑每个数据操作如何编写事务才是安全的,还需要考虑数据操作的异步性。这对需求变化迅速,量又比较大的游戏,做的好真的是很困难。

我思考了很久,几经易稿,大约得到了这么一个东西:

但是我们应该提供一个强的校验系统,所有的虚拟物品发放、转移,都应该经过这个校验系统。一切操作都需要经过事后的核对。由此系统来修正数据异常。

在历史工程上修补是件麻烦的事情。

IO负荷过重最终怎样影响到游戏服务的性能,这个暂时不过于深入探讨。我这两天主要是分析以有的系统结构,并想一下改进方案。

意外的了解到,现在磁盘IO性能居然成了梦幻西游服务器的瓶颈。而不是CPU或是网络带宽。据我所知,梦幻西游的服务器数据储存是这样做的:

主游戏进程不负责储存,一切都在内存中。所有玩家的数据就是内存数据结构。只是在玩家登陆的时候去读取一下本地的文本文件,以及登出的时候把数据序列化成文本,然后保存在本地文件中。

为了防止中途发生意外,游戏进程会定期把内存全部数据序列化,然后通过共享内存的方式让另一个IO进程不断的把数据保存在磁盘上。

这些都是10年前做的设计决策,无论是否合理,都已经稳定运行了很多年了。不少朋友问起,我们的游戏服务用的什么数据库系统,我都只好说,我们没有用数据库,只用了文件系统。面对诧异的目光,我都不想过多解释。好吧,其实我也觉得SQL神马的都是浮云。

目前在8千人以上同时在线的服务器上,磁盘IO非常繁忙,据说已经影响到了正常的游戏。由于长年的修修补补,整个系统已经不是上面提到的那些单纯。还有一些额外的IO操作,这些被定期写盘的IO操作影响到了。

梦幻西游在去年出了个新版本,在这个版本中,采用了3d技术渲染人物。我参加过部分的技术讨论。总的来说,对于公司的关键产品,是以稳定性为第一。所以不希望对程序做大改动。最好以独立扩充的模块为主。所以最终采用的技术是用3d技术渲染成图片,再依靠旧的程序框架跑起来。

采用3d渲染,主要是为了解决人物换装时的图片资源组合爆炸问题。当然还有更绚的特效等。

最近,梦幻西游的项目经理提出,新的版本性能依旧有些问题。当老的版本可以同时打开5个客户端时,新的版本只能开两个。允许用户同时开多个客户端,对梦幻西游这款产品非常重要。我最近几天就开始着力解决这个优化问题。

美术同学给我们的游戏做了段片头视频,正要加到产品中去时,才发现我们的引擎居然没有提供视频播放的功能。我想这个东西开源库一大堆,那做起来还不是小菜一碟。可没想到还是折腾了一整天才搞定。

9年前,我设计了网易游戏的资源包以及补丁包的数据格式。

当初的设计目的是:方便解析,快速定位资源包内的文件,方便更新、每次更新尽可能的节约带宽。这些年来,虽然各个项目修修补补的改进了资源包的格式,但本质上并没有特别大的修改。

一开始我们直接把需要打包的文件连接起来,在文件末尾附上文件索引表。当初为了快速定位文件名,文件名做了hash处理,可以用hash值直接定位文件。而资源包里并没有储存文件名信息,而是保存在一个额外的index文件中。这个index文件并不对外发布。所以直接对资源包解包是无法准确还原文件名的。

btw,暴雪的mpq文件也是作类似处理的。除非你猜测出文件名,否则也很难对文件名还原。网上许多mpq解包工具都针对特定游戏附了一个额外的文件名列表。

如果频繁更新客户端,对于用户,这会有很讨厌的等待。

同时,还有另一个方式更新新的资源。那就是将需要更新的文件单独打包,以相同文件名(后缀不同)保存在用户硬盘上。游戏引擎在读取资源的时候,优先在更新的资源包内检索。这个方式在Idsoft的Quake/Doom系列中也有采用。

为了保证用户补丁更新速度。我们的补丁中并不是保存的资源包内的小文件。而是在开发机上以增量方式重新打包。补丁文件其实是整个资源包的diff文件。由于前面所述的打包方案,这个2进制diff文件其实可以做到很小。尤其对某些文件的局部修改,对整个资源包的影响很小。

的确,2进制diff的作用有限,现在很多项目改用文本数据格式,很小的修改就会影响整个文件的diff结果。不过原始的设计也有其历史原因。因为10年前硬盘I/O速度很慢,而大话西游在设计时又需要实现无缝加载的大地图。所以地图文件的格式是经过特别设计的。这种方式很适合地图文件的修改和更新。另外,对于未压缩的图片文件的更新也有其意义。

把网络游戏服务器分拆成多个进程,分开部署。这种设计的好处是模块自然分离,可以单独设计。分担负荷,可以提高整个系统的承载能力。

缺点在于,网络环境并不那么可靠。跨进程通讯有一定的不可预知性。服务器间通讯往往难以架设调试环境,并很容易把事情搅成一团糨糊。而且正确高效的管理多连接,对程序员来说也是一项挑战。

正如TCP协议解决了互联网上稳定可靠的点对点数据流通讯一样。游戏世界实际需要的是一个稳定可靠的在游戏系统内的点对点通讯需要。

我们可以在一条TCP连接之上做到这一点。一旦实现,可以给游戏服务的开发带来极大的方便。

这个光源模块设计要解决的问题在于:

GPU的处理能力,目前看来比较有限,不可能实时处理无限数量的光源。所以,当你的场景里设置了许多的光源时,必须可以拣选出最可能影响被渲染物体的光源信息,把这个信息交给驱动去处理。

是这样的。我们的3dengine中的骨骼动画的插值融合总有点问题。如果是两个变化比较大的动画想从一个过度到另外一个,效果总是不太对。对于图形程序,很难用测试驱动的方式开发的缘故之一,就是因为对与不对有很大程度是人的视觉感受,而不是可以简单校验的数值。想写一个判定正确与否的程序,估计不比把代码写正确简单。

所谓不对呢,问题其实老早也想明白了。现象就是有些动作之间过度像纸片一样,而不是直觉中,人物用合理的方式运动。这时,只显示骨骼动画信息中的关键帧反而效果更好。(这些关键帧,是美术人员在max这样的软件中制作出来并导出的:或是用动作捕捉而来的)

其问题在于,没能对骨骼的变换信息做正确的插值。

原来这部分代码写的很脏乱,且原作者已经离开。后继人员要么知其然而不知其所以然的在上面修补,要么就没心思把这个东西真的弄对。(毕竟暂时看起来,这并不是迫切需要解决的问题。它不会引起程序崩溃,也不影响开发进度。)

我倒是一直很想自己来重新设计制作这一块,可一直忙于更多别的事情。加上,大学线形代数也没学好,把其中的数学基础知识补上,还是颇要些精力的。

拖了这么久,终于,这周一咬牙,求人不如求己。还是自己弄吧。

首先,基于KISS的考虑,删除了原有协议中的一些不必要的细节。比如,不再通知AOI模块,对象的移动速度。移动速度虽然对优化很有意义,但毕竟是一冗余数据。考虑这些,容易掉入提前优化的陷阱。

其次,增加了一条设置默认关心区域的协议。这是出于实用性考虑。

最近两年似乎大家一致的想把网络游戏向所谓动作感、打击感的这个方面推进。我想是因为,已经有太多人厌倦了打木桩吧。

我们在三年前就一直在考虑这个问题,但似乎走向了歧路。一个劲的考虑如何解决网络同步问题,怎样在一定网络延迟下保持公平,怎样避免作弊……

结果,尝试了多次后的结论就是,我们依然没有找到正确的方法解决以上问题,暂时把这一块搁置起来。最近,公司又有许多同事热切希望给游戏加入更多的操作感。我们又重拾这个课题。这次,换了个角度看这个问题:假设我们完全不考虑网络这个因素,就算做单机游戏,我们其实也没有太多动作游戏的设计实现经验。那么首先要解决的是如何把游戏的操作手感做出来,之后才应该考虑怎样在网络环境下实现它们。

今天有同事问了我一个涉及贴图管理的问题,看起来他们是想改进他们项目现在用的3dengine(前些年由网易方舟工作室开发的)。我们随便聊了一下,最后的结果是他们取消了一开始的一个需求。

是的,知道自己最终需要什么是特别重要的,不要把过程当成目的。

下面要写的和今天的讨论无关,只是想记录一下:我们的3dengine中的贴图资源管理方案。

而贴图资源作为一种特殊资源,又有所不同。仔细斟酌之后,我把它放在高一层次专门管理。

那么,第一个问题是:为什么其是特殊的?

要写过多少代码才能得到哪怕一点真谛?

多少年过来,我在潜意识的去追求复杂的东西。比如我自幼好玩游戏,从小到大,一直觉得玩过的游戏过于简单(无论是电子游戏还是桌面游戏),始终追寻更复杂规则的游戏,供我沉浸进去。或许是因为,有了更高的理解和控制复杂度的能力,就可以更为轻松的驾御复杂性。

这很好的解释了2000年到2004年我对C++的痴迷。还有对设计模式的迷恋。

EricS.Raymond说:尽量不要去想一种语言或操作系统最多能做多少事情,而是尽量去想这种语言或操作系统最少能做的事情——不是带着假想行动,而是从零开始。禅称为“初心”(beginner'smind)或者叫“虚心”(emptymind)。

代码写多了,问题见过了,甚至是同一问题解决多了。模式这种东西自在心底,不必拿出来。时时的从零去想,总能重新明白一些道理。

为什么说语言重要也不重要,算法和数据结构重要也不重要。对要解决的问题的领域的理解很重要(即明白真正要做什么)。理解了,我们才可以用面向对象,用模式去套问题;可理解了,我们又不真的需要这些繁杂的抽象。

闲话放一边,今天想谈谈树结构的管理。

wow开放了一套用户自定义的插件系统,很多人都认为,这套系统是wow成功的因素之一。反观国内乃至韩国的网游,至今没有一款游戏能提供相当自由度的用户自定义插件系统。

最开始,暴雪是想让用户可以由用户甚至第三方自定义操作界面。后来,这套基于XML和lua的插件系统不仅仅用来做界面了。

后来,我看了bigworld的方案,就是在地面高度网格的顶点上记录一个贴图混合系数。最多在一个chunk(记得似乎是25格)里最多可以用四张不同的贴图,然后根据地面网格的顶点混合系数,混出最终的效果。

这个方法比较节省资源,听说很多3dengine对地面的渲染都这么干。

不过bigworld默认的地面网格,每个格子单位长为4米。结果地面的过度就显得非常粗糙了。这点,天下2的场景美术私下跟我抱怨过。说是不能做出wow里的效果。到了创世西游,应美术的强烈要求,单位格被改成了2米,很大程度是为了让地表渲染的更漂亮。当然数据量也增加了4倍。

最近在实践中印证了我的一些想法,是关于资源管理的。确认自己的猜想是正确的总是件开心的事情。

先简单回顾一下以前写的几篇东西:

这不是最早的思路,但是是我能找到的最早在blog上公开并实际实现的思路。里面提到的部分东西,尤其是多线程设计部分,后来都一一被换掉,理由是过于复杂并实际上没能达到要求。或是细节上很难有stable的实现。

经过一些思考,以及经历了许多实践后。对上面的东西,一部分做了肯定,一部分做了否定。肯定的是,资源管理的大策略是对的:那就是和其它部分正交化,尤其是要独立于游戏逻辑之外。生存期不应和游戏逻辑耦合。

至于多线程预读的问题,资源模块已经可以了解资源数据文件间的关联信息。简单的开一条线程touch那些未来可能读到的文件即可。把预读和缓冲的工作交给OS,这应该是一个简洁有效的设计。并不需要太多顾虑线程安全的问题。

那么今天想说什么?

大凡RPG游戏(包含MMORPG),在制作期都需要开发一个地图编辑器。

早年2d游戏就有这个需求,基于Tile的Engine如是,基于整图的Engine亦如是。到了3d游戏,这个东西更少不了。

对于3dengine中附带的地图编辑器,通常有几个用途:拉地形(也有基于模型的地形)、摆放物品(以及特效)、设置玻璃墙(或是设置障碍格);有时也包括设置摄象机、事件触发点、摆放NPC等等。

后来细想,这里面可以做的东西还有许多。玩家会因为多一个信息通道,而更轻松的去玩那些需要长期驻留的游戏。游戏厂商也可以多一个挽留玩家的渠道,甚至用来宣传新游戏或游戏的增值服务,等等。好处不再列举。

其实、绑定IM帐号和绑定手机号本质上区别不大。只不过,IM帐号几乎是零费用,又不像SMS,控制权掌控在移动手里。IM更适合做双向交流(SMS的双向交流不那么方便,而且对用户和游戏运营商都有经济负担)。独立提供一个Game2IM的服务供众多游戏运营商使用也是个有趣的主意。和SMS一样,只要给出一个简单接口让游戏运营商调用,把游戏网络和IM网络互联就可以了。

实现这个想法有两个方案。其一是制作各种IM的机器人,通过机器人和用户IM沟通。这个方案技术门槛稍低,有许多现成的机器人可以使用。缺点是,受IM提供商的限制(比如好友数量限制)。无法使用机器人的签名针对性的向用户传递特有的消息。除非你为每个游戏用户定制一个机器人,但那样,每个机器人都需要单独一个连接,对资源消耗过大。

第二个方案就是使用已有的IM互通方案,自己提供一个特有的Game-IM网络,跟已有的IM网络互通。比较流行的IM互通协议用基于SIP的SIMPLE和起源于Jabber的XMPP。

既然是独立进程,设计协议是最重要的。经过一番考虑,大约需要五条协议,四条是场景服务器到AOI服务器的(列在下面),一条由AOI服务器发送消息回场景服务器。

这套协议相对简单,可以满足游戏的一般需要,并隐藏AOI服务的实现细节。对象全部由handle方式传递,由场景服务器自己保证handle的唯一性。在我这次的实现中,每个AOI对象同时只能拥有一个AOI半径触发器,但是协议本身无此限制。

下面,我们再来看一下实现细节。

应该是在blog上第2次讨论AOI的问题了,另外还要算上一篇写完没有公开的。

昨天刚出差回来,晚上跟前大唐的服务器主程聊了一下。扯到了AOI的问题,我提了个分离这个模块的方案,在此记录一下。

AOI主要有两个作用,一个是在服务器上的角色(玩家或NPC)做出动作时,把消息广播到游戏地理上附近的玩家。当进程中负责的玩家不是很多的时候,可以直接对整个进程内的连接广播。这样的处理最简单,随着硬件的提高,可能是未来的主流方法。但是目前,为了减少处理的数据量,尤其是带宽,我们通常需要AOI模块裁减一些数据。

第二,当玩家接近NPC时,一旦进入NPC的警戒区域,AOI模块将给NPC发送消息通知,以适合做一些AI的反应。

周末跟同事聊天,问了一下最近公司新项目的一些需求。发现现在无论是玩家还是策划,纷纷要求引擎可以实现自动寻路。虽然我对此不以为然,但其实这也不是什么难题,打算随便做一个好了。

我们现在的engine是用矢量线段描述场景中的障碍的。这些线段无所谓人工标记还是从场景模型中自动生成出来,又或者是用传统的打格子的方法变换出来。在这些障碍线构成的矢量场景中寻路其实不是什么难事。

我们只需要在场景中标记出若干waypoint,这些waypoint一般在大片无障碍区域的中央,以及分叉路口。游戏中故意设计迷宫为难玩家绕来绕去又没有什么玩点内容在里面是没有什么意义的。(除非是“不可思议的迷宫”这种以迷宫为重要玩法的游戏)所以,大多数游戏场景,路径的拓扑关系复杂度非常有限。waypoint靠人工设置就足够了。

btw,自动生成waypoint也不是难事,反正这个东西不会是性能要点,机器生成waypoint不会比人工设置的糟糕太多。算法就不展开谈了。waypoint的设置要点是:场景中所有可达区域都能看到至少一个waypoint,这个要求可以用程序检测。

我们把所有waypoint间可以直线连接的线路连起来,得到一张图。可以预存下这张图,以后的工作则非常简单。

嗯,那么3d引擎是什么?跟3dapi(Direct3D或openGL)有什么区别?固然,engine如果只是做3dapi的一层薄薄的封装,抹平各套3dapi的差异。那么,就过于底层,显得小了。

如果为特定形式的游戏写死代码,让开发者写一些MOD插件就可以形成不同的游戏,那么又显得太高。在这种高层次上,游戏类型会限制于engine的实现。比如魔兽争霸3就直接用户写MOD,并的确有人以此发展出许多玩法。但你永远不可能在魔兽争霸3的基础上写一个MOD实现第一人称射击游戏。

所以我指的3dengine,是处于3d游戏软件结构中间地位的东西。

那么,我们的3dengine到底要解决的是什么问题?做engine绝对不是以我能在3dapi的基础上扩展出什么东西为设计向导。因为,对于完成一个软件,是一个从机器实现域映射到问题域的过程。这两个领域的模型是不同的。3dapi完成的是实现域的扩展,engine则应该完全从实现域到问题域的一个变换,让开发者可以用最接近问题域的语言来表达问题。

目前是我第一次设计3dengine,虽然主要贡献3d方面代码的同事不是第一次做了。我们做了两年多,大方向上是我在把握设计。但是毕竟是没有什么经验,也就老在修改。

这个周末再审以前做的东西,觉得接口上还需要调整一下。比如精灵的控制接口、摄象机的控制接口、场景描述的接口等等。以一个游戏开发人员的角度来看,我需要接口是什么样子的?

我希望是最方便的描述虚拟世界中各样东西的相互关系,引擎当隐藏住不必要的细节,比如3d方面的数学知识,专业化的术语,复杂的坐标转换等等。今天写下这些,不是定论,而是做些思考的记录。

我想知道,对于3d引擎的使用人员,如何描述虚拟场景,暴露出怎样的接口是比较合适的。

我们在实现游戏界面时,用了一张RGBA4444的贴图做buffer。最近同事测试效率时总不满意,发现上载4444贴图时,openGL表现出来的性能实在是太差。(显卡为ATIX300,最新版驱动)

几个人一开始怀疑是显卡或驱动程序的缺陷,进而想换总做法,不用软件渲染界面的方案。改把界面元素全部放到显存里。前几年,我为天下二设计界面的模块时,也有同事有此疑惑,编写代码做过比较。记得当时的结论是:仅仅从性能角度上考虑,把界面所用资源全部放到显存里,并不能提高太多速度(反而可能性能更低)。当时用的Direct3D做底层,似乎没有今天遇到的问题。

最近做游戏数值有点头大。也研究了一些游戏的设定,有点心得。举个很小的例子来谈谈:

wow里的护甲对物理伤害吸收是乘一个百分比的,其公式为:

min(护甲/(护甲+400+85*敌人等级),0.75)

怎样理解这样一个公式的内在含义?为什么会设置成这样?

10多年后,我在学校的机房上网,泡bbs,做个人主页,写一些关于游戏的自己的想法。交了许多的网友。大家都是业余的,年轻气盛,想自己做出好玩的游戏。王欣是第一个给我发email的职业游戏人。更早的97年,国内有两家大的游戏公司,前导和腾图,有如台湾的大宇和智冠。可惜生不逢时,前导做不了大陆的大宇,腾图也远不及智冠。

腾图命中注定的散掉,王欣的八爪鱼工作室从腾图分的出来。98年,王欣邀请我在假期去北京玩,他把我带进游戏这个圈子。我又有机会站在天安门广场,原来并没有那么大。我想,如果毛泽东纪念馆挪一下地方的话,广场会更宽广一些,和我儿时记忆中的一样。小时候怎么就没这种感觉呢:)。

那个年代,我成月成月的逃课,去北京帮王欣做些兼职工作,并听他说些早年北京游戏圈子有趣的八卦,有如今天我给新人说起往事。回忆起来,自己其实什么也没做,似乎又做过点什么,反正最后一次,我拿了一小笔兼职工资。不过没有花掉,因为回到学校的时候,一个同班同学缺钱交学费,我全部借给了他,毕业那天他才凑起来还我。

当然,身在北京,我就有机会四处拜访网友同好。随即发现,其实许多网友都已经专职在做游戏了。有些人后来再没怎么联系,比如曾经在金洪恩做《自由与荣耀》的3dengine的rickhuang,我还记得他在那个晚上,他抱怨他的后继者让代码从几万行膨胀到十几万;又比如做《独闯天涯》的郭巍,他念叨过来北京前没有钱吃饭,小组里每个人一个冷馒头就可以啃一天,脑子里只想着把游戏做出来……

明天就是五一假期了,同事都已放假。我不打算在假期加班,因为加班也无事可做,手头上的工作都需要与人合作。

我这人有个优点,选择性记忆,那些不快的回忆很容易随风而去。活在我记忆中的人们,对他们只留下感激。我也曾经爱写日记,很早我就写电子日记,记在自己的机器上,PDA上,当我有一些不愿意再回忆的事情时,我会个将整个文件加上密码,长长的一次性密码,保证自己只能记住一小段日子。当这段日子过去,密码就消失在记忆中。然后再也打不开这些文字,等到下次更换硬盘,无论我多么的想再看一眼当年的自己,也无能为力,只好把加密过的文件删去。

是的,我想讲一个真实的故事,一个拥有数千万玩家的游戏诞生的故事。我并不喜欢这个游戏系列本身,但是我为这个产品自豪。我的代码曾运行在几千万用户的机器上,作为一个程序员,还有什么比这更让人满足的呢?也许有,比如让这个用户数量再扩大10倍。

鉴于我在游戏设计领域还没有什么建树,远不如游戏程序设计方面有发言权。甚至对游戏设计说三道四的话,还不如在软件开发上乱侃几句有分量。在下充其量也就是个没吃过猪肉天天能见着猪跑的家伙,如果有猪肉已经吃腻味的方家读到这里,请一笑了之,别跟我一般见识。

本来在本文成文前打的腹稿中想举出些实际的例子来,可是发现怎么都会或多或少的得罪圈子内的策划朋友。罢了,只说些空话好了,算我做了半个月游戏数值,闲暇时发的点牢骚。

从为我们的游戏设定角色基础属性,以及设计战斗伤害的计算公式开始。

曾经看过这样一种赌徒的策略:假设在一场赌大小的赌博游戏中,赔率是1:1,而庄家不会出千,开大和开小的概率均等(皆为50%)。赌徒一开始压一块钱,如果他压错了,那么下一次就压两块,再错继续加倍。一旦压对,赌徒永远可以保证有一块钱的进帐。并且从1块钱重新开始。

看起来,这种策略能保证永远包赚不赔。但实际上为什么没有人用这个方案发财呢?

放到现实中,试图采用这个策略去赌博的人,几乎都会赔的倾家荡产(当然只要是赌博,差不多都是这个结局)。不要怪运气,不要怪庄家出千,因为这个策略从一开始就注定了失败。

但实时人机互动软件则不一样,我们得把人看成系统的一部分。人和机器的交互是通过人眼、人耳、键盘、鼠标等完成信息交换的。如果把人脑看成一个线程、计算机看成另一个线程,我们几乎没有能力实现一个资源锁,利用锁的方式保证系统工作永远正常。人脑在全速运转时,适时得不到正确的信息输入,整个系统就可认为出现了故障。

最近在解决3dengine接口的一处设计问题。我们知道,在3d游戏中,engine的接口设计往往比性能更加重要,这决定了engine是否好用,甚至是否能用。简单的功能堆砌不是engine。

目前我想弄清楚的一个技术点就是,当模型置入场景后,如果播放的动画本身有位移,引擎应提供怎样的接口,让使用者最为方便的表达他意图。

具体到一个问题点来说,当我们的美术人员制作了一组四足动画奔跑的动画后,怎么在游戏中最自然的表现出来。

就我有限的眼界所知,有许多人在制作3dengine时是这么规定的:美术需要把动画调整成原地奔跑的样子。(或者换个方式,在从制作软件中导出时,让原本具有位移的动画变为原地移动)如此,就可以在最终表现时,自由控制播放的速度。

但是稍加思考,就知道这样做导致的图象表现和我们期待的样子是有偏差的。下面让我们来分析一下。

很久没写blog了,主要是忙。项目排的很紧,一个小项目上线,发现不少问题,所以把多余精力都投进去了。最后人手不够,亲自上场写代码。在不动大体的情况下,最大能力的修改了一些设计,并把能重写的代码重新写过了。

闲话扯到这里,总结下这两天做项目的一点经验。那就是:当我们需要一个7*24小时工作的系统时,实现热更新需要注意的要点。当然,这次我用的lua做实现,相信别的动态语言也差不多。只是我对lua最为熟悉,可以快速开发。

btw,在服务器上装php时,因为开始ports没有更新,出了好多问题。mysql一开始忘记装gbk的支持,困扰我老半天。鄙视一下公司购买的某著名php写的论坛系统,居然默认不是用utf-8编码的。

自从最底层引擎的架构完成后,我们的客户端留了两个人在全职写代码,一个人负责C层面的3dengine,另一个人负责设计高层面的应用接口,并在此基础上完成游戏demo。

从程序员角度上看,人都是相当不错的。要设计有设计能力,有编码有编码能力。代码审美观上大家也很一致,所以我们几个人一起工作的很愉快。

我希望游戏能有比较流畅的操作感,可以灵活的操作游戏角色做出各种动作,包括战斗时的腾挪,组合技能等。写了不少脏代码尝试我希望的手感时,终于发现一些设计问题了。

周末在写demo,为了给同事示范我希望得到的操作手感。作为一个能写点程序的游戏设计者,自己随时实现出来看看恐怕是不可多得的优势了。

我们的游戏有一定的动作成分在里面,所以我不想采用鼠标控制角色的行动,以对拍砖头的形式表现格斗场面。本质上我更偏好consolegame和游戏手柄的操作感。不过在PC平台上,手柄并不普及,只能在最普及的输入设备——键盘和鼠标上下点工夫了。

手柄控制方向主要有两种:

其一是模拟杆,在PSP,PS,Wii等手柄上均有安装。好处是至少可以感知两级力度,并可提供非常细的方向信息输入。控制游戏角色行动时,操作感很不错。

鼠标通过移动可以提供方向信息,甚至通过移动速度可以模拟出力度的差别。但毕竟鼠标缺乏力的反馈,且必须通过移动才能提供信息,本身不能保留状态(鼠标的button可以提供状态,所以很多fps把前进操作设置在鼠标的按键上),和手柄的感觉还是差了很远。

btw,IBM的TrackPoint如果特别针对去开发,其实是个不错的游戏输入设备,可惜还是太专有化了。

其二,就是类似任天堂的十字键。由于有专利,到别的厂家那里换成了其它形式,不过大体还是差不多的。这类设备可以精确的输入八个方向,在格斗游戏中用的最多。比如我的最爱VF4EVO的PS2版,干脆就禁止了模拟杆的输入,只允许用八方向键。

对应到PC上,大多数游戏用WASD四个按键模拟这个设备。不过要小心的是,由于键盘硬件设计的缘故,大多数键盘处理三个以上按键同时按下时,会发生所谓锁键问题。比如我的DELL键盘,同时按下W和D键后,再按E键,系统就接收不到E键的消息。这类情况试硬件决定,不同键盘处理能力不同。据说有比较强悍的键盘可以处理任意7个按键同时按下的消息,我就没有试过了。

好在现在的键盘处理两个按键的能力还是绰绰有余的,用WASD模拟八个方向的输入足够了。

最近几天,有测试人员向我抱怨,我们游戏中某些概率设定总感觉有点怪怪的。似乎跟文档上的不同。

这种抱怨并不少见,许多网络游戏玩家都在抱怨系统生成的随机数不太对劲。善良点的玩家会归咎到自己的RP上,阴谋论者则指责系统作弊。运营着的游戏背后,数值策划和程序员们有苦说不出。

有必要科普一些数学常识,也作为我周末读书的一些笔记。

晚上在办公室晃荡,对面的同事在加班写代码。我凑上去看看在写什么。我向他了解了后明白了,大约是服务器上角色buff的实现吧。

BUFF这个术语是现在网络游戏中非常常见的。给角色加一个BUFF通常意味着对虚拟角色的一些数值上的临时修正:例如,攻击力+5,防御-10%,速度加倍,等等。

玩过魔兽世界的朋友应该很容易理解这些。通常游戏里的BUFF设定比我上述的例子更加的复杂。

这里不谈游戏设定,谈谈实现。

晚上已经回到办公室,飞机下降的时候天气不好,颠簸的很厉害,居然有点晕机的感觉。可能是这几天身体不是好的缘故吧。下了飞机大雨,直接打车到楼下的KFC吃垃圾食品,然后就坐到了熟悉的椅子上。

真还别说,环境不同,心境就不同。一坐上办公桌前的椅子上,就有做事情的感觉。沾着家里的椅子就是放松休息了。

btw,玩现在的MMORPG往往配置更大的物理内存得到的性能提升,比升级CPU或显卡要划算的多,就是源于这个道理。通常MMORPG都需要配置海量的资源数据,来应付玩家的个性化需求。

天下组的同事暂时只做了骨骼动作信息的压缩。这个这里暂且不谈,我想写写关于模型顶点数据的压缩。记录下这两天在思考的一个有损压缩算法。

首先,我认为现在的网络游戏中,货币并不完全等同于现实社会中的货币。现实社会中,货币只是一种能直接起交换手段或支付媒介作用的东西,它本身没有价值;货币的投放由央行控制。但在大多数网游中,货币本身有了实用价值,它可以从系统那买到各种补给品,或是可以直接增加虚拟角色的能力。而且货币本身也多由玩家自己的主动行为生产。

同时,在健康的虚拟经济环境中,虚拟货币也起到了现实货币同样的作用——作为支付媒介。

虚拟货币的多重职能,使得在架构虚拟社会的规则时,需要万般小心。而虚拟货币也往往因其多重职能,使得游戏中的市场调节能力减弱。现存的几乎所有网游中都没有设计出一个央行的角色,还真怪不得通货膨胀得不到有效的控制了。

那么,能否构建出一个更接近现实社会的经济模型呢?对于设计一个较之现存游戏更有趣好玩的经济系统,即使这不是唯一之道,但至少是一条值得尝试的路。今天在这里记录一些尚未深思熟虑的想法,权作自己的笔记。方家读过,请一笑了之。

讨论告一段落后,我浏览了一遍聊天记录,觉得有必要总结一下:)

三年前,我跟公司一些策划同事有接连几天的争执:到底玩家是否需要一个宏大的虚拟世界?或是我们若实现这样一个虚拟世界,到底有没有价值?该怎样维护这样的一个大世界,让它不会受到一些暂时性设计bug的毁坏性影响?

可以说,三年前的争论是导致我成立独立团队做现在这样一个项目的重要起因之一。因为当时,我几乎得不到任何的支持,策划,程序,美术方面等等几乎都是不赞成的意见。

那个时候,我也的确找不到确凿的证据来说服大家。但今天,思路清晰了许多。

网络游戏世界的构建有越来越大的趋势,游戏设计者希望更多的人可以发生互动。技术人员的工作就是满足这些越来越BT的需求。

我们目前这个项目由于是自己主导设计,而我本人又是技术方案的设计者。所以,技术解决不了的问题就不能乱发牢骚了。作为游戏设计者,我希望整个游戏世界的参于者可以在一个唯一的大世界中生存,他们永远有发生互动的可能。注意这里只是保留这种可能性,实际上,即使是现实社会,每个人的社交圈子都不大。即使是千军万马的战场上,无论是将军还是士兵,都不需要直接跟太多人互动。

我们的游戏的技术解决方案仍旧是将游戏大世界分成若干独立服务器组,人为的将人群切分成更小的独立单位。这里,技术上需要解决的是:服务器组间可以灵活的交换数据。

一直以来,我们的新引擎一直以跨平台为设计目标。这倒不是说,我们有多重视非Windows平台的用户。只是我觉得,一个好的设计一定是很容易做到平台无关的。对于做跨平台开发这件事情,公司里支持的人寥寥。光老丁都几次三番劝我不要把精力花在无谓的地方。

唉,怎么说呢。写了这么多年程序,我一直把编写代码和设计软件作为一件很有趣的事情在做。所以我并不认为我做的一切是一种工作,它是我的玩具。早就不需要担心后半辈子的生活问题,所以没有人可以阻止我做想做的事情,更何况我认为良好的设计造就优秀的产品。今天看似多花的精力,日后慢慢的会给出回报。

回想当年做西游的客户端,我固执的把内存的占用量控制在64M左右,让低配置的机器也可以流畅的运行。为了达到这一点,当年多花了好多的精力。做内存中的精灵压缩,做地图的动态加载,做图象数据的cache和主动交换,改动许多我认为会更多占用内存的数据结构,阻止美术的一切可能过于消耗内存的设计。

这些在我离开西游的开发后的这么多年里,配合硬件的发展,往日做的那些,得以让后来的不断扩展玩法和美术资源可以稳定的进行。后期的开发维护人员可以用足够的东西来“浪费”。前期的种种约束和正是为了后期的无拘束扩展,直到这些项目顺利的走完生命期。

我希望几年之后,依旧有人感谢我当下做过的努力。

花了两天和同事一起做了一次数据挖掘工作。

一些若干年前风光过的显卡,如VoodooTrident等现在依然有人用,但如今已经非常稀少。当然,还有人用一些更为稀少的专业显卡,或是我没听过的品牌的垃圾卡(疑是电子垃圾)在玩游戏:D

一起做引擎的同事希望即使在硬件不支持的时候,我们也能正常加载并使用贴图,所以便有了对DXT软解码的需求。

游戏服务器在设计时,会有许多组播的需求。比如一个NPC需要向周围的观察者播出它的状态信息。无出不在的各种聊天频道中需要向频道中的参于者广播聊天消息等。

通常,我们会为每个组播的请求维护一张列表,然后再把需要的信息包发送给指定列表上的多个连接。这个过程在很多地方都会遇到,一个设计的不太好的引擎,再这里有可能滋生诸多bug。尤其在多服务器的设计时尤其如此。

这两天,我试图寻找一种简洁统一的解决方案。

最近在考虑为一组游戏服务器配置多个连接入口。这个需求来至于我们的国情。作为大的游戏运营商,势必要考虑国内的网络状况——南北不通的现状。以往别的公司的代理游戏,由于不是自己开发,都选择了一个实际的方案:在北网通和南电信各放若干组服务器。北边来的在北边玩,南方住的安居在南方。

我们的游戏却不行,因为我需要一个完整的大世界,必须解决南北互通的问题。据我所知国内运营的游戏EVE是比较好解决了这个问题的。

我们自己的游戏大多也解决了,只是宣传上还是鼓励玩家登陆相应的服务器。我们的解决方案本质上很简单。建立有多个出口的机房,同时拥有电信和网通的线路。或是用自己的线路互联电信和网通的机器。这后者普通用户自己在家也可以做,只要你肯花钱,同时购买电信的ADSL于网通的宽带即可。目前许多城市两者都向大众提供服务。

当然,最终我们还是需要编写服务器的程序员做一些配合。

当然中国人或是韩国人也喜欢下围棋,而日本人也没以前那么爱围棋了。八十年代的时候,似乎在国内校园里也非常流行打桥牌。而桥牌在美国现在还是不是那么那么流行,就不得而知了。所谓国民的特性从所喜爱的游戏中可看出总总之观点,我一直是不置可否的。今天写的跟这些无关。

由于家庭渊源,我个人不喜欢打麻将。但对麻将也没有感情上的厌恶。小时候即没见父母打过,也不曾见过传送中的因为麻将而导致的家破人亡、妻离子散。只觉得麻将无非一种有竞技性质的桌上游戏而已。

那么,在我看来的一个普通游戏,甚至没有统一明确的规则,为何风靡整个华人世界呢?

游戏的美术工作有时是挺枯燥乏味的。我在公司这几年看到太多美术,连续几天的辛勤工作,只为了将一批图按同样的模式修改成另一个样子。甚至这个工作只是修修通道、转转图片格式等等。

今天碰到件事,我们需要给一些人物角色图片勾一下边。也就是把动画图素展开成带alpha通道的图片序列。然后把通道部分提取出来,轮廓扩大、羽化、上色,再叠加回原来的图。这样做的目的是可以把游戏里的角色从背景中突显出来。

当游戏服务器群达到一定规模后,让用户只从一个入口连入会给这个入口带来很大的压力。这样,我们就需要让服务器群中的多台机器都允许用户直接连接。

当服务器开放给用户直接登陆后,必须面临的一个问题就是用户身份认证的问题。

大多数提供网络服务的公司都做了一套统一的用户认证系统,比如微软的passport,网易的通行证,等等。为了避免重复验证用户身份而给用户认证系统带来过大的负担,云风在这里给出一个参考解决方案。

我们的游戏引擎已经开发了一年半了,其中3d引擎部分也做了近一年。

一个好的引擎,渲染部分总是容易做的。因为面对的问题比较单一,资料也很丰富。需要克服的问题很有针对性,即使短期解决不了的问题,放在那里也关系不大。我们团队有两个同事拥有完整3dengine开发经验,所以在渲染引擎接口设计部分也不会走太大弯路。(不过事实上,因为基础构件的重写,渲染引擎也是几经修改的)

最近3dengine部分又面临一次大的重构。起因是这样的:

我始终认为,在MMORPG里采用多边形碰撞检测是件很傻的事情。当然傻不是坏事,基于多边形碰撞检测,一帧检查一次的这种做法,实现起来非常简单。很符合kiss原则。我们只需要一点点数学知识和一点点编程技能就能做出来了。反正client上,也就检查一个主角。加上可以使用简化的碰撞模型,进一步的减少运算量。

但是放在服务器上,这个运算量可不小。所以这几天我寻思着找个更好的方法出来。

在Windows平台下写游戏,相比console等其它平台,最麻烦之事莫过于让游戏窗口于其它窗口良好的相处。

即使是全屏模式,其实也还是一个窗口。如果你不去跑窗口的消息循环,一个劲的刷新屏幕,我估计要被所有Windows用户骂死。

那么怎样让你的游戏程序做一个Windows下的良好公民呢?

周末一直在考虑怎样在游戏服务器中保存和管理那些有平面坐标的对象。希望能方便的查到每个对象附近的东西。以往用过的方法是给(平面)场景打上格子,然后再把每个对象放入相应的格子中。

这次又遇到这个问题,却不想用网格的方法来解决问题了。

原因主要是这么几点:一,用网格对于大场景特别消耗内存,而且不太适合做无限延展的场景。二,查询每个对象周围的东西时,不太方便。格子的粒度越小,速度越慢。

虽然这两个问题,都可以通过改进算法来回避。不过这次,我想尝试一下用四叉树解决这个问题。

今天开始正式开始做数据服务器。在这点上,我希望一组服务器上所有的逻辑服务遇到的数据存取需求都通过一个单一的数据服务器完成。而且,写入数据是单向通讯的。即,逻辑服务器只提读写盘请求,而无须确认。写数据好说,读数据稍微难处理一点,我现在的方案是,数据服务器加载数据的部分只对位置服务器负责,把数据提交到位置服务器即可。位置服务器可以通过分析数据知道玩家的数据流应该流向哪台逻辑服务器。

以上逻辑是基于每个玩家有独立的数据的,而且一个玩家同时只存在于唯一一个场景。也就是说,当一组数据存活的时候,只唯一属于一台逻辑服务器。这样做的好处是,切换场景非常的简单,只是让玩家从一个场景退出,给数据服务器发出写盘指令,并发送所有数据。数据服务器写盘的同时也cache了这些数据,并向位置服务器提交新的位置,并把这些数据转发向位置服务器。位置服务器可以再转交给新的场景。

对于玩家登陆和登出的处理并没有特别之处,我们可以设立两个虚拟场景,一个负责登入,一个负责登出。每个新连接自动导入登入场景,这个场景负责发出加载指令(下面可以看到,甚至无须设置加载数据的协议)。然后再做一个自动的场景切换操作即可。而玩家登出,则是转入登出场景,这是一个黑洞,玩家的连接可以在这里安全的断掉。

当玩家不停的穿梭于各个场景之间时,为了避免频繁的数据转发,我们可以给玩家数据做一个赃标记。如果没有弄赃,实际的数据可以不被转发。这个标记的另一个意义在于,若数据没有弄赃,而数据服务器的cache中又没有数据时,就需要从外存加载了。其实这就是一个读请求。

月初我在公司内部讲解这个设计时,遭到了一些同事的疑问。最典型的一个是,帮派信息如何处理。的确,类似帮派的信息,不属于任何一个玩家。如果你单独设一个非玩家对象保存这些数据时,可能会分布到不同的逻辑服务器上。的确对数据服务器的设计是一个挑战。

今天仔细考虑过以后,我发现可以从设计上避免这个问题。方法如下:

目前,我们的游戏服务器组是按多进程的方式设计的。强调多进程,是想提另外一点,我们每个进程上是单线程的。所以,我们在设计中,系统的复杂点在于进程间如何交换数据;而不需要考虑线程间的数据锁问题。

如果肆意的做进程间通讯,在进程数量不断增加后,会使系统混乱不可控。经过分析后,我决定做如下的限制:

性能问题上,我是这样考虑的:

我们应该充分利用多核的优势,这会是日后的发展方向。让每个进程要么处理大流量小计算量的工作;要么处理小流量大计算量的工作。这样多个进程放在一台物理机器上可以更加充分的利用机器的资源。

单线程多进程的设计,个人认为更能发挥多核的优势。这是因为没有了锁,每个线程都可以以最大吞吐量工作。增加的负担只是进程间的数据复制,在网游这种复杂逻辑的系统中,一般不会比逻辑计算更早成为瓶颈。如果担心,单线程没有利用多核计算的优势,不妨考虑以下的例子:

计算a/b+c/d+e/f,如果我们在一个进程中开三条线程利用三个核同时计算a/bc/de/f固然不错,但它增加了程序设计的复杂度。而换个思路,做成三个进程,第一个只算a/b把结果交给第二个进程去算c/d于之的和,再交个第三个进程算e/f。对于单次运算来算,虽然成本增加了。它需要做额外的进程间通讯复制中间结果。但,如果我们有大量连续的这样的计算要做,整体的吞吐量却增加了。因为在算某次的a/b的时候,前一次的c/d可能在另一个核中并行计算着。

具体的设计中,我们只需要把处理数据包的任务切细,适当增加处理流水线的长度,就可以提高整个系统的吞吐量了。由于逻辑操作是单线程的,所以另需要注意的一点是,所有费时的操作都应该转发到独立的进程中异步完成。比如下面会提到的数据存取服务。

对于具体的场景管理是这样做的:

MMO的engine中,需要解决的最重要的问题之一,就是如何把游戏世界中的状态改变消息正确的通知给需要知道这条信息的玩家。

通常,我们会设定每个玩家的AOI(AreaOfInterest)。当一个对象发生改变时,它会把消息广播出去;那些AOI覆盖到它的玩家会收到这些广播消息。

但是,在我们现在的游戏系统中,简单的AOI系统是不够用的。比如,类似wow中的盗贼隐身,明明已经离你很近,但是你的client却看不见他。诚然,我们可以在client判断这个逻辑,对盗贼不于显示,而engine依然广播盗贼移动的消息。对于不作弊的client,这是个简单的解决方案。但在理论上却提供了看见隐身人的可能性,所以,我期望有更好的方法让engine可以只将消息广播到那些必须接收这些消息的client。但是,在实现这个同时,又不能让底层engine牵扯太多逻辑信息。

这里提出一个简单的方案:

今天,我在两个服务器之间加入了一个控制心跳的服务器。其原始作用很简单,就是按心跳(目前的设定是10Hz)从连接服务器上拿到数据,再转发给逻辑服务器。并把逻辑服务器发出的数据转出。

我们一开始的游戏逻辑层是基于网络包驱动的,也就是将client消息定义好结构打包发送出去,然后再server解析这些数据包,做相应的处理。

首先由处理逻辑的server调用client的远程方法在client创建出只用于显示表现的影子对象;然后server对逻辑对象的需要client做出相应表现的操作,变成调用client端影子对象的远程方法来实现。

这使得游戏逻辑编写变的清晰了很多,基本可以无视网络层的存在,和单机游戏的编写一样简单。

group>solo就我自己来说,是反对的。不过之前一直没说出口。我自己打wow,一直到60级都是solo并且觉得非常有趣。而且每个任务都不放过,每个副本都要完成(当然不包括后期必须太多人的)。前期的5人副本,都是在不太高的级别,邀请几个非常熟悉的同事以3~4人完成的。那种克服困难后的喜悦无以言表。尤其是在24级的时候单身一人完成圣骑士的圣锤任务,跑断了腿不说,还在死生之间转了很多次。

整个游戏过程,没有接受任何经济上的资助,最多跟同级的玩家借点点钱。

现在我已经不玩wow了,但是我有同事还整天泡在里面。听取了他的观点后,更坚定了我的想法。raid的确是wow的硬伤之一。当然,还有文中提到的荣誉系统等。

实际上,我经历了好几个游戏项目后,那么久远的代码已经被抛弃不用,但是由于工作原因,新的代码没能公开。现在回首那些老代码,居然发现如此的丑陋不堪。

不过,一些思路还是可以保留下来使用的,其中之一就是脏矩形技术。

随着工作的深入,这两天想了更多东西。

使用kqueue这些,变成了派一些个人去站岗,鬼子来了就可以拿到通知,效率自然高了许多。不过最近我在反思,真的需要以这些为基础搭建服务器吗?

3d游戏会用到大量的帖图,许多显卡要求贴图的尺寸必须是2的整数次方。这样,许多贴图的边角都会被浪费掉。尤其是大量无关的小贴图,我们通常想把他们合并在一张贴图上,尽量充满整个区域。

游戏中有大量的资源数据,这部分数据通常数以百兆,甚至上G。如何在运行时管理好这些数据,也是游戏软件相对其他许多软件的一大难题。

如果总的资源数很大的时候,我们就需要cache管理。因为在32位OS下,虚拟地址空间是有限的。这里主要谈cache的管理。

游戏的client最文件数量最多,数据量最大的,往往是美术资源。(几乎所有的商业游戏都会在游戏发布时对资源文件打包,我们这里讨论的是开发期未打包的文件)当一个游戏的规模大到一定时,我们需要调动巨量的美术人力来制作这些资源。伴随着游戏规模和制作团队的扩大,设计资源文件的存放目录结构和文件命名规则往往成了项目一开始的头等大事。

2d游戏这个问题稍微轻一点,3d游戏更是有模型,贴图,骨骼等等的文件关联性在里面。这次我们的3d项目,让我又一次面对和思考这个问题。

最近我们立了个新项目,一个2d游戏。和另一个主要的3d项目同时进行。这样比较方便更充分的利用资源。2d游戏的技术已经很成熟了,所以我希望在两三个月内可以完工。

虽然以前的engine是现成的,但是最近脚本研究的比较多,所以我还是给出了个关于嵌入脚本的计划,把原来的engine重新包装一下,改成完全脚本驱动的。

现在做半职业游戏策划了,但是还是改不了程序员的老毛病。尤其是在写策划案的时候,方更加觉得程序的优美。程序是可以调试的,可以用工程的方法做质量控制,可以有测试的机制。大多数时候是可以被验证的。而且掌握了正确的方法,我们可以一步步的把程序搭出来,逐步看到成果。

THE END
1.怎么找到自己的精准客户在当今竞争激烈的市场环境中,找到精准客户成为企业营销成功的关键之一。精准客户不仅能够帮助企业提高转化率,还能显著降低营销成本和资源浪费。为了实现这一目标,企业需要深入分析客户数据、了解市场需求,并采取有效的营销策略。客户的需求洞察、数据分析、合适的沟通渠道和个性化营销是找到精准客户的核心要素。其中,客户数据的https://blog.csdn.net/xgggyxgs/article/details/144092695
2.房地产行业客户分类房地产行业作为一个多元化、庞大的行业,客户群体的分类是必不可少的。客户分类有助于企业更好地了解不同群体的需求和特点,有针对性地提供服务和解决方案。本文将探讨房地产行业的客户分类,以期为相关从业人员提供一些参考和帮助。 二、用途居住客户 在房地产行业中,最常见的客户分类就是用途居住客户。根据不同的需求http://www.tianmaone.com/fangchan/120.html
3.客户关系管理(客户关系管理)书评潜在客户是指有可能购买但还没有购买的客户,目标客户则是企业主动“瞄上”的尚未有购买行动的客户,属于企业“单相思”的对象。 现实客户是指已经购买了企业的产品或者服务的人群。按照客户与企业之间关系的疏密,可以将现实客户又分为:初次购买客户、重复购买客户和忠诚客户。 https://book.douban.com/review/15448203/
4.嘉友国际:首次公开发行股票并上市招股说明书股票频道公司应收账款的账龄绝大部分为 1 年以内,且公司与主要客户均保持长期良 好的合作关系,客户质量良好。同时,公司在应收账款管理方面制定了一整套科 学且行之有效的管理政策,指定相关部门分别负责应收账款的核算、分析、对账 和客户信用等级确定、信用限额标准指定以及账款催收。公司财务部门负责人和 总经理会定期审批https://stock.stockstar.com/notice/JC2018012300000076_15.shtml
5.客户关系管理之客户关系的建立与维护客户关系管理 ——客户关系的建立与 维护 客户管理 ? 第一篇 客户关系概论 ? 第二篇 客户关系的建立 ? 第三篇 客户关系的维护 ? 第四篇 客户关系的恢复 第章 客户关系概论 ? 客户关系的定义 ? 企业管理客户关系的意义 ? 客户关系管理的研究内容 客户关系的定义 《现代汉语词典》对“关系”https://doc.mbalib.com/view/b2e427ba9c30c7b4f1557f4a68f1f6d8.html
6.展后如何做好客户跟踪,你真知道吗?调整客户名录,重新划分类别的目的是为了区别对待不同客户,实施差异化的营销策略。客户名录通常将客户分成现实客户和潜在客户两大类。 (一)现实客户 指那些已与企业建立业务联系并购买过产品或服务的客户。以建立关系时间长短区分,有老客户,也有在展会初次成交的新客户;以业务量大小区分,有成交金额大、次数多的大客户,http://eventsguide.cn/news_detail/257/cid/
7.2018年创业必备的105种工具如果你有客户,你会需要进行客户支持。也许你目前是通过手机、聊天、电子邮件或多种社交平台来处理这件事的。你需要将所有的互动信息放在一个客户支持平台上。Freshdesk则能够让你轻松设定一个中心位置来处理所有客户互动。Freshdesk很容易操作,客户支持也可以变成一个可扩展的流程。 https://36kr.com/coop/yidian/post/5122441.html
8.房地产项目营销策划服务合同9篇(全文)3.1.3 乙方应当向丙方提交客户资料,包括客户姓名、手机号码、联系地址,丙方指定人员应于乙方带看客户时予以查证来访客户资料,及时在《客户确认单》上予以确认。3.1.4 未经《客户确认单》确认或者未提供上述客户资料的均视为无效客户。3.1.5 乙方凭有效的《客户确认单》结算服务费。 https://www.99xueshu.com/w/filer3p716k8.html
9.客户关系管理试题及答案细分的目的是什么?答:2.(1)客户细分:又成市场细分,是只营销者通过市场调研,依据消费者的需要和欲望,购买行为和购买习惯,客户生 命周期和客户价值等方面的差异,把某一产品的市场划分为若干个消费者群,以提供有针对性的产品服务和营销模式的市场分类过程.(2)目的: http://www.360doc.com/document/24/0707/15/79930059_1128185916.shtml
10.CRM的定义与内涵(一)1.11对客户与客户关系的全面理解 1.客户的概念 客户几乎存在于商业社会的各种日常活动之中。客户是指购买企业产品或服务的个人或企业组织;同时也泛指企业的内部员工,代理商、分https://www.csundec.com/information/CRM/4462.html