« December 2014 | Main | February 2015 »

January 30, 2015

Lua 5.3 升级注意

最近在慢慢把公司的几个项目从 Lua 5.2 迁移到 Lua 5.3 ,为发布 skynet 1.0 alpha 版做准备。

在更新代码时发现了一些注意点,罗列一下:

Lua 5.3 去掉了关于 unsigned 等的 api ,现在全部用 lua_Integer 类型了。这些只需要换掉 api ,加上强制转换即可。通常不会有什么问题。

最需要细致 review 代码升级的是和序列化相关的库。在 skynet 里是序列化库、sproto、bson 等。我们还用到了 protobuffer ,也和序列化有关。

这是因为,Lua 5.3 提供了整型支持,而序列化工作通常需要区分浮点和整数分开处理。json 这种文本方式则不需要,同样还有 redis 的通讯协议也是如此。

过去判断一个 number 是浮点还是整数,需要用 lua_tonumberlua_tointeger 各取一份做比较。虽然到了 Lua 5.3 这种代码理论上可以不用改动,但正确的方法应该是使用 lua_isinteger

浮点和整数的问题还存在于部分 lua 代码中。一些函数只接收整数参数,如果传入浮点数就会有运行时错误。比如用 string.format 格式化数字的时候用了 %d ,但传入了浮点数,之前没有问题,现在就需要多调用一次 math.floor 了。很多浮点数和除法有关(无论是否除尽,除法的结果一定是浮点数),整数除法应该尽量使用 // 新操作符。

bit32 库已经被废弃了,我个人建议不要用兼容方案,直接找到用到的代码换成 lua 5.3 支持的位操作符比较好。

同样,我们原来直接写了 utf8 库,应当换成 lua 5.3 官方版本。至于原来写的那个 int64 支持的过渡方案更应该废弃。

lua 5.3 提供的 string.pack 基本就是非官方的 struct 库,使用它可以节省不少对二进制流的处理代码。

我们项目的代码中也很多地方因为 unpack 这个函数而需要修改为 table.unpack 。这个函数从 lua 5.2 开始就从全局函数移到了 table 下,只是因为部分同学的习惯问题而一直没有改过来。这次 lua 5.3 把它从全局变量里删掉了,就逼大家改习惯了。

__ipairs 在 Lua 5.3 里去掉了,现在 ipairs 的行为改为从 1 开始迭代,直到第一个为 nil 的项停止。对于大多数库来说不太需要修改。但我们项目内部用的一个模块却因为这个修改导致了代码死循环。

追溯原因是因为该模块被设计成,每次调用 __index 都会生成一个默认对象。因为作者希望用的人可以随便写 a.b.c (即使 b 没有事先构造出来)。这个设计我个人并不喜欢,因为它修改了 __index 的字面意义。也因为如次, ipairs 永远取不到为 nil 的项,而死循环下去(直到内存耗净)。

有些 C 库在区分 Lua 5.1 和 Lua 5.2 时,直接比较 C 中的版号宏,把不是 5.2 的情况都当成 5.1 ,这种做法不再合适(比如 lpeg 就是这样)。

此外,Lua 5.3 的 lua_gettable 现在会返回取出的值的类型,这对需要检查 gettable 的项是否为空时,可以少一次 c api 调用。如果看到有类似处理的地方简单优化一下也好。


总的来说,Lua 5.2 到 5.3 的升级比 5.1 到 5.2 的过程要顺利的多。因为没有 jit 版本的问题,我个人建议在使用 Lua 5.2 的项目,如果肯花一点时间的话,还是都升级到 5.3 吧。

skynet 目前有一个 lua53 分支,它是 1.0 alpha 版的备选。希望跟进 skynet 开发的同学都切到这个分支上帮忙看看有没有什么问题。

我会在我们自己的项目跑一段时间后,把它切到 master 上,并发布 1.0 alpha 版。

January 18, 2015

Lua 5.3 中文手册

一边在群里嘻嘻哈哈扯淡,一边翻译着 Lua 5.3 的中文手册。这项工作终于在本周内完成了。

