Main

December 25, 2023

避免帧间不变像素的重复渲染

上周五在公司内做了一个技术分享,介绍我们最近五年来自研的游戏引擎,以及最近一年用这个引擎开发的游戏。大约有一百多个同学参加了这次分享会,反响挺不错。因为这些年做的东西挺多,想分享的东西太多,很多细节都只是简单一提,没时间展开。

我谈到,我们的引擎主要专注于给移动设备使用,那么优化的重点并不在于提高单帧渲染的速度,而在于在固定帧率下,一个比较长的时间段内,怎样减少计算的总量,从而降低设备的能耗。当时我举了几个例子,其中有我们已经做了的工作,也有一些还没做但在计划中的工作。

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

December 12, 2023

游戏引擎中的可视化编辑器

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

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

November 30, 2023

游戏数据包的补丁和更新

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

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

之前一直没时间做这方面的工作,直到最近才考虑这个问题。我们到底应该设计一个怎样的补丁更新系统。

November 24, 2023

贴图管理模块及 UI 上的 3D 模型

我们游戏引擎的 UI 使用的是类似网页的技术,是将 RmlUI fork 出来的自行维护的版本 。目前游戏中大量遇到的一个需求是:把 3d 模型作为 UI 组件使用。这个需求在我经历过的历史项目中都曾遇到过,在不同的游戏引擎中我见过各种解决方案。

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

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

后来,我们额外构造了一个 render target ,改造了一点 RmlUI ,让它可以支持一个矩形区容纳这个 rendertarget 的画布 。这样,3D 模型渲染就比较好的和 UI 模块融合在一起。但是需要单独编写 UI 上 3d 元素的相关代码,尤其是管理它 ( rendertarget )的生命期。

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

November 06, 2023

虚拟文件系统的资源惰性编译

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

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

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

October 25, 2023

游戏引擎的虚拟文件系统

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

我们的游戏引擎的大部分是用 Lua 开发的,这也意味着文件系统中不光有游戏用的资源素材,还包括了代码本身。甚至包括了虚拟文件系统自身的实现。这个东西比一开始想的要麻烦,我们这几年不断地修改它,直到最近。比如一开始认为最麻烦的自举部分 ,在去年就去掉了,为的就是减少系统的复杂度。

October 20, 2023

手机游戏交互的两点改进

最近在手机上玩我们正在开发的游戏时间比较多(之前主要运行的是 Windows 版),发现有许多操作值得改进。这里列举两个值得一提的交互优化。

第一,触摸屏上点击特别小的物件时很难精确。去年我提过一个解决方案 ,我们的游戏一直是这样处理的:连续 tap 屏幕同一个位置,会轮询这个位置附近的物件。这样,一次点不准的话,可以多点几次选到想要的焦点。这不仅可以解决小物件密集的时候,tap 不好区分的问题,还可以选到重叠的物件。这个方法其实在鼠标游戏中已有这么使用的了,比如 rimworld 就是用类似方法处理同一个格子上有多个可选物件的。

September 11, 2023

特效接口的重构

上次提到,经过数据分析发现,我们引擎(游戏)目前特效系统占了很大的 CPU 比例。虽然特效的计算已经放在独立线程,不影响帧率,但 CPU 的开销会导致电池消耗,最终会引起手机热量上升,最终让手机降频。所以,当时想了一些牺牲准确性的方案来做优化:即,不在镜头内的特效不运算。让视觉裁剪同时也裁剪掉特效粒子片的计算。

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

August 07, 2023

手机游戏引擎的优化

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

其实这些核心代码总量并不算大。例如在 profile 中表现出来的非常消耗 CPU 的一个场景树更新系统,用 C 重写了也才 200 行代码 ,但在优化前 Lua 版本会消耗超过 1ms 的时间,而用 C 重写后,时间已可以忽略不计。

另外,我们采用了类似 skynet 的 ltask 做多线程框架,把业务尽量拆分到多线程中并行处理,这也极大的减少了每帧的耗时。除了主业务逻辑外,UI 、粒子系统、IO 被分为几个并行线程。且渲染底层的 bgfx 也是按多线程渲染设计的。这些并行流程间只通过少量的消息通讯,所以,并行的总工作量并没有比单线程模型更多。ltask 也可以很方便的调节工作线程的个数,用来更好的适配手机的 CPU 。

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

June 21, 2023

手机游戏交互的一点想法

这一篇是延续去年的想法 。最近正在按这些想法改进我们正在制作的一款类异星工厂的手机游戏。

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

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

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

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

March 30, 2023

短 ID hash 映射的问题

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

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

为了在 C 和 Lua 间快速共享这些配置数据,我还专门写了一个 cache 模块

问题出在 ID 的持久化上。因为游戏中的物品种类并不是特别多,出于时间以及空间性能的考量,我把 ID 设计为 16bits 。64K 种物品种类的上限看起来足够了。但 ID 的分配却比较麻烦。

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

January 20, 2023

最近开发中解决的一些性能问题

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

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

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

Lua 项目的优化无非两条路:使用 jit 技术、把热移到 C side 。正如 Roberto 所言,"Finally, keep in mind that those two options are somewhat incompatible" 这两条路需要的操作往往是互斥的。而我不喜欢 jit 带来的复杂度和不确定性,所以选择了后者。

为了解决大数量对象的批量操作问题,我在 2021 年引入了 luaecs 。这个库现在已经是引擎的核心,专门用来解决 Lua 处理大数据重复事务的性能问题。通过 luaecs ,我们可以把对象的初始化这个繁杂的过程使用 Lua 编写,而每帧都迭代的事务用 C 来实现。

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

January 10, 2023

虚拟文件系统的 mod 机制

上次谈游戏引擎的虚拟文件系统是去年 10 月了。最近我们又做了一些修改。

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

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

December 13, 2022

触摸屏上的一些交互设计问题

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

btw,直接在手机上开发比在 PC 上开发还有另一个好处,就是随时能关注画面元素(尤其是字体)的大小是否合适。

October 08, 2022

引擎 IO 模块的变化和发展

我们游戏引擎的 IO 模块其实一直在修改。最初的版本到现在有四年多了

一直没有定稿的一部分原因是因为我们给引擎设定了一个比较高的需求:引擎本身也是可以从网络自更新的。而更新引擎本身必然依赖 IO ,包括 IO 模块自身。而我们引擎又基于一个多线程版本的 Lua 框架 ltask ,ltask 本身也是需要依赖 IO 模块启动的。这些后续的设计要晚于最初 IO 模块的设计,反复重构也就是必然了。

September 30, 2022

货物和货车的匹配

我们在制作一款类似 Factorio 以自动化为主题的游戏。在游戏开发中遇到了不少算法上的问题,解决过程都非常有趣。比如之前就写过一篇 流体系统

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

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

September 02, 2022

路网中路径的储存

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

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

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

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

July 20, 2022

重构数学库

我们引擎中使用的数学库已经修修补补很久了。期间积累了很多想法。最近在对引擎做性能优化,把一些找到的热点系统用 C/C++ 改写,顺便就重构了一下数学库,让它更好的兼顾 Lua API 和 C API 。

上一次对数学库的改进是三年前的事情了。这三年的使用,我们发现,基于一个栈的 DSL 虽然可以减轻 Lua 和 C 之间的通讯成本,但是使用起来并不方便。大部分时候,我们还是倾向更传统的接口:一次数学运算调用一次函数。而高复杂度的数学运算完全可以放在独立的 C 模块中完成。结合 ECS 系统,我们可以在 C side 集中批量处理相同但数量巨大的数学运算。

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

July 08, 2022

ECS 系统中 Entity 的生命期管理

我们的游戏场景是由若干场景节点构成的,每个场景节点是一个 Entity 的 Component 。而一个复杂的场景可以在编辑器中生成一个预制件 Prefab,像搭建乐高积木那样堆砌已经做好的部件。关于预制件的设计,之前有过两篇 blog 讨论。分别是:游戏引擎中预制件的设计预制件和对象集的管理

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

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

July 06, 2022

分组功能在挂接系统上的使用

前段时间我们给引擎增加了对象分组的功能 。做这个特性的动机是当整个游戏世界中的对象数量远超同时需要处理(显示)的对象数量时,有一个快速筛选出待处理代码集合的机制。

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

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

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

May 19, 2022

RogueLike 原型开发工具

我很喜欢 RogueLike 游戏,我是说字面意义上的像 Rogue 那样的游戏 。在这种游戏中,画面是最不重要的部分,只要能清晰表达出游戏需要的交互意义就够了。

我对用字符拼凑出来的游戏界面有种特别的爱好,小时候自制游戏就是从 text mode 开始的。 在今天,如果只是想验证一下某个游戏的原型,一个 Rogue 风,text mode 的交互界面,可能还是最省事的。因为你不必刻意的去准备美术素材,考虑这些素材如何和代码协同工作,制定繁杂的工作流。为游戏创作一点有趣的 ascii art 不仅不用太长的时间,可能还是写代码过程中的一点有趣的调剂。

前段时间在做项目中一个试验性模块时,我尝试做了一个简单的 Lua 库 帮助我用 text mode 搭建交互。最近又碰到一个更复杂的需求,评估了一下,基于 SDL2 做一个更完善一点的 RogueLike 库或许更一劳永逸。

May 07, 2022

游戏数据的展示

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

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

传统的面向对象设计中,很多人倾向于按对象划分,每个对象有数据部分和展示部分。对于游戏,我不喜欢这种设计。我更倾向于把数据和展示完全分离,再用 id 把同一个对象关联其数据模块和展示模块中分离的实体。这是因为,游戏尤其是虚拟世界广阔的游戏,屏幕展示的仅仅是虚拟世界很小的一部分。在同一时间,大部分的数据都不必展示。甚至数据也未必存在于本地的内存中。

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

March 18, 2022

effekseer 的 shader 转译

我们的游戏引擎一开始是自己实现的粒子系统 。在实现完之后,做配套编辑工具的阶段,开发工具的同学建议换成其它开源的成熟系统,这样就不必花太多精力在维护一套工具了。

他推荐了 Effekseer ,并完成了 Effekseer 和我们引擎的整合工作。

最近,我在推特上看到有个同学也在寻找 bgfx 下的粒子系统的方案,他希望有一个比 bgfx 自带粒子演示更完善的东西,同时又表示整合 popcorn 实在是过于麻烦。我向他推荐了 effekseer 。

January 18, 2022

我们需要一个怎样的动画模块

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

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

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

January 10, 2022

流体系统

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

Factorio 的流体系统也经历过多次改动。在开发日志上有记载的就有三次:New fluid systemFluid optimisationsNew fluid system 2。作为一个 5 年近 2000 小时的老玩家,我感觉的到流体系统的修改一直在做,不只这三次;而且直到今天,流体系统在游戏中依然会出现一些反直觉的行为。

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

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

December 16, 2021

异星工厂的机械臂模块

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

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

October 12, 2021

球面地图网格的设计

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

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

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

September 18, 2021

Paradox 脚本语言的一点研究

因为一直给群星维护汉化 Mod 的缘故,我花了不少时间去理解 Paradox 的配置脚本语言。

我认为它用了类 lisp 的语法来描述数据。用数据去描述游戏逻辑。P 社的游戏都有玩家共建的 Wiki 提供了丰富的资源来帮助玩家创作 Mod 。学习它的脚本语言是非常容易的。

最近一段时间,我仔细阅读了 Wiki 中所有关于 Modding 的文章,相比之前零星的了解,算是做了一次系统的学习。

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

August 20, 2021

预制件和对象集的管理

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

最开始源于我重新设计了 ECS 框架。在新设计下,可以用 C/Lua 混合组织数据。为未来优化热点做好准备。我们借此机会重新思考了 ECS 框架下应该如何组织代码的问题。发现一个关键点就是,要尽量去掉系统中对象之间的引用关系。每类对象最好是成组分批的处理业务,每个模块都只做最简单的事情。但同一件事情尽量处理更多的数据、对象。

