使用Cocos2d-JS开发H5游戏

2019年12月06日 阅读数:61
这篇文章主要向大家介绍使用Cocos2d-JS开发H5游戏,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

如何学习css

如何开始学习Cocos2d-JS?我我以为比较好的方式是:html

1)看测试例:测试例前端

2)看API文档:
html5

3)看源码python

另外还有大神录制了进阶视频教程:Cocos2d-JS进阶视频教程,里面关于自学的艺术仍是讲的挺好的,在这里我就不累赘了。jquery


如何开始

如何开始很简单了,就两条:ios

  • 搭环境web

  • 编码

若是你是作纯web的游戏,搭建环境很简单:

  • 下载引擎包

  • 下载Python2.7.6Ant,若是没有安装java,请去下载jdk至少1.7的版本

  • 配环境:无疑就是配置java环境、python环境、ant环境变量。基本上就是在系统环境变量path加入相关的bin目录地址。不会的自行google吧。

  • 建立项目:cocos new -l js ProjectName

  • 开始编码:sublime、webstorm等等用你喜欢的开发工具吧。

官网也有相关文章:用Cocos Console工做流开发网页/原平生台游戏(JSB开发环境简介)


如何编码

如何编码?若是团队协做,遵循团队的编码规范咯。这里我只是简单说一点点:

1. 关于project.json文件的配置信息

这个其实在main.js里面有注释说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  debugMode: 0 
  //0 不显示任何错误信息 
  //1 显示cc.assert, cc.warn, cc.log
 
  showFPS :  true 
  //为真会在屏幕左下方显示游戏帧率 id: gameCanvas 
  //游戏画布的id
 
  renderMode: 0 
  // 0 : 自动选择渲染引擎:webGl、canvas
  // 1 : canvas 
  // 2 : WebGL(在webgl模式下drawNode会存在严重的锯齿) 
  // modules : 游戏引用的模块 
  // 模块的名字能够在frameworks/cocos2d-html5/moduleConfig.json 
  // 里面找到,因此后期在上线的时候能够选择本身用到的模块引入来减小js文件的大小


2. 关于CCBoot.js

CCBoot.js是入口js文件,因此你颇有必要认真看一看其中的代码,好比其中提供一些可用的工具:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  //建立元素 
  cc.newElement
  //监听事件 
  cc._addEventListener
  //循环操做 
  cc.each
  //继承 
  cc.extend 
  cc.isFunction 
  cc.isNumber 
  cc.isString 
  cc.isArray 
  cc.isUndefined 
  cc.isObject
  ...
 
  //原生的渲染引擎: CanvasRenderingContext2D/WebGLRenderingContext 
  cc._renderContext
 
  //包裹游戏画布的外层
  div  cc._gameDiv / cc.container

好比一些你可能想要修改的东西:

包裹游戏的外层div的id名字:

1
2
  //大约CCBoot.js的1864行:
  localContainer.setAttribute( 'id' 'Cocos2dGameContainer' );

初始加载的时候的画布颜色:

1
2
  //大约CCBoot.js的641行: 
  canvasNode.style.backgroundColor =  "black" ;

frameworks/cocos2d-html5/core/platform/目录下也有不少东西,主要是平台相关的东西。

如简单操做dom的工具miniFramework.js里面的:

1
  cc.$

屏幕适配相关的东西’CCEGLView.js’里面的:

1
2
3
  cc.view.adjustViewPort
  cc.view.setDesignResolutionSize
  cc.view.resizeWithBrowserSize ...

文件加载相关的CCLoader.js:

Cocos2d-JS的文件加载是经过文件后缀的,如后缀名为["png", "jpg", "bmp","jpeg","gif", "ico"]的文件会经过cc._imgLoader来加载,理解了这里能够用于后期自定义文件加载插件。

还有一些编码上面的东西放在下面再说。


关于音频

音频,在移动端上一直是个巨坑的问题。能自动播放,不能自动播放、不能循环播放、根本就不播放等等简直就是到处都是坑啊。

开始游戏中的音频用的是cocos里面的封装的音频函数,会对音频文件进行预加载、预处理。可是这里有个严重的问题,那就是加载的时候很容易卡在音频那里,并且循环播放也有问题。因而决定用会原生的audio标签。

1
<audio id= "gameAudio"  src= "res/gameMusic.mp3"  loop= "loop"  ></audio>