http://cloudwu.github.io/lua53doc 这里可以在线浏览。

得益于强大的 github ,让我可以专注于翻译,而不用在意打字太快造成的各种错别字。这使得翻译工作进行得很流畅。只花了 6 天就完成了 350K html 文本,大约六万汉字的翻译工作。中间还出去跟人吃了个饭、打了一晚上 XCOM ,去岩馆抱了石,回了好些 email ,在群里各种吐槽,以及给 ejoy2d 做了点优化工作。

当然,整个过程并不算轻松。我一天写 2000 行代码时,都没打字到指头酸痛过。

有很多同学帮着做校对。每天中午起床后,只需要合并一下错别字修改的 PR 即可。这里一并感谢你们。你们的名字及贡献永远留在 github 仓库里了:)

如果你在阅读过程中发现什么问题,也可以 fork 这个仓库 ,改好 pull request 即可。

January 13, 2015

Lua 5.3 正式发布以及文档翻译计划

Lua 5.3 正式发布了。

我的三个计划就需要开动了。

计划一:把文档重新翻译一遍。

八年前,我翻译了 Lua 5.1 的中文手册。今天我想再从头做一次翻译工作,借这个机会可以理一遍(以及更新)和 Lua 相关的知识。这次,我选择在 github 上做这个工作,而不是闷头搞完了再发布。

这个项目在 https://github.com/cloudwu/lua53doc ,如果你只是想阅读最终的手册,可以访问这里。如果发现了错别字,不用给我留言,你只需要在 github 上提个 PR ,我会合并的。

今天下午开始的,目前译完了 readme目录页、和正文的很少一部分。我估计需要全职至少 3 个工作日才能全部译完。这次我选择尽量把英文术语翻译成中文,对于我按个人喜好选择的译词,我专门列在一个页面中。

计划二:基于 Lua 5.3 重新写那本 Lua 源码欣赏。至于原来没写完的那个版本,就不给出链接了。google 的话应该找得到。

原来是用 LaTex 编排的,这次我打算直接在 github 上用 markdown 写,这样效率更高。

不过,距离这本书完稿估计要至少一年了。

计划三:把 skynet 带的 lua 升级到 5.3 ,然后发布 skynet 的 1.0 版。这个工作已经在 skynet 的一个叫 lua53 的分支上进行了。希望有同学可以帮我 review 代码。

等确认修改后没有什么问题时,我会发布 skynet 1.0 的 alpha 版。

January 08, 2015

如何拼接 PVR 压缩贴图

2d 游戏通常都用到很多图素,由于显卡硬件特性,我们又不会把单个图素放在独立贴图中。这样会导致渲染批次过多。在移动设备上,非常影响渲染效率。

所以,游戏运行时,这些图素一般都会合并在很少几张贴图上。要么采用离线合并的方式(利用 texture packer 这样的工具),或者在运行时使用装箱算法。

最近,朱光同学一直在为 ejoy2d 编写运行时合并图素的模块。今天我们讨论了一下他做的诸多尝试。

为了提高贴图利用率,我们可以把大的不规则图素先拆分成小块,然后再组合起来。对于不压缩的贴图,怎么拆分合并都是没有问题的。而对于压缩贴图就要多一些技巧了。

对于 S3TC(DXT) 和 ETC 等压缩贴图,都是按 block 为单位压缩的。block 大小通常是 4x4 像素,block 之间是没有关联的。所以只要按 block 为单位切开,再合并时就不会有任何损失。

PVR 贴图要复杂的多。

了解 PVR 算法可以读一下这一篇 paper

这里简述一下:

PVR (4bpp 版本)贴图以 4x4 block 为单位,为每个 block 生成一个 Color A 和一个 Color B 。然后在采样的时候,根据 A 和 B 以及这个像素对应的两个 Modulation bits 插值计算出这个像素的 Color 。

如果仅是这样,对之前的 S3TC 算法改进就有限。