比如对象的构建和销毁,通常会随着对象构成的复杂度上升而演变为一件越来越复杂的事务。我们之间在设计 ECS 框架时,就设计了大量的机制来正确执行 Entity 的构建流程。一个 Entity 可以由若干 Component 类型动态组合,并非每个 Component 都能独立初始化。有时它们是相关联的。例如 A B C 组合成一个 Entity ,初始化 A 和 B 后,才能根据 A B 的结果初始化 C 。

March 22, 2021

fbx 到 gltf 转换问题

我们的游戏引擎采用的资源格式是 gltf 2.0

gltf 在这几年发展很迅猛,我认为是 3d 文件格式中标准化做的最好的一个。可惜,游戏行业中,美术创作人员常用的 max maya 等工具对其支持还有瑕疵。Autodesk 在 2019 年作为 contributor 成员加入了 Khronos 组织,在 max maya 这些 Autodesk 工具中看到官方的 gltf 支持应该不会等太久。来自官方的消息 ,‎在 2020 的 3 月底,gltf 加入官方支持已经处于 Under Review 状态。希望今年内可以看到。

December 04, 2020

粒子系统中的材质组织

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

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

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

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

November 27, 2020

粒子管理器的 C++ 封装

这篇接着上一篇 粒子系统的设计

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

我们用 C 实现了一个基于 ECS 结构的粒子系统的管理器,代码 psystem_manager.h 在这里

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

即:传统的面向对象的数据结构中,一个对象 particle 可以有很多属性 a,b,c 。通常是用一个结构体(或类)静态定义出来的,这些属性也可以看作是 a b c 组件,它们构成了粒子对象。而在 ECS 结构中,我们在每个时间点,并非去处理一个对象的多个属性,而是处理同一个属性的多个对象。所以,我们最好按属性分类将多个对象的同一属性聚合起来,而不是按对象,把同一对象的不同属性聚合在一起。

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

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

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

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

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

August 19, 2020

银河竞逐的设计

银河竞逐 RFTG 是我最喜欢的桌游之一。我认为直到今天,它仍旧是最棒的引擎建造类卡牌游戏。最近,我看了银河竞逐的作者 Tom Lehmann 在 GDC2018 上的演讲 非常有收获,所以写这篇 Blog 分享一下。

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

这样可以使得创造出 strategic tension 。

July 21, 2020

裁剪和空间管理

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

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

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

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

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

July 16, 2020

动态字模的管理

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

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

我在设计 ejoy2d 的时候实现过一版动态字模的管理。但我觉得略显简陋。最近重新做了一版。

July 09, 2020

游戏 UI 模块的选择

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

  1. 开发工具所用到的 UI 。
  2. 游戏本身所用到的 UI 。

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

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

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

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

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

June 05, 2020

层次结构和状态继承

在 blog 上,我写过好几篇关于场景管理模块的树结构的文章。这些也是我这两年在做游戏引擎中对象管理的思考历程。

通常游戏引擎中会把可渲染对象以树结构储存,这是场景管理模块最常见的作法。顺便说一句,GUI 界面也是用类似的方式。但是,我始终认为,从 gameplay 的层面上来看,游戏逻辑需要关注的对象并不需要用层次结构的方式管理。因为,空间结构上的层次很可能发生变化,从而引起关注的对象的层次路径变化。我们最终关注的那些东西不变,但它们在空间中的位置却会经常改变。

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

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

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

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

April 30, 2020

游戏引擎中预制件的设计

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

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

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

April 15, 2020

资源模块的重构

这篇是对 游戏引擎中的资源生命期管理问题 的延续。

最近对我们游戏引擎的资源模块做了一次重构,大概花了一周的时间,其中核心模块的代码实现花了 2 天。比之前的方案简洁很多。新方案的设计是基于以下原则来实现的:

  1. 引擎应该围绕数据来设计。ECS 更是数据驱动的模型。
  2. 数据全部都用一致的数据结构来表达,方便统一处理。因为我们采用 lua 做开发,所以,一切数据都是 lua table 。我们的引擎与其说是基于 lua 开发,不如说是基于 lua 的数据结构开发。即使某些模块因为性能因素用 C/C++ 实现,操纵的还是 lua table 。读写 lua table 的性能和读写 C struct / array 相比,并无显著的劣势。
  3. 在使用上尽量不区分外部不可修改的静态数据和运行期动态修改的数据。
  4. 惰性加载,延迟异步加载,替代资源,这些尽可能的隐藏起来,不必对外透露细节。尽可能的减少外部干预。
  5. lua 虽然有 metatable 这个神器,可以帮助我们抹平不同数据、不同策略之间的差异。但不要过多依赖语言特性。

April 10, 2020

场景层次结构的排序

这篇是对 场景层次结构的管理 的再思考。

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

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

具体是这样的:

March 11, 2020

不变量及运算优化

去年的时候,我们对正在开发中的游戏引擎做了一点 profile 工作。后来发现,在场景中对象很多的时候,有一处运算占据了 10% 以上的 cpu 时间。当时我的判断是,这处地方值得优化,但并不是工作重点,所以就搁置了。

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

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

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

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

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

October 10, 2019

三国志战略版服务器卡顿问题

我们的新作品 三国志战略版 上线有一小段时间了。市场反应不错,获得了许多玩家。随着玩家数量增加,服务器也产生了严重的卡顿问题,在每天高峰期尤其严重。

这个产品的服务器开发团队在立项之初并没有接触过 skynet ,可以说是从头学习起,在很短的时间内就完成了项目,还是很不错的。我没有参加过这个项目的开发,在问题显露的那几天正巧国庆假期在国外度假,远程参与了一些讨论。帮助分析了问题,等休假完毕后,昨天又开了一整天的会,大致了解了游戏的玩法(需求),结合前两天的思考,给了几套不同的改进方案。

September 16, 2019

场景层次结构的管理

今年上半年的时候,就想把我们游戏引擎中场景层次结构管理模块的设计记录一下。每次想写的时候都在做小调整。直到最近,算法和数据结构才稳定下来。今天做一个记录。

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

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

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

August 16, 2019

游戏引擎中的资源生命期管理问题

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

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

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

因为我们整个引擎的框架是用 lua 搭建,那么,最直接的想法就是利用 lua 自带的 gc 来回收那些不被引用的资源对象。我不太喜欢这个简单粗暴的方法。因为首先, gc 不会太及时,其次 gc 方法触发的时机很难控制,容易干扰正常的运行流程。图形显示模块是时间敏感的,如果因为资源释放占用了 cpu 的话,很容易变成肉眼可查的卡顿。

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

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

July 17, 2019

法线贴图的压缩格式比较

这几天一直在忙着在 bgfx 上增加 ASTC 格式的法线贴图支持

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

不过这种方法有一个问题,贴图采样的时候,由于是线性插值,所以计算出来的第三个向量可以和实际差的较远。通常的解决方法是保存球形投影的 X Y 分量,减少 Z 的偏差。具体可以看看 Nvidia 的这篇 Real-Time Normal Map DXT Compression

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

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

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

July 08, 2019

用 skynet 实现 unity 的 cache server

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

我不太想的明白,一个几乎不需要在内存中保留什么状态的服务,为啥会吃掉那么多内存。简单看了一下 cache server 的官方实现 感觉实现的挺糟糕的。它的业务很简单,还不如按协议自己实现一个。

February 18, 2019

最近对 ECS 框架的一些想法

我们的游戏引擎采用 ECS 框架。最近一年的开发,为 ECS 框架的应用积累了不少经验。我在 blog 上也写过数篇 ECS 相关的东西:

Lua 下的 ECS 框架

ECS 中的 Entity

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

January 31, 2019

设计了一个数据格式

最近一段时间在忙着设计和实现我们游戏引擎用到的数据格式。

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

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

January 09, 2019

粒子系统的设计

因为需要为我们的 3d engine 添加特效系统的模块,我最近读了一篇文章:Efficient CPU Particle Systems 。文章的作者为很多 MMO / MOBA 游戏设计过粒子系统,其中最有名的是上古卷轴 Online 。所以我认为他的实践很有参考价值。

文章很长,夹杂着设计思路,优化,算法实现,渲染实现。对于我来说,由于过去我做过好几版粒子系统,所以读起来不太费力,很多细节可以直接略过,我今天写一篇 blog 把我认为文章中对我最有参考价值的部分列出来。

January 08, 2019

一种 16 倍抗锯齿字体渲染的方法

昨天读了几篇文章,讲解了一种新的抗锯齿字体渲染的方法

我觉得颇有意思,就试着实现了一版 CPU 版本,想看看针对中文的效果。虽然最后觉得这个算法对游戏领域的实用性不大,不过还是挺有启发的。这里写写我对这个算法的理解,以及我所理解的算法局限性。

November 30, 2018

ECS 中的 Entity

我认为 ECS 框架针对的问题是传统面向对象框架中,对象数量很多而对象的特性非常繁杂,而针对对象的不同方面 aspect 编写处理逻辑会非常繁杂。每个针对特定的方面执行业务,都需要从众多对象中挑选出能够操作的子集,这样性能低下,且不相关的特性间耦合度很高。

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

那么,Entity 是什么呢?

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

September 20, 2018

动作游戏中的击打判定

最近在玩怪物猎人世界,断断续续差不多 100 小时了,加上之前花在这个系列上的几百小时,不敢说是个老猎人,忠实粉丝还是算得上的。

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

我知道动作游戏和格斗游戏侧重点不同,但也有类似之处。格斗游戏尤其是 2D 格斗游戏资料比较全,制作方法成熟,从网上能找到不少资料介绍原理。浅谈格斗游戏的精髓——方块的战争 讲的非常清楚。为了性能,也为了简化及明确规则,2D 格斗游戏用了若干 AABB 轴对齐的矩形来做击打/碰撞判定。格斗游戏会精确到帧来做规则判定,在特定帧勾勒出 hurtbox (惯例上用红色框)作为攻击范围判定;如果击打判定帧的 hurtbox 覆盖了那一帧对手的受击盒 hitbox (惯例用蓝或绿色框)区域,就认为击打有效。另外,还会设定出碰撞盒 collisionbox 用来避免和对手重叠,或和障碍物做碰撞检测。

August 27, 2018

lockstep 网络游戏同步方案

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

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

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

August 16, 2018

虚拟文件系统的自举

我们给游戏引擎设计了一个虚拟文件系统,可以挂接不同的文件系统实现,比如本地文件系统模块,内存文件系统模块,网络文件系统模块。比如前几天谈到的资源仓库,就是一个文件系统模块。

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

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

August 15, 2018

Lua 虚拟机的封装

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

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

其实这更接近 HTML5 流行之后,用 javascript 设计游戏引擎框架:虽然 javascript 的虚拟机本身是用 C++ 开发的,但和游戏引擎相关的部分全部用 javascript 实现,直到涉及渲染的部分,又通过 WebGL 回到 C++ 编写的代码中。这里,我只是把 javascript 换成了 Lua 而已。

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

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

让 Lua VM 置入 App 和操作系统打交道的这部分代码显然是平台相关的,Lua 的 C API 固然简洁,但是还是很庞大的。如果每个平台都直接用 Lua C API 控制虚拟机,这些平台相关的代码还是略显繁杂。我认为,把平台相关代码约束到一个足够小的范围,还需要对 Lua C API 再做一次抽象。

August 14, 2018

游戏资源仓库及升级发布

去年底,我为我们的 3d engine 设计了资源仓库的结构

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