看起来应该是不错的样子,可是当咱们在iphone4s微信内置浏览器里面测试的时候发现这个audio标签竟然占据页面空间(不是说加了controls这个属性才会占空间么,坑啊!),因而天然而然想到的解决方案就是none掉:

1
<audio style= "display:none"  id= "gameAudio"  src= "res/gameMusic.mp3"  loop= "loop"  ></audio>

感受好像轻易解决了这个问题,因而开始编写一堆音频处理的代码。编写完毕以后,拿起iphone试一试,我去啊iphone上音频不能循环播放了啊,能不能再坑点!这个问题困扰了很久不知道什么缘由,拿着代码去找师傅去吧!师傅只有一句话:ios上面音频若是不显示或者在屏幕以外有可能就会出问题哦!终于把display:none去掉一切又好了。孩子你仍是太年轻了啊!对于占据空间的问题咱们如今只能把音频标签做为body最后一个child节点,依然没有很好的解决方案。

对于音频的自动播放,目前只发如今iphone6 plus是能够的(iphone 6没有因此没试过),因此只能经过用户触摸事件来加载。若是在HTML标记中使用了autoplay属性,将会忽略这个属性,而且不会在加载页面时播放此文件,对于 preload 属性,一样会忽略。惟一能解决的就是用户进入页面是,让用户触发 touch 事件,而且只能在touch事件的回调里面加载才有效。

1
2
3
4
5
6
7
8
9
10
var $audio = document.getElementById( 'gameAudio' );
//用户点击屏幕的任何一个地方就去加载音频
document.addEventListener( 'touchstart' , function(e){    
     $audio.load();
}, false );
 
//在某个cc.MenuItemImage/cc.MenuItemSprite/onTouchBegan的回调里面执行播放操做
try {    
     $audio.play();
} catch (e){}

音频的暂停:

1
2
3
try {    
     $audio.pause();
} catch (e){}

音频的中止:

1
2
3
4
try {    
     $audio.currentTime = 0;    
     $audio.pause();
} catch (e){}

若是是多个音频文件,可使用音频audio sprite,全部的音频综合到一个单音频流中,而后播放此流的各个部分。

1
2
3
4
5
6
7
8
9
10
var audioData = {
     bg: {
         start: 0,        
         length: 1
     },    
     run: {
         start: 1.3,        
         length: 1.5
     }
};

要播放bg这段声音:

1
2
3
4
try {    
     $audio.currentTime = audioData.bg.start;   
     $audio.play();
} catch (e){}

当播放结束时:

1
2
3
4
5
6
7
$audio.addEventListener( 'timeupdate' , function()
{    
     if  ( this .currentTime >= audioData.bg.start + audioData.bg.length) 
     {        
         this .pause();
     }
},  false );

须要注意的是,更改 currentTime 并非百分百正确的。假设 currentTime 设为 3.2,而实际获得的倒是 3.4。每一个 audio sprite 之间须要少许的空间,以免寻找到另外一个 sprite 的头部。


一些工具

至关好用的精灵图制做工具,虽然收费,可是能够申请免费的密钥,具体申请地址本身查吧。

用它制做精灵图的时候注意一个地方就能够了:

TexturePacke.jpg

勾上这个Reduce border artifacts,它会让你的精灵图边缘没有锯齿,其它的配置地方我基本上都是默认的。

位图字体实际上是一张图片,而后记录每一个文字的位置和大小,所以字体的大小在生成的时候已经定义了,因此在用的时候最好不要改变字体的大小,若是改变了大小会对图片进行缩放,可能会出现锯齿。

Bitmap Font Generator这个软件制做的字体会生成一个png文件和一个fnt文件。选择左上方的Options->Font settings


bmfont.jpg

设置字体格式和字体大小。其它基本上都是默认设置就行了。

能够在主界面上面一个个点击选择须要制做的文字,也能够把要选择的字体放在一个txt文件里面(txt文件编码为Utf-8),而后选择菜单栏的Edit->Select chars from file来选择要制做的文字。

Bitmap Font Generator还支持从image文件生成字体纹理图,选择菜单栏的Edit->Open Image Manager->Image->Import image(注意路径里面不要有中文)

bmfonti.jpg

Id那里填写对应文字的ASCII码,如我上传的图片里面的文字是2,其对应的ASCII码是50。若是不知道某个文字的ASCII码,能够去这里查:ASCII码对照表

选择好以后点击菜单栏Options->Export options

bmfonte.jpg