S3TC 压缩有一个问题是在跨越 block 边界上的两个像素很容易发生明显的跳变。这是因为每个 block 独立编码造成的。PVR 是这样解决这个问题的:

贴图上每 2x2 个像素,都会对应到 4 个 block 。也就是说,采样时,会从周围的四个 block 取到 4 对 Color A/B 。每个像素的采样都先先从这对 Color 中做线性插值,再利用 Modulation bits 再插一次。


也就是说,PVR 贴图上的每个像素最终的颜色,都贴图上一个 8x8 区域相关。这可以在同等压缩比下,提高图像的质量。但坏处是即使是采用一些简化算法, PVR 的压缩代价还是很大;如果使用 CPU 解压缩,也会很慢。在 N7 上测试,一张 2048 的贴图,pvr4 编码需要 1.7s , 解码 1.4s 。

但如果根据 pvr 算法的特性,我们还是可以找到方法绕过编解码过程来做切分合并工作:

把每个单独的图素的轮廓外留下 4 像素宽的空白带(alpha 为 0 ),再进行离线 PVR 压缩。作为边界上的空 block ,我们只需要保留其中 Color A/B 的 32bit ,而抛弃 32bit 的 Modulation Data 。因为后 32bit 一定是固定值(0xAAAAAAAA) 。

ps. 这里要提一下 PVR 贴图为 alpha 通道做了特别的优化。只为需要 alpha 通道的 block 描述 alpha 通道,且专门为 alpha 为 0 的情况做了处理。

在合并后,也同样需要让图素间保持 2 倍 block ,也就是 8 像素的距离。

为什么 skynet 提供的包协议只用 2 个字节表示包长度

skynet 提供了一个 lua 库 netpack ,用来把 tcp 流中的数据解析成 长度 + 内容的包。虽然使用 skynet 的同学可以不使用它,而自己另外实现一套解析协议来解析外部 TCP 数据流(比如 skynet 中的 redis driver 解析 redis server 的数据流就是用的换行符分割包),但依然有很多同学询问,能不能自定义包头长度。

这里的这个库定义的协议中,包长度是用 big-endian 的 2 个字节表示的,也就是说一个包的长度不得超过 64K 。这让很多人很为难。已经几次有同学建议,把长度放宽成 4 个字节,因为他们的应用环境中大部分包不会超过 64K ,但总有少量超过这个限制的。

历史上,skynet 的 gate 还是用 C 实现的时候(那个版本依然可以使用)的确可以自定义是使用 2 个字节还是 4 个字节表示包长。但经过一番考虑,我还是去掉了这个选择。

一个好的库,应该简洁,且引导使用者用正确的方法做正确的事情;而不应该提供让用户犯错的机会。在和游戏客户端通讯的时候,如果你只采用一个 TCP 连接,那么允许很长的数据包本身就是错误的。甚至 64K 都太大。

游戏通常需要比较快的响应速度,如果你允许在单个 TCP 连接中插入一个太大的数据块,比如 100K ,那么在比较弱的网络条件下(例如手机网络),处理这个包可能就需要超过 1 分钟的时间。而这么大的数据块,在业务逻辑上大多不期待立刻能发出或收到的。一个典型的应用场景就是用户在拍卖行中查询所有的上架物品,如果把所有返回数据都放在一个数据包中,很容易就变得很大。而查询大量这个操作,用户本身就对立刻回应没有期待。

而在单个 TCP 连接上,这样一个大数据块会阻塞住整个信道,后面本来需要快速送到的数据全部被延迟了。

如果你想在业务层做一个心跳包检测网络是否超时,很容易就把心跳包拦在后面。而通常网络处理层不会提供接口让业务层探知是否正在接受数据(skynet 的 gateserver 就没有提供这样的接口,虽然它很容易提供,只需要修改几行 lua 代码),只能在一个完整的数据包收到后才会交给业务层处理。

正确的做法是,在长度+内容这个协议上再加一个层次。加一条协议叫大数据块,允许把一个大数据块分几段发送。可以在这条协议中加上数据块 id ,在后面引用这个数据块的包中附上数据块 id 即可。

