Cocos2D-JS

cocos2d-html5开发消灭星星(popstar)


touchsnow带来的cocos2d-x editor工具开发消灭星星。

背景

上一节,我们已经把消灭星星的界面搭建好了,流程也跑通了。 这一篇涉及到程序的算法,也许是最难的部分了,理解起来需要多花点时间,而且我提供的算法未必就是最好的,如果读者有更优更好的算法,希望分享出来,我可以链接到你那里。大概的思路是这样的,第一次点击一个星星,立刻寻找四周相同颜色的,第二次点击,消除他们并产生粒子效果;接着星星数组重新走位掉落,补全空缺;然后还要检测纵行是否出现空缺,有的话,合并到一起;最后必须检测死局;大概如此。

  • 这是一个系列博文,代码不会一下子全部放出来,每写一篇放出相应的代码。因为笔者也是抽空编一点程序,然后写一篇博文,断断续续的,没有整块时间;
  • 代码是基于javascript语言,cocos2d-x游戏引擎,cocos2d-x editor手游开发工具完成的;
  • 运行demo需要配置好cocos2d-x editor,暂不支持其他工具。demo是跨平台的,可移植运行android,ios,html5网页等。

源代码下载

请到代码集中营下载(第二篇算法):http://blog.makeapp.co/archives/319

不同平台下的效果图:(windows、html5、android)
popstar_win

popstar_h5

popstar_android

代码分析

主要集中在MainLayer.js下面的分析

第一步,10*10星星群检测触摸事件,通过this.sameColorList.length可以判断是第一次触摸还是第二次触摸;

@@    >1表示第二次触摸,这里又有分支,触摸的是刚才同一颜色区域还是其他区域?如果是原来颜色区域,删除this.removeSameColorStars(),如果不是原来颜色区域,恢复原状,然后新的检测

@@     <=1表示第一次触摸  直接检测颜色相同区域

    MainLayer.prototype.onTouchesBegan = function (touches, event) {  
        var loc = touches[0].getLocation();  
        this.ccTouchBeganPos = loc;  
      
        for (var i = 0; i < this.starTable.length; i++) {  
            var sprites = this.starTable[i];  
            for (var j = 0; j < sprites.length; j++) {  
                var pSprite0 = sprites[j];  
                if (pSprite0) {  
                    var ccRect = pSprite0.getBoundingBox();  
                    if (isInRect(ccRect, this.ccTouchBeganPos)) {  
                        if (this.sameColorList.length > 1) {  
                            if (this.sameColorList.contains(pSprite0)) {  
                                cc.AudioEngine.getInstance().playEffect(PS_MAIN_SOUNDS.broken, false);  
                                this.removeSameColorStars();  
                            } else {  
                                for (var k = 0; k < this.sameColorList.length; k++) {  
                                    if (this.sameColorList[k]) {  
                                        this.sameColorList[k].runAction(cc.ScaleTo.create(0.1, 1));  
                                    }  
                                }  
                                this.checkSameColorStars(pSprite0);  
                                if (this.sameColorList.length > 1) {  
                                    cc.AudioEngine.getInstance().playEffect(PS_MAIN_SOUNDS.select, false);  
                                }  
                            }  
                        } else {  
                            this.checkSameColorStars(pSprite0);  
                            if (this.sameColorList.length > 1) {  
                                cc.AudioEngine.getInstance().playEffect(PS_MAIN_SOUNDS.select, false);  
                            }  
                        }  
      
                        break;  
                    }  
                }  
            }  
        }  
    };  

第二步,建立单个星星的四个方向检测,上下左右,把颜色相同的放在一个数组里面,回调这个数组;其实最后用这个函数的时候主要是判断数组的大小;数组大于1,说明四周有相同颜色的;

    MainLayer.prototype.checkOneStarFourSide = function (sprite) {  
        if (sprite == null) {  
            return;  
        }  
        // cc.log("checkOneStarFourSide");  
        var fourSideSpriteList = [];  
        var color = sprite.starData.color;  
        var col = sprite.starData.indexOfColumn;  
        var row = sprite.starData.indexOfRow;  
      
        //up  
        if (row < 9) {  
            var upSprite = this.starTable[col][row + 1];  
            if (upSprite != null && upSprite.starData.color == color) {  
                fourSideSpriteList.push(upSprite);  
            }  
        }  
      
        //down  
        if (row > 0) {  
            var downSprite = this.starTable[col][row - 1];  
            if (downSprite != null && downSprite.starData.color == color) {  
                fourSideSpriteList.push(downSprite);  
            }  
        }  
      
        //left  
        if (col > 0) {  
            var leftSprite = this.starTable[col - 1][row];  
            if (leftSprite != null && leftSprite.starData.color == color) {  
                fourSideSpriteList.push(leftSprite);  
            }  
        }  
      
        //right  
        if (col < 9) {  
            var rightSprite = this.starTable[col + 1][row];  
            if (rightSprite != null && rightSprite.starData.color == color) {  
                fourSideSpriteList.push(rightSprite);  
            }  
        }  
        return fourSideSpriteList;  
    }  

第三步,检测相同颜色区域,这里的算法比较复杂;有两个数组this.sameColorList和newSameColorList,前者是全局星星数组,后者是每次扩展新加入的星星;比如这样情况,一个星星左右上有相同的星星,上面的上面还有一个星星,总共五个相同星星:三次检测情况是this.sameColorList为1---4----5 ,而newSameColorList为1--3--1,各种曲折,读者好好理解下;

    MainLayer.prototype.checkSameColorStars = function (sprite) {  
        if (sprite == null) {  
            return;  
        }  
        this.sameColorList = [];  
        this.sameColorList.push(sprite);  
        var newSameColorList = [];  
        newSameColorList.push(sprite);  
      
        //by logic ,check the same color star list  
        while (newSameColorList.length > 0) {  
            for (var i = 0; i < newSameColorList.length; i++) {  
                var fourSide = this.checkOneStarFourSide(newSameColorList[i]);  
                if (fourSide.length > 0) {  
                    for (var j = 0; j < fourSide.length; j++) {  
                        if (!this.sameColorList.contains(fourSide[j])) {  
                            this.sameColorList.push(fourSide[j]);  
                            newSameColorList.push(fourSide[j]);  
                        }  
                    }  
                }  
                newSameColorList.splice(i, 1);  
            }  
        }  
        cc.log("sameColorList length==" + this.sameColorList.length);  
        if (this.sameColorList.length > 1) {  
            for (var k = 0; k < this.sameColorList.length; k++) {  
                var simpleStar = this.sameColorList[k];  
                if (simpleStar) {  
                    simpleStar.runAction(cc.ScaleTo.create(0.1, 1.08));  
                }  
            }  
        }  
    }  

第四步 移除相同的星星,并产生粒子效果

    MainLayer.prototype.removeSameColorStars = function () {  
        for (var k = 0; k < this.sameColorList.length; k++) {  
            var simpleStar = this.sameColorList[k];  
            if (simpleStar) {  
                var col = simpleStar.starData.indexOfColumn;  
                var row = simpleStar.starData.indexOfRow;  
                this.starTable[col].splice(row, 1, null);  
                this.rootNode.removeChild(simpleStar);  
                if (sys.platform != 'browser') {  
                    var starParticle = cc.StarParticle.create(this.rootNode, (36 + col * this.starSize), (36 + row * this.starSize), "spark");  
                    starParticle.runAction(cc.Sequence.create(cc.DelayTime.create(0.8), cc.CleanUp.create(starParticle)));  
                }  
            }  
        }  
        this.sameColorList = [];  
        this.fallStar();  
    }  

第五步 星星掉落 填充空缺,主要是如果一个地方有空缺,就把它上面的星星位置和数据交换,用到数组的方法splice,可到网上查看js数组的一些方法应用

    MainLayer.prototype.fallStar = function () {  
        for (var i = 0; i < this.starTable.length; i++) {  
            var sprites = this.starTable[i];  
            var length = sprites.length;  
            for (var j = 0; j < length; j++) {  
                var pSprite0 = sprites[j];  
                if (pSprite0 == null) {  
                    var k = j + 1;  
                    while (k < length) {  
                        var upSprite = sprites[k];  
                        if (upSprite != null) {  
                            upSprite.starData.indexOfColumn = i;  
                            upSprite.starData.indexOfRow = j;  
                            this.starTable[i].splice(j, 1, upSprite);  
                            this.starTable[i].splice(k, 1, null);  
                            k = length;  
                            var flowTime = 0.2;  
                            var fallAction = cc.MoveTo.create(flowTime, cc.p(36 + i * this.starSize,  
                                36 + j * this.starSize));  
                            upSprite.runAction(fallAction);  
                        }  
                        k++;  
                    }  
                }  
            }  
        }  
      
        this.deadStar();  
        // this.combineStar();  
    }  

