« October 2011 | Main | December 2011 »

November 26, 2011

开发笔记 (3)

这周的工作主要是写代码。

开发计划制定好后,我们便分头写代码去了。我们希望一期早点做出可以运行的东西来,一切都从简。整体的代码量并不多,如果硬拆成很多份让很多人来做的话,估计设计拆分方案,安排工作,协调每个人写的东西这些比一个人全部实现一遍的工作量还要大的多。

所以,最终就是两个人在做。怪物公司在弄客户端的东西,蜗牛同学包干了服务器。好吧,基本没我的事了,我就是那个打酱油的,好听点说,就是设计方案。当然,事情没多少,空下来的时间也可以干活。训练自己可以找到事情做,并真的做有用的事情,还是很难的。

话说回来,我们在这么一个简单的框架下,一开始确定了要采用一些现成的技术方案,即要用到 Redis , Google Protobuffer , ZeroMQ 。

Redis 和 ZeroMQ 是我最早选的,想了很久。


采用 Redis 是因为历史上,我参与的项目都没有大规模使用 SQL 数据库的传统。这和 MMO 这种特定应用有关。在 Web 开发中,面对的用户是临时的,不依赖固定连接的。你不确定用户在不在那里,你不确定同时要面对的用户有多少。你需要从小到大,采用一种可扩展容量的方案。这个时候,成熟的 SQL 数据库方案是首选。

从软件开发角度看,数据库是 MVC 模式中的 M 。以 MVC 模式解决问题,M 如何实现,采用 SQL 方案只是一种可能,绝不是唯一选择。换个角度考虑,如果是一个桌面软件,为什么大部分的 M 却没有采用数据库,而更多的是在内存中直接构建数据结构呢?性能恐怕只是一个原因,更重要的原因是面对的用户的行为不同。

为什么 MMORPG 服务器,至少在网易历史上的多款游戏,没有使用 SQL 服务做 M 。一部分原因是,网易游戏的开发源头是 Mud ,Mudos 并没有使用 SQL 作为 M ,另一部分原因是,MMORPG 面对的,同时需要服务的用户有限。而用户需要操作的数据大部分限制在用户相关的数据体内。之外的数据体非常少。及时数据总量很大,但一层索引简单(以用户 id 为索引)。每个用户都是为它持续服务,数据易变。这种行为下,从 MVC 角度看,更接近网络应用之前的软件设计。

当然你甚至可以把 M 只在逻辑上划分出来,物理上并不切换,也就是没有独立意义上的数据库服务器。这样绝对性能最高,其实只是实现了一个简单的单机游戏,允许通过网络输入多条操作流,并把行为反馈通过网络发送回去罢了。甚至比单机游戏更简单,因为没有图形控制部分。

如果程序不出问题,机房不停电,可以一直的跑下去,不用考虑数据储存问题。数据持久化不过是为了容灾罢了。把一些结构化数据持久化到硬盘上,最简洁的方案就是写操作系统层面的文件,一定比再使用一个数据库要轻量,干净的多。

有了以上背景,就不难理解,为什么我对 Redis 天生有好感。我们并没有改变设计思路,它是一直延续下来的。Redis 帮助了我们将数据服务拆分出来。当然,MMORPG 也在发展,以上提到的用户应用环境也在逐步变化,我们在软件设计上也会跟进这些变化。这些就是后话了。


ZeroMQ 呢,我是希望有一个稳健,简洁的多进程通讯方案的基础。ZeroMQ 是不错的一个。至少比 OS 的 Socket 库要实用的多。它提供了更好的模式 。这是我最为看中的。另外,这是一个 C 接口的库,容易 binding 到不同的语言下使用。

在这个问题上,蜗牛同学是反对使用 ZeroMQ 的。对他的所有反对意见,我都持保留态度。但我尊重开发人员的意愿。毕竟许多代码不会是我自己来写。蜗牛同学希望采用 Erlang + C Driver 的方式来驱动整个框架。也就是用 Erlang 来做通讯上的数据交换。其它可能采用的开发语言,都通过 Driver 的方式插入到 Erlang 的框架中去。

