« January 2016 | Main | March 2016 »

February 16, 2016

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

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

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

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

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

我对墙壁的旋转很感兴趣,一开始以为是预渲染了若干张图。后来试着解开了 .kwad 资源包(可自行 google 到工具)发现里面的资源图片只有一或两张。ps. 图片是用 zlib 压缩过的 RGBA 位图,格式也可以 google 到。

以门为例,只有正面(或背面)的矩形图片。想来是在引擎中变形为斜视角下需要的平行四边形。

我简单推导了一下变换矩阵,需要一点初中平面几何知识就够了。

以 30 度倾角为例,x:y 大约是 2:1 。所以斜切变换时的转角是 arctan(tan(x)/2) 度,变换矩阵为 [1, tan(x)/2, 0 , 1, 0, 0 ]

另外需要先在 x 轴上做一个缩放,缩放比为 cos(x) 。

虽然 tan 在 90 度时会变成无穷大,但是两个矩阵相乘后,tan 函数消失了,变成了 sin 。最终的变换矩阵为:

[cos(x), sin(x)/2 , 0, 1, 0, 0 ]


我从 Invisible Inc 的资源包里解了一张门的图片如下:

wall.png

我们可以用 css 的 transform 给一个变换矩阵 style="transform: matrix(0.707, 0.3535, 0,1,0,0);" ,模拟在 30 度倾角下,旋转 45 度的效果:

wall.png

当然,因为这个是仿射变换,没有透视,所以还是会有些怪怪的。


最后,Invisible Inc 是个相当不错的游戏,推荐。

创意工场里有中文 Mod ,但是版本比较老,不推荐。如果想用需要自己改一下。

有两个问题:

  1. 游戏的排版引擎是靠空格分词折行的,汉化 mod 制作的同学不清楚这点,为了避免长句子后半截断了,结果把字体调的特别小,看起来很难受。其实只需要在长句子的每个汉字后加个空格就可以解决分行的问题了,这个写个脚本处理一下翻译文本即可。另外,字库要重新做一下。

  2. 游戏之前的格式化串是用的 C 风格,比如用 %d 替代运行时要插入的数字。但是最新的引擎改成用 {1} 引用第一个参数,{2} 引用第 2 个参数这样的了,所以在汉化 mod 用于最新版时,会出现大量 第 %d 天这样的文字,看不到数字,很干扰游戏。需要自己重新在 credit 界面按 ctrl insert, 弹出控制台,输入 localize 重新导出一份文本改一下。

以上我过年在家临时改了一份,上班后没带过来,懒得重新弄了。本来游戏就不太需要汉化就可以玩的。

ps. 为什么游戏引擎要修改文本格式化方案呢?这是因为在不同语言中,单词的次序可能是不一样的。在英语里,说的是 A 导致 B ,换到中文(或其他语言) 中,可能表述方式变成了 B 被 A 导致。两个词的次序就反了。如果用 C 风格的格式化,依赖参数传递次序,翻译就乱了。(我简单校对了一下原来的汉化文本,就有这种乱序错误)

另外,升级后的格式化方案还可以对输入参数做多次引用,以解决一些语言中词需要变形的需要。比如老版本中,%d turn(s) 在新版本中可以写成 {1} {1:turn|turns} 。根据第一个参数是否为 1 来选择使用 turn 还是 turns 。

这些是游戏做本地化的经验,在此记录一笔。

February 03, 2016

ejoy2d sprite pack 的空间优化

在 ejoy2d 里,我将 sprite 的结构信息储存在一组叫 sprite pack 的结构中。其中包括动画的 frame 数据,sprite 由若干部分组成,每个部分的变换矩阵,对应贴图的编码和坐标等等。

通常这些数据不会太大,所以我建议一次加载到内存就不再删除。而动态生成的 sprite 对象则直接引用这些数据,不必做引用计数。这些数据之间的交叉引用(可以像搭积木一样用很多部件构成复杂的 sprite )也不需要额外记录。但如果保存了大量的动画信息,或 sprite 是由非常多的小部件构成,数据量也可能非常可观。

在我们的 心动庄园 里,达到了数十 M 内存之多。前几天同事提到这个问题,我便动手做了一点简单的优化,居然省出了几十兆内存。

其实方法很简单。在 64bit 平台上,我将 sprite pack 结构中的指针都改成了一个 32bit 的相对 pack 头的偏移量。

在设计之初,我就是把整个 sprite pack 存放在连续单个内存块上的。为之做了一个非常简单的 bump allocator :资源打包的时候,统计整个要用的内存数量,然后在包加载时就分配出来,然后每个小对象都紧贴在一起保存。这样可以节省下大量小内存块的头,并将常规内存管理中可能出现的内存碎片率减少到零。

由于原本就是连续内存块,把指针更替成偏移量不需要修改太多代码

把指针体积减少一半后,数据结构对齐也会变得更紧凑,在我们公司三个项目实际比对下来后,发现 sprite pack 占内存量大约可以减少 40% 。


做次修改后,还获得了一个额外的好处。sprite pack 在内存中已经完全不包含指针,也就是说可以随意在内存中移动了,变得和地址无关。

我们可以在打包时直接把 sprite pack 的内存块直接 dump 到文件,加载时可以绕过 import 过程。想来可以快上不少(还没有测试)。即使不能提高太多性能,我们的资源加载过程会变得和资源文件大小严格相关,也就是说,可以通过资源文件体积来准确预测加载时间,可以实现一个非常匀速准确的 loading 条了。我想这对用户体验来说,是个不错的改善。

opengl bug 一则

这是个老问题,但是公司不同的同学先后被坑,所以必须记录一笔。这样可以增加事后被 google 到的概率,千万别来第三次了。

我原本以为 opengl 在 bind VBO 后,如果修改了 VBO 的数据,是不需要重新 bind VBO 对象的。所以早先的 ejoy2d 在这里的处理就做了一点优化,并没有重复 bind ,可以减少一些 API 调用。

大约在 2015 年 6 月左右,由于需要跟进 mac osx 的系统更新,ejoy2d 增加了对 VAO 的支持。我再修改相关实现的时候发现在某些设备上,出现了 bug 。

当时猜想可能是新的手机的驱动做了一些过去没有做的奇怪的优化。我没有太多确认 opengl 是否对此有要求,不知道是否是驱动的 bug ,不过还是做了一些修改。

这个 patch 中,我在更新 VBO 的数据后,设置了脏标记,最后提交时会重新 bind VBO 。

当时并没有刻意做为一个独立 commit 提交,所以被同事疏忽了。

前不久,我们的 心动庄园 在新版锤子手机上出现花屏,同事追查过原因,发现只需要更新 ejoy2d 就解决了。定位了一下原因,就是上面提到的 patch 解决的。

前几天,另一个同事自己用 ejoy2d 开发的小游戏 爆裂方块 在 360 平台审核时被报告有显示 bug ,又查了好久,今天终于定位到是同一问题。

在同样的问题上浪费时间真是不划算。

所以:

  1. 要勤作记录并让 google 可以检索到。

  2. 开源项目要记得保持同步。