第六步 合并星星,如果最底部有空缺,星星必须向左合并,这里笔者调试有问题,时间匆忙 来不及修改,读者可以自行研究修改;不解释了

    MainLayer.prototype.combineStar = function () {  
        for (var m = 0; m < this.starTable.length; m++) {  
            var mSprite0 = this.starTable[m][0];  
            if (mSprite0 == null) {  
                if (m == (this.starTable.length - 1)) {  
                    for (var j = 0; j < this.starTable[m].length; j++) {  
                        this.starTable[m].splice(j, 1, null);  
                    }  
                }  
                else {  
                    for (var i = (m + 1); i < this.starTable.length; i++) {  
                        // this.starTable.splice((i - 1), 1, this.starTable[i]);  
                        for (var j = 0; j < this.starTable[i].length; j++) {  
                            var pSprite0 = this.starTable[i][j];  
                            this.starTable[i - 1].splice(j, 1, pSprite0);  
                            if (pSprite0 != null) {  
                                pSprite0.starData.indexOfColumn = (i - 1);  
                                var col = pSprite0.starData.indexOfColumn;  
                                var row = pSprite0.starData.indexOfRow;  
                                var moveAction = cc.MoveTo.create(0.1, cc.p(36 + col * this.starSize,  
                                    36 + row * this.starSize));  
                                pSprite0.runAction(moveAction);  
                            }  
                        }  
                    }  
                }  
            }  
        }  
        this.deadStar();  
    }  

第七步 游戏到最后 会发生死局情况,程序自动判断消除;这里主要是循环检测每一个星星,如果所有的星星四周都没有相同星星的时候,就确认为死局,程序自动消除星星

    MainLayer.prototype.deadStar = function () {  
        var isDead = true;  
        for (var i = 0; i < this.starTable.length; i++) {  
            var sprites = this.starTable[i];  
            var length = sprites.length;  
            for (var j = 0; j < length; j++) {  
                var pSprite0 = sprites[j];  
                if (pSprite0 != null) {  
                    if (this.checkOneStarFourSide(pSprite0).length > 0) {  
                        isDead = false;  
                        return;  
                    }  
                }  
            }  
        }  
      
        if (isDead) {  
            for (var jj = 9; jj >= 0; jj--) {  
                for (var ii = 0; ii < 10; ii++) {  
                    var pSprite0 = this.starTable[ii][jj];  
                    if (pSprite0 != null) {  
                        var delay = 4 + 0.3 * ii - 0.4 * jj;  
                        pSprite0.runAction(cc.Sequence.create(  
                            cc.DelayTime.create(delay),  
                            cc.CleanUp.create(pSprite0)  
                        ));  
                        var starParticle = cc.StarParticle.create(this.rootNode, (36 + ii * this.starSize), (36 + jj * this.starSize), "spark");  
                        starParticle.runAction(cc.Sequence.create(cc.ScaleTo.create(0, 0),  
                            cc.DelayTime.create(delay), cc.ScaleTo.create(0, 1), cc.DelayTime.create(0.8),  
                            cc.CleanUp.create(starParticle)));  
                    }  
                }  
            }  
        }  
    }  

基本的流程就是这样      触摸——检测颜色——消除星星——掉落移动——合并星星——检测死局——结束  消除类的游戏思路都差不多是这样,把这个demo理解透了 任何消除类的游戏都很简单

开发工具(2013-02-14 已更新到1.0Beta)

cocos2d-x  editor,它是开发跨平台的手机游戏工具,运行window/mac系统上,javascrip/luat脚本语言,基于cocos2d-x跨平台游戏引擎, 集合代码编辑,场景设计,动画制作,字体设计,还有粒子,物理系统,地图等等的,而且调试方便,和实时模拟;

cocos2d-x editor 下载,介绍和教程:http://blog.csdn.net/touchsnow/article/details/19070665

cocos2d-x  editor 官方博客:http://blog.makeapp.co/

Cocos2d-html5 王者之剑实现 (3)

Cocos2d-html5 王者之剑实现 (1)

  • 项目组织
  • 360 度 可触摸摇杆实现
  • 攻击 与 特效攻击

Cocos2d-html5 王者之剑实现 (2)

  • 游戏中的操作控制
  • 操作控制之键盘映射
  • 用户信息更新

Cocos2d-html5 王者之剑实现 (3)

  • 动作,帧动画 VS 骨骼动画
  • 版本库升级
  • 角色动作组织

源码下载地址:https://github.com/iTyran/Tutorials/tree/master/html5/Arthur/src

动作,帧动画 VS 骨骼动画

游戏中人物的走动,跑动,攻击等动作是必不可少,实现它们的方法一般采用帧动画或者骨骼动画。在本文的两个角色里,一个采用帧动画,另一个采用骨骼动画(使用CocoStudio 的动画编辑器),同时也能很清楚的区别,两种方式的优劣,以及使用方式 ~ 有以下几个角度。

图片资源:首先对比一下使用帧动画和骨骼动画的所需要的图片资源。





如上图所示,角色英雄使用了帧动画(实际上图没有显示全,因为较多),他有各种动作,站立,跑动,攻击等效果,我们要为每一个动作创建几个“帧”,而动画的流畅性,取决于“帧数”的多少,但要知道,图片资源的大小也取决于你“帧数”的多少(浪费比特是不对的 ~),需要什么效果,需要多少帧,有多少动画,都需要自己权衡 ~

而怪物图片资源采用骨骼动画,资源是一块块小的“骨骼”,这无疑节省了资源大小,而动作信息则保存在一个 json 文件里面,后文会提到,而此时,随着动作的增加,所增加的比特(Byte)几可忽略不计。

使用方式:对于两者的使用方式,关键代码如下 ~

// *********************帧动画加载与调用************************

// 动作的加载
initAction:function(){
    // 站立动作
    var sa = cc.Animation.create();
    for (var si = 1; si < 4; si++){
        var frameName1 = "res/Hero" + si + ".png";
        sa.addSpriteFrameWithFile(frameName1);
    }
    sa.setDelayPerUnit(5.8 / 14);
    sa.setRestoreOriginalFrame(true);
    this._actionStand = cc.RepeatForever.create(cc.Animate.create(sa));

    // 跑动动作
    var animation = cc.Animation.create();
    for (var i = 1; i < 12; i++){
        var frameName = "res/HeroRun" + i + ".png";
        animation.addSpriteFrameWithFile(frameName);
    }
    animation.setDelayPerUnit(2.8 / 14);
    animation.setRestoreOriginalFrame(true);
    this._actionRunning = cc.RepeatForever.create(cc.Animate.create(animation));

    // 普通攻击
    var anAttack = cc.Animation.create();
    for (var attackIndex = 1; attackIndex < 6; attackIndex ++){
        var attackFrame = "res/HeroAttack" + attackIndex + ".png";
        anAttack.addSpriteFrameWithFile(attackFrame);
    }
    anAttack.setDelayPerUnit(1.8 / 14);
    // anAttack.setRestoreOriginalFrame(false);
    this._actionAttack = cc.Animate.create(anAttack);

    // 跳跃攻击 ...

    // 突刺攻击 ...

    // 其它动作,如果有 ~

}

// 动作的调用
this._sprite.runAction(this._actionStand);      // 站立
this._sprite.runAction(this._actionRunning);    // 跑动
// ...

// *********************骨骼动画加载与调用************************

// 加载骨骼资源
var s_Robot_png = "res/armature/Robot.png";
var s_Robot_plist = "res/armature/Robot.plist";
var s_Robot_json = "res/armature/Robot.json";

cc.ArmatureDataManager.getInstance().addArmatureFileInfo(
    s_Robot_png,
    s_Robot_plist,
    s_Robot_json);

this._armature = cc.Armature.create("Robot");

// 使用方法
this._armature.getAnimation().play("stand");    // 站立
this._armature.getAnimation().play("run");  // 跑动
// ...

如上代码,对于动作的初始化,可以看到对于帧动画来说,非常繁琐,需要加载每一帧的图片,组合成一个动作动画,而骨骼动画则不然,资源的加载非常简单,调用方式也很简单。实际,在 CocoStudio 中也能够使用帧动画,并且使得动画的加载过程变得简单!