这段时间,我重新思考了资源仓库应该怎样设计更合理。越细想越觉得和 git 要解决的问题基本一致。我们的引擎的一个重要特性就是,在 PC 上开发,在移动设备上运行调试。我们需要频繁的将资源同步到设备上,这其实和 git 的运作方式是类似的。我们重新实现的该模块在本地文件系统上的数据组织结构最终也和 git 仓库差不太多了。

January 12, 2018

通过斜切变换 2d sprite 提高装箱率

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

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

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

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

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

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

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

December 20, 2017

资源文件系统的设计

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

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

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

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

October 26, 2017

近期在 bgfx 上做的一些工作

这段时间玩 bgfx ,除了前段公布的 lua binding 外,还颇做了一些别的工作。

首先是帮 bgfx 的 directx 12 driver 打了补丁,使之可以用 mingw build 出来正确运行。也就是之前 blog 上提到的 Direct3D12 的接口设计 bug 。这个 pr 已经被官方接纳。

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

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

最开始提交的 pr 果不其然被拒了

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

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

这个 patch 现在可以在 bgfx 里使用 ,不过尚未合并到主干。看起来作者也没有拒掉,目前还留在 open 的 pr 列表中。

October 21, 2017

BGFX 的一个 lua 封装库

前两年有同学给我推荐了 BGFX 这个库,第一眼被它吸引是它的口号:"Bring Your Own Engine/Framework" style rendering library 。这动不动就说自己是 3d engine 的时代,好好做好一个渲染库,仅仅做好渲染库,是多难得的一件事情。

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

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

August 29, 2017

MMORPG 的同步设计

前段时间有一个友商来了一个技术团队到我公司交流,主要是想探讨一下他们即将上线的一款 MMORPG 游戏在内部测试中发现的网络卡顿问题是否有好的解决手段。

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

关于 MMORPG 的网络部分的设计,我之前写过很多 blog 。最近两年写过这样两篇:MMORPG 客户端的网络消息框架如何只基于请求回应模式实现 MMO 级别的场景服务

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

July 06, 2017

Paradox 的数据文件格式

Paradox 是我很喜欢的一个游戏公司,在所谓 P 社 5 萌中,十字军之王和钢铁雄心都只有浅尝,但在维多利亚和群星上均投入了大量时间和精力。

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

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

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

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

June 26, 2017

浅谈《守望先锋》中的 ECS 构架

今天读了一篇 《守望先锋》架构设计与网络同步 。这是根据 GDC 2017 上的演讲 Overwatch Gameplay Architecture and Netcode 视频翻译而来的,所以并没有原文。由于是个一小时的演讲,不可能讲得面面俱到,所以理解起来有些困难,我反复读了三遍,然后把英文视频找来(订阅 GDC Vault 可以看,有版权)看了一遍,大致理解了 ECS 这个框架。写这篇 Blog 记录一下我对 ECS 的理解,结合我自己这些年做游戏开发的经验,可能并非等价于原演讲中的思想。

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

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

这么做其实是有极大的缺陷的,我相信很多做过游戏开发的程序都会有这种体会。因为游戏对象其实是由很多部分聚合而成,引擎的功能模块很多,不同的模块关注的部分往往互不相关。比如渲染模块并不关心网络连接、游戏业务处理不关心玩家的名字、用的什么模型。从自然意义上说,把游戏对象的属性聚合在一起成为一个对象是很自然的事情,对于这个对象的生命期管理也是最合理的方式。但对于不同的业务模块来说,针对聚合在一起的对象做处理,把处理方法绑定在对象身上就不那么自然了。这会导致模块的内聚性很差、模块间也会出现不必要的耦合。

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

April 21, 2017

MMORPG 客户端的网络消息框架

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

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

客户端发起的请求分两种:一种是通知服务器,我扮演的角色状态发生了改变,请求服务器仲裁;另一种是,我期望获取服务器对象的最新状态。后一种有时以服务器主动推送来解决,也可以用主动请求,两者主要的区别在于流量控制。

April 10, 2017

为什么美术和策划在使用 git 时会遇到更多麻烦

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

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

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

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

April 06, 2017

Unity3D 的大场景内存优化

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

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

和 PC 环境不同,手机上是交换分区的机制来对应一些临时突发性内存需求的。而手机必须保证一些系统服务(某些高优先级后台业务)的运行,所以在接电话、收取推送等等意外任务发生时,有可能多占用一些内存,导致操作系统杀掉前台任务让出资源。

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

January 16, 2017

10 连抽保底的概率模型

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

但大 R 玩家是大爷,需要小心伺候。如果感受不好,人跑了就亏大了。概率这个东西靠改进伪随机数发生器是解决不了体验问题的,大爷要是连抽 20 张都出不来橙卡,那是要怒删游戏的。

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

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

January 08, 2017

在 Unity3D 的 Mono 虚拟机中嵌入 Lua 的一个方案

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

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

Mono 和 C 通讯使用 P/Invoke ,用起来不算麻烦,但是要小心暗地里做的 Marshal 的代价,特别是对象传递时装箱拆箱的成本。Lua 和 C 通讯有一套完善的 C API ,但完全正确使用并不容易。核心难点是 Mono 和 Lua 各有一套自己的异常机制,让它们协调工作必须很小心的封装两个语言的边界,不要让异常漏出去。我在 2015 年写过一篇 Blog 做过相关讨论

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

December 12, 2016

用分布式压缩贴图加快 Unity3D 的打包过程

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

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

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

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

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

November 16, 2016

ETC 图素的合并

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

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

缺陷 1 :装箱过程其实并不需要了解图片上的内容,而只需要知道图片的尺寸。所以装箱过程需要的内存量应该只和被装的图素个数相关,和图片大小无关。而 texture packer 并不是这样做的,它把装箱和合成贴图两部工作放在一个黑箱里了,会导致运行时无谓的内存消耗(更不用说它本质上是一个 GUI 程序),不是很适合自动化工具链。

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

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

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

October 17, 2016

继续谈网络游戏的同步问题

前面一篇谈了 放置类游戏的网络同步 ,我想把其方法推广到其它类型的游戏,比如 MMORPG ,比如动作游戏。尤其是动作类游戏,非常需要客户端可以即时处理玩家的操作,而不能等待服务器确认。

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

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

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

October 12, 2016

放置类游戏的网络同步

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

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

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

September 14, 2016

Shop Heroes 的公会系统

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

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

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

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

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

Shop Heroes 的经济系统

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

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

让我感兴趣的是游戏内部的经济系统,经过数十小时的游戏体验,我隐约感觉的到设计者希望设计出一个以玩家交易税收为主体的商业模式。而这种模式很多人都提出(我从 10 年前就反复考虑,在 blog 上也做过一些思考记录)并希望实现,可全部都失败了。比如著名的 Diablo 3 ,曾经就宣称自己打算用交易税来维持游戏的利润,最后因为破坏了游戏体验而关闭了现金拍卖场。国内也有征途等打着以交易税为收入来源旗号的游戏,而实际上却只是个幌子。

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

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

August 19, 2016

pvp 游戏如何解决玩家匹配等待时间过长的问题

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

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

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

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

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

August 11, 2016

群星的汉化及其它

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

这里有我写了一篇评测。大致谈了群星是一个怎样的游戏,如何快速入门。小提示:即使是新手,也推荐用铁人/疯狂模式。第一次玩只需要把银河调小一点就好了。这样乐趣才能充分体现出来。

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

需要的同学可以直接订阅。个人认为,利用 github 做合作创作,对于做汉化这件事特别合适。这个 mod 直接放在了 github 上

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

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

July 01, 2016

如何只基于请求回应模式实现 MMO 级别的场景服务

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

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

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

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

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

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

April 30, 2016

有创意必须实现出来才有意义

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

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

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

April 14, 2016

排行榜奖金的发放方法

最近有数据显示,我们的游戏《流星庄园》有玩家利用规则漏洞,通过排行榜奖励刷钻石。

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

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

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

February 16, 2016

用 2d 缩放及斜切变换模拟斜视角下的旋转

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

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

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

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

January 26, 2016

资源包的设计

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

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

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

July 29, 2015

第一次提交绿光

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

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

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

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

May 19, 2015

卡通图片的压缩

昨天看到一个好玩的项目 https://github.com/nagadomi/waifu2x 可以将卡通图片高质量的放大到 2x ,比直接用普通过滤器放大效果好的多。我试了将那张小的卡通妹子不断迭代放大到 8 倍,效果依然是很不错的。

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

November 04, 2014

RLA 文件中的法线信息提取

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

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

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

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

September 27, 2014

随机地形生成

玩过 矮人要塞 Dwarf Fortress 的同学都会惊叹于它的随机地形生成系统。如果你对 ascii art 无感,那么可以 google 一下 stonesense 的图片。

矮人要塞是 3d 的地形系统。在游戏 wiki 上有介绍地形生成系统的参数细节

大体上是这样的: 首先有一张高度图(Elevation),决定了每个坐标的高度。然后给出一张降雨图 (Rainfall),来影响当地的植被和河流。通常降雨图是根据高度图(海岸和季风影响)计算出来的。这里有一篇文章简单描述了这个过程。

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

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

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


August 02, 2014

Unity3D asset bundle 格式简析

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

网上能查到的资料并不是官方给出的,最为流行的是一个叫做 disunity 的开源工具。它是用 java 编写的,只有源代码,而没有给出格式说明(而后者比代码重要的多)。通过阅读 disunity 的代码,我整理出如下记录:


March 04, 2014

谈谈陌陌争霸在数据库方面踩过的坑( Redis 篇)

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

在陌陌争霸之前,我们并没有大规模使用过 Redis 。只是直觉上感觉 Redis 很适合我们的架构:我们这个游戏不依赖数据库帮我们处理任何数据,总的数据量虽然较大,但增长速度有限。由于单台服务机处理能力有限,而游戏又不能分服,玩家在任何时间地点登陆,都只会看到一个世界。所以我们需要有一个数据中心独立于游戏系统。而这个数据中心只负责数据中转和数据落地就可以了。Redis 看起来就是最佳选择,游戏系统对它只有按玩家 ID 索引出玩家的数据这一个需求。

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

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

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

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

March 03, 2014

谈谈陌陌争霸在数据库方面踩过的坑(芒果篇)

我们公司开始用 mongodb 并不是因为开始的技术选型,而是我们代理的第一款游戏《 狂刃 》的开发商选择了它。这款游戏在我们代理协议签订后,就进入了接近一年的共同开发期。期间发现了很多和数据库相关的问题,迫使我们熟悉了 mongodb 。在那个期间,我们搭建的运营平台自然也选择了 mongodb 作为数据库,这样维护人员就可以专心一种数据库了。

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

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

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

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

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

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

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

March 01, 2014

谈谈陌陌争霸在数据库方面踩过的坑(前篇)

陌陌争霸 这个项目一开始不叫这个名字,它在 2013 年中的时候,还只是一个我们公司 用来试水移动游戏的试验项目。最开始的目标很明确,COC 是打动我的第一款基于移动平台网络游戏,让我看到了和传统 MMO 不同的网络游戏设计方向。我觉得只需要把其中最核心的部分剥离出来,我们很快可以做出一个简单的却不同于以往 MMO 的游戏,然后就可以着手在此基础上发展。

至于后来找到陌陌合作,是个机缘巧合的故事。我们的试验项目完成,却没想好怎么推给玩家去玩(而这类游戏没有一定的玩家群体基本玩不起来),而陌陌游戏平台刚上线,仅有的一款产品(类似泡泡龙的游戏)成绩不佳。因为我们公司和陌陌的创始人都曾经在网易工作,非常熟悉。这款游戏也就只花了一个月时间就在陌陌游戏平台发布了。