我个人觉得这样做的确可行,加上蜗牛同学有好几年 Erlang 开发经验,他能担负起实现框架的责任。我不是特别喜欢这个方案是因为,Erlang 这个东西还是太庞大了。我对庞大的东西天生反感。虽然以蜗牛同学的原话说,Erlang 以及他的 OTP 能写出这么多行代码来有他的必要性。我们用 C/C++(Python/Lua/Golang 等等) + ZeroMQ 实现一个拙劣的方案出来,只会漏洞百出。

我个人是不以为然的,毕竟已经做了 10 年的网络游戏,对这个领域已经很熟悉了。对于陌生领域,我们会面对许多未知的问题;但在熟悉领域,无论怎么做,方案都不可能太拙劣。只要保持最基本的简洁,我是有信心保证可以解决 MMORPG 中的各种需求的。做出来的东西也能很稳定。关键点在于,它会足够简单,能轻松的理解实现的每一行代码。

不过争论都放在一边。我的原则是,最终采用实现者自选的方案,只要它没有明显的问题。

我们最终不采用 ZeroMQ 。


Google Protobuffer 我不是很喜欢的。但采用它是多个角度妥协的结果。其实我更愿意自定义一套协议。而只是裁剪 GPB 的功能。

我认为,GPB 在最底层的协议编码定义上,做的还是不错的。改进它是多余的。作为一种协议定义,协议描述语言也算定义的不错。但只到此为止。接下来的部分就不甚满意。

GPB 协议本身,默认也是用 GPB 本身定义编码出来的。这看起来很 Cool ,但我不甚喜欢。当然所有完备的类似协议都应该有描述自己的能力。对于描述自己这件事情来说,GPB 还是稍显复杂了。

比如,如果你不借助已经有的 GPB 代码和工具,很难解析一个 GPB 协议。就好比,如果世界上第一款 C 编译器,就是最难实现的 C 编译器。因为大部分的 C 编译器是用 C 写的,实现一个 C 编译器,就陷入了先有鸡还是先有蛋的问题。

在这类问题上,据说 Lisp 比 C 要干的漂亮。不过我还是和世界上大多数程序员一样,用 C 多一些。好吧,我们还是继续用 GPB 好了。

google 在实现和使用 GPB 的时候,默认采用了一种为每个协议,生成一组代码的方式。而不是提供一套 C/C++ 库,供其它语言做 binding 。这也是我所不喜的。或许是为了性能考虑,但总觉得别扭。如果把 GPB 换成正则表达式来看,你就能理解我的心情。

现存的大多数正则表达式的实现,都是提供一组 API ,允许使用者把需要的模式以一种人类可读的串形式,编译为另一种计算机方便处理的数据结构。当你需要的时候,使用这个数据结构,交给库,就可以匹配,替换字符串了。如果默认的选择是把正则表达式编译成 C 代码,然后你用的时候再 link 到你的项目中,恐怕用的人要疯掉了。当然,生成代码这种可以带来更高的运行性能。

唔,其实这只是怎样使用 GPB 这种协议的问题,和协议定义关系不大。可惜 google 在开源之初就给出了官方的方案,引导其它语言也如法泡制,成了 GPB 的惯用法。老实说,对于 C++ ,这么做性能是不错的(其实也未必)。换到 Python 里,就非常低下了。去年我按我的思路实现了 lua 的 protobuf 解析库 ,性能可以达到和 C++ 版本差距不到一个数量级,甚至快过 java 版。

这周的剩余时间我都在写一个纯粹的 C 版的 protobuf 库,不依靠代码生成器的。希望能够作为它语言使用 GPB 的基础。别的语言只需要做 binding 就够了。这玩意挺难写,光接口设计我就改了两版。今天终于快收工了。过两天再写一篇文章专门谈谈这个问题吧。当然,还有开源。

既然在 GPB 上花了这么多功夫,当然,采用 GPB 就是最后的决定了。

November 19, 2011

游戏数值策划

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

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

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

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

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

前同事,罗诚同学在微博上回复,“用经济学解释,任何行业,分工都意味生产力的进步。不需要分工,只会因为市场规模小,而不是因为有人全能。”。其实我反对的并不是分工,而是分工的方式,和那种把 MMORPG 制作往劳动密集型产业发展的方向。网易的游戏策划分工,从近年的策划专业评审制度来看,就是想培养游戏设计人员往专职的所谓系统策划,数值策划专业方向区格开。然后把全才培养定义为主策划方向。