显示效果: 就显示效果而言,帧动画有如播放电影同样,只有帧率很高的时候才能达到不错的显示效果,然而骨骼动画,其帧率和游戏的帧率同样,唯一的区别,就是需要制作骨骼动画,但是对于这里英雄角色帧动画的制作过程而言,也是先制作成每一块骨骼,然后为每一帧调节其位置关系,并且少了骨骼节点,位置角度等也都不好控制。这里从制作过程到显示,都可以发现骨骼动画的优势所在。



项目版本库升级

为了得到更好的骨骼动画支持,将这个小项目所使用的 H5 版本库,从 2.1.5 升级到了 2.1.6,这其中修复了些 骨骼动画中的 Bug,并且也对核心库的一些 API 做了修改,如:

// 新的触摸注册方式 ~
cc.Director.getInstance().getTouchDispatcher().addTargetedDelegate(this, 0, false);
cc.registerTargetedDelegate(0, true, this);

cc.Director.getInstance().getTouchDispatcher().removeDelegate(this);
cc.unregisterTouchDelegate(this);

// 精灵翻转
// 2.1.5
sprite.setFlipX
// 2.1.6
sprite.setFlippedX

角色动作组织

游戏中,对于英雄和怪物来说,有一些通用的方法或者代码结构,为此提取出一个 ActionSprite 以标示这样一个角色:

var ActionSprite = cc.Node.extend({
    // 初始化方法
    init:function(obj){...},
    // 攻击
    acceptAttack:function(obj){...},
    // 是否翻转,图片“左右”走动
    isFlip:function(){...},
    // 设置精灵
    setSprite:function(image, pos){...},
    // 开始跑动 附带方向,方向是一个小于 360 的角度
    runWithDegrees:function(degrees){...},
    // 跑动,改变方向
    moveWithDegrees:function(degrees){...},
    // 停止跑动
    idle:function(){...},
    // 每帧更新
    update:function(dt){...},
    // 简单 ai 实现
    ai:function(){...},
    // 屏幕检测,人物不能走出屏幕之外 并且只能在下方
    checkLocation:function(){...},
    // 站立
    hStand:function(){...},
    // 跑动
    hRunning:function(){...},

    // ...
});

对于英雄和怪物的实现来说,有所不同,除了前文中繁杂的动作初始化方法之外,其它实现如下:

hAttack:function(at){
    var aa = null;
    if (at == AT.ATTACK){
        aa = this._actionAttack;
        this._attackRangt = 150;            
    }else if (at == AT.ATTACK_A){
        aa = this._actionAttackJump;
        // 当前位置跳跃
        var jump = cc.JumpTo.create(
            0.6, cc.pSub(this.getPosition(), cc.p(this._flipX ? 200: -200)), 120, 1);
        this.runAction(jump);
        this._attackRangt = 300;
    }else if (at == AT.ATTACK_B){
        aa = this._actionAttackT;
        // 当前位置移动
        var move = cc.MoveTo.create(0.3, cc.pSub(this.getPosition(), cc.p(
                this._flipX ? 200:-200, 0)));
        this.runAction(move);
        this._attackRangt = 300;            
    }

    if (aa){
        this._sprite.stopAllActions();
        var action = cc.Sequence.create(
            aa,
            cc.CallFunc.create(this.callBackEndAttack, this));
        this._sprite.runAction(action);
        this._state = AC.STATE_HERO_ATTACK;
        this.postAttack();
    }
},
attack:function(at){
    this.hAttack(at);
},
callBackEndAttack:function(){
    if (this._isRun){
        this.hRunning();
    }else{
        this.hStand();
    }
}

对于怪物,实现如下:

var Robot = ActionSprite.extend({
    _armture:null,

    init:function(){
        var bRet = false;
        if (this._super()){

            cc.ArmatureDataManager.getInstance().addArmatureFileInfo(
                s_Robot_png,
                s_Robot_plist,
                s_Robot_json);

            this._armature = cc.Armature.create("NewProject");
            this.setSprite(this._armature, cc.p(500, 300));
            this.setZLocatoin(-90);
            this.hStand();

            this.runWithDegrees(180);

            this.setRoleType(AC.ROLE_ROBOT);
            this._imageflipX = true;
            bRet = true;
            this._speed = 150;
        }       
        return bRet;
    },
    setSprite:function(armature, pos){
        this._sprite = armature;
        this.addChild(this._sprite);
        this.setPosition(pos);      
    },  
    hAttack:function(at){
        this._attackRangt = 150;                    
        this._sprite.stopAllActions();
        this._sprite.getAnimation().play("attack");
        this._sprite.getAnimation().setMovementEventCallFunc(this.callBackEndAttack,this);
        this._state = AC.STATE_HERO_ATTACK;
        this.postAttack();
    },
    hStand:function(){
        this._sprite.getAnimation().play("stand");
        this._state = AC.STATE_HERO_STAND;
    },
    hRunning:function(){
        this._sprite.getAnimation().play("run");
        this._state = AC.STATE_HERO_RUNNING;        
    },
    attack:function(button){
        this.hAttack(button);
    },
    callBackEndAttack:function(armature, movementType, movementID){
        if (movementType == CC_MovementEventType_LOOP_COMPLETE) {
            if (this._isRun){
                this.hRunning();
            }else{
                this.hStand();
            }
        }
    },
    _timestamp: (new Date()).valueOf(),
    _attackIndex: 0,
    _moveIndex: 0,
    ai:function(){
        var newTs = (new Date()).valueOf();
        var value = newTs - this._timestamp;

        if (this._moveIndex < value / 3000){
            this._moveIndex += 1;
            var r = Math.random() * 360;
            this.moveWithDegrees(r);
        }
        if (this._attackIndex < value / 6000){
            this._attackIndex += 1;
            this.attack();
        }

    }
});

看到上面代码中的最后一小段,一个不是 AI 的 AI,每三秒钟做一次随机方向的走动,每六秒钟做一次攻击操作。

攻击判断:我们知道,只有在英雄和怪物站在一起时,才能攻击的到,表现在游戏画面中,那便是脚部所在的位置,在同一个 Y 坐标上,或者 Y 坐标的值在一个范围之内才能有效,所以在初始化的时候,设定了一个属性来标示它 (setZLocation),在攻击的时候,会去判断它们是否在有效的 Y 坐标之内,如下图中脚下的黄色线条(这线条素材不过时其它素材借来一用而已:D),除了上下位置关系的判断,当然也还有距离判断,则在代码中的 ActionSprite 实现:



在攻击之时,攻击者,发送一个消息,所有的可被攻击者都会收到这个消息,然后判断是否被攻击到,而后做相应的操作,如掉血等 ~

Cocos2d-html5 王者之剑实现 (2)

Cocos2d-html5 王者之剑实现 (1)

  • 项目组织
  • 360 度 可触摸摇杆实现
  • 攻击 与 特效攻击

Cocos2d-html5 王者之剑实现 (2)

  • 游戏中的操作控制
  • 操作控制之键盘映射
  • 用户信息更新

Cocos2d-html5 王者之剑实现 (3)

  • 动作,帧动画 VS 骨骼动画
  • 版本库升级
  • 角色动作组织


源码下载地址:https://github.com/iTyran/Tutorials/tree/master/html5/Arthur/src

在 《Cocos2d-html5 王者之剑实现 (1)》 中,实现了触摸摇杆和按钮点击的实现,这里继续。

游戏中的操作控制

使用 H5 来开发 Cocos2d-x 游戏的一个优势是它可以在浏览器中运行,这意味着,你无需安装部署到客户端即可发布运行,这时我们可以将一些游戏当中的触摸操作转换成键盘映射,就像我们平时玩 PC 游戏一样,比如跑动,跳跃等操作。

在之前,我们已经实现了一个摇杆和攻击按钮的实现,但是并没有一个统一的控制管理,以让它们很好的协同工作,这也是我们现在所要做的事情,首先我们有一个游戏层,里面包含了一个控制操作实现:

var GameLayer = cc.Layer.extend({

    init:function(){
        var bRef = false;
        if(this._super()){
            // ...

            // 添加控制层
            var hudLayer = HudLayer.create();
            this.addChild(hudLayer);
            hudLayer.setDelegate(this);

            // ...

            bRef = true;
        }
        return bRef;
    },
    actionJoypadStart:function(degrees){

    },
    actionJoypadUpdate:function(degrees){

    },
    actionJoypadEnded:function(degrees){

    },
    attackButtonClick:function(button){

    }
});