一开始我们只把刚完成狂刃 启动器项目的阿楠调过来换掉我来做这个项目,我在做完了初期的图形引擎工作后,就把游戏的实现交给了他。我们只打算做客户端,因为只有这部分需要重新积累技术经验;而服务器不会和传统 MMO 有太大的不同。而我们公司已经围绕 skynet 这套服务器框架开发有很长一段时间了,随时都可以快速把这个手游项目的服务器快速搭建起来。

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

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

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

February 10, 2014

如何让玩家相信游戏是公平的

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

为了保障庄家利益,三个相同的数字算不大不小。从概率上讲,这让长时间内庄家必胜。概率分析见这里

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

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

January 12, 2014

COC Like 游戏中的寻路算法

同样是一篇老文章, 2013 年初我刚玩 COC 的时候写给公司内部分享的。由于当时公司还没有决定开手游项目,但有意向做一款 COC Like 的产品,并希望开发期间保密,所以相关技术文章都没有公开。

目前,我们的游戏上线,就可以把当初写的东西陆续贴到 blog 上了。


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

January 11, 2014

斜视角游戏的地图渲染

这篇文章是在大半年前, 我们的手游 刚刚做预研的时候写给公司内部分享的。由于这个项目公司不希望在开发阶段对外暴露信息,所以文章也没有在 blog 贴出来。

现在游戏已经上线 就没有消息保密的需求了,我就可以把当初开发时写的一些东西逐步贴出来。


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

            /\
           /  \
          /    \
         /     / /\
  /\    /  B  / /C \
 /A \  /     /  \  /
 \  / /     /    \/
  \/ /     /
     \    /
      \  /
       \/

December 28, 2013

Ejoy2D 开源

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

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

我们这个项目还是做的太急了,只花了一个月时间做引擎,然后在上面做了太多应急的各种修改,到项目完成(10 月)时,已经很不堪了。我不想把这块东西带到下一个产品,所以打算重新把这块代码理一下。

开源是一开始就想做的事情,可以帮助到别人,也同时督促自己把代码写的更整洁。从这周开始,我满头写了一周代码,从头写了 4000 行代码,就有了这么一个项目:ejoy2d

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

September 03, 2013

字体勾边渲染的简单方法

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

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

我们的手游项目以及 3d 端游项目都大量使用了勾边字体,我希望直接利用系统字体而不用离线预处理字体把字体文件打包到客户端中。前段时间还专门实现了一个动态字体的贴图管理模块 。btw, 苹果的平台提供了高层 API 可以直接生成带勾边效果的字模。

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

April 02, 2013

动态字体的贴图管理

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

即,动态生成一张贴图,把最近常用的汉字画在上面。几乎所有成熟的基于 3d api 的图形引擎都需要有相关的模块才可以对汉字更好的支持。但我到目前为止,还没有看到有把这个模块独立出来的。大多数开源引擎都是在自己的框架内来实现差不多的功能。我觉得,这部分管理功能和如何管理贴图其实没有关系、和取得字形的方法也没有关系、不必和 3d api 打交道,也不用涉及到底用 freetype 还是 os 自带的 api 取得汉字字形,所以值得独立实现。

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

我设计了如下接口:

struct dfont_rect {
    int x;
    int y;
    int w;
    int h;
};

struct dfont * dfont_create(int width, int height);
void dfont_release(struct dfont *);
const struct dfont_rect * dfont_lookup(struct dfont *, int c, int height);
const struct dfont_rect * dfont_insert(struct dfont *, int c, int width, int height);
void dfont_flush(struct dfont *);

February 22, 2013

Clash of Clans

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

对于移动平台,最大的不同在于,玩家的游戏时间碎片化。所以能集中精力的游戏持续时间不宜过长。应该在五分钟以下。当然,也需要提供玩家长时间持续游戏的途径。

玩家的游戏时间很自然的分为了在线时间和离线时间。所以,离线玩法相当重要。

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

January 14, 2013

Pixel light 中的场景管理

这几天无意中发现一款开源的 3d engine ,名为 pixel light文档 虽然不多,但写的很漂亮。从源码仓库 clone 了一份,读了几天,感觉设计上有许多可圈可点的地方,颇为有趣。今天简略写一篇 blog 和大家分享。

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

这款引擎开发历史并不短(从 2002 年开始),但公开时间较晚(2010 年),远不如 OGRE 等引擎有名。暂时我也没有看到有什么成熟的游戏项目正在使用。对于没有太多项目推动的引擎项目,可靠性和完备性是存疑的。不推荐马上在商业游戏项目中使用。但是,他的构架设计和源代码绝对有学习价值。

December 25, 2012

模糊逻辑在 AI 中的应用

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

在《游戏人工智能编程案例精粹 》 和 《 Windows 游戏编程大师技巧 》 中都分别有一章谈及模糊逻辑。记得前几年我的同事 Soloist 同学曾经研究过一小段时间,给我做过简单介绍,我便仔细把这两章书读了一遍。感觉都是点到为止,所以又翻了一下 Wikipedia 的 Fuzzy Logic 的介绍。午饭时跟做 AI 的同事交流了一下,觉得可以做一点笔记记录理解的部分。


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

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

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

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

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

December 06, 2012

网络游戏中商人系统的一点想法

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

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

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

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

November 23, 2012

相位技术的实现

魔兽世界从巫妖王之怒那个版本开始完善了他的 相位 (Phasing) 技术。简单说,就是在场景的相同区域,不同的玩家根据一些条件(一般是任务进度)的不同,会感受到不同的环境:场景可以不同、能交互的 NPC 不同等等。

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

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

November 21, 2012

开发笔记(29) : agent 跨机 id 同步问题

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

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

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

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

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

November 01, 2012

开发笔记(28) : 重构优化

正如上一篇笔记 记载的,我们第 2 里程碑按计划在 9 月 30 日完成,但因为赶进度,有许多 bug 。性能方面也有很大问题,大家都认为需要重构许多模块。所以,在最后几天修补 bug 时,许多补丁是临时对付的(因为整个模块都需要重写了)。为此,我们留下了一个月专门重构代码、修改 bug 、并对最后的结果再做一次评测。

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

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

之前我曾经提到 ,我们的老系统处理 80 人同一战场混战就让服务器支撑不住了。当时我们的服务器 CPU 达到了 790% 。虽然我们的服务器硬件比较老,配置的是两块 Intel Xeon E5310 @ 1.60GHz ,更新硬件可以有所改善。但这个结果绝对是不能满意的。从那个时候起,我从重写最底层框架开始一步步起着手优化。

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

ps. 参考这份报告 ,我们计划采购的 [Dual CPU] Intel Xeon E5-2620 @ 2.00GHz Benchmark 16707 分,而目前使用的 [Dual CPU] Intel Xeon E5310 @ 1.60GHz 仅 4160 分。即使仅考虑单线程分数,也在两倍以上。

October 19, 2012

星际争霸2编辑器的初接触

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

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

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

游戏开发和很多其他软件开发的一个巨大区别就是,你无法把程序得到正确结果当成任务的完成。运行时间往往成为重要的约束条件。如果一件事情在规定的时间片执行不完,代码实现的再正确都没有意义了。而局部的优化热点往往也意义不大。因为如果只是需要小幅度的提高性能,那么采购好一些的硬件就够了。一个模块的性能,要从数量级上的提高,必须重新思考需求,改变需求,重定义我们要做什么。

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

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

有限的时间内,我没有从网上找到太多的相关资料。在暴雪的官方网站也没能看到完整的文档。星际2 的编辑器内建了一个叫做银河(Galaxy)的脚本语言,似乎所有 GUI 界面上的编辑器操作,都可以完整的对应成一段 galaxy 脚本。很可惜的是,暴雪似乎鼓励玩家用 GUI 编辑器创造地图,而脚本只是背后之物,galaxy 的手册我并没有找到。

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

October 11, 2012

开发笔记(27) : 公式计算机

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

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

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

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

在半年前,我曾经参于过这部分的设计与实现。参见之前的一篇开发笔记

不过在写完这个模块后,我的工作从这部分移开。其他同学接手正式实现策划的需求时,并没有使用我实现好的模块。因为策划的需求更为复杂,最终采用了更间接和可配置的方案。相关的实现我未能跟进,Mike 同学就用 Lua 实现了一个基本等价的东西。

September 20, 2012

开发笔记(26) : AOI 以及移动模块

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

加了一些时间检测模块。由于已经重新编写了服务器底层框架,并修改了消息协议,这样底层就有能力监控每条服务的相应时间了。

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

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

不过,经过实测,大量的 CPU 时间是消耗在 lua 虚拟机内部。也就是说,在底层框架上做改进已经没有多少可以压榨的了。或许进一步优化通讯协议还有一些工作可以做(合理的协议可以使得上层的数据处理更简洁),不过,工作中心移到 Lua 层面会更有效一些。

相关工作从多个方面入手。

首先想到的是使用 LuaJIT 。不过,我们一开始是工作在 Lua 5.2 上面,而 LuaJIT 似乎不打算完全支持 5.2 版,我需要适度的做一些向前兼容工作。之前我已经记录了一些相关的东西 。在写完上篇 blog 后,我加入了 LuaJIT 的 mailinglist ,LuaJIT 的作者相当的勤奋,基本上提交的 bug 都在半天内可以修正;大多数合理的功能需求也可以满足。或许向前兼容这个事情不一定需要完全自己搞定。我们不需要完全支持 Lua 5.1 ,只需要支持 LuaJIT 2.0 即可。那么,推动 LuaJIT 2.0 去支持一些 Lua 5.2 的新特性也不错。比如就在昨天,LuaJIT 支持了 Lua 5.2 中增强的 debug.getlocal 。

由于我们的进度压力,LuaJIT 的兼容问题暂时搁置了。还有一些工作需要其他同学的协力支持,而他们,正在为努力完成游戏特性而努力。暂时不太可能安排的出时间来做这些调整。

September 03, 2012

Skynet 设计综述

经过一个月, 我基本完成了 skynet 的 C 版本的编写。中间又反复重构了几个模块,精简下来的代码并不多:只有六千余行 C 代码,以及一千多 Lua 代码。虽然部分代码写的比较匆促,但我觉得还是基本符合我的质量要求的。Bug 虽不可避免,但这样小篇幅的项目,应该足够清晰方便修正了吧。

花在 Github 上的这个开源项目上的实际开发实现远小于一个月。我的大部分时间花了和过去大半年的 Erlang 框架的兼容,以及移植那些不兼容代码和重写曾经用 Erlang 写的服务模块上面了。这些和我们的实际游戏相关,所以就没有开源了。况且,把多出这个几倍的相关代码堆砌出来,未必能增加这个开源项目的正面意义。感兴趣的同学会迷失在那些并不重要,且有许多接口受限于历史的糟糕设计中。

在整合完我们自己项目的老代码后,确定移植无误,我又动手修改了 skynet 的部分底层设计。在保证安全迁移的基础上,做出了最大限度的改进,避免背上过多历史包袱。这些修改并不容易,但我觉得很有价值。是我最近一段时间仔细思考的结果。今天这一篇 blog ,我将最终定稿的版本设计思路记录下来,备日后查阅。

August 29, 2012

开发笔记(25) : 改进的 RPC

自从动了重新实现 skynet 的念头,最近忙的跟狗一样。每天 10 点醒来就忙着写代码,一句废话都不想说,一直到晚上 11 点回家睡觉。连续干了快一个月了。

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

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

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

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

另一方面,一旦固定采用单进程多线程方案,之前的多进程共享数据的模块就显得过于厚重了。新的方案更为轻量,也更适合 lua 使用。这项工作在上一篇 blog 中提到过。这和 skynet 的重写原本是两件事情,但我强行放在一起做迁移,增加了许多难度。但考虑到,原本我就需要梳理一次我们的全部服务器端代码(包括大量我没有 review 过的),就把这两件事情同时做了。

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

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


