我们的第一个手游差不多做完了,预计在明年 1 月初推广,目前内测的情况非常不错,我们也可以考虑开始下一步在手游领域立新项目了。
上个项目做的太匆忙,今年 4 月份才开始。因为决定做一个 2d 游戏,我觉得在 2d 游戏引擎方面我有超过 15 年的经验,使用一个流行的开源引擎,比如大家都在用的 Cocos2d-X 还不如自己写。这样对引擎的可控性更强,可以方便的定制自己需要的功能,并在性能上做针对性的优化。手机设备的硬件性能远不如 PC 机,即使程序性能足够,我们也要考虑硬件的能耗,让电池用的更久一点,让设备不那么放烫。优化引擎也是游戏程序员的乐趣之一。
我们这个项目还是做的太急了,只花了一个月时间做引擎,然后在上面做了太多应急的各种修改,到项目完成(10 月)时,已经很不堪了。我不想把这块东西带到下一个产品,所以打算重新把这块代码理一下。
开源是一开始就想做的事情,可以帮助到别人,也同时督促自己把代码写的更整洁。从这周开始,我满头写了一周代码,从头写了 4000 行代码,就有了这么一个项目:ejoy2d 。
当然它还很不完整,有兴趣的同学可以跟踪这个项目,我和我的同事会逐步完善它。同时欢迎其他同学推送 Pull-request 。下面对 ejoy2d 做一个简单的介绍:
首先,它仅仅是一个图形引擎,所以你在里面不可能看到诸如音频处理,网络处理等等和图形渲染无关的部分。而且更相当的称呼可能是基于 OpenGL ES 的图形库,它的设计目标是很容易的嵌入你自己的游戏引擎中。
那么,一个为移动设备开发的引擎,目前为什么只能在 Windows 和 Linux 平台上运行呢?
因为,我还没有想好以什么形式提供 iOS 和 Andriod 平台开发的工程文件和相关代码。如果你熟悉 iOS 或 Andriod 开发,把这个库嵌入进去也不是很难的事情。不久以后,我也会提供主流移动平台的相关模块。btw, 稍微修改一下 linux 的编译流程,应该可以 port 到 MacOS 上,暂时我还没有时间做这个工作。
可能和你见过的其他更庞大的游戏引擎不同,ejoy2d 甚至没有提供一个场景管理模块。这是引擎,我不觉得场景管理模块对于图形引擎是必需品。提供一个模块的场景管理模块反而会限制二次开发人员的想像力。ejoy2d 只提供绘制 sprite 的 API 。如果你希望更底层的控制渲染器,还可以使用 ejoy2d.shader 子模块中的多边型渲染 API 。
ejoy2d 采用 C + Lua 开发,一切高层 API 都是以 Lua 函数和对象形式提供的。二次开发人员完全可以用 Lua 来开发游戏,由于设计之初就把 lua 做为唯一上层开发语言,ejoy2d C 模块的 lua binding 模块都尽量保证高效。
sprite 是 ejoy2d 里最重要的数据类型,但引擎几乎不提供运行时的 sprite 构造方法,一切都描述在资源文件里。查看 examples/asset/sample.lua 你会看到一个资源包的描述文件的示范。
每个 sprite 都由一个 lua 表描述,type 字段表示了它的类型。目前 sprite 有四种类型:四边型图片(picture),多边形(polygon),动画(animation),文字框(label)。
id 字段是这个 sprite 在一个包文件里的唯一 id 。id 可以从 0 到很大的数字,一个包内的 id 不要求连续,但不建议散的太开。因为引擎是用一个数组来索引这些 sprite 的,这个数组的大小取决于最大的 id 号。
picture/polygon 表示了一个多边型从贴图到屏幕的映射关系。ejoy2d 为四边型做了一定优化,但同时也支持多边型。注:每个 picture/polygon 对象可以由不只一块多边型构成。排在前面的多边型绘制的时候更先渲染(在更下面)。
每个描述文件配了一张或多张贴图,开发的时候应该尽量把小图片拼合在同一张贴图内。游戏采用尽量少的贴图有利于手机平台的游戏性能。我们公司购买了 TexturePacker 放在我们的开发工具链中,当然你自己写一个工具做这个图片拼合工作也不算太难。一个资源包可以配多张贴图,在 picture 数据结构中写明引用的哪一张即可。这里遵循的 lua 习惯,贴图号是 base 1 的。
picture 的 src 字段 8 个整数表示的是 4 组贴图坐标,以像素为单位。screen 字段的 8 个整数则表示对应的屏幕坐标,是以屏幕像素为单位,但放大了 16 倍。你也可以认为这是一种定点数表达。
animation 是 ejoy2d 中最复杂的数据结构。一个 animation 是由若干 component 构成的。每个 component 可以用 id 引用其它的 sprite 对象(不限于静态图片组还是另一组动画),但你得保证不能成环。
animation 可以又多组动画序列,每组是一个 action 。当然你也可以只有一组默认动画组,这样不需要给 action 起名字。当有多组的时候,需要在描述数据里加上 action 字段,给一个字符串名字。
一组动画序列(action)是由若干 frame 组成的。每个 frame 可以由这个 animation 中定义的 component 任意组合。我们可以在资源文件中描述组合关系,如果简单引用一个 componenent ,就写上其编号( base 0 )。也可以加上一个变换矩阵,以及两个颜色值( color ,additive )。渲染的时候引擎会把这个 component 的颜色乘上 color 加上 additive 。
{
type = "animation",
export = "mine",
id = 38,
component = {
{id = 34 },
{id = 35 },
{id = 33 },
{id = 36 },
{id = 37 },
{id = 23, name = 'resource' },
},
{
{ 0,1,2,3,4,5 },
},
},
这是从 sample.lua 中截取出来的一段示例:它描述了一个动画 sprite ,只有一个默认 action ,这个 action 只有一帧 。这一帧由 6 个组件构成,依次是 id = 34, id = 35, id = 33, id = 36, id = 37, id = 23 的 sprite 。每个组件都没有特殊的位置变换,以原有的坐标渲染。这个 sprite 被命名为 mine ,这样,开发人员可以通过 mine 这个名字从资源包中检索到它,而不需要记住 id = 38 这个特殊数字。
id = 23 这个特殊组件被命名为 resource 。一个被命名的组件可以在运行时从父节点对象中取出,并对它做一些运行时操作。
ejoy2d 的 sprite 渲染遵循如下原则:
如果一个组件没有命名,那么在设置这个 sprite 的当前 frame (被渲染的帧),所有匿名组件都会相应的改变。这可以方便美术人员组合做好的动画,而不用在程序里干预。而如果一个组件有名字,则必须在程序中去设置它的帧号,而不会自动跟随父节点的帧变化。
在 sample.lua 里还可以看到这样的帧描述信息:
{ { index = 3, mat = { -1024,0,0,1024,0,0 } } },
它表示引用第 4 个 component ( base 0 ),并需要在渲染的时候附加一个变换矩阵。这是一个 2*3 的矩阵,可以描述 2D 空间的旋转、位移、斜切、缩放等变换。为了提高运行效率,引擎采用的是定点数,把实际小数放大了 1024 倍。所以 -1024,0,0,1024,0,0 实际是一个 (-1,1) 的缩放变换,也就是横向做一个镜像。
注意:不推荐在这里利用变换矩阵做镜像。更好的做法是在 picture 结构里就把 screen 的坐标点翻过来,这可以在运行时节省一次矩阵乘法。这里这样写仅仅是为了示范。一般这个变换矩阵只用于有一个部件想被多个动画公用的情况。
关于资源打包:
ejoy2d 目前只提供了一个叫 simplepackage 的模块,它简单的把描述文件,图片文件结合在一起。阅读代码 可以看到,它仅仅是在文件目录下按一定规则去读相关的文件。
这只是方便开发,在产品发行时不应该这样做。我不想提供一个模块的包文件格式和相关代码,这块可以留给用的人自己写。
sprite pack 提供了低阶接口,可以把一个以 lua 代码形式提供的描述文件变成一个二进制数据块(ejoy2d.spritepack.pack),然后再用 ejoy2d.spritepack.import 导入这个二进制数据块。你可以在发布工具链上,写一个简单的脚本把你的描述文件从 lua 转换成二进制格式,然后在产品发布后,运行时直接导入二进制数据块。这样可以使加载速度快的多(或许你还需要加一点代码对数据压缩、加密)。
贴图文件目前只支持二进制 ppm 和 pgm 格式,分别描述 RGB 的颜色通道和 Alpha 通道。这是因为 ppm 的格式最容易处理,只需要不到 200 行代码。我不想嵌入一个 libpng 这样的库去处理类似 tga png jpg 等等更多的图片格式。这些工作很容易做,不需要放在这个引擎中。而且通常游戏发行时不需要支持这么多图片格式。更有可能的是,你需要诸如 pvr 压缩贴图的支持,根据项目需要增加一个 pvr 数据加载模块要有意义的多。
Ejoy2d 的编译和使用流程:
你需要安装 lua 5.2 以上的库,以及最新版的 glew 。如果你需要在 Linux 下开发,可能还需要安装 freetype 库。
Windows 下我只测试了 Mingw 的编译环境。直接 make 就会在项目目录下生成 ej2d.exe 这个执行文件。如果编译不过,可能是依赖库的路径设置不对。
引擎运行需要 ej2d.exe 这个执行文件,ejoy2d 目录必须和这个执行文件在同一目录下。ejoy2d 目录下放置的是引擎的 lua 部分。
用 ej2d 带一个脚本名就可以做测试了。比如 ej2d examples/ex01.lua
我的 example 写的相当简陋,需要慢慢完善。sample 需要的资源在 examples/asset 目录下。图片看起来会比较粗糙,我是从我们上一个手游项目中截取出来的。只是起一个示范作用,为了不至让代码仓库体积过大,我把原图缩小了 16 倍。
在 lua 脚本中 require ejoy2d 就可以用了。你需要编写 update/drawframe/touch 三个函数,用 ejoy2d.start 注入到引擎,就开始工作了。
update 会以每秒 30 次的固定频率调用,而每次引擎接到系统的渲染请求时都会调用 drawframe 函数。注意:当程序处理性能不够时,依然会尽量保证每秒 30 次的 update 调用,但不保证 drawframe 被调用多少次。所以不要在 drawframe 里做游戏逻辑。
touch 函数是用来处理触摸消息的。Windows 只简单用鼠标点击模拟了一下。未来需要做更多的多点触摸和手势的处理。移到真正的移动设备上后,需要做更复杂的处理。
引擎还需要提供更多的系统消息,比如系统暂停,系统恢复,系统退出等等。这些在未来我会逐步加上。
Sprite API
sprite 对象从资源文件中构造。使用 ejoy2d.sprite(包名,导出名) 创建。创建出来后,你可以用 spr:draw 来绘制它到屏幕。
draw 函数可以带一个 SRT 的表,描述这个 sprite 最终绘制的位置变换。通常只需要填写屏幕 x,y 。如果你需要做整个场景的缩放,scale 也是经常会用到的。当然我也提供了 rot 来旋转 sprite 。
每个 sprite 都可以设置它自己的局部变换矩阵。ejoy2d 提供了一个简单的 matrix 库( ejoy2d.matrix ),用 spr.mat = mat 来设置变换矩阵。注意:这个设置的 matrix 和资源文件里给组件附加的变换是不冲突的。渲染时引擎会逐级把变换矩阵乘在一起。没有 API 可以取到资源数据中为 sprite 预设的变换。
sprite 也有 color 属性。它通常用来实现 sprite 的半透明。当然你可以做一些变色特效。同样,颜色变换在资源描述里也有一份,和运行时设置的变换颜色相互独立。
在做一些特殊效果(比如对按钮加亮)时,光用颜色乘法还不够,这个使用可以设置 additive 数据,累加一个颜色上去。
在引擎内部,每个 sprite 都由一颗树构成。如果在资源描述文件中指定了组件(子树)的名字,那么用 sprite:fetch(名字) 就可以取到这个子节点。匿名子节点是没有办法在运行时获取到的。如果子节点的名字和固有属性名(mat/color 等) 不重复的话,你也可以只用用 sprite["name"] 取到它。但记住,fetch 子节点是一个略微重量的操作。如果要反复操作一个子节点,建议只获取一次,放在 local 变量中待用。
引擎不支持动态增加子节点。但可以用 sprite:mount("name", child) 把一个 sprite 挂接到资源文件中描述的位置上,替换掉默认的子树。同样为了方便,也可以直接用 sprite["name"] = child 来做这件事情。
简单说,ejoy2d 不提供运行时构建 sprite 的方法,但可以通过灵活的资源文件来弥补这一点。(你也可以通过 ejoy2d.spritepack 的 API 在内存中动态创建资源包)
关于粒子系统:
我们的新项目想使用 Cocos2d 官配的粒子编辑器。所以我移植了 Cocos2d-X 中的粒子系统的代码。雷雨同学这两天把这个模块整合到了 ejoy2d 中。但我对这个粒子系统并不算满意,有机会会重新写一遍。
从 Cocos2d 移植过来的粒子系统我已经把渲染相关的代码剥离,变成一个纯粹的数据处理器.它只负责计算每个粒子片的变换矩阵。除了在运行时利用这些矩阵渲染粒子片以外,你也可以用这些数据生成 sprite 包的描述文件,这样就不必要在运行时去计算粒子了。损失一些运行时的随机性,但可以提高一些效率。
关于 UI 系统:
UI 对象可以完全用 sprite 来实现。但除了渲染,你需要的是获取 UI 消息。ejoy2d 提供了一个类似 draw 的方法 test 来做到这一点。test 和 draw 不同。它不真正把 sprite 画出来,而是判定一个坐标点是否落在它的包围矩形中。你可以设置 sprite 的某个子节点的 message 属性为 true 来捕获 test 消息。如果 test 的坐标落在任意一个 message 属性为 true 的对象(包括它的子节点)上,函数会返回捕获到消息的对象。
test 方法为实现一个完整的 UI 提供了可能。但把我们公司项目中 UI 模块(完全用 Lua 实现的)转移到 ejoy2d 这个开源项目里来还需要一点时间。
关于字体渲染:
这部分开源代码目前还很粗糙,刚刚能用而已。我们自己的项目使用了另一套相关模块,但暂时还没有精力搬过来。这部分需要逐步完善。
关于编辑器:
要把 ejoy2d 用起来是需要许多工具的。这方面我们也在积累,自己开发了一些编辑器,同时也用改造一些开源工具来输出 ejoy2d 能识别的数据。这部分暂时没有开源的计划。系统有热心的同学可以为它发起编辑器的开源项目。
关于文档:
有空再慢慢写吧,最近恐怕是项目压身,没那么多时间了。有问题可以在 github 的 issue 里提出来。