我们在 GameLayer 里面定义了 四个操作控制方法 ,摇杆控制,和按钮点击,我们实现的这四个方法,将会由 HudLayer 层来进行控制,显然对于 HudLayer 来说,它需要知道什么时候去调用这些方法,而对于 GamerLayer 本身来说,它 只关心 当触发了这些控制操作之时,需要多哪些操作,如触发摇杆操作,我们移动一个人物的走动,点击攻击,触发人物的攻击等。至于什么时候被触发,怎么被触发的,那它完全不关心。那么谁关心呢?显然不是你我 ~~

HudLayer 完成了这样一个功能,它包含了三个攻击按钮和一个摇杆控件,而且将由它 直接或者间接 完成对 四个操作控制方法 的调用。为什么说是直接或者间接?实际上这就是 委托机制 ,四个操作控制方法 就是回调函数,在 GameLayer 将回调函数委托给了 HudLayer ,当然 HudLayer 也可以继续委托给别人做,或者由自己来做。

还记得前文中 ActionButton 实现的 click 方法么!如果忘了,可以回头看看,有一句关键代码this._delegate.attackButtonClick(this.getAttackType());,它便是调用委托回调函数,而 _delegate 是谁,这不重要,重要的是它一定实现了 attackButtonClick 方法。至于什么时候 click 被触发,我想这里可以从前文代码看出来,在onTouchBegan 达成点击触摸操作时,当然包括了一些其它判断,如点击区域检测。这里的 _delegate 是谁:

var HudLayer = cc.Layer.extend({
    _winSize: null,
    _pCenter: null,
    _delegate: null,

    mJoypad: null,

    mAttack: null,
    mAttackA: null,
    mAttackB: null,
    ctor:function(){
        this._super();
        _winSize = cc.Director.getInstance().getWinSize();
        _pCenter = cc.p(_winSize.width / 2, _winSize.height / 2);
    },
    init:function(){
        cc.log("Hud layer init ..");
        var bRet = false;

        if(this._super()){

            // 添加控制器
            this.mJoypad = Joypad.create();
            this.addChild(this.mJoypad);

            // 添加攻击按钮
            this.mAttack = AttackButton.initWithImage(s_Attack);
            this.mAttack.setPosition(cc.p(mWinSize.width - 80,  80));
            // 设置攻击按钮的 delegate
            this.mAttack.setDelegate(this);
            this.mAttack.setAttackType(AT.ATTACK);
            this.addChild(this.mAttack);

            // 其它攻击 ...
            // ...

            bRet = true;
        }
        return bRet;
    },
    setDelegate: function(delegate){
        this._delegate = delegate;
        this.mJoypad.setDelegate(delegate);
    },

    attackButtonClick:function(button){
        if (this._delegate){
            this._delegate.attackButtonClick(button);
        }
    },
    keyAttack:function(btnType){
        if (btnType == AT.ATTACK_A && this.mAttackA.isCanClick())
            this.mAttackA.click();
        if (btnType == AT.ATTACK_B && this.mAttackB.isCanClick())
            this.mAttackB.click();
        if (btnType == AT.ATTACK && this.mAttack.isCanClick())
            this.mAttack.click();

        // this.attackButtonClick(bunType);
    },
    keyAttackUp:function(btnType){
        if (btnType == AT.ATTACK) //  && this.mAttack.isCanClick())
            this.mAttack.clickUp();     
    }
});

攻击按钮的 _delegage 就是 HudLayer 本身,所以在攻击按钮的 click 触发之时,HudLayer 的 attackButtonClick 方法被触发,而 HudLayer 本身的 _delegate 是谁呢,当然是 GameLayer 了,所以最终 GaleLayer 的 attackButtonClick 方法被触发,以达到攻击按钮点击作用于游戏控制的目的。这便是由 HudLayer 直接控制 GameLayer 的 attackButtonClick,别被 攻击按钮的 _delegate 和 HudLayer 的 _delegate 相通方法名 attackButtonClick 给骗了,它们完全可以不同,所以说是直接的。

我们已经注意到,在 HudLayer 虽然实现了调用 _delegate 的 attackButtonClick 方法,但是并没有看见摇杆控制的调用,如 actionJoypadStart 方法。这是因为在 HudLayer 的 setDelegate 方法中,在设置其本身 _delegate 的时候,也同样将它通过 this.mJoypad.setDelegate(delegate); 设置给了 Jodpad,这就意味着,在 Joypad 中,我们可以通过调用它的 _delegate 来控制 GameLayer 的操作,它将直接作用于 GameLayer,由 HudLayer 间接的传递委托(js 中没有明文的规定,所以你可以任意的将委托传递,这同样也是 js 的灵活之处,如果运用得当:否则不应该称之它为“灵活”了)。

HudLayer 的存在是为了托管控制操作,以便很容易的进行控制或者扩展,我们当然可以将所有的内容都一股脑的放在 GameLayer 中去实现,让它去接受触摸事件,去判断点击,去进行游戏的控制,但那样做,不利于我们后期的维护与扩展。例如,我想通过扩展,实现键盘操作控制,再如在游戏中,我可以通过网络发送命令来控制主角的走动,而对于 GameLayer 来说,显然它可以不用知道谁控制它,全部交由 HudLayer 来完成,下面对游戏进行简单的扩展,实现通过键盘来操作游戏。

操作控制之键盘映射

在使用键盘之前,我们需要先检测键盘是否可用,并且启用它,后通过一个数组(更准确的说是字典)来保存按键信息:

// GameConfig.js
// AC.KEYS 的定义
var AC = AC || {};

AC.KEYS = [];

// GameLayer.js init 方法
if (sys["capabilities"].hasOwnProperty('keyboard'))
    this.setKeyboardEnabled(true);

// 保存按键信息
onKeyDown:function(e){
    // 保存所有的按键信息
    AC.KEYS[e] = true;
},
onKeyUp:function(e){
    AC.KEYS[e] = false;
},

通过以上方式,将按键的信息保存在了 AC.KEYS 里面,用以在任何失去判断我们关系的按键是否被按下,以便完成一些操作。

var KeyMap = cc.Layer.extend({
    _delegateJoypad: null,
    _delegateAttack: null,
    _pJoyKeyDown: false,
    _pJKeyDown: false,
    _pUKeyDown: false,
    _pIKeyDown: false,
    init:function(){
        this._super();
        this.scheduleUpdate();
        return true;
    },
    setDelegateJoypad:function(delegate){
        this._delegateJoypad = delegate;
    },
    setDelegateAttack:function(delegate){
        this._delegateAttack = delegate;
    },
    update:function(dt){
        this._super();

        // 控制杆键盘映射处理
        var au = false;
        var al = false;
        var ad = false;
        var ar = false;

        // 属性值的判断操作
        // ...

        var newDegrees = -1;
        // 通过按键判断摇杆方向,具体实现细节请看源码

        if (this._delegateJoypad){
            if (au || al || ad || ar){
                if (!this._pJoyKeyDown)
                    this._delegateJoypad.keyStart(newDegrees);                  
                this._pJoyKeyDown = true;
            }
            else if(this._pJoyKeyDown){
                this._pJoyKeyDown = false;
                this._delegateJoypad.keyEnded(newDegrees);
            }
            if (newDegrees != -1 && this._pJoyKeyDown){
                this._delegateJoypad.keyUpdate(newDegrees);
            }           
        }

        // 攻击按钮控制映射
        var keyJ = false;
        var keyU = false;
        var keyI = false;
        if (AC.KEYS[cc.KEY.j])
            keyJ = true;
        if (AC.KEYS[cc.KEY.u])
            keyU = true;
        if (AC.KEYS[cc.KEY.i])
            keyI = true;

        var pressJ = false;
        var pressU = false;
        var pressI = false;

        if (keyJ){
            if (!this._pJKeyDown){
                this._pJKeyDown = true;
                pressJ = true;
            }           
        }else{
            if (this._pJKeyDown){
                // 发送一个攻击键松开的操作
                this._delegateAttack.keyAttackUp(AT.ATTACK);
            }               
            this._pJKeyDown = false;
        }
        // 其它按键的判断
        // ...

        if (this._delegateAttack){
            if (pressJ)
                this._delegateAttack.keyAttack(AT.ATTACK);
            if (pressU)
                this._delegateAttack.keyAttack(AT.ATTACK_A);
            if (pressI)
                this._delegateAttack.keyAttack(AT.ATTACK_B);
        }

    }
});