今天想重点谈谈下面一段时间我希望做的改进。是关于服务间 RPC 的。

August 17, 2012

Skynet 的一些改进和进展

最近我的工作都围绕 skynet 的开发展开。

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

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

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

August 06, 2012

Skynet 集群及 RPC

这几天老在开会,断断续续的拖慢了开发进度。直到今天才把 Skynet 的集群部分,以及 RPC 协议设计实现完。

先谈谈集群的设计。

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

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

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

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

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

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

为了缩短开发时间,我利用了 zeromq 来做 harbor 间通讯,以及 master 的开发。蜗牛同学觉得更高效的做法是自己用 C 来写,并和原有的 gate 的 epoll 循环合并起来。我觉得有一定道理,但是还是先给出一个快速的实现更好。

July 29, 2012

开发笔记(24) : Lua State 间的数据共享

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

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

最早在设计的时候,是按多进程共享的需求来设计的。希望不同的进程可以利用共享内存来共享一组结构化数据。所以实现了这么一个东西 。这个东西实现的难点在于:一、共享内存不一定在不同进程间有相同的地址,所以不能在结构中用指针保持引用关系;二、不希望有太复杂的锁来保证并发读写的安全性。

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

最后,就完成了这么一个东西。我认为到目前这个阶段,这个模块还是比较独立的,适合开源分享。以后的工作可能会和我们具体项目的模块整合在一起,还需要做一些修改,就不太适合公开了。有兴趣的同学可以在我的 github 上看到代码。https://github.com/cloudwu/lua-stable

July 19, 2012

开发笔记(23) : 原子字典

问题是早就提出了的。在 开发笔记 18 中,就写到一个需求:一个玩家数据的写入者,可以批量修改他的属性。但是,同时可能有其他线程在读这个玩家的数据(通过共享内存)。这可能造成,读方得到了不完整的数据。

我们可以不在乎读方得到某个时间的旧数据,但不可以读到一份不完整的版本。就是说,对玩家数据的修改,需要成组的修改,每组修改必须是原子的。

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

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

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

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

July 13, 2012

开发笔记(22) : 背包系统

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

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

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

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

June 11, 2012

开发笔记(20) : 交易系统

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

物品掉落和交易,本质上是一件事情。区别在于玩家之间的物品交换于玩家和系统之间的物品交换。这部分的设计,我在去年初曾经写过一篇 blog 总结过。

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

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

在游戏服务中,玩家或其他实体储存的物品,同样会保存在玩家数据中,利用共享内存读写 。 每件物品记录在对应的包裹格类,直接记录物品名称或指代名称的 id 。即,同样的物品用同样的标识。之外,再额外记录一个(或若干个)物品唯一 id (用 64bit 或 128bit ) 。这样做,避免在访问物品的功能时,额外去查一张表。

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

May 15, 2012

开发笔记(19) : 怪物行走控制

这段时间项目进展还算顺利,叮当同学在盯项目进度,我专心解决程序上的各种小问题。

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

目前,我们的进度还处在玩家可以通过客户端登陆到服务器,可以在场景上漫游,以及做一些简单的战斗和技能动作的阶段。按最初的设计原则,我们的每个玩家是在服务器上有一个独立的 agent 服务的。目前写到现在,和中间想过的一些实现方法有些差异,但大体上还是按这个思路进行的。 关键是在于去除大部分的回调方式的异步调用;编写的控制流程自然完整,不需要太多的去考虑 agent 行为之外的交互性。

如果丝毫不考虑性能问题,我很想把每单个怪物和 NPC 都放在独立服务中。但是估算后,觉得不太现实。在 这一篇 的最后一段已经提过这个问题了。

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

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

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

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

这个子系统是这样的:

April 18, 2012

开发笔记(17) : 策划表格公式处理

前段时间为策划提供过一些技术上的支持, 设计过一个简单的 DSL 。但随着更多策划加入团队,我觉得这个思路不能很好的贯彻下去。

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

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

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

我更多看到的是一种声明式的表达,而不是过程式的。比如,策划会定义,“血量”这个属性,实际上是等价于“耐力 * 10”。我们把后者定义为一个公式。

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

对于这种声明式表达,程序要做的工作是进行一次拓扑排序,把需要先求值的属性排在前面,有依赖关系的属性求解放在后面。这样就可以转换为过程式的指令。

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

法术伤害物理伤害
战士0.51
法师10.5

March 31, 2012

开发笔记(16) : Timer 和异步事件

这几天,安排新来的王同学做数据持久化工作。一开始他是将 sharedb 里的数据序列化为文本储存的。这步工作做完后,开始动手把数据放到 Redis 数据库中。我们的系统主干由 Lua 构建,所以需要一个 Lua 的 Redis 库。google 来的那份,王同学不满意。三下五除二自己重写了一个。据说把代码量减少到了原来的三分之一(开源计划我正在督促)。唯一的问题是,如果直接采用系统的 socket 库,不能很好的嵌入我们的整个通讯框架。我们的 skynet 全部是通过异步 IO 自己调度的,如果这个数据服务单方面阻塞了进程,会使得别的进程获得不了时间片。

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

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

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

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

March 26, 2012

开发笔记(15) : 热更新

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

这部分的工作,我曾经在过去的一个项目中尝试过 。这些工作,在当时一段时间与广州网易其他项目交流时,也对网易其他项目的设计产生过一些影响,之后,也在实战中,各个项目组逐步发展出许多热更新的系统来。

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

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

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

按我们现在的游戏服务器设计,大多数服务也会遵循这个结构,所以许多底层的子模块简单重启进程就可以了。但和游戏逻辑相关的一些东西却可能要考虑更多东西。

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

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

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

March 22, 2012

开发笔记(14) : 工作总结及玩家状态广播

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

pbc 自从开源以来,收到了公司之外的许多朋友的反馈。因此找到了许多 bug ,并一一修复了。一并感谢那些在不稳定阶段就敢拿去用的同学。我相信 bug 会初步趋进于零的,会越来越放心。

之前的 RPC 框架 经历了很大的修改,几乎已经不是一开始的样子了。简化为主。几乎只实现了简单的一问一答的远程调用。但在易用性上做了更多的工作。coroutine 是非常重要的语言基础设施,所以这个很依赖 lua 。主要就是提取出函数定义的元信息,把远程调用描述成简单的函数调用,把函数的参数以及返回值映射为通讯协议中的消息结构。这部分和整个框架结合较紧,本来想做开源,但不太想的明白怎么分拆出来。

skynet 是由蜗牛同学用 erlang 实现的,原来是很依赖每个服务的 lua 虚拟机来跑具体应用。最近一个月,我们把 lua 虚拟机从 skynet 模块中完全剥离出来了。而服务则仅以 so 的形式挂接到框架中。lua 服务做为一个具体应用存在。这样,独立启动 lua 和 luajit 也是可以的了。这个过程中,因为 lua 的链接问题 ,我们再次调整了 makefile 中的链接策略。完整的读过了 dlopen 的文档,把每个参数都熟悉了一遍,要避免插件式的 so 服务工作时不要再出问题。

上面这个工作的起因之一是,在我们这里实习的同学回学校了,把他在做的 sharedb 的部分工作交接到新同事手里。需要把这个模块整合进 skynet ,然后围绕它来实现后面完整的场景服务。在一开始我实现的时候,因为早于 skynet 的实现,我用到了 zeromq 做一些内部通讯,这次想一并拿掉。由于不想在这个 C 写的底层模块上嵌入 lua 来写服务,所以整理了框架的 C 接口。

sharedb 这一系列的工作中还包括了数据格式的定义,解析,持久化。我们也做成了独立的服务。关于持久化,目前暂时用的文本文件,尚未 整合入 redis 。具体数据储存怎样做,是以后的独立工作可以慢慢来。毕竟在运行期都是直接通过共享内存直接访问的。

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

March 06, 2012

开发笔记 (13) : AOI 服务的设计与实现

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

所谓 AOI ( Area Of Interest ) ,大致有两个用途。

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

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

在服务器上,我们一般推荐把 AOI 模块做成一个独立服务 。场景模块通知它改变对象的位置信息。AOI 服务则发送 AOI 消息给场景。

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

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

第二,空间切割监视的方法。把场景划分为等大的格子,在每个格子里树立灯塔。在对象进入或退出格子时,维护每个灯塔上的对象列表。对于每个灯塔还是 O(N * N) 的复杂度,但由于把对象数据量大量降了下来,所以性能要好的多,实现也很容易。缺点是,存储空间不仅和对象数量有关,还和场景大小有关。更浪费内存。且当场景规模大过对象数量规模时,性能还会下降。因为要遍历整个场景。对大地图不太合适。这里还有一些优化技巧,比如可以把格子划分为六边形 的。

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

接下来我要来实现这个 AOI 服务了,仔细考虑了一下,我决定简化以前做的设计

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

March 03, 2012

开发笔记(12) : 位置同步策略

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

哦,这里值得一提的是,我写的 protobuf C 库 慢慢算是可用了。自从提交到 github 上后,有两处 bug 是不认识的网友指出的。当然我自己在用的过程中发现修正的 bug 更多。现在算是基本完善了吧。接下来还会大量使用到,等整个项目做完,应该就基本没问题了。目前主要是用它的 lua binding 。为了用起来更方便,昨天我甚至自己实现了一套 proto 文件的 parser ,作为一个选项,可以不依赖 google 官方提供的工具来编译了。

前两天我们开程序例会。dingdang 主持会议。提到,在工作全面展开前(还有几个程序和策划没有到位),我们最后的一点时间应该把一些技术点解决掉。其中之一就是解决好即时战斗游戏中的位置同步问题。要做到好的操作手感不太容易,至少现在看到的国内的 MMO 没有做的特别让人满意的。我们比较熟悉的网易的产品,天下二,这方面就比较糟糕。

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

话说到这里,我想起 6 年前,我们就做过这方面的探索 ,并写了许多代码验证。花了不少的时间。这次怪物公司同学回头又开始看 paper 重新研究。当年和我一起做这块的人不在了,换了一拨人,感觉场景仿佛相识。不过这次技术储备更完备一些,许多工具,Engine 什么的也完整。应该会更顺利吧。

这块 dingdang 认为很重要,如果一开始不做好,以后没完没了的任务堆过来,就不再能回头弄了。天下就是如此。我当年也是这样想的,只不过花了太多时间去弄了,项目开发的节奏控制的不好。下面把我当初理的思路在整理一次,重新列出来,算是个记录。

February 14, 2012

开发笔记 (10) :内存数据库

离上一次写 开发笔记 快有一个月了。当然,中间我们放了 10 天的长假。

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

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

话要说回 开发笔记第六篇 ,我谈过结构化数据的共享存储。这个模块细化其实有挺多工作要做。核心部分我自己完成了,没有用太多时间。然后,有位华南理工的同学想来实习。想到他们学校距离我们办公室仅有十分钟步行距离,我便考虑让 logicouter 同学过来试试接手这个模块,做后续开发。当然,他不能全职来做,对旧代码也需要一定时间熟悉,进度比较慢。

我的规划大约是这样的:

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

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

January 09, 2012

开发笔记 (8) : 策划公式的 DSL 设计

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

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

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

在生成动画树的数据方面,我们的交换格式使用了某中文本中间格式,最终利用 protobuf 来做二进制持久化。在解析文本格式方面,我操起了多年前耍过的 LPEG 。(我曾经用 lpeg 写过 protobuf 文本的解析工具)这个绝对是解析文本的利器啊。午饭时在食堂大家围在桌子边吐槽 Java ,说道 Java 社区最二的莫过于抓着个 XML 当救命稻草不放,所以便有了各种淡疼的基于 XML 的框架。如果早期 Java 社区能多那么几个受过 Unix/C 传统熏陶过的程序员,就能知道设计 DSL 来解决问题是多么爽快的事情。也不至于在 XML 的树上吊死了。唉,搞得现在积重难返了。