为什么要用这种比较绕的方式,而不直接把包头从 2 字节改成 4 字节?当你做这个设计时,就已经表明你重视了上面提到的问题。


当你把数据包都分割的比较小时,才能实现单个 TCP 连接上承载多个信道的能力。对于网络游戏,并不是所有的数据包都是上下文相关的,你可以看成隐含着有多条线索。比如聊天频道的信息和场景同步的信息就是相互独立的。skynet 为这种场景还提供了额外的 socket api 支持。socket.lwrite 可以把一个字符串(一个数据包)写到低优先级通道。只有等默认通道(高优先级)的包全部发送完后,低优先级通道上的包才至少被发送一个(单个包可以保证原子性)。

比如,你可以用它来发送聊天信息,就不会因为聊天信息泛滥把其它重要数据包都塞住。同样,你可以用来发送被分割后的大数据块。如果同时你还有很多其它重要的数据需要传输给客户端,那么这些数据块就会被打散穿插在其间。

当然,你也可以把所有给客户端的数据全部用 lwrite 发送,而仅仅把心跳包放在常规高优先级通道,可以保证心跳频率更稳定。


另外,采用 4 字节的包长度还有一个安全漏洞,可能被攻击利用。

一般的分割包的代码,在收到包头时,都会根据长度信息预先分配出相当长的空间,等着后面的数据达到后填入。如果攻击者不断在新建立的连接上发送一个恶意的长度信息,比如 2G ,服务器内存很容易被快速消耗光。

早期 skynet 的 gate 实现时,采用的是共享一个固定长度的 ringbuffer 的实现,可以避免这种攻击。但新的版本由于不再允许 4 字节长度,就没有做特别处理了。

如果你的应用环境非常特殊,坚持一个允许更大长度的数据包协议。那么我建议你慎重的实现一个分包模块,而不是简单的把 netpack 库中的 2 改成 4 。

January 06, 2015

从 Lua 5.2 迁移到 5.3

在 2015 年的新年里,Lua 5.3 发布了 rc3 版

如果回顾 Lua 5.2 的发布历史,Lua 5.2 的 final 版是在 rc8 之后的 2011 年 12 月 17 日发布的,距离 rc1 的发布日 2011 年 11 月 24 日过去不到 1 个月。我们有理由相信正式版不远了。( 5.3 的 rc1 是 2014 年 12 月 17 日发布的)

这次升级对 Lua 语言层面的影响非常的小,但新增加的 int64 支持,以及 string pack 、utf8 库对开发帮助很大。所以我强烈建议正在使用 Lua 5.2 的项目尽快升级到 5.3 。相对而言,当初 5.1 向 5.2 升级的时候就痛苦的多(去掉了 setfenv ,增加了 _ENV)。

我计划在 Lua 5.3 正式发布后,将 skynet 内置的 Lua 版本升级到 5.3 ,然后着手进行 skynet 1.0 的发布工作。

在 skynet 的应用环境下,我还是需要对 lua vm 的实现打一个 patch 让 不同的 lua vm 间可以共享 Proto 。但这个工作可以先不忙做,等正式发布后再来也可以。

目前可以先逐步升级 skynet 下的 lua 库。

我已经在 github 项目下创建了一个叫 lua53 的分支,做了一些工作。希望有同学可以帮忙一起 review 这部分代码。有兴趣的同学可以对照 最新的 commits 来检查这些升级做的变更。

必须做的修改是去掉 unsigned 有关的 api 调用。

lua 5.3 去掉了 lua_pushunsigned lua_tounsigned 等 api ,现在一律使用 lua_pushinteger 等。这些 api 默认操作 lua_Integer 这个数据类型。按文档的说法,在你的代码中,应该尽可能的使用 lua_Integer 。它默认等价于 long long ,至少保证 64 位字长(lua 5.3 可以配置成使用 32bit 整数,但在 skynet 的应用环境不会这么做)。如果需要无符号整数,可以再在 C 代码中做强制类型转换。