另,如果单从生产力发展来看,生产力越高,就越不需要太多人力来做一件事情。

当然,我明白,罗诚同学实际反感的是“老外怎样怎样”的话语。我想说的是,“老外怎样”只是方便于找个论据而已。我们总觉得我们在 MMORPG 领域比起世界范围的同行有几年的先发优势,强调相关经验。反而我认为,具体经验依赖反而是行业不成熟,没有提升到有理论指导的标志。04 年的时候,魔兽世界刚刚面世,回想国内网游圈子里我听过不少人在玩到游戏前都对这款游戏不以为然,觉得暴雪的同学没有网游经验,很有可能做不好;即使到 05 年 wow 进入中国,依旧有许多所谓业内分析人士认为 wow 不会适合中国国情。

接下来的两年呢,魔兽“借鉴”风就席卷全国。据我的记忆,天下二的开发组在 04 年玩过魔兽美服后,就大幅修改了游戏设计。

结论呢,MMORPG 是和传统游戏有所不同,但开发它所依赖的根本技能,还是从传统电子游戏领域中衍生而来的。正如程序员学写程序,学习期的积累,根本无所谓他未来是做游戏还是做别的软件,是做客户端程序还是服务器程序。只有白纸般的新手,才在学校里抱起一本类似 “DIrect3D 游戏开发指南”,或是“21 天精通 Python”这样的书,希望可以学会编写游戏程序。


最后列点数据:

魔兽世界的 Credits

  • Lead Designers : Robert Pardo, Ayman Adham
  • Exterior Level Designers : Bo Bell, James Chadwick, Mark Downie, Alen Lapidis, Matt Sanders
  • Game Designers : Tom Chilton, Eric Dodds, Michael Heiberg, Kevin Jordan, Jeffrey Kaplan, John Yoo
  • Quest Designers : Alex Afrasiabi, Michael Backus, S. Christine Brownell, Shawn F. Carnes, Michael Chu, Jeffrey Kaplan, Pat Nagle
  • World Designers : Geoff Goodman, Andy Kirton, Joshua Kurtz, Steven Pierce
  • Additional Design : Tom Cadwell, David Kristofer Fried, David Hale, Eric Maloof, Scott Mercer, Matthew Morris, Jennifer Powell, Dean Shipley

其实暴雪已经把 Designer 的种类分的很细致了,但是没有看到把数值设计单独列出一类来。

以上人名太多,没有精力逐个调查,下面再看我玩过的另一款网游 EVE online ,策划人数少一点方便 google 。

EVE online 的 Credits

  • Lead Designer : Kjartan Pierre Emilsson 这个是传说中的主策划,在 06 年的一篇采访稿 里提到,"Kjartan Pierre Emilsson: I have been Lead Game Designer of EVE Online these last 5 years, overseeing general design of the game"
  • Game Design : Hrafnkell Smári óskarsson 从这里看像是编剧
  • Game Design : Reynir Hardarson 从这里看是 CCP 创始人 ,他同时是 Art Director
  • Game Design : Húni Hinrichsen 从这里我理解他是后期的主策和制作人 ,在 youtube 上也有一个视频,他是以主策身份介绍游戏的。他在 Credits 上还有一个名头是 Researcher
  • Game Design : Kári Gunnarsson 看起来以 QA 工作为主 并且也是 Senior 3D & Texture Artist
  • Game Design : Hjörtur Bjarnason 同样列在了 Senior 3D & Texture Artist 栏

November 18, 2011

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

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

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

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

用户系统使用 email 来做用户名,但在数据库中的唯一标识是一个 uid 。用户应该允许修改登陆名(用户很可能更换 email)。用户的身份识别是用 id 来定位的。所以,在数据库中就应该有如下几组 Key :

  • account:count id
  • account:userlist set(id)
  • account:email:[email] id

这里,account:userlist 对应的 value 是一个 set ,里面存放了所有存在的 user id 。用于遍历所有的 user 。这个暂时可能用不上,而且当用户量相当大的时候可能有问题。不过暂时不用考虑这么多问题,等以后改进。