下午正式和策划进行沟通,观看他们这段时间写的各种 excel 表格。我说,你们放开了想怎么把问题表达清楚吧,只要逻辑清晰有条例,信息不要漏掉,怎么表达那些公式都行。我慢慢看,然后规范写法,最终方便程序解析。

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

December 15, 2011

开发笔记 (6) : 结构化数据的共享存储

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

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

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

我个人比较有信心解决高性能的进程间数据共享问题。上一篇 谈的其实也是这个问题,只是这次更进一步。

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

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

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

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

方案如下:

December 07, 2011

开发笔记 (5) : 场景服务及避免读写锁

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

这部分问题,前不久思考过 。需求归纳如下:

  1. 每个 agent 可以了解场景中发生的变化。
  2. 当 agent 进入场景时,需要获取整个世界的状态。
  3. agent 进入场景时,需要可以查询到它离开时自己的状态。关于角色在场景中的位置信息由场景服务维护这一点,在 开发笔记1 中提到过。

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

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

请求回应模式最直观的方式就是设计成异步接口,柔韧性更好。适合分离,但性能不易优化。流程上也比较难使用。(使用问题可以通过 coroutine 技术来改善 )即使强制各个进程(不一定是 os 意义上的进程)在同一个 CPU 上跑,绕开网络层,也很难避免过多的数据复制。虽有一些 zero-copy 的优化方案,但很难跨越语言边界。

December 04, 2011

开发笔记 (4) : Agent 的消息循环及 RPC

话接 开发笔记1 。我们将为每个玩家的接入提供一个 agent 服务。agent 相应玩家发送过来的数据包,然后给于反馈。

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

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

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

游戏服务逻辑复杂度很高,比起很多 web 应用来说,要复杂的多。我们不能简单的把外界来的请求都看成的独立的。把输入包设计成 REST 风格,并依赖数据服务构建这套系统基本上行不通。几乎每个包都有相关的上下文环境,就是说,输入和输入之间是有联系的。简单的把输入看成一组组 session 往往也做不到 session 间独立。最终,我把游戏服务器逻辑归纳以下的需求:

November 19, 2011

游戏数值策划

这篇很淡疼的文章来源于我在微博上的争论。需要列数据,字数限制不合适,所以单列一篇了。

昨天翻出了元帅同学的一篇旧文,游戏数值设计(1):定义与目标 , 转发到微博上。我一向是喜欢看他吐槽的,这篇吐国内的 MMORPG 游戏策划分工的文章,深得我心。

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

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

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

November 18, 2011

开发笔记 (2) :redis 数据库结构设计

接上回,按照我们一期项目的需求,昨天我简单设计了数据库里的数据格式。数据库采用的是 Redis ,我把它看成一个远端的数据结构保存设备。它提供基本的 Key-Value 储存功能,没有层级表。如果需要两层结构,可以在 Value 里保存一组 Hashes 。

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

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

November 16, 2011

开发笔记 (1)

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

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

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

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

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

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

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

October 22, 2011

游戏收费方式的一点思考

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

回想那一年,所谓免费游戏之风逐步侵蚀中国网络游戏市场之时,网易的开发团队起初是有所抵触的。我刚到杭州不久,远离广州的开发团队,静心思考了许多。所谓公平的游戏,我想我比大多数人都在乎。把玩游戏变成一个金钱投入的游戏,我是万般不愿意的。但我相信道具收费的方式有它内在的逻辑。实际上,在网易成熟的时间收费游戏,梦幻西游中,点卡交易系统早就存在,并良性运作了很久了。许多玩家已然在游戏中花掉了远远超过时间收费收取的金钱。

那段日子,记得 wding 和我讨论过道具收费的问题。我想了很多谈了很多。后来,wding 希望我回一趟广州,把我的想法和广州的策划同事们分享一下。我便去了趟广州,与各个项目的策划同事开了一天的会。主要是我在讲,但也了解了许多我想了解的具体数据。回来后,写了这么一篇 blog 。可以说,这是我第一次书面总结对时间收费之外的思考。

又经过了 5 年多,市场已经证明了时间收费的局限性。但我依然认为,对游戏本身来说,纯粹的时间收费最容易保证游戏世界构建合理。作为设计者,我们抛开挣钱的问题不谈,时间收费可以保证经济系统做良性发展。当我们把网络游戏真的看作一个虚拟世界时,不干涉虚拟世界中的活动是最好的。玩家的时间固定,玩家的规模容易保持在一定规模,那么玩家整体在虚拟世界中的创造活动就可以被估算出来,达到一个平衡。我们的数值设计都是围绕这点而做的。

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

游戏的公平性问题反而是次要的,无论如何,我们无法阻止游戏中的玩家用金钱来换取时间。除非完全禁止掉物品交易,但那恐怕已经不是网络游戏了。

August 10, 2011

MMORPG 中场景服务的抽象

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

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

每条状态信息其实是由三部分构成,状态对象名(key)、状态值(value)、时间。

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

June 10, 2011

传统 MMORPG 通讯模式实现的一点想法

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

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

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

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

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

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

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

May 31, 2011

游戏开发中美术资源的管理

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

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

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

April 11, 2011

如果从头开发新的 3d engine

最近闲了下来。断断续续的再想,如果业余时间弄一个开源项目玩儿,什么是比较好的题材,适合自己来做。想来想去,3d engine 是一个比较好的选择。以我来看,对外开源如果要吸引到足够的有经验的同学参加,必须项目有一定价值。要可以迅速的搭建工作环境并直接看到结果,这样才能有兴趣迭代做下去。起步必须自己一个人来做,东西得有个雏形。所以必须是自己比较熟悉的领域。

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

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

March 16, 2011

服务器排队系统的一点想法

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

以前考虑过这个问题,写过一篇 blog 。我今天想到,其实可以把排队完全独立出来。和原有系统分离。这样,所有不支持排队的游戏系统,只要简单加上就可以用了,不用对系统结构做大的调整。

想法是这样的:

March 09, 2011

梦幻西游服务器 IO 问题

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

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

最终,当磁盘操作堆积起来,尤其是阻塞方式请求,并行的过程全部都在磁盘访问处串行起来了。固然整个系统的处理能力并没有下降。但用户的反应时间却会变长。

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

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

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

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

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

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

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

February 25, 2011

ZeroMQ 的模式

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

最近读完了 ZeroMQGuide。写的很不错。前几年一直有做类似的工作,但是自己总结的不好。而 ZeroMQ 把消息通讯方面的模式总结的很不错。

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

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

January 26, 2011

极不和谐的 fork 多线程程序

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

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

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

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

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

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

January 12, 2011

网络游戏物品校验系统的设计

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

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

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

数据存储和合法性校验应该分开,独立为不同的服务,这样才容易做的稳固。也就是说,数据服务不必做到完全的完备,简单的去读写修改数据即可。这部分,我倾向于用简单的 key/value 方式储存数据到数据库,可以自己实现,也可以用 redis 这样的现成产品。不必使用事务机制。

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

January 06, 2011

梦幻西游服务器的优化

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

前两天说起梦幻西游服务器的优化。这几天我到广州住下来,打算专门花一周时间搞定这件事。由于以前都是网上聊天,只有坐到一起才能真正理解问题。

目前,梦幻西游,只使用单台机器,最高配置 8 个 CPU ,配置 8G 内存。就算最热闹的服务器,也用不完这些资源(大约只用满了 3 个 CPU ,一半的内存)。核心程序差不多就是 10 年前写的,从大话西游延续至今。这两年一直在享受免费的午餐,随着硬件配置提升,现在单台服务器同时在线容量达到一万两千人。观察服务器回应速度的图表可以发现,目前的问题在于,定期会出现反应迟钝的现象。周期性的,服务器回应时间会超过 1000ms 。查得原因在于那个时候,磁盘 IO 非常拥塞。有定期保存玩家数据的服务对 IO 的占用,以及 SA 做的定期备份数据的脚本占用了大量的 IO 时间。最终造成了机器负荷过重。

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

December 24, 2010

梦幻西游服务器 IO 的一点优化

关注梦幻西游服务器的性能问题,是源于前几天跟同事的聊天。谈到能否把梦幻西游服务器做成无盘站,或是放进虚拟机里,便于日常维护管理。

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

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

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

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

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

November 05, 2010

多进程资源共享及多样化加载

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

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

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

August 31, 2010

在游戏引擎中播放视频

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

第一件事是考察 License 。结果用的人做多的 ffmpeg 是 GPL 的,不适合我这种商业应用。虽然有一部分功能可以以 LGPL 的方式使用,但是遵守起来也是麻烦一大堆。想了一下还是做罢。我可不想学暴风影音和 QQ Player 那样上耻辱榜

最终考察结果,居然没太多的选择。还是 google 的 vp8 最好。License 最为宽松,所以就去下载了一份 libvpx 的最新版试用。

August 26, 2010

游戏资源的压缩、打包与补丁更新

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

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

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

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

和许多其它游戏 Client (比如暴雪的 MPQ 文件)不同。我们的包格式里文件与文件之间是允许有空洞的。这是考虑到资源包文件都比较大。如果用传统的打包软件运作的方式:从包内删除一个文件,就重新打包或移动内部数据。在玩家更新资源的时候,就会有大量的文件 IO 操作。比如 WOW 或 SC2 在更新的时候,下载更新包的时间往往只占整个更新时间的一小部分,大部分时间花在把补丁打在已有的资源包上。

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

所以当初考虑到这个因素,我们在删除包内文件时,并不移动资源包内的数据,而是把空间留下来。如果新增加的文件较之小,就重复利用这个空间。如果利用不上,就浪费在那里。这有点像内存管理算法,时间久了,资源包内会有一些空洞,但也是可以接受的。

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

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

在公司,有后来的同事质疑过这种方式,觉得其对减少补丁体积的作用不大。反而增量打包增加了许多制作补丁包的时间。主张直接在补丁中放入更新的小文件,然后让最最终用户机上以小文件为单位做 patching 。

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

July 19, 2010

游戏多服务器架构的一点想法

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

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

前些年,我也曾写过好几篇与之相关的设计。这几天在思考一个问题:如果我们要做一个底层通用模块,让后续开发更为方便。到底要解决怎样的需求。这个需求应该是单一且基础的,每个应用都需要的。

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

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

December 29, 2009

点光源的管理

我们 3d engine 的点光源相关的代码,以前设计的是比较糟糕的。最近几天,我决定自己动手重新设计和实现这块东西。性能倒是次要的东西,重要的是要把模块分离,减低耦合度。

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

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

November 26, 2009

骨骼动画的插值与融合

花了三天时间搞定了困扰了我半年的事情,心情非常之好。

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

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

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

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

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

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

September 18, 2009

AOI 的优化

去年写过几篇过于 AOI 模块的设计。将 AOI 服务独立出来的设计早就确定,并且一定程度上得到了其他项目组的认可。(在公司内部交流中,已经有几个项目打算做此改进)

最近一段时间,我在这方面做进一步的工作。(主要是实现上的)

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

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

September 09, 2009

游戏动作感设计初探

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

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

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

为此,我纠结了很久。花了很长时间思考,以及编写了大量的代码印证,总算感觉自己的思路稍微清晰了一点。做一下记录,不一定正确,只是写出来或许可以给其他同僚提供一些思路。在内部讨论的过程中,这些也被广泛的质疑,许多人都不太认同。所以,一家之言,辜枉看之。

July 29, 2009

3d engine 中的贴图资源管理

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

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

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

资源管理是一个很复杂的模块,当然我们应该尽量简化它。在此之前,我曾经记录过我们的资源管理模块的设计变迁。上篇文章到现在也有几个月了,我又做了些设计上的简化,不过变化不大,暂时不提。

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

