« skynet lua 服务的内存管理优化 | 返回首页 | Lua 远程调试器 »

Ejoy2D 开源

我们的第一个手游差不多做完了,预计在明年 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 里提出来。

Comments

刚在mingw中编译,跑起来了,感觉还是个不错的轻量级引擎,可以做的更完善

不错,留个脚印,ejoy2d

并不是说把自家代码开源,就可以称之为一款开源引擎。
开源项目,更多的要靠社区的力量来推动和运营。更多的是一种服务广大开发者的心态,一种贡献的心态。开发者即是用户。
我相信云风大神肯定没有这样心态,所以这样的东西是没人敢用的。就像多年前实现的lua的protobuf绑定:一句话就把大家吓退了:“可能与google官方的protobuf不兼容”

自己重写,不如把一些核心的理念和可以改善的东西贡献到2dx里面去,为什么不可以呢?
跨平台里面那么多坑,2dx团队和社区经过多年完善,开发者才敢用啊。


非常赞同。。。。。。

我想问下例子中的sample.lua文件中的内容除了手写,有工具将*.ppm *.pgm文件转换成lua描述表么

我希望每个模块可以由明确的输入对应明确的输出。

云风,请教个问题:
lib/particle.c中的随机函数
RANDOM_M11和c库rand()相比有啥特点?为何要用它来代替rand()呢?

inline static float
RANDOM_M11(unsigned int *seed) {
*seed = *seed * 134775813 + 1;
union {
uint32_t d;
float f;
} u;
u.d = (((uint32_t)(*seed) & 0x7fff) << 8) | 0x40000000;
return u.f - 3.0f;
}

字体渲染的另一套相关模块 你们开源吗?

字体的渲染:字符串生成Texture,可以使用harfbuzz排版和freetype2。
纹理的大小是一个字符一个纹理还是一张大的纹理啊?谢谢

感觉osx下使用x11的gl库不是个好点子, 也只有少数游戏真正需要用Xquartz(目前只见矮人要塞需要), 希望改用osx系统内的gl库, 望采纳.

你好,我在ubuntu下make成功生成ej2d执行程序,但是执行第一个例子时报错。提示如下:
./ej2d examples/ex01.lua
compile failed:0:1(1): error: syntax error, unexpected NEW_IDENTIFIER

source:
precision lowp float;
varying vec2 v_texcoord;
varying vec4 v_color;
uniform sampler2D texture0;
uniform vec3 additive;

void main() {
vec4 tmp = texture2D(texture0, v_texcoord);
gl_FragColor.xyz = tmp.xyz * v_color.xyz;
gl_FragColor.w = tmp.w;
gl_FragColor *= v_color.w;
gl_FragColor.xyz += additive.xyz * tmp.w;
}
帮忙看看,谢谢。。

实在抱歉,我看错了。。。

不明白 dfont.c 中第71行的df->line 是怎么回事?df没有line只有line1啊,并且为什么可以通过编译,即使开启了-wall选项?新手求教!

另外,接一个google breakpad来找crash挺有用。

lua的效率是非常高的,目前我做的一个手游项目中,一个LuaState中跑了10万+行Lua代码,性能很赞!

ejoy2d写的真是不错,里面对文字和资源的管理都非常精巧,C和lua结合的也非常紧密,期待继续更新 :)

Mac OS X 10.9 我编译的时候有很多问题,折腾了半天,最后还是编译过了,改了下makefile,我的方法:http://codingmelody.com/a/macosx_ejoy2d_make

从每个模块的初始设计上就支持脚本,这真的很不错。我们这边用的JS,效率低了点,Lua没试过,不知道云风对脚本执行效率比有没有些心得呢?

好干净,简洁的代码,+一套UI工具后在完善代码的话。。。很有享受的感觉哦

可以写一下在mac 环境下的编译和使用流程吗

在linux下编译,不增加-ldl项编译出错
/usr/bin/ld: note: 'dlopen@@GLIBC_2.1' is defined in DSO /lib/i386-linux-gnu/libdl.so.2 so try adding it to the linker command line
/lib/i386-linux-gnu/libdl.so.2: could not read symbols: 无效的操作

我是双显示器,在WIN7(64位) 执行实例1,例子一下占了2个屏,黑乎乎一片,刷新下桌面才恢复。

<a target="_blank" href="http://shang.qq.com/wpa/qunwpa?idkey=73f78d290a73508e47826ead9657ab2e50df796d936ca2feb7ed6c3c86033068"><img border="0" src="http://pub.idqqimg.com/wpa/images/group.png" alt="EJoy2D引擎技术交流" title="EJoy2D引擎技术交流"></a>

@天麒

在一个轮子上缝缝补补还是造一个新轮子。如果不是因为缝补的工作太困难了,相信谁都不愿意去造新轮子。

@天麒

在一个轮子上缝缝补补还是造一个新轮子。如果不是因为缝补的工作太困难了,相信谁都不愿意去造新轮子。

并不是说把自家代码开源,就可以称之为一款开源引擎。
开源项目,更多的要靠社区的力量来推动和运营。更多的是一种服务广大开发者的心态,一种贡献的心态。开发者即是用户。
我相信云风大神肯定没有这样心态,所以这样的东西是没人敢用的。就像多年前实现的lua的protobuf绑定:一句话就把大家吓退了:“可能与google官方的protobuf不兼容”

自己重写,不如把一些核心的理念和可以改善的东西贡献到2dx里面去,为什么不可以呢?
跨平台里面那么多坑,2dx团队和社区经过多年完善,开发者才敢用啊。

又来走走,看看有更新没

确实够简洁,但要真做游戏用起来还要写不少东西。用 lua 做描述,可视化编辑器实现估计比较麻烦。

支持开源,谢谢分享技术~

你需要把 lua52.dll 复制到 /user/local/bin

Windows 下不要静态链接 liblua.a

不知道为什么,我在MinGW下编译,lua版本是5.2.3,我把makefile里面的-llua52 改成-llua才编译过。

期待看到优化后的字库渲染部分

云风, 如果依赖不复杂可以很容易用我的aimake将它port到macos, linux, mingw, android, ios等系统, https://github.com/ashun/aimake

ej2d交流群119121319

不用考虑 lua 描述性能。

因为资源是 2 步导入的.
第一步是从 lua 表到一个 blob, 第二步才是 blob 到 C 对象.

这样方便开发,资源版本 diff 也好管理.

我在 blog 里已经写了, 发布版直接导入 blob 即可.

支持

lua描述表肯定不是人工填写的,如果工具链齐全的话,不会让美术有什么压力,只是不知道这样的lua描述表在效率上比2进制差了多少

是不是需要一个工具将texture2d生成的plist转换成lua描述表

简单看了下API设计,真是太糟了。资源包描述文件要是人肉填写的话简直是要死人啊,就算在上面实现了编辑器也是效率低下的。这样的设计导致整个美术生产流水线的效率会很低下。

支持下

我知道了。我在 windows 下一直都用的 msys/bash 没用过原生 shell

@漂流

你试过?windows shell 这点都做不到?

winfw.c里面startscript中path的正则表达式会取最后一个反斜杠前面的作为path,
所以在windows下像文中这样运行程序会导致path为nil:
ej2d examples/ex01.lua
改成这样就不会出错了:
.\ej2d examples/ex01.lua

这是干货啊,造福后来的开发者,顶一个!

记得以前上计算机图形课还写过迷宫程序,不过很简陋的,这下有时间研究一下2D引擎。

最后预祝你们的手游完美亮相哈~

Post a comment

非这个主题相关的留言请到:留言本