以上,我们通过 _delegateJoypad 和 _delegateAttack 来分别完成不同功能的调用,至于调用时机,由内部判断完成,也就是根据按钮的点击做出相应的响应,再作用于两个 delegate。

// 在  HudLayer 中 init 方法添加如下:
var keyMap = KeyMap.create();
keyMap.setDelegateJoypad(this.mJoypad);
keyMap.setDelegateAttack(this);
this.addChild(keyMap);

// HudLayer 添加实现
keyAttack:function(btnType){
    if (btnType == AT.ATTACK_A && this.mAttackA.isCanClick())
        this.mAttackA.click();
    if (btnType == AT.ATTACK_B && this.mAttackB.isCanClick())
        this.mAttackB.click();
    if (btnType == AT.ATTACK && this.mAttack.isCanClick())
        this.mAttack.click();

    // this.attackButtonClick(bunType);
},
keyAttackUp:function(btnType){
    if (btnType == AT.ATTACK) //  && this.mAttack.isCanClick())
        this.mAttack.clickUp();     
}

而 _delegateJoypad.keyStart 等方法的实际调用则在 Joypad 中完成,从而在去控制 GameLayer 的操作。在添加这样一个新的功能时,我们并没有对 GameLayer 进行太大的修改,只是在已有的实现,多添加一种触发条件而已,让 KeyMap 去调用 HudLayer 的 keyAttack,并且判断实际的攻击按钮,最终再去调用按钮的 click,完成按钮操作所应有的功能,如按钮再点击时的一些特效之类。而 Joypad 的调用异曲同工 ~

用户信息更新

为用户添加状态信息,如角色名称,血条等,我们最终显示的效果是这样的:

 

为此我们需要准备一系列素材。



开发之时,这里只实现了,血条的改变,而并没有添加其它 值 的状态改变,但这并不影响实现它们。对于英雄和机器人(系统人物)来说,它们都有血量状态,都能改变,为此,设定一个抽象的数据类型来标示状态,让代码得到重用:

var State = cc.Node.extend({
    _bloodSprite: null,
    _roleType: null,
    ctor:function(){
        this._super();
    },
    setBloodSprite:function(obj){
        this._bloodSprite = obj;
    },
    init:function(){
        var bRet = false;

        if(this._super()){
            cc.NotificationCenter.getInstance().addObserver(this, this.notifyChangeStatus, "status", null);

            bRet = true;
        }

        return bRet;
    },
    notifyChangeStatus:function(obj){
        if (obj.getRoleType() == this._roleType){
            cc.log("notify status ...");
            this.setBlood(obj.getBloodPercent());
        }
    },
    setBlood:function(value){
        // 显示血量百分比
        if (value < 0)
            value = 0;
        if (value > 1)
            value = 1;
        this._bloodSprite.setScaleX(value);
    },
    setRoleType:function(type){
        this._roleType = type;
    }
});

State.create = function(){
    var state = new State();
    if (state && state.init()){
        return state;
    }
    return null;
};

这里使用通知机制,来完成对状态的更新,需要注意的是,在当前使用的版本 H5 - 2.1.5 中,默认并没有启用 NotificationCenter,你可以将 "[H5]/cocos2d/support/CCNotificationCenter.js" 添加到 "[H5]/cocos2d/CCLoader.js" 中去。

State.createHero = function(){
    var state = State.create();

    var s1 = cc.Sprite.create(s_HeroState1);
    var s2 = cc.Sprite.create(s_HeroState2);
    var s3 = cc.Sprite.create(s_HeroState3);
    var s4 = cc.Sprite.create(s_HeroState4);

    s1.setPosition(cc.p(-80, 3));
    s2.setPosition(cc.p(33, 15));

    s3.setPosition(cc.p(-45, -12));
    s3.setAnchorPoint(cc.p(0, 0));

    state.setBloodSprite(s3);

    state.addChild(s1);
    state.addChild(s2);
    state.addChild(s3);
    state.addChild(s4);
    state.setRoleType(AC.ROLE_HERO);

    var title = cc.LabelTTF.create("Lv7 一叶", "Tahoma", 14);
    title.setPosition(cc.p(-15, 30));
    state.addChild(title);

    return state;
};

State.createRobot = function(){
    var state = State.create();
    var s1 = cc.Sprite.create(s_RobotState1);
    var s2 = cc.Sprite.create(s_RobotState2);
    var s3 = cc.Sprite.create(s_RobotState3);
    var s4 = cc.Sprite.create(s_RobotState4);

    s1.setPosition(cc.p(50, -16));
    state.setBloodSprite(s1);
    s1.setAnchorPoint(cc.p(1, 0));

    // s1.ignoreAnchorPointForPosition(true);

    s2.setPosition(cc.p(-20, -7));
    s4.setPosition(cc.p(65, 1));

    state.setRoleType(AC.ROLE_ROBOT);

    state.addChild(s2);
    state.addChild(s1);
    state.addChild(s3);
    state.addChild(s4);

    var title = cc.LabelTTF.create("Lv5 子龙山人", "Tahoma", 14);
    title.setPosition(cc.p(-15, 12));
    state.addChild(title);

    return state;
};

上面使用两个方法创建了玩家状态和机器人状态,两者有类似的功能,唯一需要注意的是,血量的锚点设置,基于百分比的血量展示,用基于百分比的放大缩小控制。设置好锚点才能保证显示的效果。

Cocos2d-html5 王者之剑实现 (1)

Cocos2d-html5 王者之剑实现 (1)

  • 项目组织
  • 360 度 可触摸摇杆实现
  • 攻击 与 特效攻击

Cocos2d-html5 王者之剑实现 (2)

  • 游戏中的操作控制
  • 操作控制之键盘映射
  • 用户信息更新

Cocos2d-html5 王者之剑实现 (3)

  • 动作,帧动画 VS 骨骼动画
  • 版本库升级
  • 角色动作组织


源码下载地址:https://github.com/iTyran/Tutorials/tree/master/html5/Arthur/src

前面,《手把手,快速搭建 Cocos2d-HTML5 开发调试环境》 与 《如何自定义 Cocos2d-HTML5 Loading 界面》 两篇文章,帮助了我们搭建了其开发环境,并了解了 H5 ( 以下对 Cocos2d-HTML5 简称 H5,显然,它不是 * 流感,没那么大破坏力) 的大致加载流程,此文开始就要使用 H5 来制作一个简单的动作游戏 王者之剑,完成效果如下所示(所有的源码你可以在 【这里】查看),显示效果可以在 【这里】查看:



这是一个简单的游戏打斗场景,一个英雄,一个怪物,可以控制英雄来回走动并且攻击,怪物实现简单 AI 并且自动攻击,有着不同的血量槽,控制系统,可以使用触摸,但为了操作的体验,同样实现了 键盘映射 ,可以使用 W、A、S、D 来控制人物的走动,J、U、I 实现一个普通攻击和两个特效攻击。

项目组织

为了使项目的代码结构清晰,前期规划好功能是必须的,先从整体看一下,项目的组织结构,然后会对其中内部实现做些必要的解说:



如上所示, Arthur 为游戏项目的主目录,与它同级的目录,是 H5 的库目录,当然截图中,为了发布,删除了一些不必要的文件,在 Arthut 目录下,包含一般项目都包含的结构组织。

  • index.html : 这是游戏的展示界面,其中包含 “gameCanvas” ,作为游戏绘制的所在,它引用加载了 cocos2d.js
  • cocos2d.js : 项目初始化在这里进行,并完成系统库和项目源码的 js 加载,最后将控制权交给 main.js 文件
  • main.js : 当 H5 库加载完毕,执行内中代码,完成项目资源加载,并运行第一个场景
  • src : 此目录包含了游戏中编写的 js 源代码文件
  • res : 游戏所需的资源,如图片,字体等

在这个游戏中相对复杂一点的就是控制系统了, HudLayer 中添加实现了 ActionButton 普通攻击按钮, Joypad 可触摸 360 度 摇杆功能,和 KeyMap 游戏控制键盘映射方案。Characters 实现了人物和怪物的功能,各种动作控制。Loading 替换了 H5 的默认加载界面,使用了一个进度条显示加载进度。GameLayer 作为游戏的主场景,各种游戏的流程控制在这里进行。

360 度 可触摸摇杆实现

