概述
cocos2d-js 是一个开源的可用于web、native环境的游戏引擎,它是HTML5版本的 cocos2d-x。 cocos2d-js 包含两部分,一部分是用于web game的cocos2d-html5,另一部分则是用于native game的 cocos2d-x javaScript binding (JSB)。cocos2d-js 提供了 cocos2d-html5 和 cocos2d-x jsb 兼容的 API,使得用cocos2d-js编写的game可以无缝、不需要任何修改的就可以跑在基于cocos2d-x jsb的native环境里。
基于javaScript技术栈,在开发效率上有不少的优势。众所周知,web开发是相当高效的,开发过程无需编译、打包,直接刷新下浏览器即可,而且现在的浏览器都支持强大的开发工具,比如chrome dev tools,包含了比如单步调试、网络数据访问、动态修改、内存/CPU性能分析、交互式控制台。
cocos2d-js 正是基于这一点,通过上层提供统一兼容API,快速的迭代,无缝的跨平台部署,进行游戏产出。
不过,在追求开发效率的道路上还不能仅仅这样就可以满足了,由于是基于javaScript栈开发,就需要考虑比如:
- 怎么进行依赖管理?
- 如何使用第三方库?
- 怎么做单元测试?
- 代码如何进行复用?
接下来会针对这些问题,提出探讨,并给出相应的解决方案,最后会提供一个 cocos2d-js 的 example 以供参考
cocos2d-js 开发存在的问题探讨
依赖管理
javaScript 依赖管理是一个老生长谈的话题,因为javaScript语言本身到目前为止还没推出统一的模块、依赖管理的机制(es6 module还需要好久),cocos2d-js 在这方面使用的是:
- 依赖管理挂载在 global 对象下面
- 脚本加载用 project.json 里的 jsList进行手动配置
基于全局的依赖管理作为引擎这样子使用问题还不大,因为我们最终使用cocos2d-js的地方都从cc对象获取即可,但是如果是你自己的应用层代码呢?
很明显,自己在做应用(游戏)开发时,要尽量避免使用全局变量,使用全局变量则必须小心,要考虑所有使用到的地方,否则一步留神就改出了问题,同时使用全局变量,对于一个文件依赖全局变量很难一眼看出这个文件原来依赖了某个全局变量,这样子尤其是在项目的后期带来了不少的麻烦,更不利于多人协作的项目
脚本加载需要手动配置,也是存在问题的。如果文件之间相互有加载顺序的依赖关系呢?(比如a.js依赖于b.js那么b.js就需要先加载完才行)这样一来就必须小心的写好jsList里面的顺序,否则就加载不成功了
因此,一个完善的依赖管理机制还是需要的。解决这个问题的关键是要把脚本的加载过程与脚本之间的依赖处理过程分开。
目前也有不少解决方案,AMD、CommonJS、Dependency Injection,关于这三种方式的比较与讨论可以参考另外一篇博文 javaScript 依赖管理
可以看到基于 Dependency Injection 模式进行管理,可以极大程度的松散耦合,前后端代码进行复用,并且可以直接使用基于CommonJS的模块(npm module)。目前,bearcat 已经对 cocos2d-x jsb 环境进行了支持, 意味着我们可以编写同一套代码逻辑,然后跑在浏览器和jsb环境中,当然也可以在node.js中进行复用。而 AMD(只适用于浏览器)、CommonJS(依赖于文件I/O,浏览器环境受到限制) 都不能很好的适用于所有的环境,也包括cocos2d-js的jsb环境
第三方库的使用
第三方库其实也是一种依赖,目前最大的javaScript module生态圈是npm,我们可以使用 browserify 直接使用 npm 里面现有的 module,而非拷贝代码,然后手动添加,如果要升级第三方库呢?再拷贝,再添加。明显不太优雅。使用 browserify 的时候,配置个 package.json,然后添加依赖,build一下即可,由于依赖并不是频繁更改的,因此这个browerify的build的过程也是可以接受的
单元测试
使用全局global变量的情况下是很难做单元测试的,要做单元测试首先引用的全局变量要是一个拷贝,这样子可以防止由于在这里的单元测试修改了全局变量,而造成在另外的单元测试里面失败。
比如:
var fs = require('fs');
module.exports = function(path, cb) {
fs.readFile(path, 'utf-8', function(err, content) {
cb(null, JSON.parse(content));
})
}
这里有几个依赖?怎么做单元测试?
(依赖了 fs 和全局的 JSON)
Dependency Injection则没有这个问题,因为依赖是传入的,并不是自己获取的,传入的这个依赖可以很方便的进行mock进行单元测试。
换成依赖注入则一目了然
module.exports = function(fs, JSON) {
return function(path, cb) {
fs.readFile(path, 'utf-8', function(err, content) {
cb(null, JSON.parse(content));
})
}
}
对于javaScript,我们可以使用 mocha 作为测试驱动框架,使用 should 作为断言库
代码复用
由于我们基于javaScript技术栈,代码共享肯定能带来不少好处,游戏开发中不少的逻辑是可以直接复用的,比如model定义、数据的校验、寻路逻辑等等,这里的代码复用不是指的代码拷贝,真正的复用是同一个文件直接在客户端和服务端引用
cocos2d-js + bearcat
接下来会以改造 cocos2d-js 官方 helloworld 例子为例,说明如何使用 cocos2d-js + bearcat 来进行开发
创建 cocos2d-js 项目
直接使用 cocos 命令
cocos new -l js bearcat_cocos2d_js_example
然后在web上跑起来
cd bearcat_cocos2d_js_example/
cocos run -p web
配置项目
添加 package.json
package.json 中,我们可以填写需要依赖的第三方库,然后利用npm进行库的维护与管理
这里我们依赖库需要 bearcat,开发库需要 grunt 以及相关 grunt task
package.json
{
"name": "bearcat-cocos2d-js-example",
"version": "0.0.1",
"dependencies": {
"bearcat": "0.4.x"
},
"devDependencies": {
"grunt": "~0.4.2",
"grunt-bearcat-browser": "0.x",
"grunt-browserify": "3.2.x"
}
}
添加完依赖,我们执行 npm 命令,安装依赖
npm install
添加 gruntfile.js
利用 grunt 我们可以建立起自动化的开发构建发布流程
比如,我们可以配置 browserify build task,bearcat 生成 bearcat-bootstrap.js task
使用grunt前,需要全局安装grunt
npm install -g grunt
gruntfile.js
'use strict';
module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-browserify');
grunt.loadNpmTasks('grunt-bearcat-browser');
var src = [];
// Project configuration.
grunt.initConfig({
// Metadata.
pkg: grunt.file.readJSON('package.json'),
bearcat_browser: {
default: {
dest: "bearcat-bootstrap.js",
context: "client-context.json"
}
},
// browserify everything
browserify: {
standalone: {
src: ['client.js'],
dest: 'main.js',
options: {
}
}
}
});
// Default task.
grunt.registerTask('default', ['bearcat_browser', 'browserify']);
};
这里加入了 browserify 和 bearcat 的 task,然后我们执行grunt命令即可运行这些tasks
browserify 里配置 source 文件 client.js,目标文件为 main.j。client.js 其实就是之前的 main.js 文件,只不过使用 browserify 提供的 require 加载了第三方库,main.js 则是已经打包好第三库的主文件,供cocos2d-js作为项目入口文件加载
bearcat 里配置根据 client-context.json 来生成 bearcat-bootstrap.js 以便支持script的加载
grunt
如果还想加入单元测试、代码覆盖率的工作流,加入相关mocha、coverage task即可
创建源码文件夹
由于是javaScript技术栈,考虑前后端代码共享,那么我们可以把源码文件夹分成三个,比如
创建 client 源码文件夹
mkdir app-client
创建 server 源码文件夹
mkdir app-server
创建共享源码文件夹
mkdir app-shared
修改 project.json 文件
这里采用bearcat进行script脚本加载与依赖管理,因此修改 project.json 文件,把 jsList 的配置置为空数组
project.json
{
"project_type": "javascript",
"debugMode": 1,
"showFPS": true,
"frameRate": 60,
"id": "gameCanvas",
"renderMode": 0,
"engineDir": "frameworks/cocos2d-html5",
"modules": ["cocos2d"],
"jsList": []
}
添加 client-context.json
这个是 bearcat 在 client 端的配置,配置着需要扫描的源文件路径,这里我们扫描 app-client 与 app-shared 这两个文件夹,相应的,对于node.js开发,有一个context.json,里面配置的扫描文件夹则是 app-server 与 app-shared
client-context.json
{
"name": "bearcat-cocos2d-js-example",
"description": "client context.json",
"scan": ["app-client", "app-shared"],
"beans": []
}
添加 client.js
client.js 就是之前的main.js,只不过使用了browserify进行第三方库依赖、使用bearcat进行业务层代码的管理
client.js
require('./bearcat-bootstrap.js');
var bearcat = require('bearcat'); // 依赖bearcat库
window.bearcat = bearcat; // using browserify to resolve npm modules
cc.game.onStart = function() {
cc.view.adjustViewPort(true);
cc.view.setDesignResolutionSize(800, 450, cc.ResolutionPolicy.SHOW_ALL);
cc.view.resizeWithBrowserSize(true);
var self = this;
//load resources
bearcat.createApp();
bearcat.use(['helloWorldScene']);
bearcat.start(function() {
var resourceUtil = bearcat.getBean('resourceUtil');
var g_resources = resourceUtil.getResources();
cc.LoaderScene.preload(g_resources, function() {
var helloWorldScene = bearcat.getBean('helloWorldScene');
cc.director.runScene(helloWorldScene.get());
}, self);
});
};
cc.game.run();
至此客户端项目环境已经配置完毕,我们可以愉快的coding
使用bearcat编程
添加 HelloWorldLayer
使用cocos2d-js创建一个layer相当容易
var HelloWorldLayer = cc.Layer.extend({
ctor: function() {
}
});
我们继承cc.Layer,然后在子类里实现我们具体的逻辑
但是这个 HelloWorldLayer 放在哪儿呢?全局吗?当然 say no!
在bearcat里,你可以编写一个管理HelloWorldLayer的factory bean也即工厂类,由这个factory bean维护着这个HelloWorldLayer,需要时,依赖这个factory bean,然后通过factory bean的工厂方法获取HelloWorldLayer的实例,同时,如果HelloWorldLayer需要依赖其他的script文件,也在factory bean通过bearcat提供的依赖注入来进行即可
// HelloWorldLayer 工厂bean
var HelloWorldLayer = function() {
this.$id = "helloWorldLayer";
this.$init = "init";
this.ctor = null;
// 如果需要依赖,直接在这里用bearcat依赖注入
// this.$xxxUtil = null;
}
HelloWorldLayer.prototype.init = function() {
var self = this;
// 初始化HelloWorldLayer
this.ctor = cc.Layer.extend({
sprite: null,
helloLabel: null,
ctor: function() {
}
});
}
// 工厂方法
HelloWorldLayer.prototype.get = function() {
return new this.ctor();
}
bearcat.module(HelloWorldLayer, typeof module !== 'undefined' ? module : {});
添加依赖
在HelloWorldLayer中我们需要依赖一个resourceUtil来处理资源的管理,那么我们可以简单的这样做就行
var HelloWorldLayer = function() {
this.$id = "helloWorldLayer";
this.$init = "init";
this.$resourceUtil = null;
this.ctor = null;
}
使用时,我们直接用 this.$resourceUtil 即可拿到依赖,调用里面的方法
实现逻辑
在HelloWorldLayer中,我们可能有这样的逻辑
- 添加一个close按钮
- 添加helloWorld标签
- 添加splash背景
- 添加action动画
HelloWorldLayer.prototype.init = function() {
var self = this;
this.ctor = cc.Layer.extend({
sprite: null,
helloLabel: null,
ctor: function() {
// 1. super init first
this._super();
self.addCloseItem(this);
self.addHelloWorldLabel(this);
self.addSplashScreen(this);
self.runAction(this);
return true;
}
});
}
然后我们可以这么进行抽象与封装,把每个逻辑代理到factory bean的无状态的方法上去,并把this context作为参数传入
这样子实现好处也是明显的:
- ctor函数里的逻辑进行了函数封装,便于后期维护
- 每个封装的函数都是无状态的,可以轻松实现component,来进行代码复用
在这里就是ui组件的复用(当然,对于ui,我们已经有了cocosstudio来进行编辑生成,而不用自己一个个去编写代码,但是代码实现的思路是一致的)
时刻不忘代码的可维护性、复用性
实现具体逻辑
实现就大胆使用cocos2d-js里提供的各种方法即可,这里的self参数则是ctor里的this context
HelloWorldLayer.prototype.addCloseItem = function(self) {
// 获取 resourceUtil 依赖
var res = this.$resourceUtil.getRes();
// 2. add a menu item with "X" image, which is clicked to quit the program
// ask the window size
var size = cc.winSize;
// add a "close" icon to exit the progress. it's an autorelease object
var closeItem = new cc.MenuItemImage(
res.CloseNormal_png,
res.CloseSelected_png,
function() {
cc.log("Menu is clicked!");
}, self);
closeItem.attr({
x: size.width - 20,
y: 20,
anchorX: 0.5,
anchorY: 0.5
});
var menu = new cc.Menu(closeItem);
menu.x = 0;
menu.y = 0;
self.addChild(menu, 1);
}
其它代码也类似,不一一说明,完整例子在 bearcat-coscos2d-js-example
部署与运行
写完代码,执行 grunt 进行构建
grunt
对于web平台,直接执行cocos命令
cocos run -p web
对于android平台,则需要修改 cocos2d-js 库下的 frameworks/runtime-src/proj.android/build-cfg.json 文件
把我们自定义的源代码文件添加进打包apk配置中
build-cfg.json
{
"ndk_module_path": [
"../../js-bindings",
"../../js-bindings/cocos2d-x",
"../../js-bindings/cocos2d-x/cocos",
"../../js-bindings/cocos2d-x/external"
],
"copy_resources": [{
"from": "../../../app-client",
"to": "app-client"
}, {
"from": "../../../app-shared",
"to": "app-shared"
}, {
"from": "../../../res",
"to": "res"
}, {
"from": "../../../main.js",
"to": ""
}, {
"from": "../../../project.json",
"to": ""
}, {
"from": "../../js-bindings/bindings/script",
"to": "script"
}]
}
这里我们添加了 app-client 与 app-shared 文件夹
然后,我们就可以直接用cocos命令编译部署到android设备(模拟器)上
cocos run -p android
ios 平台,由于没有mac,暂时无法测试,还望读到这的同学实际测试下,并给予反馈
总结
cocos2d-js非常不错,基于javaScript我们可以用一份代码就可以发布到web、pc、android、ios等平台上,大大减少了开发成本。当然cocos2d-js开发并不是非常完美,这其中也有javaScript这门语言本身的不足。本文就对这些问题进行了探讨,并给出了使用bearcat来编写cocos2d-js项目的例子,抛砖望引玉,enjoy coding with bearcat