这部分工作做完后,整个代码就可以正确编译了。

但是,和序列化有关的库还需要为 lua 5.3 优化。因为 lua 5.3 原生支持了整型,不需要全部转换成 double 类型储存数字。

之前在做数据序列化工作时(seri 库 和 bson 库等),为了区分一个 number 类型到底是浮点数还是整数,我采取的方法是用 lua_tonumberlua_tointeger 分别取一次,然后比较两个数值是否相等。在 lua 5.3 中,直接提供了更高效的 lua_isinteger 来做判断。

由于现在直接支持 64bit 整数,就不再需要使用 lightuserdata 来保存长整数了。所以我去掉了 int64 库

相应的,相关的库应该做一些调整。pbc 库目前没有打包在 skynet 项目中,但我已经修改完毕,晚一点再放出来。skynet 内自带的序列化库,以及 bson ,redis 都需要做一些调整。

btw, 再修改序列化库时发现一个 bug ,再不支持非对齐地址访问的架构下会有点问题,这次一并修改了。

这次,lua 5.3 中把 __ipairs 去掉了,并且重写了 table 库。为了 Conceptual Integrity ,而敢于删改过去的东西,一直是我很欣赏 lua 的地方。

lua 5.3 同样去掉了 bit32 库(打开 5.2 兼容模式时,这个库还是存在的),而且这个库只对 32bit 整数有效,位操作现在提供了原生的操作符支持。(注:xor 是用 ~ 而不是 ^ ,因为 ^ 已经被用于 pow 操作了)我检索了整个代码,发现用到 bit32 最多的是那个从 openresty 移植来的 mysql driver 。

但实际上,在 lua 5.3 中不必再使用位操作去解析数据流了。因为有新的 string.pack 这个强大的 api 。比如:

local function _get_byte8(data, i)
    local a, b, c, d, e, f, g, h = strbyte(data, i, i + 7)

    -- XXX workaround for the lack of 64-bit support in bitop:
    local lo = bor(a, lshift(b, 8), lshift(c, 16), lshift(d, 24))
    local hi = bor(e, lshift(f, 8), lshift(g, 16), lshift(h, 24))
    return lo + hi * 4294967296, i + 8
end

这个函数可以被简化成:

local function _get_byte8(data, i)
    return strunpack("<I8",data,i)
end

在修改过程中,我发现 openresty 里这块代码写的很不 lua ,比如这个 dump 函数,

local function _dump(data)
    local len = #data
    local bytes = new_tab(len, 0)
    for i = 1, len do
        bytes[i] = format("%x", strbyte(data, i))
    end
    return concat(bytes, " ")
end

按 lua 的惯用法应该写成:

local function _dump(data)
    return string.gsub(data, ".", 
        function(x) return format("%02x ", strbyte(x)) end)
end

这样既简洁,性能也好很多。

其实这是个普遍的问题。由于 Lua 天生是门嵌入语言,几乎所有的 Lua 程序员都用过别的语言。所以许多 Lua 程序员带着其他语言的经验来写。前段时间我就发现过另一个例子


由于 mysql 这块改动最多,所以特别需要有人来一起 review 和测试。当然这块代码还有很多可以改进的地方,暂时就没有精力做了。如果有同学有兴趣,还可以把那块尚未完成的编码设置加进去。

January 03, 2015

新的一年

转眼就是 2015 年,我们的公司成立都 3 年多了。

记得 2013 年底,公司账户里的资金仅够支持不到一年的开发费用,而我们的 MMORPG 斗罗大陆还看不到可以立刻上线盈利的希望。那个时候公司只有 60 人。而时过一年,我们已经有 3 款移动游戏正式上线,第 4 款也在内部测试中,公司终于扭亏为盈,挣来了未来两年的发展基金,人员规模也扩大到了 120 人。

2014 年底的最后三天,公司的管理层 7 人驱车到阳江海边封闭。除了回顾总结一年的工作外,主要是讨论来年要做的项目。