这里的摇杆,默认是为了触摸实现,之后添加的键盘映射,只是为了让操作更为方便而已(在 PC 浏览器中),触摸不同于摇杆的所在,就是这里的摇杆可以是 360 度以内的任意角度,也就是可以控制任意以任意方向移动,这是键盘所不具备的,上下左右四个键,再加上每两个方向的组合也就八个方向。

var Joypad = cc.Layer.extend({
    _winSize: null,
    _pCenter: null,
    _pControlSprite: null,
    _pDefaultPoint: null,

    _pDefaultRotation: null,
    _pRotation: null,

    _pDelegate: null,
    _pKeyDown: false,
    ctor:function(){
        this._super();

        _winSize = cc.Director.getInstance().getWinSize();
        _pCenter = cc.p(_winSize.width / 2, _winSize.height / 2);

    },
    init:function(){
        var bRet = false;
        if (this._super()){
            cc.log("Joypad init ..");
            // 控制杆所在位置
            this._pDefaultPoint = cc.p(110, 110);
            // 默认旋转角度,以使开口正对右侧
            this._pDefaultRotation = 26;
            // 实际旋转角度
            this._pRotation = 0;

            this.setPosition(this._pDefaultPoint);

            this.addChild(cc.Sprite.create(s_Joypad1));
            this.addChild(cc.Sprite.create(s_Joypad2));
            this._pControlSprite = cc.Sprite.create(s_Joypad3);
            this.addChild(this._pControlSprite);
            this.addChild(cc.Sprite.create(s_Joypad4));

            this.updateRotation();

            bRet = true;
        }
        return bRet;
    },
    keyStart:function(degrees){
        if (this._pDelegate)            
            this._pDelegate.actionJoypadStart(this._pRotation);
    },
    keyUpdate:function(degrees){
        this._pRotation = degrees;
        this.updateRotation();
        if (this._pDelegate)
            this._pDelegate.actionJoypadUpdate(this._pRotation);                
    },
    keyEnded:function(degrees){
        if (this._pDelegate)            
            this._pDelegate.actionJoypadEnded(this._pRotation);     
    },
    onEnter:function(){
        this._super();
        cc.Director.getInstance().getTouchDispatcher().addTargetedDelegate(this, 0, true);
    },
    onTouchBegan:function (touch, event){
        // 点击点的范围判断
        var curPoint = touch.getLocation();
        if (curPoint.x > _winSize.width / 2 || curPoint.y > _winSize.height / 2 ){
            return false;
        }

        // var sp = cc.pSub(this._pDefaultPoint, curPoint);
        // var angle = cc.pToAngle(sp);

        this.updateTouchRotation(touch, event);
        this.updateRotation();
        if(this._pDelegate)
            this._pDelegate.actionJoypadStart(this._pRotation);
        else
            cc.log('_pDelegate is null ... ');

        // cc.log("Joypad touch ...");
        return true;
    },
    onTouchMoved:function (touch, event){
        this.updateTouchRotation(touch, event);
        this.updateRotation();

        if (this._pDelegate)
            this._pDelegate.actionJoypadUpdate(this._pRotation);
        else
            cc.log('_pDelegate is null ... ');

        // var a = cc.pAngleSigned( curPoint, this._pDefaultPoint);
        // cc.log("Joypad touch mvove ..." + rotation) ;
    },
    onTouchEnded:function (touch, event){
        this.updateTouchRotation(touch, event);
        this.updateRotation();
        if (this._pDelegate)
            this._pDelegate.actionJoypadEnded(this._pRotation);
        else
            cc.log('_pDelegate is null ... ');
    },
    updateTouchRotation:function(touch, event){
        var curPoint = touch.getLocation();
        var sp = cc.pSub(curPoint, this._pDefaultPoint);
        var angle = cc.pToAngle(sp) ;// * -57.29577951;
        var rotation = angle * -57.29577951;
        rotation = rotation < 0 ? 360 + rotation: rotation;
        this._pRotation = rotation;     
    },
    updateRotation:function(){
        this._pControlSprite.setRotation(this._pDefaultRotation + this._pRotation);
    },
    setDelegate:function(dg){
        this._pDelegate = dg;
    }
});



在初始化方法中,加载了摇杆资源文件,它分解成几个组成部分,以便于很好的控制,并且保存了可旋转元素精灵的引用this._pControlSprite,以便于随时控制它的旋转角度,如图中 Joypad3.png 图片。

以触摸的动作来控制动作的执行,Joypad 中包含了一个名为 _pDelegate 的属性,它作为控制摇杆的代理,以通知其它 (如 人物),摇杆现在变动了,分别在 onTouchBegan 中调用,this._pDelegate.actionJoypadStart(this._pRotation);onTouchMoved 中调用this._pDelegate.actionJoypadUpdate(this._pRotation); 和在 onTouchEnded 中调用this._pDelegate.actionJoypadEnded(this._pRotation);

只需要在传入的 _pDelegate 中实现此三种函数,就可以通过摇杆来控制其操作了,H5 使用 javascript 相比如 C++ 倒也省去了接口定义等繁杂的操作。可以看见,在三个函数调用中,所传入的参数为触摸的角度,在触摸是通知控制显示摇杆中 “罗盘” 的旋转。Joypad 对内通过触摸控制显示,对外通过触摸调用代理,以达到显示和控制相一致的目的。通过触摸的点相对摇杆原点的位置关系,很容计算出其角度。

由于这里的摇杆设计是 360 度任意角度,所以在 delegate 中传出一个参数,以标示角度关系,如果并不需要那么复杂的控制,如前文所言,只需固定八个方向的控制,那么这里传出的参数可以使用 枚举 类型,代表八个不同的方向,也会使得游戏逻辑变得稍微简单。

最后可以为 Joypad 层封装一个简单好用的调用方式:

Joypad.create = function(){
    var joypad = new Joypad();
    if (joypad && joypad.init()){
        return joypad;
    }
    return null;
};

攻击 与 特效攻击

在这个游戏中,有一个普通攻击和两个特效攻击,这两个不同,但很显然,他们都是攻击,却又相同,先看看他们的共同点:

// ActionButton.js

var ActionButton = cc.Node.extend({
    _sprite: null,
    _rect: null,
    _delegate: null,
    _attackType: null,

    _childObj: null,
    rect:function(){
        var size = this._sprite.getContentSize();
        return cc.rect(-size.width / 2, -size.height / 2, size.width, size.height);
    },
    setChindObj:function(obj){
        this._childObj = obj;
    },
    init:function(image){
        this._super();

        this._sprite = cc.Sprite.create(image);
        this.addChild(this._sprite);
        return true;
    },
    setDelegate:function(delegate){
        this._delegate = delegate;
    },
    setAttackType:function(at){
        this._attackType = at;
    },
    getAttackType:function(){
        return this._attackType;
    },
    onEnter:function(){
        cc.Director.getInstance().getTouchDispatcher().addTargetedDelegate(this, 0, false);
        this._super();
    },
    onExit:function(){
        cc.Director.getInstance().getTouchDispatcher().removeDelegate(this);
        this._super();
    },
    containsTouchLocation:function(touch){
        return cc.rectContainsPoint(this.rect(), this.convertTouchToNodeSpace(touch));
    },
    onTouchBegan:function(touch, event){
        // 区域判断
        if (!this.containsTouchLocation(touch))
            return false;
        this.click();
        // 播放点击动画
        return true;
    },
    click:function(){
        if(this._delegate && this._childObj.isCanClick()){
            this._delegate.attackButtonClick(this.getAttackType());
            this.beganAnimation();          
        }
    },
    onTouchEnded:function(touch, event){
        this.endedAnimation();
    },
    beganAnimation:function(){
    },
    endedAnimation:function(){
    },
    isCanClick:function(){
        return true;
    }
});

定义了一个 ActionButton 攻击按钮类型,它实现了 onTouchBegan 作为按钮点击的触发场所,触发了 click 事件,再由 click 处理调用代理的事件,传出一个参数,以标示攻击的类型 AttackType,在判断点击的时候还需要检测点击区域是否在按钮的可点击范围之内,当然再触发攻击动作之时,按钮本身也实现了一些动画特效,如点击效果,技能冷却效果,它由beganAnimation 方法实现。

但我们看见在 ActionButton 并没有实现 beganAnimation,在方法里面并没有实现任何代码,这因为 ActionButton 只是作为 攻击按钮 的抽象,它只定义了攻击按钮具体由那些功能,能做哪些事情,如可以播放点击时的动画,但具体的动画内容,需要根据具体的攻击按钮有着不同的实现。