如前辈所言:确保特殊的情况是真的特殊。在《The Elements of Programming Style》和 《Unix 编程艺术》中都有提及。

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

May 10, 2009

树结构的管理

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

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

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

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

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

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

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

April 06, 2009

为 lua 插件提供一个安全的环境

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

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

从我在网游行业从业这么多年的经验,游戏界面相关的开发也是颇费人力的。甚至于,Client 开发到了维护期,几乎都是在 UI 上堆砌人工。一套自由的插件系统,对于开发商和用户是双赢的。

但是,插件系统也是双面剑。最典型的负面问题就是安全。越是自由,越是给机器人制作开放了自由之门。这里暂且不提这个方面的问题。首先关注一下另一个:尽可能的保护系统中不想被插件系统访问的数据,避免利用插件编写木马。

March 25, 2009

关于地表贴图

首先,我不是混 3d 圈的人,所以我对圈子里各种地表的渲染方法并不熟悉。今天写这一篇,纯粹是因为前段时间帮负责 3d 模块开发的同事解决了一个算法问题。

从我有限的 3d engine 相关知识来看,我最早了解的是类似 2d tile 拼接的地面渲染方式。玩过星际的地图编辑器的就知道我指的什么。等到魔兽三,虽然 engine 改成 3d 的了,但是方法没有大的变化。就是用贴图做成图素去拼接起来。依稀记得无冬之夜也是这种方法,时间太久,不敢确认。

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

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

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

March 10, 2009

关于游戏中资源管理的一些补充

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

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

资源的内存管理及多线程预读

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

胡思乱想续

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

重构

到目前最后一次在 blog 上记录思考和资源管理相关的问题。认清的最重要一点就是:

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

那么今天想说什么?

February 28, 2009

关于地图编辑器的一些想法

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

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

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

在很长时间里,我都倾向于制作一个大而全的编辑器 IDE 。但最最近两年,随着编程风格及开发习惯的改变,我对这种做法产生了怀疑。

November 29, 2008

XMPP 简单研究

最近想做一个游戏服务器和 IM 互通的服务。最初的想法是可以增进游戏帐号的安全,比如游戏用户可以通过绑定一个 IM 帐号,从而不用登陆游戏就向游戏服务器发一些指令。这些指定通常是用来冻结一些帐号的功能。而游戏服务器也可以通过 IM 帐号向离线用户发送一些关键消息。这样,只需要解除绑定 IM 帐号需要一定的时间,或使用更安全的途径,即可以让游戏帐号更加安全。(至少,游戏用户可以从 IM 上获知他的游戏帐号每次登陆登出的时间、IP 等等)

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

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

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

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

我最常用的 IM 是 google talk ,本身就实现了标准的 XMPP Client 和 XMPP Server 协议;而我们的 网易 popo 也实现了 XMPP 的 s2s 网关。我想研究一下 XMPP 是个不错的选择。

November 25, 2008

AOI 服务器的实现

以前谈过多次 AOI (Area of Interest) 的实现,因为我们的游戏尚在开发,模块需要一个个的做。前期游戏世界物件不多的时候用个 O(N^2) 的算法就可以了:即定时两两检查物件的相对距离。这个只是权益之计,这几天,我着手开始实现前段时间在 blog 上谈过的 独立的 AOI 服务器

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

  1. 创建一个 AOI 对象,同时设置其默认 AOI 半径。注:每个对象都有一个默认的 AOI 半径,凡第一次进入半径范围的其它物体,都会触发 AOI 消息。

  2. 删除一个 AOI 对象,同时有可能触发它相对其它 AOI 对象的离开消息。

  3. 移动一个 AOI 对象,设置新的 (2D / 3D) 坐标,并给出线速度的建议值。

  4. 设置一个 AOI 对象相对另一个的 AOI 半径,覆盖其默认设置。注:AOI 半径可分两种,一为进入半径,二而离开半径。通常一开始,每个 AOI 对象为其它对象均设置一个进入半径;当消息触发后,由场景逻辑重新设置一个离开半径。例如,一个 AOI 对象的默认半径是 10 米,当它被创建并指定坐标后,任何物体进入它的 10 米范围内,都会立刻由 AOI 服务器发送出一个 AOI 消息;而后两者之间不会再自动触发消息。场景服务器收到消息后,可主动向 AOI 服务器设置新的 AOI 离开半径 12 米,当此物体远离到 12 米远后,离开消息触发;下一步再由场景服务器重置进入半径。

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

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

July 27, 2008

把 AOI 的部分独立出来

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

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

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

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

July 20, 2008

一个简单的寻路算法

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

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

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

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

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

June 24, 2008

摄象机接口的设计

最近在调整 3d engine 的接口。前段时间把 GC 模块加到底层后,很多部分都需要修改实现了(可以简化许多实现代码)。既然重构代码,不如就再次审查一遍接口的设计,结合最近的一些想法,重新弄一下。

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

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

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

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

May 31, 2008

3d 引擎中对场景数据的接口设计

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

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

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

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

May 23, 2008

关于 openGL 的 4444 贴图

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

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

May 07, 2008

数值调整、模拟器、编辑器

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

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

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

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

May 01, 2008

那些日子(二)

我在北京生活了半年。接触北京这个城市要更早一些。大约 85 年的时候去过,一周时间几乎玩遍了北京所有的景点。小时候的记忆特别美好,我在那个时候爱上了北京。记忆中,天安门广场是那么的大,紫禁城的宫殿如此雄伟…… 以至于回到学校时,不知道用怎样的形容词才能形容。

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

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

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

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

两个很重要的朋友,也是在那段时间见的面 —— 余雪松和吴东黎。我们通过在网易个人空间(当时叫 N-SPACE)交换链接认识的。他们搭档了很多年(直到今天),经历很传奇。

April 30, 2008

那些日子(一)

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

前几天和新同事吃夜宵,大家聊的异常兴奋,我也忍不住开始想当年。当年那些美好的日子,记忆已经很模糊了。我想再过个两年,估计我都不能准确回忆起那些曾经对我影响深刻的日子准确的时间。是时候记下点什么,对自己是一种纪念。

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

我想我就是这么成长过来,没有什么挫折的感觉被反复咀嚼,都已经抛在脑后。生命中没有什么不可以失去的,这个道理很早就明白了。我曾经懊恼过丢失了大量的源代码、自以为写的不错的文章、早年的聊天记录、珍贵的日记、数年的电子邮件…… 最后我明白了,一切的一切不过是身外之物,我能拥有回忆中最美好的部分,那么已经是特别幸福了。

不过也正是如此,以下的记录也只能是我努力的回忆。或许因为时间久远,跟真实有所偏差,或许从我的角度只看到的事物的一面,但是、我可以保证,并没有故意在叙述中掺差虚假的东西。

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

April 27, 2008

游戏数值公式的表象与本质

周末、睡的比较晚,起的也比较晚。总的来说比平常睡眠时间长一些,所以到了这周一的第一个小时,我还是全无睡意。随便写点什么,关于游戏的,关于游戏设计方面的东西。

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

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

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

April 21, 2008

不那么随机的随机数列

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

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

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

April 10, 2008

游戏的帧率控制

我曾在不同场合,多次向人表达我对游戏程序设计的一个重要观点:时间控制,对于游戏程序设计至关重要。

这是因为,大多数电子游戏,都是一个实时人机互动系统。在非人机互动的软件中,软件及硬件一起是一个封闭系统,时间参数对这个系统是否正确工作是无意义的量。这样的系统,我们尽量应该设计成自动化工作模式,只要有正确的输入,得到正确的输出即可。通常,这样的系统的开发,还应该有自动化测试的流程去驱动它。

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

可见,在这样的系统中,时间控制显得多么的重要。

April 06, 2008

负反馈系统在模型动画控制中的应用

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

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

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

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

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

March 15, 2008

基于 lua 的热更新系统设计要点

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

这一周,写了三天半程序(其中通宵了一次)。平均每天写了千多行程序。基本上把原来项目里用的几万行东西替换下来了。重构总是能比老的设计更好,不是么? :) 不过这事不能老干,个人精力再充沛。故而还是找到称心的合作人比较好,也不能啥都自己做啊。程序员的效率差别可不只十倍二十倍的差别这么少。btw, 前段时间通过这里找到的新同事不错,呵呵。如果有缘,还想再找一个能干的。我相信,聪明人在一起做事可以获得更多快乐。

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

March 02, 2008

MMO 的排队系统

这两周那是忙的天昏地暗。都是些琐碎的事情,两个项目的。理理代码,发发邮件,打打电话,改改 bug ,开开会,签签字,写写报告。周末也加了一天班,工作居然是安装一个论坛系统,外加修改 css ,以及修改模板,调整版面。没办法,时间紧,人手少。

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

闲话扯到这里,今天想谈一下上月底,四年逢一次的好日子里,我们公司憋了好久的《天下2》终于又一次体验测试的事情。

February 19, 2008

角色动作控制接口的设计

最近一段时间工作的侧重点转移,我从服务器的设计转到客户端这边来。

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

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

唯一美中不足的是,做 demo 的程序员游戏接触的太少,所以在操作手感调整方面有点相形见绌。需要经常性的跟负责战斗动作系统的策划沟通调整。而策划毕竟不太明白程序背后再用怎样的方法实现,最终还是有点不尽人意。如果有足够的时间,倒总是能调整好。不过现在项目进度比原来计划有所提前,我就直接参于进来参合了。加快这方面的开发进度。

上周花了几天时间通读了所有相关的代码,整体设计还是不错的。不过这两天用下来,还是觉得有点别扭。

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

February 17, 2008

键盘毕竟不是手柄

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

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

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

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

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

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

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

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

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

November 30, 2007

讲稿

如果不出意外的话,我现在正在准备在 2007软件开发2.0大会 上的一个演讲:大世界网络游戏服务器的构架 。马上就要上讲台了。

其实主要是介绍下我们这两年正在开发的网络游戏引擎服务器部分的设计。这里有 PPT 可以下载

November 25, 2007

随机数有多随机?

作为一个常识,每个程序员在做入门学习时,都会被老师谆谆教导:我们用的编程语言中的随机函数,只能产生出伪随机数。它有它的内在规律,只能作为对显示世界的随机事件的近似模拟。接下来,我们通常会被传授随机种子的概念。以及用物理上更随机的量做种子。比如系统时间、两次敲击键盘的时间间隔、多次移动鼠标的偏移、甚至系统出错的出错信息码等等。

作为游戏数值策划,除了加减乘除,用的最多的数学概念恐怕就是随机数了。有经验的数值策划或许从他的前辈那得知计算机中程序产生的随机数并不太可靠;或者他本身就受过程序方面的训练。如果游戏项目更幸运一点,担当数值策划的他是一个数学爱好者,并读过诸如《计算机程序设计艺术》这样的技术书籍,那么事情会好的多。可惜大多数境遇下,策划们从不深究计算机随机数背后的细节,也不太关心所谓“伪”随机数究竟“伪”到什么程度。

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

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

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

November 15, 2007

思维的惯性

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

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

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

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

October 07, 2007

网络游戏的技术基础

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

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

刚才发现了透明的一篇文章:从游戏看货币,大约是对我前段时间写的一篇东西的评论。

正好这两天在家没事也在想这个问题,想着想着就不由自主多写几句了。

July 19, 2007

模型顶点数据的压缩

缘起:听天下组的同事谈起,游戏的资源数据量太大,导致了加载时间过长,影响到了操作感。最终不得不对数据进行压缩。