account:count 是一个计数器,可以用来生成唯一 id 。

account:email:[email] 用来标示每个注册的 account 的登陆名。[email] 指登陆用 email 地址。

这里,email 内可能也存在符号 ":" ,为了回避这个问题,许多对 email 进行编码。我的方案是,将字母数字 @ . _ 之外的字符编码为 %XX 的形式。用 lua 干这件事情非常简单:

local function _encode(str)
    return string.format("%%%02X",string.byte(str))
end

function emailEncode(str)
    return string.gsub(str,"([^%w_@.])",_encode)
end

当然,解码回来也很简单

local function _decode(str)
    return string.char(tonumber(str,16))
end

function emailDecode(str)
    return string.gsub(str,"%%(%w%w)",_decode)
end

之后,就是 account 下每个 id 的数据:

  • account:[id]:version number
  • account:[id]:email string
  • account:[id]:password string // md5(password..salt)
  • account:[id]:nickname string
  • account:[id]:lastlogin hashes
    • ip string
    • time string
  • account:[id]:history list(string)
  • account:[id]:available enum(open/locked/delete)

其中,密码不想保存为明文。因为任何可能的数据泄露都会导致用户的损失,我也不想任何人看到用户的密码。所以采用 md5(password .. salt) 的风格。

md5 运算前,加一个 salt 后缀,是因为单纯的文本 md5 值也是有数据库可查的。

lastlogin 下保存了用户最后一次登陆的信息,使用了一张 hashes 表,因为这些信息在未来会进一步扩充。

history 保存了用户登陆的所有历史记录,用一个 string 链表记录。

用户删除自己的账户时,不想把数据从数据库删除,只想在 available 下做一个标记。

考虑到数据库内数据结构有可能发生变化,所以加了 version 域做版本标识。


我不想让各种服务可以直接读写这份数据,所以,会单独写一个认证服务器做处理。

认证服务器提供三项服务:

  1. 用户注册

  2. 用户名 密码 认证 (用于 ssl 连接上的 web 服务)

  3. 用户名 密码 挑战式认证 (用于 client 的认证服务)


下面是基本的场景服务用的数据:

  • account:[id]:avatars set(id)
  • avatar:count id
  • avatar:[id]:version number
  • avatar:[id]:account id
  • avatar:[id]:scene string
  • avatar:[id]:available enum(open/delete)
  • avatar:[id]:data hashes
    • name string
    • figure string
  • world:scene hashes
    • [name] id
  • scene:count id
  • scene:[id]:name string
  • scene:[id]:available enum(open/close/delete)
  • scene:[id]:info hashes
    • time string
    • pc number
  • scene:[id]:pc hashes
    • [id] enum[online/offline]
  • scene:[id]:pc:[id] hashes
    • status string

用户账号下可以有许多游戏角色,列表放在 account:[id]:avatars 下。

每个角色也拥有一个唯一 id 。这个 id 原则上和 account id 是独立体系,但是为了人类好区格,avatar:count 的起点和 account:count 不同。

角色所在场景记录一个字符串的场景名 avatar:[id]:scene ,角色的其它各种数据放在一个 hashes 里。

所有的场景索引方在 world:scene 下。如果日后有多个世界,可以采用 world:[id]:scene 。但目前不必考虑。

scene 下面的所有 pc 的在线状态放在 scene:[id]:pc hashes 中,pc 离线也把它的 id 记录在内,只有 pc 转移场景才移除。

每个 PC 的位置状态信息记录在 scene:[id]:pc:[id] 中,第一个 id 是 scene 的 id ,第二个则是 PC 的 avatar id 。

btw. 这是一份草稿,虽然思考不周,但足够满足项目一期的需求。当然许多欠考虑的地方也并非是考虑不到,而是希望尽量简单,以满足一期需求为目的。这个日后修改的代价并不大。


最后吐槽一下 Redis 的 Windows 版。办公室的 Linux 服务器还没有装好,我暂时在 Windows 下做开发。取了一份 google 搜到的 非官方 Redis 的 Windows 版 。为了图方便,使用的是 luajit ffi 去调用 hiredis 的 dll 。一开始怎么都搞不定。建立不了 socket 连接,出错码也取不到。