为何须要加载字体?由于有时候页面上会有不一样大小的特殊字体,用位图又比较麻烦,加载整个字体库又太大了,因而我找到了这个软件。

选择菜单栏File->New Project,填写你的字体名字和字体样式。而后选择菜单栏File->Open,打开你要精简的字体。(在新建项目的时候有一个Include outlines/Don’n include outlines的选项。若是选择Include outlines它会默认帮你添加一些字符)

fontc.jpg

若是是能够直接看获得的字符,我能够直接在原有的字体文件中选中这个字符,而后复制粘贴到个人项目里面对应的位置上去,若是不是的话,咱们能够经过下面的方法添加进去。

ctrl+f键查找某个字符,如咱们要添加的字是“我”,而后选择Glyph Properties

fontc1.jpg

复制Codepoints里面的参数,而后选中本身的项目,选择菜单栏Insert->Charaacters,把刚才复制的Codepoints粘贴到对应的位置。

fontc2.jpg

这时你会发如今你项目字符的最后会添加一个灰色方块,回到原始字体那里,选中那个字直接复制粘贴到刚才添加的那个方块里面就能够了:

fontc3.jpg

在上面的图里面你能够看到有不少灰色的方块,看看里面显示的字符,若是你用不到你能够直接点Delete键把它删掉。

除了用这个软件外,最近发现一个工具也能够用来精简压缩字体,感兴趣的能够试试:字蛛——中文字体自动化压缩工具


这个其实在咱们的项目中没有用到,因此在这里提一下而已,有用到的能够本身去琢磨一下。


项目发布

项目发布意味着什么?简单来讲就意味着资源压缩、代码合并。首先来讲下资源压缩吧。


让你爱恨交加的closure compiler

不是说Cocos2d-JS么?怎么会扯到closure compiler。若是你想要发布你的Cocos2d游戏,我以为就不得不说说closure compiler这玩意,这也是为什么安装jdk的时候要求要jdk7以上的版本了。

在cocos项目发布的时候,有一个命令是cocos compile -p web -m release --advanced,后面加了一个advanced的参数,这种模式下面使用的是closure compiler的高级js压缩模式,压缩比例惊人,同时会优化js的执行,所以压缩以后的js性能也会获得必定提高。可是想要用这个高级压缩模式但是有坑的,极可能你会发现你的代码通过这种压缩模式压缩以后就报错了。

因此你最好去closure compiler官网(google出品,因此须要翻墙)看一看,这里我只提简单的几点。

须要保留变量名的,要么用@expose关键字申明,要么使用[]来引用,如:

1
2
3
4
5
6
7
8
9
10
11
/** @expose */
window.MM;
/** @expose */
M.add;
/** @expose */
M.add.Pos;
 
//or
window[ 'MM' ];
MM[ 'add' ];
MM[ 'add' ][ 'Pos' ];

好比在ajax请求数据的时候的请求参数和响应参数,你要写成这样:

1
2
3
4
5
6
7
//request data
//加引号
{ 'name' 'dddd' 'id' '123' }
//response data
//经过数组的方式取值
data[ 'list' ];
data[ 'list' ][ 'userName' ]

记住,任何你须要让它在压缩以后能保持原样输出的你都应该这样写。若是要了解更多你能够去它官网看看,或者看一看cocos2d-js的源码,里面有不少应对closure compiler高级压缩模式的注解。


plist文件合并

一次请求是比较耗费资源的,因此咱们把plist文件合并在一个文件里面来加载,同时也对plist进行了压缩,下面是加载解析合并后的plist文件的插件源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
  * 将plist合并成一个加载
  */
cc._pjsonLoader = {
     KEY : {
         frames : 0,
         rect : 0, size : 1, offset : 2, rotated : 3, aliases : 4,
         meta : 1,
         image : 0
     },
     _parse : function(data){
         var KEY =  this .KEY;
         var frames = {}, meta = data[KEY.meta] ? {image : data[KEY.meta][KEY.image]} : {};
         var tempFrames = data[KEY.frames];
         for  (var frameName in tempFrames) {
             var f = tempFrames[frameName];
             var rect = f[KEY.rect];
             var size = f[KEY.size];
             var offset = f[KEY.offset];
             frames[frameName] = {
                 rect : {x : rect[0], y : rect[1], width : rect[2], height : rect[3]},
                 size : {width : size[0], height : size[1]},
                 offset : {x : offset[0], y : offset[1]},
                 rotated : f[KEY.rotated],
                 aliases : f[KEY.aliases]
             }
         }
         return  {_inited :  true , frames : frames, meta : meta};
     },
     load : function(realUrl, url, res, cb){
         var self =  this , locLoader = cc.loader, cache = locLoader.cache;
         locLoader.loadJson(realUrl, function(err, pkg){
             if (err)  return  cb(err);
             var dir = cc.path.dirname(url);
             for  (var key in pkg) {
                 var filePath = cc.path.join(dir, key);
                 cache[filePath] = self._parse(pkg[key]);
             }
             cb(null,  true );
         });
     }
};
//注册这个插件,前面说过cocos文件加载是经过文件后缀名来判断使用哪一个加载器来加载的,因此这里会加载后缀名为.pjson的文件
cc.loader. register ([ "pjson" ], cc._pjsonLoader);