var AttackButton = ActionButton.extend({
    _pt: null,
    _ac: null,

    _defaultScale: 0.35,
    _maxScale: 0.5,

    _inAction: null,
    _outAction: null,

    _timestamp: null,
    ctor:function(){
        this._super();
        this._pt= cc.Sprite.create(s_AttackO);
        this._pt.setScale(this._maxScale);
        this.setChindObj(this);

        // this.addChild(this._pt);

        var aScale = cc.ScaleTo.create(0.1, this._defaultScale);
        var aFadein = cc.FadeIn.create(0.1);
        this._inAction = cc.Spawn.create(aScale, aFadein);

        var oScale = cc.ScaleTo.create(.2, this._maxScale);
        var oFade = cc.FadeOut.create(0.2);
        this._outAction = cc.Spawn.create(oScale, oFade);
    },
    beganAnimation:function(){
        var timestamp = (new Date()).valueOf();     
        this._timestamp = timestamp;

        this.removeChild(this._pt);
        this.addChild(this._pt);
        this._pt.runAction(this._inAction);

    },  
    endedAnimation:function(){
        this._pt.stopAllActions();
        this._pt.runAction(this._outAction);
    },
    clickUp:function(){
        this.endedAnimation();
    },
    isCanClick:function(){
        var timestamp = (new Date()).valueOf();     
        return timestamp - this._timestamp > 600;
    }
});

普通攻击按钮的效果,初始化设置图片素材,播放动画为一个光圈放大缩小显示,它 继承 自 ActionButton ,同样实现了 beganAnimation 方法。另外一种是特效攻击的实现:

var AttackEffect = ActionButton.extend({
    _pt: null,
    _ac: null,
    _isCanClick: true,
    ctor:function(){
        this._super();
        var h = cc.Sprite.create(s_AttackFreeze);
        this._pt = cc.ProgressTimer.create(h);
        this._pt.setType(cc.PROGRESS_TIMER_TYPE_RADIAL);
        this._pt.setReverseDirection(true);
        this._pt.setScale(0.43);

        var to = cc.ProgressTo.create(0, 99.999);
        var to1 = cc.ProgressTo.create(2, 0);
        var ac2 = cc.CallFunc.create(this.callBack, this);
        this._ac = cc.Sequence.create(to, to1, ac2);
        this.setChindObj(this);
    },
    beganAnimation:function(){
        this.removeChild(this._pt);
        this.addChild(this._pt);
        this._pt.runAction(this._ac);
        this._isCanClick = false;
    },
    endedAnimation:function(){
    },
    callBack:function(){
        // cc.log("call back");
        this._isCanClick = true;
    },
    isCanClick:function(){
        return this._isCanClick;
    }
});

特效攻击有个冷却效果,不能在一定时间范围内连续攻击,使用一个 旋转的 Progress 来达到这样的效果。

如何自定义 Cocos2d-HTML5 Loading 界面

在使用 C++ 编写 Cocos2d-x 游戏的时候,通常在运行游戏之前,需要加载游戏资源,这样是为了让游戏在运行时更为流畅,避免了在运行时加载资源,而出现卡顿现象,影响用户体验,因为加载资源是非常耗时、耗资源的操作。在 Cocos2d-html5 中也是同样,在运行游戏之前,预先加载好所有的资源(加载到浏览器缓存),以保证游戏的流畅运行。

Cocos2d-html5 的加载流程

在开始我们的替换工作之前,大致说一下必要的(只注重我们在乎的细节问题)原有的加载流程,以 HelloHTML5World 为例 。从其主页面 index.html 开始,我们需要了解三个文件的运作方式,index.html、cocos2d.js 和 main.js:

浏览器首先加载 index.html 页面,值得注意的有两点,在页面的 DOM 树中,能看到命名为 gamecanvas 的 Canvas 元素,它将会是游戏的画布,另一点,在页面的最后,加载了 cocos2d.js 文件。

cocos2d.js 内中,定义了程序运行需要的一些参数,如 显示 FPS,是否加载扩展库,物理引擎库等,其中 engineDir 设置了引擎所在的位置,appFiles 设置了,当前项目所用到需要加载的 js 程序代码。并定义了当 DOM 加载完成时运行的代码(你可以在 【这里】 查看所有代码。):

window.addEventListener('DOMContentLoaded', function () {
    // 添加脚本
    var s = d.createElement('script');
    // 这里判断了是否使用自定义的 单文件作为库加载,对库的优化压缩文件
    if (c.SingleEngineFile && !c.engineDir) {
        s.src = c.SingleEngineFile;
    }
    else if (c.engineDir && !c.SingleEngineFile) {

        s.src = c.engineDir + 'platform/jsloader.js';
    }
    else {
        alert('You must specify either the single engine file OR the engine directory in "cocos2d.js"');
    }
    document.ccConfig = c;
    s.id = 'cocos2d-html5';
    // 将脚本加载到当前文档,地址是  jsloader.js 的实际地址
    d.body.appendChild(s);
});

jsloader.js 里面设置了一堆需要加载的可执行脚本,保存在 engine 变量之中,在文件的最后,我们能够看到这样的代码:

// 将所有的 appFiles 添加到 engine 中,返回到新定义的变量 que 之中
var que = engine.concat(c.appFiles);
que.push('main.js');
...
// 后面一个 for 循环,添加所有文件到 document 中去

由以上代码,便将我们自定义的使用脚本和 main.js 添加进去了,而最后执行的也是 main.js 脚本,游戏的第一个运行场景就由此开始。main.js 里面创建了一个 cocos2dApp 的类型,它 继承(虽然在 js 中没有继承的概念,但有类似于继承的机制) 自 cc.Applicatoin ,其中我们看到非常熟悉的函数 applicationDidFinishLaunching ,有这样一段代码:

// 调用 cc.LoaderScene Loading 界面,用以加载资源
cc.LoaderScene.preload(g_resources, function () {
    // 当资源加载完毕,回调函数,运行第一个场景,而这个场景是由 cocos2dApp 的构造函数传入
    director.replaceScene(new this.startScene());
}, this);

// main.js 最后一行
var myApp = new cocos2dApp(GameLayer.scene);

在 cocos2dApp 的构造函数中,初始化了一些必要信息,并调用了cc.AppController.shareAppController().didFinishLaunchingWithOptions(); ,这会间接的调用 Application 的 run 方法,从而导致 applicationDidFinishLaunching 方法被触发。它运行了 cc.LoaderScene 的 preLoad 方法,这内中就是 Loading 界面的实现了,它传入了一个回调函数,用于确定在资源加载完毕之后启动第一个场景(Loading 其本身也是一个场景)。

怎样自定义 Loading 界面

前面我们了解了 Cocos2d-html5 的大致加载流程,而现在我们关注的是 cc.LoaderScene 所在的文件 CCLoader.js 的内部实现。里面定义了 cc.Loader 和 cc.LoaderScene 类型,Loader 内部完成了,对资源加载的所有操作步骤实现,而 LoaderScene 则是对 Loader 的进一步封装,将加载的过程,用一个界面来可视化的实现出,如用一个加载场景,上面一个 Logo 显示,同事显示了当前加载资源的进度百分比。我们要自定义实现 Loading 界面,那就是重新实现 LoaderScene 即可。对于 LoaderScene 的实现比较简单,我们参考其实现,自定义一个 Loader.js 文件,实现 Loader 类,完成自定义Loading 界面的具体实现,其中大多参考(实际是copy)了 LoaderScene 的实现,在其上修改扩充,它完成了修改 Logo 图片,并添加了一个简单的精度条,是加载过程更为一目了然,这里并没有多么炫的效果,但足以让你了解,你该如何自定义一个 Loading 界面(只贴出了相比较 LoaderScene 所修改的部分,但你可以在 【这里】 获取到源码):

// 这里定义了 Logo 图片的 Base64 编码,至于为什么,后面将会说明,这里的编码内容挺多,固做简写
logoData = "data:image/png;base64,...";