对比了源代码,发现修改版把 C Struct 结构改了,前面增加了几个域,而我以 hiredis 官方标准来定义的接口。

改好后,能够正确取出出错码了。发现万恶的 Windows socks api 需要调用 WSAStartup 才可以用。而 hiredis 的 Windows 修改版居然没有去调用。让我大费周折才改好,前后折腾了一个多小时。

再吐槽一下 hiredis 的 API 设计,居然依赖 C Struct 的布局。良好的 C 库的接口设计不会这么干的吧。比如 lua ,又比如 zmq 。唉,用这种东西有点小不爽。不过比 C++ 库还是好太多了。

November 16, 2011

开发笔记 (1)

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

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

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

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

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

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

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

我们先集体玩了一周魔兽世界,虽然大部分人已经是 wow 老玩家,但还是有一两个人玩的比较少,大家一起熟悉一下。主要是熟悉战斗系统,还有美术风格的感觉。培养点兴趣,对未来我们自己做的东西至少在表层上大家都有点感觉,有爱。

瞥开美术和策划的计划不谈,主要写写程序的计划。

在 Closed Beta 前,有 8 个历程碑,到一期截至,就开始有内部运行的版本。这次完全抛弃以往在网易积累的任何一点成品,全部从零开始做。但是,对于程序员来说,一切都在脑子里,其实工作量并不大。有机会重头来,恐怕是大部分程序员梦寐以求的机会。

当然,我们购买了一套 3d engine (不介绍,不解释),起点高一些。服务器也会尽量用一些成熟的开源库。

一期打算实现基本的用户登陆,场景漫游,包括在场景中做各种跑、跳、走、骑乘等动作。


一期程序员三人:

  • 云风:负责总体规划,总体协议设计,以及部分服务器模块的实现。

  • 怪物公司:负责客户端的设计与实现。

  • 蜗牛:负责服务器的细节设计与实现。

吵架是我们的传统,自今天就开始了。按照惯例,我无法说服项目组认可我的所有设计和技术选择。不过大家妥协的结果是,先按我的想法做,一期雏形在下周末前完成,根据实现过程中遇到的问题,我们在修正甚至全部重构目前的设计。一周半时间的代价是目前我们可以承受的。

ps. 程序员就是这么一种奇怪的生物。好的程序员都有自己独立的思想。对自己实现或即将实现的代码有爱。按照别人的思想去实现是件无比痛苦的事情,会觉得在浪费自己的生命。所以,大部分有活力的项目开始都是一个人建立起来的。在大公司,好多老程序员都喜欢招聘所谓有潜力的新人,认为他们白纸一张,好塑造。说到底就是听话。但事实的结果一般是,要么培养出来一个庸才,无法担当;要么,在技术选择上最终分道扬镳。我总是对他们说,想想你愿不愿意总听着别人的意见干活?如果你不愿意,那么就别指挥别人干。自己不愿意做的事情,就别让别人帮你做。


我的设计草稿是这样的:

整个游戏系统(一期工程需要的)大体由这样一些部分组成:

  • 客户端,运行在玩家的终端上,用一条 TCP 连接和系统通讯。它接入游戏服务器网关。

  • 网关,负责汇总所有客户端,大体上和我很多年前的想法一致 ,虽然会有一些实现上的小改变,但思想没有根本变化。

  • 客户代理(Agent) ,运行在网关的后端,在逻辑上,每个客户端都有一个 agent 负责翻译和转发客户端发来的请求,以及回应。众多 agent 可以实现在同一个进程中,也可以是多个不同的进程里。可以用脚本虚拟机的形式跑,也可以是别的形式,这些都不太重要。这一次,我们最大可能使用独立的 lua state 来实现单个 agent。

  • 数据服务,保存玩家数据,场景数据等。前端用自己的代码和协议同系统其它部分沟通,后端这次想采用 redis 做储存。

  • 场景管理器,用于管理静态场景和动态副本。

  • 若干场景服务器,用于玩家在里面做漫游。