那么如何来生成这个.pjson文件呢?下面是我用nodejs写的一个脚本,能够读取目录下面全部的.plist文件,而后生成一个.pjson文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//pjson.js
var fs = require( 'fs' );
var plist = require( 'plist' );
var pjson = {};
 
//var src = "res/*.plist";
//node pjson
fs.readdir( './res' , function(err, files) {
     if  (err) {
         throw  err;
     }
     for  (var i = files.length - 1; i >= 0; i--) {
         var file = files[i],
             ext = file.split( '.' )[1];
         if (ext ===  'plist' ){
             pjson[file] = [];
             pjson[file][0] = {};
             var data = fs.readFileSync( './res/' +file,  'UTF-8' );
             var frames = plist.parse(data.toString()).frames;
             var fileName = Object.keys(frames);
             for (var j=0; j< fileName.length; j++){
                 var dat = pjson[file][0][fileName[j]] = [];
                 var frame = frames[fileName[j]];
                 dat[0] = frame.frame.replace(/{|}/g,  '' ).split( ',' );
                 dat[1] = frame.sourceSize.replace(/{|}/g,  '' ).split( ',' );
                 dat[2] = frame.offset.replace(/{|}/g,  '' ).split( ',' );
                 if (frame.rotated)  dat[3] = 1;
             }
         }
     };
     fs.writeFile( './res/plists.pjson' , JSON.stringify(pjson), function (err) {
           if  (err)  throw  err;
           console. log ( 'done!' );
     });
});


因此你须要在nodejs命令行里面运行:

1
2
3
4
5
//第一次运行请先安装依赖包
npm install plist
 
//生成pjson文件
node pjson.js

那么如何使用这个文件呢?很简单,你只须要在resource.jsg_resources数组里面删除plist相关的,而后添加这个plists.pjson的路径就能够了,其它不用做任何改动。


图片资源压缩

关于图片和其余静态资源,我用了一个前端自动化任务工具gulp,不懂的能够看我上篇博文:Gulp上手,下面是我针对项目配置的gulp任务gulpfile.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
var gulp = require( 'gulp' ),
     imagemin = require( 'gulp-imagemin' ),
     pngquant = require( 'imagemin-pngquant' ),
     cache = require( 'gulp-cache' ),
     autoprefixer = require( 'gulp-autoprefixer' ),
     minifycss = require( 'gulp-minify-css' ),
     rev = require( 'gulp-rev' ),
     revReplace = require( 'gulp-rev-replace' ),
     useref = require( 'gulp-useref' );
 
var basePath =  'publish/html5/' ;
 
gulp.task( 'image' , function () {
     return  gulp.src(basePath+ 'res/**/*.png' )
         .pipe(cache(imagemin({
             optimizationLevel: 7,
             use: [pngquant({ quality:  '60-80' , speed: 1 })]
         })))
         .pipe(gulp.dest(basePath+ 'res' ));
});
 
gulp.task( 'style' , function () {
     return  gulp.src( 'css/**/*.css' )
         .pipe(autoprefixer( 'Android' 'BlackBerry' 'iOS' 'OperaMobile' 'ChromeAndroid' 'FirefoxAndroid' 'ExplorerMobile' ))
         .pipe(minifycss())
         .pipe(gulp.dest(basePath+ 'css' ));
});
 
gulp.task( 'static' , function () {
     var userefAssets = useref.assets();
     return  gulp.src(basePath+ 'index.html' )
         .pipe(userefAssets)
         .pipe(rev()) 
         .pipe(userefAssets.restore())
         .pipe(useref())
         .pipe(revReplace())
         .pipe(gulp.dest(basePath));
});
 
