Cocos 中的脚本绑定
语言绑定技术
绑定技术是指在目标语言中创造一套简单的 API 接口并将它指向另一种语言中的实际实现。往往其功能的实现非常庞大复杂,可能是目标语言所无法承载的,但是通过这种方式可以使目标语言具备使用这些功能的能力。
Cocos中的脚本绑定
早在2012年,时任 Zygna 工程师的 Rolando Abarca 主导开发了一套基于 Cocos2d-x 和 SpiderMonkey 的 JavaScript 自动绑定技术。这套绑定技术将 Cocos2d-x 的 C++ API 暴露到了JavaScript 语言中,使得开发者可以使用 JavaScript 来编写游戏代码,并享受 C++ 底层引擎框架高性能。后来 Cocos2d-x 团队的成员用同样的框架导出了 Lua API,完成了 C++ 到 Lua 的绑定。
这也是目前 Cocos 中脚本绑定技术的根基,由 Cocos 团队一直维护至今。目前 Cocos 的三种语言支持 C++/Lua/JS,其中的 Lua 和 JS 就是脚本绑定技术的成果。
绑定基本框架
让我们来先看一下 Cocos 引擎脚本绑定技术的框架图:
可以看到 JS 绑定和 Lua 绑定技术的结构是一致的,让我们自底向上一层一层解析:
- 首先在最底层的是 Cocos2d-x 的 C++ 实现,绑定技术归根结底只是将其实现的功能暴露到对应的语言层。
- 往上一层是 JavaScript 和 Lua 的脚本引擎,其中 JS 绑定使用的是 SpiderMonkey 作为JavaScript引擎。SpiderMonkey 是由 Mozilla 维护的世界上第一个 JavaScript 引擎,也是目前 JavaScript 执行性能最优秀的引擎之一。
- 脚本引擎这一层提供了两个重要的能力:首先当然是执行脚本的能力,但是最重要的是它还提供了对脚本层进行访问的 C API。第二个能力成为了 C++ 引擎到脚本层的桥梁,让 C++ 可以截获脚本层的 API 调用,访问脚本环境中的变量,构造新的对象,主动调用脚本层的函数等。
- 在脚本引擎提供的 C API 基础之上,Cocos 引擎中通过两种方式将 C++ API 暴露到脚本层:自动绑定和手动绑定。同时,使用纯脚本实现来改造一些 API 让其更适合脚本程序员的习惯,并且提供一些不需要绑定的 API。这三者共同构成了脚本层 API。
- 有了脚本层的 API,开发者们自然就顺理成章得可以使用脚本来开发 Cocos 游戏了。
关于自动绑定
自动绑定技术是 Cocos 引擎最引以为傲的利器之一,为什么说它是 Cocos 脚本绑定技术的根基呢?绑定本身并不困难,有了脚本引擎和它的 API 就可以完成,真正困难在于以下几点:
- 如何将 Cocos 中如此巨量的 C++ API 全部绑定出来?
- 因为 Cocos 的版本迭代所带来的变化势必会影响到其绑定代码,那么如何保证绑定代码的可维护性?
- 如何让一个不懂得绑定技术的人也可以为他所做的功能提供脚本绑定?
正因为这些困难,自动化的绑定技术才真正让 Cocos 的脚本绑定成为现实。目前 Cocos 内部主要的绑定代码都是由自动绑定工具生成的,并且版本迭代当中,都会根据代码修改自动更新自动绑定代码。除此之外,用户也可以自由使用自动绑定工具绑定自己编写的 C++ 类。
Cocos 中的自动绑定技术基于 Bindings Generator 工具,配合上 C++ 类配置信息,通过扫描 C++ 类的头文件,Bindings Generator 就可以生成所有公有 API 的脚本绑定代码。
关于手动绑定
虽然 Bindings Generator 已经非常强大,但是它依然有其局限性。目前的 Bindings Generator 的局限性主要是以下的几点:
- 只能够针对类生成绑定,不可以绑定结构体,独立函数等
- 不能够生成 Delegate 类型的 API,因为脚本中的对象是无法继承 C++ 中的 Delegate 类并重写其中的 Delegate 函数的。
- 子类中重写了父类的 API 的同时,又重载了这个 API。
- 部分 API 实现内容并没有完全体现在其 API 定义中。
- 在运行时由 C++ 主动调用的 API。
对于这些类型的 API,我们就需要通过手动编写绑定代码来将这些 API 绑定到脚本层。手动绑定出来的绑定代码的基本原理和自动绑定一致,只是自动绑定因其局限性绑定出来的内容可能会导致编译失败或者行为错误。
前三种类型的 API 通过手动绑定都可以解决,但是第四种类型,无法简单通过绑定来实现。因为绑定技术实现的是将 C++ 暴露到脚本层,使得脚本层可以调用 C++ 层实现的 API。而从 C++ 调用的 API,并不能主动得调用到脚本层,这是相反的过程。所以就需要在 C++ 源码中做一些特殊的处理,比如 Cocos 中 Node 类的 onEnter,update 函数,都有做类似的处理。同时,Cocos 中的 Action 类,都无法在脚本层被继承,根本原因也在于此,脚本层重写的 update 函数无法被 C++ 的 ActionManager调用到