网关之后的服务是相互信任的,并组成一个虚拟网络可以相互通讯。通讯底层暂时采用 zeromq ,通讯协议采用 google protobuf 。

客户端到 agent 的通讯协议与网关后各个逻辑结点之间的通讯协议将隔离,分开设计。甚至不保证采用一致的技术实现方案,以及协议设计风格。


这里的设计关键在于 Agent 的设计,大部分的工作量是围绕它而来的。

单个 Agent 的工作流程代表了项目一期的结果会展现的东西,大体上逻辑流程如下:

  1. 等待用户认证

  2. 把认证系统交给认证服务器认证,失败则返回 1 重试。

  3. 从数据库获取用户数据(一期数据很少,仅有默认的用户名,用户形象) ,并转发给客户端。

  4. 取得用户所在场景号,从场景管理器取得场景服务的位置,并申请加入场景。

  5. 从场景服务器,同步环境。

  6. 转发用户在场景中漫游的请求,并同步场景的实时数据。

  7. 一旦客户端发生异常或得到推出消息,通知场景服务器离开。

这里,和历史上我们的游戏服务器设计有一个小的不同。我认为,用户的角色在场景中的位置,动作状态,甚至以后的战斗数值状态,都是属于场景数据的一部分,而不是用户数据的一部分。

用户数据中,和场景有关的只包括他当前所属的场景。由场景自己来保存角色在场景中的状态数据。简单而具体的说,类似网易历史上的西游系列,玩家的坐标是玩家的属性之一,是会在持久化时存放在玩家数据里的。经过我这两年的思考,我觉得这种设计方法是有问题的。

从我现在的直觉上来说,虚拟角色的属性不应包括角色的地理位置信息。那是角色的外在约束。而场景更应该拥有在它其中的 PC 和 NPC 的位置以及各种状态。PC 下线只应该认为是 PC 处于一种特殊状态(不被周围的 PC/NPC 所见或影响),而并没有脱离这个场景。所以我更愿意把 PC 下线定义成 detach ,而上线则是 attach 的操作。在这点上,场景,作为一个实体,拥有它自己的数据逻辑。


agent 相关的协议粗略的设计如下:

  • auth 登陆认证
    • user/pass 取得 userid ,返回成功或各种失败
  • userdb
    • 用 userid 取得 avatar list
    • 用 avatar 取得 avatar data (包括场景名)
  • scene manager
    • 用 场景名 取得 scene id
    • 用 scene id 取得场景状态,返回允许进入或各种拥堵状态
  • scene
    • 把 avatar attach 到场景
    • 取得 avatar 的场景上下文
    • 把 avatar detach 出场景
    • avatar 设置坐标,速度,行为(跑,走,站立),方向
    • avatar 骑乘状态改变
    • avatar 做特定动作

好吧,未来一周的工作任务就是这些了。需要明天再做细化整理。然后就是实现了。

November 12, 2011

Ameba , 一个简单的 lua 多线程实现

几个月以前,在我在 blog 上曾谈及 Lua 5.2 的改进。它可以用来实现抢占式多线程

周末休息,我把这桩事挖出来娱乐一下,花了一整个晚上做了实现。把 lua 的每个线程锁定在独立的 lua state 中,强迫线程之间通过消息管道的方式通讯。经过测试,Lua 5.2 每个独立的 state 占用的内存很小。通过自定义 alloc 函数可以测算出,一个干净的 32bit state ,不含任何库函数时,占用内存量在 2K 以下(1726 bytes)。如果加载基本库,也仅仅占用不到 4K (3265 bytes)。若把所有 lua 官方标准库加载进来,才会上升到 10K 以上(12456 bytes)。

对于 luajit 2 ,这个基础开销会大一些,最小开销也在 10K 左右 (8058 bytes) 。加上 ffi 达到 30k (31605 bytes)。不过 ffi 可以使 lua 代码直接使用 C 的数据结构,在实际运用中还可以减少内存的使用。

废话不多说,我的代码放在了 github 上 ,有兴趣的同学可自取。

这个娱乐项目命名为 Ameba ,暗示每个代码单位都足够的小,功能简单。它们必须通过很少的 send/recv 和外界通讯。目前,通讯的数据类型仅限于 number boolean 和 string 。