当玩家配置的物理内存足够大时,往往资源数据压缩会适得其反。因为多出来的数据解压缩的过程会占用掉太多的 CPU 时间。物理内存够大的话,操作系统会尽量的拿出物理内存做磁盘 IO 的 cache ,即使你的程序不占用太多的逻辑内存,而反复从硬盘加载大块数据,也不会感觉到磁盘速度的影响。当数据量大一个一个临界时,被迫做不断的外存内存的数据交换,这个时候,数据压缩就会体现出优势。因为解压缩的 CPU 时间消耗是远远小于硬盘 IO 所消耗的时间的。

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

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

July 13, 2007

游戏中的货币

曾经写过一篇关于网络游戏中的货币经济系统的文章,由于我本人对经济学只是浅尝,读书不多,所知甚为有限,怕是贻笑大方了。但这不影响我对此继续抱有极大的兴趣。最近,又再思索那个困扰游戏设计者的问题:究竟如何用游戏规则来稳定虚拟货币。

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

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

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

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

July 06, 2007

唯一的游戏世界

今天写下上一篇 blog 之后,跟另一个组的同事讨论了我的这篇文章。他们的项目先是多服务器的架构,后来重构后改成了单服务器的设计。我们的讨论第一个出现的分歧就是在于对多服务器设计的复杂度会造成的影响。

讨论了一段时间后发现大家的对一些问题的定义从一开始就有区别。回头来大家换个角度讨论了一下,发现其实那些分歧都是不存在的。

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

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

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

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

July 05, 2007

游戏服务器组间的通讯

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

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

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

June 21, 2007

平台无关的游戏引擎

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

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

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

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

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

June 08, 2007

2007 年玩家主流显卡配置

本文版权归所有、谢绝任何形式的(部分或全部内容)转载

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

前几天拿到了大量的显卡标识串统计数据(百万条级别),开始做数据分析。因为数据不太规则,所以颇花了些工夫。我想数据的来源和分析结果未得到公司允许前,都不能公开。不过我想在这里透露一些大略信息,对从事游戏开发的朋友(尤其是商业 3d 游戏)会有不小的帮助。

目前,NVidia 、ATI 、Intel 三家几乎垄断了玩家的显卡市场,市场份额超过了 95% 。如果你硬要考虑一些小厂商的话,不妨关注一下 VIA(包括其收购的 S3)和 SiS 。算上这两家已经涵盖了 99% 以上的玩家机器了。

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

May 24, 2007

DXT 图片压缩

这两天在写 DDS 格式的解码程序。DDS 是微软为 DirectX 开发的一种图片格式,MSDN 上可以查到其文件格式说明:DDS File Reference

其中的 DXT 图片压缩格式,现在已经为绝大多数 3D 显卡硬件所支持。(它使用了由 S3 公司所发明的一种有损图象压缩算法。btw, 在我的那本书中,P232 有所提及)。DXT 格式 也叫作 S3TC ,现在可以被流行看图软件直接显示的图象格式中,只有 .dds 文件支持这种压缩。为了开发方便,我们的引擎也就支持了 .dds 文件的加载。

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

好在以前研究过一些,写起来也不麻烦。

May 11, 2007

资源的内存管理及多线程预读

网络游戏的 client 开发中,很重要的一块就是资源管理。游戏引擎的好坏在此高下立现。这方面我做过许多研究和一些尝试。近年写的 blog 中,已有两篇关于这个话题的:基于垃圾回收的资源管理动态加载资源

最近在重构引擎,再次考虑这一个模块的设计时,又有了一些不算新的想法。今天写了一天程序,一半时间在考虑接口的设计,头文件改了又改。最终决定把想到的东西在这里写出来,算是对自己思考过程的一个梳理。

March 30, 2007

游戏服务器内的组播

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

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

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

March 29, 2007

游戏服务器处理多个连接入口的方案

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

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

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

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

March 20, 2007

为何麻将如此流行?

曾经有种流行的说法是,中国人喜欢打麻将,日本人喜爱围棋,而美国人多打桥牌。这类文章 google 一搜一大把。

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

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

我对麻将所知非常有限,规则陆陆续续的了解了许多。大约知道各地的细则不太一样,大体却是相差不大。似乎体育总局定过一个国标麻将 ,我们的 popo 游戏 就是按这个做的,据我所知,喜欢玩这个规则的不多。大家还是按地域各打各的一套。这也是边锋游戏成功的秘诀之一。

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

February 12, 2007

我们需要 photoshop 之外的选择

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

由于有些工作流程比较繁琐,甚至可能会有些变化,并不是每次操作的人都愿意使用 photoshop 中带的批量处理模式来提高效率。最终造成了一种奇怪的现象:明明人手充足,每个人每天都在重负荷的工作,但是整体的完成度却提不上去。在许多时间里,大家干的并不是创造性的工作,而是一些并不需要多少脑力的活儿。

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

February 01, 2007

多服务器的用户身份认证方案

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

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

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

January 15, 2007

3D engine ,中间层的缺失

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

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

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

December 29, 2006

碰撞检测

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

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

December 06, 2006

在 Windows 下使用 Timer 驱动游戏

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

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

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

October 29, 2006

用四叉树管理散布在平面上的对象

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

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

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

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

October 19, 2006

数据服务器的设计

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

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

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

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

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

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

October 18, 2006

多进程的游戏服务器设计

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

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

  1. 如果一个进程需要和多个服务器做双向通讯,那么这个进程不能处理复杂的逻辑,而只是过滤和转发数据用。即,这样的一个进程 S ,只会把进程 A 发过来的数据转发到 B ;或把进程 B 发过来的数据转发到 A 。或者从一端发过来的数据,经过简单的协议分析后,可以分发到不同的地方。例如,把客户端发过来的数据包中的聊天信息分离处理,交到聊天进程处理。

  2. 有逻辑处理的进程上的数据流一定是单向的,它可以从多个数据源读取数据,但是处理后一定反馈到另外的地方,而不需要和数据源做逻辑上的交互。

  3. 每个进程尽可能的保持单个输入点,或是单个输出点。

  4. 所有费时的操作均发到独立的进程,以队列方式处理。

  5. 按功能和场景划分进程,单一服务和单一场景中不再分离出多个进程做负载均衡。

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

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

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

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

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

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

October 02, 2006

服务器消息的广播

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

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

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

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

September 16, 2006

心跳服务器

我们目前游戏服务器的初步架构是一个连接服务器处理来自多个客户端的连接数据,这个服务器将所有数据汇总后通过一个 socket 发送到后方的逻辑服务器。这个设计曾经写过一篇 blog 提到过

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

September 13, 2006

目前我们的游戏服务器逻辑层设计草案

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

写了一段时间后,觉得这种方案杂乱不利于复杂的项目。跟同事商量以后,改成了非阻塞的 RPC 模式。

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

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

June 14, 2006

魔兽世界之过?

看到这样一篇文章,魔兽世界之过 。老实说,作为一个从美服 beta 开始接触 wow ,然后在美服付费玩了大半年,既而转战国服一年的老 wow 玩家来说。我在喜爱 wow 的同时, 也赞同文中的观点。

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

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

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

June 07, 2006

网络游戏中的货币系统

现在 MMO 中经济系统往往是设计的关键。可能是网络游戏发展的时间较短,并没有形成系统的理论的缘故,我所了解的网络游戏中都没有特别好的处理好经济系统中货币的控制和流通问题。

我们知道,货币只是一种经济活动中一种媒介,货币本身是没有价值的。但是,网络游戏中却很难体现这一点。系统对游戏直接的货币投放是不可控制的,虽然很多设计人员企图控制,但只是收效不同而已。大体上,大家都是用玩家的在线时间换取系统货币总量的增加,而用玩家自身的修炼回收掉货币。还有一小部分是用上税和赌博的形式回收掉。

May 21, 2006

脏矩形演示 demo

上个世纪,我在个人主页上发布了风魂。2000 年的时候,我给它加上了脏矩形的支持。之后,就再也没有发布新的版本。

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

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

May 19, 2006

不太精准的时钟

目前,我们的游戏的同步是依赖机器时钟做预测的。这种做法的前提是,client 的机器和 server 的机器的时钟走速必须相同。这样,当我们把时刻校对了以后,可以不通过网络就可以知道时间信息。即,每个通过网络包传送过来的事件已经发生了多久。这只需要发送方为事件打上时间戳,然后接收方以自己的时候为准求出时间差即可。

April 26, 2006

广播和监督服务器

前几天写到我们把服务器组分成连接服务器和逻辑服务器的想法,这代码部分已经完成,并且比最初的构想增加了不少东西。比如允许逻辑服务器广播数据包,由连接服务器分发等。

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

April 19, 2006

IOCP , kqueue , epoll ... 有多重要?

设计 mmo 服务器,我听过许多老生常谈,说起处理大量连接时, select 是多么低效。我们应该换用 iocp (windows), kqueue(freebsd), 或是 epoll(linux) 。的确,处理大量的连接的读写,select 是够低效的。因为 kernel 每次都要对 select 传入的一组 socket 号做轮询,那次在上海,以陈榕的说法讲,这叫鬼子进村策略。一遍遍的询问“鬼子进村了吗?”,“鬼子进村了吗?”... 大量的 cpu 时间都耗了进去。(更过分的是在 windows 上,还有个万恶的 64 限制。)

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

April 15, 2006

网络游戏的对时以及同步问题

大多数实时网络游戏,将 server 的时间和 client 的时间校对一致是可以带来许多其他系统设计上的便利的。这里说的对时,并非去调整 client 的 os 中的时钟,而是把 game client 内部的逻辑时间调整跟 server 一致即可。

一个粗略的对时方案可以是这样的,client 发一个数据包给 server,里面记录下发送时刻。server 收到后,立刻给这个数据包添加一个server 当前时刻信息,并发还给 client 。因为大部分情况下,game server 不会立刻处理这个包,所以,可以在处理时再加一个时刻。两者相减,client 可以算得包在 server 内部耽搁时间。

client 收到 server 发还的对时包时,因为他可以取出当初发送时自己附加的时刻信息,并知道当前时刻,也就可以算出这个数据包来回的行程时间。这里,我们假定数据包来回时间相同,那么把 server 通知的时间,加上行程时间的一半,则可以将 client 时间和 server 时间校对一致。

April 05, 2006

贴图的合并

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

合并这些零碎贴图的算法,在学术上被称为排料问题。我尚未找到特别好的解决方法,所以这里就不展开写了。这里想讨论的是这些小贴图的管理。

March 08, 2006

基于垃圾回收的资源管理

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

管理这些资源分为加载和Cache两个方面。对于资源数量比较少的游戏,我们可以采用只加载不释放的方式,即使资源总量大于物理内存数,OS 在虚拟内存的管理方面也自然有它的优势。至于加载的时机,为了追求用户玩的时候的流畅体验,我们可以在一开始就进行 loading。当然也可以为了减少 loading 时间,去做动态加载。动态加载的问题比较复杂,可能涉及多线程,以及预读操作。这篇 blog 不展开谈。前段时间写过一篇动态加载资源 可以参考。

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

March 06, 2006

以人为本,美术资源的归档

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

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

January 05, 2006

动态加载资源

如今很多游戏engine宣称自己支持动态加载地图,也就是说可以作到跨地图时的零读取时间。听起来很高深的技术,实际不难实现,当然我们在大话西游和梦幻西游中早已经实现了。最近我正在考虑更加通用的解决方案。

先说说基本的思路。也就是我们需要把地图数据切割成小块,让每一块的数据读取解析量并不太大。然后,就可以根据玩家所在坐标读取最小数据量的数据。当玩家移动的时候,利用一些机器闲置时间去读周围的场景,或者进一步可以根据玩家移动方向把前方数据预读的优先级别调高。

December 30, 2005

当编辑器也成为游戏

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

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

October 11, 2005

游戏,一种奇怪的软件

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

但是游戏,作为一种软件则不然。