去之前的一个月,我基本都在打游戏,和同事们聊游戏,然后写案子。2013 年注册了 steam 账号后一直没有怎么用。而在最近一个月,我从 steam 2 级迅速升到了 8 级。在 steam 上购买的游戏已经 59 个了。写评测文章收获了许多 steam 好友,也逐步了解了另一个游戏生态圈:电脑游戏(特指为了游戏而游戏,从游戏过程中获得乐趣)在中国居然还活着,并在逐步壮大。

过去总是说,盗版毁了中国的游戏产业,而现在呢,盗版已几乎不是问题了。我们这些游戏人却迷失在了网络游戏挖坑中。业内同仁见面总是调侃我们,简悦真是良心企业;我们也知道,玩家不用花钱或花很少的钱玩得痛快的游戏是不招渠道欢迎的。玩家的时间就那么多,玩了你这款游戏就势必少了玩其他游戏的机会,如果你不能吸金,那么就是票房毒药,应该赶紧下架。这样对于公司来说,是很难长远发展的。道理不是不懂,可从道具收费游戏中赚到钱也是门技术活,很难做的来的,更勿说开发者的所谓情怀了。

情怀在我厂可以被调侃,但绝不能被嘲笑的。

记得公司有同学在群里聊天感叹,我们公司真的是一帮热爱游戏的人聚在一起,整天讨论的都是 gameplay 游戏性,没有理由做不出好游戏来。是的,若没了这些,还有什么理由把这么一帮人圈在一起呢?

回到现实。直到最后一刻看到一年的财务报表,我必须提醒自己:我们尚未脱离危险期。稍有不甚,大家就得散伙分行李。公司运作的开销已经远超过公司成立之时,公司的平均年龄已经接近 30 岁(这还是今年吸纳了不少 90 后拉下来的)。大部分同学都有了家庭的牵挂,肩上抗着的是满满的责任。公司必须先保障大家的生活质量,才谈得上理想。而在赢利方面,我们做的远不如预期的好,留下来年的开发及市场经费后,所剩不多。也就刚刚能发一笔勉强过得去的年终奖。在物价房价飞涨的年代,让公司所有的同学都结得起婚生得起孩子,还需要明年更加努力。

我这次的提案被毫无悬念的推延了,它不适合在明年立项。在宣讲的时候,我自己都没有激情,因为我明白,2015 年,我们还是需要做一个被主流市场认可的产品,可以在 2016 年站稳脚跟。

不过,公司的同学做的一个小 demo 终于可以被发展成一个完整的游戏了。头天晚上从 12 点到 2 点,花了两个小时和 dingdang 探讨做一款没有任何内购,纯粹靠一次性购买的游戏的可能性。争取到了一个同一战线的重要人物,使得在会议第 2 天很轻松的就让整个公司管理层通过了决策:支持一个纯粹的游戏项目立项,在严格控制开发成本的基础上,把这款游戏做出来。这是这次会议的所有结论中,我最感欣慰的一个。

会议之前,主创人员向大家展示自己的游戏 demo 。大家为画面、音乐和操作感赞不绝口时,只听到他沮丧的叨唠:唉,这些其实没用,最终还是要看数值成长的,付费上不去,一切都是白搭。

这下,可以给这些努力把游戏性做好的同学有个交代了。公司虽然积蓄不多,掏出 200 万做个纯粹的游戏还是任性得起的。不用考虑怎么挖数值坑,也不考虑怎么让大 R 掏钱,一份游戏我们就只卖一次,专心让游戏好玩就行了。希望 2015 年的 IGF 评选可以得到玩家们的认可,如果再碰到 Ken Wong ,我就可以正式回答他 2014 年问过我的问题

我们一直很努力的尊重每个人的想法,不愿意做的项目尽量不安排做。而 2014 年是我们转型的一年,生死存亡之际,大家只求活下来,也就没有太多选择。一年下来的成绩单还算漂亮。

2015 年,在遵循大家意愿的基础上,相互还是需要多一些妥协。这也是为了 2016 年我们可以更加任性 :)