每个线程(ameba)可以调用 ameba 函数分裂一个新的 ameba 出去。调度器为每个 ameba 分配了一个唯一的通讯通道 (chan) 。ameba 自己可以通过自己的全局变量 chan 拿到这个数字。

读写 chan 可以通过 send / recv 两个 api 。如果返回值为 nil ,则表示 chan 已经被关闭。(目前只可能是 ameba 自己退出死亡)。chan 的读写是阻塞的。即,当你向 chan 写入数据(可以是一个或多个)时,若同时对端 ameba 没有在读取数据,那么写入者将被挂起;反之亦然。

recv 只可以收取系统为其分配的 chan 上的数据。send 则可以制定一个数字表示向哪个 ameba 发送信息。


关于实现:

抢先式多线程是用 lua 的 debug hook 实现的。它在一定程度上会降低 lua 代码执行的性能(不到一倍),可以通过修改时间片的粒度来调整。

每个 ameba 里都有一个 chan 读缓冲区,放在注册表中。当有另外的 ameba 企图写入的时候,首先写到这个缓冲区中,再由本地的 recv 函数复制出来。

整个系统的父 state 中保留有一个表,映射了所有 ameba 的读写队列。用于调度器的工作。这张表存在于父 state 的注册表中。

ameba 之间没有父子层级关系。

这个库是设计成可以递归使用的,可以在 ameba 内重新启动库,这样就可以有层级关系。但,chan id 是一个 C 里的全局变量,会一直累加。当然也可以修改实现,把这个变量放在 lua state 中。

目前的实现中,每个 ameba 的 state 只初始化了 lua 的基础库,没有加载完整的标准库。

自定义的 lua alloc 函数未来用于控制内存分配和管理,目前并没有特别使用。

整块代码娱乐性质为主,也就是写着玩儿。不保证质量。

代码必须装有 lua 5.2 beta 版才可能正常编译,经过少许修改也可以用于 luajit 2 beta 。但由于 lua 早期版本的实现限制,无法用于 lua 5.1 或更早的版本。

November 01, 2011

正式开始前

20111101 , 选了个好日子 , 我们最初的几个人入住了新办公室。小光棍节呐,果然一帮孤家寡人要开始干活了。

明天早上 10 点开始上班,作为程序员,数字控,20111102 这个回文数当然是更带感了。

这个月也没闲着,不过都是瞎忙。办公室装修有人在负责,不过我住的近,时常过来看看。看着地板被平整,墙刷白了,灯光按我的意思做了调整。05 年我刚到杭州的时候,也是我,租了写字楼,修改了装修的图纸;这次,又重新来了一次。有了上次的经验,当是做的更好一些了。

写字楼在华南理工附近,华工操场上竖了攀岩墙,比杭州岩馆的高。这倒是意外收获了,可以经常保持练习。希望忙过这两年,可以回去把 天柱岩 征服掉。

这边的线路还爬的不太习惯,一条 5.10a 的线爬了好几次都在最后的难点没能完成。虽然我一直都是进步很慢的,但信念比较坚定,爬不了就慢慢来,相信自己总会有长进的。何况在杭州时,这个难度的线路都可以搞定。最终, 那条线在上周完成掉了, 很开心。其实真的不难,一旦搞定,连爬了两次都爬的比较轻松。下次开始再来磕另一条 5.10c 的。说来惭愧,几乎跟我在攀岩这件事上花了差不多时间的人都能爬 5.12 了。当然,体验着自己一点点进步还是件非常愉悦的事情。

前几天想找点程序写了热热手,不然一两个月不写代码会手生的。花了几天时间给 luajit2 实现了 google protobuf 的模块。这种东西呢,实现起来并不难,难得是怎么实现的高效。老有人嘀咕,其实性能不重要,重要是的正确简洁实现。我心中的一半非常认可这一点;但另一半又是性能的狂热追求者。要得到好的性能,就得对 luajit 有很好的认识,对 protobuf 有深刻的理解,清楚的明白自己在干什么。我相信每一段高性能的代码,都需要对它们有爱,花了时间去理解和学习得来的。好吧,其实这是一种爱好的副产品,就跟有人热衷于填字谜一样。