Loading = cc.Scene.extend(
    _logo: null,
    _logoTexture: null,
    _texture2d: null,
    _bgLayer: null,
    _label: null,
    _winSize:null,
    _processLayer: null,    // 相比 LoaderScene  的实现,添加了两个属性,标示进度条层和进度条长度
    _processLayerLength: null,

    // 构造函数
    ctor: function () {
        this._super();
        this._winSize = cc.Director.getInstance().getWinSize();
    },
    init:function(){
        cc.Scene.prototype.init.call(this);

        // logo 图片和 label 的添加 .. 这里省略,于 LoaderScene 同样

        // 设置进度条层,它就是一个红色颜色层,通过长度来标示加载的进度
        this._processLayerLength = 500;
        this._processLayer = cc.LayerColor.create(cc.c4b(255, 100, 100, 128), 1, 30);
        this._processLayer.setPosition(cc.pAdd(centerPos, cc.p(- this._processLayerLength / 2, -logoHeight / 2 - 50)));
        // 可以启用锚点,并设置以满足自己的需要
        // this._processLayer.ignoreAnchorPointForPosition(false);
        // this._processLayer.setAnchorPoint(cc.p(0, 0));

        this._bgLayer.addChild(this._processLayer);
    },
    // 以下方法的实现并没有跟 LoaderScene 有什么不同
    // _initStage: ...
    // onEnter ...
    // onExit ...
    // initWithResources ...
    // _startLoading ...
    // _logoFadeIn
    // 每帧更新
    _updatePercent: function () {
        var percent = cc.Loader.getInstance().getPercentage();
        var tmpStr = "Loading... " + percent + "%";
        this._label.setString(tmpStr);

        // 设置当前进度条层的长度
        this._processLayer.changeWidth(this._processLayerLength * percent / 100);

        if (percent >= 100)
            this.unschedule(this._updatePercent);
    }
});
// 这里于 LoaderScene 的实现同样
Loading.preload = function (resources, selector, target) {
    if (!this._instance) {
        // 创建一个 Loading
        this._instance = new Loading();
        this._instance.init();
    }
    // ...
    return this._instance;
};

这里我们只是 copy 了一份 LoaderScene(copy 修改会让这里的操作步骤简化) ,重新命名 Loading 然后在此基础添加了一个进度条显示,当然这里的 Loading 类完全由你自己定义,它有哪些显示,你可以随意定制,只要在 _updatePercent 方法实时获取当前进度,并且更新到界面显示即可。

我们注意到在 Loading 文件,定义了一个 logoData 变量,它保存的是一张图片的 Base64 位格式编码,这样做的好处是,在运行显示图片之时,不会出现此图没有加载的情况,但是也由于它是 Base64 编码的图片,所以图片不宜过大,否则编码后的数据量很大,其次解码也需要耗时。

完成 Loading.js 后,我们需要加载它,并让它运行,以替换 LoaderScene 的运行。首先修改项目 cocos2d.js 文件,在appFiles 添加 Loading.js 文件。其次修改 main.js 文件内 cc.LoaderScene.preload 为 Loading.preload 即可,刷新 index.html 界面,将能看见 Loading 界面已经替换。它加载速度的快慢取决于游戏资源内容的多少 ~

loading

手把手,快速搭建 Cocos2d-HTML5 开发调试环境

在这篇文章中,你将学会如何快速搭建 Cocos2d-HTML5 的开发和运行环境,对于脚本来说,大多编辑器提供语法高亮显示,而没有语义补全,虽然有些开发环境提供了自动补全功能,但都不大好用。这里推荐使用 WebStorm,作为一个 IDE(集成开发环境),它有非常强大的代码补全,而且其补全相当智能,提高了开发效率。还能与 Google Chrome 浏览器配合,完成实时编辑和调试功能。当然你可以有其它选择。下面详细介绍它的详细配置步骤。

环境准备

在开始之前,首先下载需要的文件,依赖等,对于 Cocos2d-HTML5 使用当前的最新稳定版本,2.1.5 。你可以在 这里 下载。WebStorm 请访问官网,根据自己的系统环境选择下载,这里使用的是 6.0.2 版本。 为了能够使得 IDE 与浏览器协同编辑调试,需要安装 Chrome jetbrains-ide-support 插件。有了以上三者,就可以进行配置。这里以 Mac OS X 作为系统环境。

配置 WebStorm 项目,代码补全功能

在开始开发之前,第一步需要将 Cocos2d-html5 导入 WebStorm 项目工程。

  • 首先解压缩 Cocos2d-html5-v2.1.5.zip 压缩包。
  • 打开 WebStorm 进入欢迎界面,选择又侧 Create New Project from Existing Files 项
  • Select scenario 选择最后一项一项“Source files are in a local directory ...”,点击下一步

  • 浏览文件夹到 Cocos2d-html5-v2.1.5 目录,然后点击 Project Root 设定跟目录

  • 点击 “Finish” 完成项目导入。

完成以上步骤,便已经能够在 WebStorm 里面打开所有内容,编辑 HelloHTML5World/src/myApp.js 已经能够完成自动补全功能,这是因为当前整个项目包含了所有 js 源码。

如果是其它位置的项目没有包含 js 库完成自动补全的需要手动添加库(根据实际需要)。完成对 Cocos2d-html5 的补全,使用以下方式添加 js 库:

  • 点击菜单 WebStorm Preferences 进入 Settings 界面
  • 定位到 Project Settings 中 JavaScript 的 Libraries 选购项

  • 点击 Add 按钮,进入 库 添加配置界面

  • 如图所示,填写库名称,然后 Attach 浏览添加目录,其中根据需要包含库目录,OK!

以上是在其它独立的项目,使用 Cocos2d-html5 库时的环境配置。能够以如此的方式在任何项目添加 Cocos2d-html5 的库,可以在项目的 “External Libraries” 看到所有的依赖库。

浏览器同步编写实时刷新,单步调试

以上是使用 WebStorm 开发环境的步骤,但同样需要浏览运行,查看效果。

右击项目主目录的 index.html 选择 调试 index.html 后,将以 Chrome 打开此页面(系统默认浏览器),而打开的方式,我们看到是系统文件路径,如 :

url = “file:///Users/leafsoar/Cocos2d-html5-v2.1.5/index.html”

需要注意的是,以此方式所打开的页面,静态页面正常浏览,但选择 Hello World 运行,会加载不了,这是由于 cocos2d-html5 的处理机制与浏览器的兼容问题造成的(在其它浏览器会有不同的结果),官方推荐以服务的方式运行,如http://localhost 的方式。

在这里我们在浏览器中输入 http://localhost:63342/Cocos2d-html5-v2.1.5/ 来访问当前项目,在 Chrome 我们也同样安装了 JetBrains 插件,以此协同工作。这样在修改项目中静态页面的时候,将会自动更新至页面,看到实时效果。http://localhost:63342 是由 WebStorm 提供了服务,后面跟项目名称,以服务的方式访问页面,查看效果。

调试步骤,我们需要修改 右击 调试 index.html 的 url 地址。点击工具条,调试配置,index.html 右侧下拉 Edit Configurations,将默认的 Path 地址由:

“/Users/leafsoar/Cocos2d-html5-v2.1.5/index.html” 修改为 “http://localhost:63342/Cocos2d-html5-v2.1.5/” 如图:



完成以上配置后,我们在如 “HelloHTML5World/src/myApp.js” 中的代码设置以断点,后调试运行项目,测试 Hello World 便可以单步调试了:



我们可以修改 HelloHTML5World 来快速学习它们,其中 template 提供了一个项目模板,如果需要建立自己的项目,可以将它拷贝,然后基于它编写自己的代码程序。

COCOS2D-HTML5系列教程PART-2 制作“Helloworld”

本教程由泰然教程组出品。翻译:sile,Sharyu,蓝羽;校对:Iven。转载请注明出处并通知泰然。


就像在上个教程中承诺的那样,在这篇教程中我们会深入的了解并开始编码。和其他教程一样,我们会用Cocos2D-HTML实现HelloWorld。我在一开始就警告你,这将会是一个看起来比其实际要复杂很多的过程。有相当一部分样板代码需要你保存可以运行的环境,但总的来说它们不是那么困难。在以后的游戏中,你只需要作为模板复制粘帖就行了。

下面的内容将涉及到很多细节,比后面的教程都详细。后面有一个TL;DR(注:Too Long; Didn't Read 太长,不必细究)概要,所以如果你喜欢的话,只需要简单的看下代码示例,然后跳到此篇文章的尾部读下概要,就可以理解究竟发生了什么。

阅读全文»

Cocos2D-HTML5系列教程PART-1 配置开发环境

本教程由泰然教程组出品。翻译:benna,jesse;校对:Iven。转载请注明出处并通知泰然。


本教程涉及到Cocos2D-html开发环境的配置以及运行。如果你的环境已经可以使用就可以跳过这一步。这也将涉及到一个可选的web 服务配置。不管怎样,让我们先开始吧。

第一步你要做的事情是下载cocos2d-html5代码,从这里可以下载

阅读全文»

?>