gulp.task( 'default' , [ 'image' 'style' 'static' ]);

由于其中用到了对每次生成的js、css文件进行md5命名和合并操做,因此你须要在你的html文件里面做以下配置:

1
2
3
4
5
6
7
8
<!-- build:css css/main.css -->
<link rel= "stylesheet"  href= "css/a.css" >
<link rel= "stylesheet"  href= "css/b.css" >
 
<!-- endbuild --><!-- build:js game.js -->
<script src= "frameworks/cocos2d-html5/CCBoot.js" ></script>
<script src= "main.js" ></script>
<!-- endbuild -->


整个项目发布

接着我写了一个整个项目的发布脚本build.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var exec = require( 'child_process' ).exec;
 
var buildProcess = exec( 'cocos compile -p web -m release --advanced' , {});
buildProcess.on( 'close' , function () {
     console. log ( 'start gulp task' );
     var nextProcess = exec( 'gulp default' , {});
     nextProcess.on( 'close' , function () {
         console. log ( 'gulp task end' );
     });
     nextProcess.stdout.setEncoding( 'utf-8' );
     nextProcess.stdout.on( 'data' , function (data) {
         console. log (data);
     });
     nextProcess.stderr.setEncoding( 'utf-8' );
     nextProcess.stderr.on( 'data' , function (data) {
         throw  new  Error(data);
     });
});
buildProcess.stdout.setEncoding( 'utf-8' );
buildProcess.stdout.on( 'data' , function (data) {
     console. log (data);
});
buildProcess.stderr.setEncoding( 'utf-8' );
buildProcess.stderr.on( 'data' , function (data) {
     throw  new  Error(data);
});

因此每次项目发布的时候,你只须要打开nodejs命令行,而后执行一下一个命令就完事:

1
node build.js


一些可能对你有用的代码

ajax简单封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
Utils.ArrayProto = Array.prototype;
Utils.slice = Utils.ArrayProto.slice;
 
Utils.decode = decodeURIComponent;
Utils.encode = encodeURIComponent;
 
Utils.defaults = function(obj){
     cc.each(Utils.slice.call(arguments, 1), function(o){
         for (var k in o){
             if  (obj[k] == null) obj[k] = o[k];
         }    
     });
     return  obj;
};
 
Utils.formData = function(o) {
     var kvps = [], regEx = /%20/g;
     for  (var k in o) kvps.push(Utils.encode(k).replace(regEx,  "+" ) +  "="  + Utils.encode(o[k].toString()).replace(regEx,  "+" ));
     return  kvps.join( '&' );
};
 
Utils.ajax = function(o){
     var xhr = cc.loader.getXMLHttpRequest();
     o = Utils.defaults(o, {type:  "GET" , data: null, dataType:  'json' , progress: null, contentType:  "application/x-www-form-urlencoded" });
     //ajax进度的
     //if(o.progress) Utils.Progress.start(o.progress);
     xhr.onreadystatechange = function() {
         if  (xhr.readyState == 4){
             if  (xhr.status < 300){
                 var res;
                 if (o.dataType ==  'json' ){
                     res = window.JSON ? window.JSON.parse(xhr.responseText): eval(xhr.responseText);
                 } else {
                     res = xhr.responseText;
                 }
                 if (!!res) o.success(res);
                 ////ajax进度的
                 //if(o.progress) Utils.Progress.done();
             } else {
                 if (o.error) o.error(xhr, xhr.status, xhr.statusText); 
             }
         }
     };
     //是否须要带cookie的跨域
     //if("withCredentials" in xhr) xhr.withCredentials = true;
     var url = o.url, data = null;
     var isPost = o.type ==  "POST"  || o.type ==  "PUT" ;
     if ( o.data && typeof o.data ==  'object'  ){
         data = Utils.formData(o.data);
     }
     if  (!isPost && data) {
         url +=  "?"  + data;
         data = null;
     }
     xhr.open(o.type, url,  true );
     if  (isPost) {
         xhr.setRequestHeader( "Content-Type" , o.contentType);
     }
     xhr.send(data);
     return  xhr;
};
 
Utils.get = function(url, data, success){
     if (cc.isFunction(data)){
         success = data;
         data = null;
     }
     Utils.ajax({url: url, type:  "GET" , data: data, success: success});
};
 
Utils.post = function(url, data, success){
     if (cc.isFunction(data)){