我们新公司的融资不算是一帆风顺,只能算是运气比较好,却有一些困难要克服吧。资金没那么快到位前,也就不急着招人了。否则,我们自己掏腰包的那点钱,几个月就把自己玩死了可不行。不过我跟 dingdang 也感叹,现在通货膨胀也太厉害了。怎么千万 RMB 就不算钱了呢,预算随便做做就能花光。想来,若是 10 年前,这怎么也算笔巨款了,够我组个开发团队好几年开销了。是不是年龄大了,成家了,反而某些创业的路子就走不通了?10 年前,拿着 1000 块的工资玩创业可以屁颠屁颠的穷开心;现在扳着指头算,5000 块生活费已经撑不住一个家庭了。好吧,预算后面再添一个零,我们也得省着点花。

创业初期太多人,队伍是全了,问题也多。好在大家都是直率的人,有什么就说出来。分配总是最烦心的事情。我也为自己找过占有过多股份的理由;为什么是我,而不是别人。想来想去,我想必须认可别人的价值的同时,正确认识自己的价值:有足够的资源,包括汇集人员、吸引资金;可以承受创业失败的经济损失;以及自己怀着的一颗公平公正的心。相信自己做领袖,比其他人做的更好的概率更大。当然这些,并非否定同样有能力的其他人,而是多承担一份责任。最后,作为产品开发第一线的重要参与人员,能维持开发工作的顺利进行,这是创业初期的生死命脉。

我也固执的认为,作为需要快速决策的创业公司,三人团体已经是极限了。再大,很难做到充分的信任;不会把各种事情拖陷在无边的讨论和妥协过程中。同时,我也反对网易式的一人独断的方式。容易犯错,且不让多角度的声音得到真正的尊重。

协调初期团队的股权分配是真正的挑战。若能做到一人或几人垄断股份,倒是没有那么多问题了。我所认识的一些创业公司,都是在初期很少的几个伙伴进行创业,其他人员或是打工身份,拿着一份薪水,或是有一些微不足道的股权奖励。而这次,难得 dingdang 和我以及一位我不太提及的同学,三人可以共识,我们只想做成一件想做的事情,而希望把这件事情成功可能带来的经济收益分给足够多的人,让更多的有理想的人摆脱经济上的负担,未来可以更纯粹的做点事情。我们三人在保证新公司的控制权,最低 51% 的股份的基础上,把剩下的股权(除去为了融资出让给投资方的一部分)全部分给其他伙伴。

怎么分配,却折磨了我们几个月。每个人都认为自己其实非常重要,至少和其他人一样重要,甚至更甚。人很多,平均制显然不合理,也最不能平息每个人心中的不平感。每个人都有足够的理解说服自己去争取更多的份额。我一点都不为大家拼命给自己争取感到奇怪或失望。仿佛在事情尚未做好前,前谈利益分配,不太符合中国人的传统观念。但我想这也真正体现了我们团队间的坦诚。正如昨晚 dingdang 愧疚的说,未能让哪怕一个人满意。网易游戏的几乎所有人都知道,dingdang 做了很多年的巨额奖金分配。他自己分到的不多,尽心竭力,却依然抱怨声四起。这次我设计了股权分配方案,把自己从利益体独立出来,中立的来做股权分配方案,依然很糟糕。唉,恐怕是最糟糕,却最终可以被接受的方案了吧。

新公司应该还是做游戏。网络游戏。或许会沾点移动互联网,但肯定还是 MMORPG 。为这个项目具体立项,我们讨论了一晚又一晚,也是诸多的分歧意见。网易毕业的同学们大多选择了 clone 自己做过的成功产品,有成功的,也有不成功的。但我们不想这么干。作为游戏玩家这么多年,想法太多,也做了太多思考和研究,总应该做一点点新东西吧。

作为程序员,软件架构师,我也实在抵挡不住诱惑,抛弃原来做过的所有,重新来一个新的东西。20 年的开发经历,让我能保持一些清醒的头脑,何况现在押上了十多人的梦想和数年的青春。过去犯过的总总错误,获得的点点收获,都应该能帮助我这次尽可能的回避失误,少走弯路。

明天,开始干吧。