我也是第一次使用cocos2d_html5,对js和html5也不熟,看引擎自带的例子和引擎源码,边学边做,如果使用过cocos2d-x的话,完成这个游戏还是十分简单的。游戏体验地址:
1. 首先去cocos2d-x官网下载Cocos2d-html5-v2.2.2(目前最新版本)压缩包
2. 下载安装WampServer(http://www.wampserver.com/en/),后期在浏览器运行程序的时候,需要用到wampserver。WampServer是一款由法国人开发的Apache Web服务器、PHP解释器以及MySQL数据库的整合软件包,本身也不大,才30多M。我这里安装到e盘,安装到最后会出现选择explorer的提示,定位到WINDOWS目录下explorer.exe或者其他自己安装的浏览器目录下的explorer.exe文件。安装好后启动,在桌面的右下角会有一个绿色图标。
3. 解压Cocos2d-html5-v2.2.2到E:\wamp\www目录下,此时打开浏览器,输入localhost,就可以看到下面的界面了,点击cocos2d_html5,就可以看到项目列表了:
4. 复制cocos2d_html5目录下的HelloHTML5World例子,命名为flappybird,这是引擎自带的Hello World demo。在引擎目录下创建projects目录,然后把flappybird放到projects目录下。修改引擎根目录下的index.html文件,在MoonWarriors下添加
<li><a href="projects/flappybird/index.html">flappybird </a> <span> - Game</span></li>
刷新浏览器,会看到刚添加的flappybird了:
flappybird目录结构如下:
res目录存放的是图片等资源文件,src存放自定义的js源码,build.xml是打包文件,使用ant打包。cocos2d.js文件是配置一些属性,比如:是否显示fps,是否使用物理引擎,指定引擎目录,指定自定义的js文件等,类似于android的Android.mk文件。index.html是游戏显示的界面,在这里指定canvas尺寸。main.js里定义了Application,加载游戏的入口Scene,类似于c++的AppDelegate.cpp。
5. 开始编写游戏
src目录下有两个文件,myApp.js(游戏主要代码在这个文件里)和resource.js(定义游戏所使用的资源),由于这个游戏代码量比较少,就直接在这两个文件添加代码。
游戏有三个状态:READY、START、OVER。
READY表示游戏刚开始时显示logo,然后提示玩家点击开始游戏;
START表示游戏进行中;
OVER表示游戏结束,显示游戏结算界面。
游戏中出现的精灵有:小鸟、底部不停滚动的地面、背景图片、一直向左滚动的水管。其实小鸟在水平方向是一直不动的,只有水管和地面在滚动。
游戏中需要完成的主要功能点:小鸟自身的动画、点击屏幕时小鸟上升和下降的动画、小鸟死亡动画、地面滚动动画、水管滚动动画、添加水管、小鸟和地面及小鸟和水管的碰撞检测、游戏分数存储。

首先加载资源和添加游戏背景:

        this.winSize = cc.Director.getInstance().getWinSize();
        cc.SpriteFrameCache.getInstance().addSpriteFrames(res.flappy_packer);
        this.bgSprite = cc.Sprite.create(res.bg);
        this.bgSprite.setPosition(this.winSize.width / 2, this.winSize.height / 2);
        this.addChild(this.bgSprite, 0);

游戏资源使用TexturePacker打包在flappy_packer.plist文件中,函数名跟cocos2d-x c++版本是一样的。
初始化地面:

Helloworld.prototype.initGround = function() {
    //cc.log("initGround");
    this.groundSprite = cc.Sprite.create(res.ground);
    var halfGroundW = this.groundSprite.getContentSize().width;
    var halfGroundH = this.groundSprite.getContentSize().height;
    this.groundSprite.setAnchorPoint(0.5, 0.5);
    this.groundSprite.setPosition(halfGroundW / 2, halfGroundH / 2);
    this.addChild(this.groundSprite, GROUND_Z);
    var action1 = cc.MoveTo.create(0.5, cc.p(halfGroundW / 2 - 120, this.groundSprite.getPositionY()));
    var action2 = cc.MoveTo.create(0, cc.p(halfGroundW / 2, this.groundSprite.getPositionY()));
    var action = cc.Sequence.create(action1, action2);
    this.groundSprite.runAction(cc.RepeatForever.create(action));
};

js可以使用proptotype来为类型添加行为,不理解的可以google一下。当然也可以跟init函数一样写在里面,像这样:

var Helloworld = cc.Layer.extend({
init:function () {
},

initGround::function() {
}
);

这里为地面定义两个动作,因为地面图片宽度是840px,而游戏屏幕分辨率指定是720×1280,所以先让地面向左移动120px,再迅速回到原位置。

初始化小鸟动画:

Helloworld.prototype.initBird = function() {
    //cc.log("initBird");
    var animation = cc.AnimationCache.getInstance().getAnimation("FlyBirdAnimation")
    if(!animation) {
        var animFrames = [];
        var str = "";
        var birdFrameCount = 4;
        for (var i = 1; i < birdFrameCount; ++ i) {
            str = "bird" + i + ".png";
            var frame = cc.SpriteFrameCache.getInstance().getSpriteFrame(str);
            animFrames.push(frame);
        }
        var animation = cc.Animation.create(animFrames, 0.05);
        cc.AnimationCache.getInstance().addAnimation(animation, "FlyBirdAnimation");
    }

    this.flyBird = cc.Sprite.createWithSpriteFrameName(res.fly_bird);
    this.flyBird.setAnchorPoint(cc.p(0.5, 0.5));
    this.flyBird.setPosition(this.winSize.width / 2, this.winSize.height / 2);
    this.addChild(this.flyBird, BIRD_Z);
    var actionFrame = cc.Animate.create(animation);
    var flyAction = cc.RepeatForever.create(actionFrame);
    this.flyBird.runAction(cc.RepeatForever.create(flyAction));
};

小鸟自身动画是一个帧动画,创建成功后添加到缓存中。

初始化ready界面,就是游戏开始的时候提示用户点击的画面:

Helloworld.prototype.initReady = function() {
    this.readyLayer = cc.Layer.create();
    var logo = cc.Sprite.createWithSpriteFrameName(res.logo);
    logo.setAnchorPoint(cc.p(0.5, 0.5));
    logo.setPosition(this.winSize.width / 2, this.winSize.height - logo.getContentSize().height - 50);
    this.readyLayer.addChild(logo);

    var getReady = cc.Sprite.createWithSpriteFrameName(res.getReady);
    getReady.setAnchorPoint(cc.p(0.5, 0.5));
    getReady.setPosition(this.winSize.width / 2, this.winSize.height / 2 + getReady.getContentSize().height);
    this.readyLayer.addChild(getReady);

    var click = cc.Sprite.createWithSpriteFrameName(res.click);
    click.setAnchorPoint(cc.p(0.5, 0.5));
    click.setPosition(this.winSize.width / 2, getReady.getPositionY() - getReady.getContentSize().height / 2 - click.getContentSize().height / 2);
    this.readyLayer.addChild(click);

    this.addChild(this.readyLayer);
};

效果如下:


添加点击屏幕时小鸟上升和下降自由落体的动画:

Helloworld.prototype.runBirdAction = function () {
    var riseHeight = 50;
    var birdX = this.flyBird.getPositionX();
    var birdY = this.flyBird.getPositionY();
    var bottomY = this.groundSprite.getContentSize().height - this.flyBird.getContentSize().height / 2;

    var actionFrame = cc.Animate.create(cc.AnimationCache.getInstance().getAnimation("FlyBirdAnimation"));
    var flyAction = cc.RepeatForever.create(actionFrame);
//上升动画
    var riseMoveAction = cc.MoveTo.create(0.2, cc.p(birdX, birdY + riseHeight));
    var riseRotateAction = cc.RotateTo.create(0, -30);
    var riseAction = cc.Spawn.create(riseMoveAction, riseRotateAction);
//下落动画
//模拟自由落体运动
    var fallMoveAction = FreeFall.create(birdY - bottomY);
    var fallRotateAction =cc.RotateTo.create(0, 30);
    var fallAction = cc.Spawn.create(fallMoveAction, fallRotateAction);
    this.flyBird.stopAllActions();
    this.flyBird.runAction(flyAction);
    this.flyBird.runAction(cc.Spawn.create(
        cc.Sequence.create(riseAction, fallAction) )
    );
};

这里自定义了一个自由落体的Action:FreeFall:

var FreeFall = cc.ActionInterval.extend( {
     timeElasped:0,
     m_positionDeltaY:null,
     m_startPosition:null,
     m_targetPosition:null,

    ctor:function() {
        cc.ActionInterval.prototype.ctor.call(this);
        this.yOffsetElasped = 0;
        this.timeElasped = 0;
        this.m_positionDeltaY = 0;
        this.m_startPosition = cc.p(0, 0);
        this.m_targetPosition = cc.p(0, 0);
     },

    initWithDuration:function (duration) {
        if (cc.ActionInterval.prototype.initWithDuration.call(this, duration)) {
            return true;
        }
        return false;
    },

    initWithOffset:function(deltaPosition) {
        var dropTime = Math.sqrt(2.0*Math.abs(deltaPosition)/k_Acceleration) * 0.1;
        //cc.log("dropTime=" + dropTime);
        if (this.initWithDuration(dropTime))
        {
            this.m_positionDeltaY = deltaPosition;
            return true;
        }
         //cc.log("dropTime =" + dropTime + "; deltaPosition=" + deltaPosition);
        return false;
    },

    isDone:function() {
        if (this.m_targetPosition.y >= this._target.getPositionY()) {
            return true;
        }
        return false;
    },

    //Node的runAction函数会调用ActionManager的addAction函数,在ActionManager的addAction函数中会调用Action的startWithTarget,然后在Action类的startWithTarget函数中设置_target的值。
    startWithTarget:function(target) {
        //cc.log("startWithTarget target=" + target);
        cc.ActionInterval.prototype.startWithTarget.call(this, target);
        this.m_startPosition = target.getPosition();
        this.m_targetPosition = cc.p(this.m_startPosition.x, this.m_startPosition.y - this.m_positionDeltaY);
    },

    update:function(dt) {
        this.timeElasped += dt;
        //cc.log("isdone=" + this.timeElasped);
        if (this._target && !(this.m_targetPosition.y >= this._target.getPositionY())) {
            var yMoveOffset = 0.5 * k_Acceleration * this.timeElasped * this.timeElasped * 0.3;
            if (cc.ENABLE_STACKABLE_ACTIONS) {
                var newPos = cc.p(this.m_startPosition.x, this.m_startPosition.y - yMoveOffset);
                if (this.m_targetPosition.y > newPos.y) {
                    newPos.y = this.m_targetPosition.y;
                    this._target.stopAction(this);
                }

                this._target.setPosition(newPos);

            } else {
                this._target.setPosition(cc.p(this.m_startPosition.x, this.m_startPosition.y + this.m_positionDeltaY * dt));
            }
        }
    }

});

FreeFall.create = function(deltaPosition) {
        var ff = new FreeFall();
        ff.initWithOffset(deltaPosition);
        return ff;
    };

模仿了CCActionInterval.js中的其他内置的Action,如MoveBy,主要重写了initWithDuration,startWithTarget,update,isDone函数。
initWithDuration是设置该action运行的时间,时间的长短决定下降的速度。
startWithTarget函数由ActionManager调用,设置_target的值。
update函数在ActionInterval的step函数中会调用,在这个函数中不断更新精灵的坐标,使用了自由落体计算位移的公式。
isDone函数设置了动作是否运行结束。
重力加速度和action运行的时间需要不断调试。

添加水管:

function getRandom(maxSize) {
    return Math.floor(Math.random() * maxSize) % maxSize;
}

Helloworld.prototype.addPipe = function () { 
    cc.log("addPipe");
    var ccSpriteDown = cc.Sprite.createWithSpriteFrameName(res.holdback1);
    var pipeHeight = ccSpriteDown.getContentSize().height;
    var pipeWidth = ccSpriteDown.getContentSize().width;
    var groundHeight = this.groundSprite.getContentSize().height;
        //小鸟飞行区间高度
    var acrossHeight = 300;
    var downPipeHeight = 100 + getRandom(400);
   // cc.log("downPipeHeight=" + downPipeHeight);

    var upPipeHeight = this.winSize.height - downPipeHeight - acrossHeight - groundHeight;
    var PipeX = this.winSize.width + pipeWidth / 2;
    ccSpriteDown.setZOrder(1);
    ccSpriteDown.setAnchorPoint(cc.p(0.5, 0.5));
    ccSpriteDown.setPosition(cc.p(PipeX + pipeWidth / 2, groundHeight + pipeHeight / 2 - (pipeHeight - downPipeHeight)));

var ccSpriteUp = cc.Sprite.createWithSpriteFrameName(res.holdback2);
    ccSpriteUp.setZOrder(1);
    ccSpriteUp.setAnchorPoint(cc.p(0.5, 0.5));
    ccSpriteUp.setPosition(cc.p(PipeX + pipeWidth / 2, this.winSize.height + (pipeHeight- upPipeHeight) - pipeHeight / 2));

 this.addChild(ccSpriteDown, PIPE_Z);
    this.addChild(ccSpriteUp, PIPE_Z);

  this.PipeSpriteList.push(ccSpriteDown);
    this.PipeSpriteList.push(ccSpriteUp);

 this.score += 1;
};

分为上下两根水管,随机设置上下水管的高度,固定小鸟飞行区间的高度为300。然后把创建的水管放到数组中,同时每添加一排水管就增加一分。

添加碰撞检测函数:

Helloworld.prototype.getRect = function(a) {
     var pos = a.getPosition();
     var content = a.getContentSize();
     return cc.rect(pos.x - content.width / 2, pos.y - content.height / 2, content.width, content.height);
};

Helloworld.prototype.collide = function (a, b) {
    var aRect = this.getRect(a);
    var bRect = this.getRect(b);
    return cc.rectIntersectsRect(aRect, bRect);
};

Helloworld.prototype.checkCollision = function () {
    if (this.collide(this.flyBird, this.groundSprite)) {
        //cc.log("hit floor");
        this.birdFallAction();
        return;
    }
    for (var i = 0; i < this.PipeSpriteList.length; i++) {
        var pipe = this.PipeSpriteList[i];
        if (this.collide(this.flyBird, pipe)) {
            cc.log("hit pipe i=" + i);
            this.birdFallAction();
            break;
        }
    }
}

采用最简单的方式:判断矩形是否相交。把小鸟分别跟地面和数组中的水管进行检测,如果发生碰撞,则执行小鸟死亡动画:

Helloworld.prototype.birdFallAction = function () {
    this.gameMode = OVER;
    this.flyBird.stopAllActions();
    this.groundSprite.stopAllActions();
    var birdX = this.flyBird.getPositionX();
    var birdY = this.flyBird.getPositionY();

    var bottomY = this.groundSprite.getContentSize().height + this.flyBird.getContentSize().width / 2;
    var fallMoveAction = FreeFall.create(birdY - bottomY);
    var fallRotateAction =cc.RotateTo.create(0, 90);
    var fallAction = cc.Spawn.create(fallMoveAction, fallRotateAction);
    this.flyBird.runAction(cc.Sequence.create(cc.DelayTime.create(0.1),
        fallAction)
    );

    this.runAction(cc.Sequence.create(cc.DelayTime.create(1.0),
        cc.CallFunc.create(this.showGameOver, this))
    );
}

让小鸟旋转90度,然后垂直下落,然后显示game over画面:

Helloworld.prototype.showGameOver = function () {
    var userDefault = cc.UserDefault.getInstance();
    var oldScore = userDefault.getIntegerForKey("score");
    var maxScore = 0;
    if(this.score > oldScore) {
        maxScore = this.score;
        userDefault.setIntegerForKey("score", maxScore);
    }else {
        maxScore = oldScore;
    }

    var gameOverLayer = cc.Layer.create();
    cc.log("gameover=" + res.gameover);
    var gameOver = cc.Sprite.createWithSpriteFrameName(res.gameover);
    gameOver.setAnchorPoint(cc.p(0.5, 0.5));
    gameOver.setPosition(this.winSize.width / 2, this.winSize.height - gameOver.getContentSize().height / 2 - 150);
    gameOverLayer.addChild(gameOver);

    var scorePanel = cc.Sprite.createWithSpriteFrameName(res.scorePanel);
    scorePanel.setAnchorPoint(cc.p(0.5, 0.5));
    scorePanel.setPosition(gameOver.getPositionX(), gameOver.getPositionY() - gameOver.getContentSize().height / 2 - scorePanel.getContentSize().height / 2 - 60);
    gameOverLayer.addChild(scorePanel);

    if(this.score > oldScore) {
        var gold = cc.Sprite.createWithSpriteFrameName(res.gold);
        gold.setAnchorPoint(cc.p(0.5, 0.5));
        gold.setPosition(68 + gold.getContentSize().width / 2, 72 + gold.getContentSize().height / 2);
        scorePanel.addChild(gold);
    }else {
        var gray = cc.Sprite.createWithSpriteFrameName(res.gray);
        gray.setAnchorPoint(cc.p(0.5, 0.5));
        gray.setPosition(68 + gray.getContentSize().width / 2, 72 + gray.getContentSize().height / 2);
        scorePanel.addChild(gray);
    }

    var newScoreLabel = cc.LabelAtlas.create(this.score, res.number, 22, 28, '0');
    newScoreLabel.setAnchorPoint(cc.p(0.5, 0.5));
    newScoreLabel.setScale(1.2);
    newScoreLabel.setPosition(scorePanel.getContentSize().width - newScoreLabel.getContentSize().width - 90, newScoreLabel.getContentSize().height / 2 + 180);
    scorePanel.addChild(newScoreLabel);

    var maxScoreLabel = cc.LabelAtlas.create(maxScore, res.number, 22, 28, '0');
    maxScoreLabel.setAnchorPoint(cc.p(0.5, 0.5));
    maxScoreLabel.setScale(1.2);
    maxScoreLabel.setPosition(newScoreLabel.getPositionX(), maxScoreLabel.getContentSize().height / 2 + 75);
    scorePanel.addChild(maxScoreLabel);

    var start = cc.Sprite.createWithSpriteFrameName(res.start);
    var startMenuItem = cc.MenuItemSprite.create(start, null, null, this.restartGame, this);
    var startMenu = cc.Menu.create(startMenuItem);
    startMenu.setAnchorPoint(cc.p(0.5, 0.5));
    startMenu.setPosition(this.winSize.width / 2 , scorePanel.getPositionY() - scorePanel.getContentSize().height / 2 - start.getContentSize().height / 2 - 60);
    gameOverLayer.addChild(startMenu);

    this.addChild(gameOverLayer, GAMEOVER_Z);
};

显示game over时保存游戏数据,显示这局游戏的分数和历史最高分。


点击开始游戏按钮,就可以重新开始游戏:

Helloworld.prototype.restartGame = function() {
    var scene = cc.Scene.create();
    scene.addChild(Helloworld.create());
    cc.Director.getInstance().replaceScene(cc.TransitionFade.create(1.2, scene));
};

记得在init函数中清空水管数组:

 this.PipeSpriteList = [];

下面是Helloworld类的代码:

var Helloworld = cc.Layer.extend({
    gameMode:null,
    bgSprite:null,
    groundSprite:null,
    flyBird:null,
    PipeSpriteList:[],
    passTime: 0,
    winSize: 0,
    screenRect:null,
    readyLayer:null,
    score: 0,
    scoreLabel:null,

    init:function () {
        cc.log("helloworld init");
        this._super();
        this.PipeSpriteList = [];
        this.winSize = cc.Director.getInstance().getWinSize();
        cc.SpriteFrameCache.getInstance().addSpriteFrames(res.flappy_packer);
        this.bgSprite = cc.Sprite.create(res.bg);
        this.bgSprite.setPosition(this.winSize.width / 2, this.winSize.height / 2);
        this.addChild(this.bgSprite, 0);
        this.initGround();
        this.initReady();
        this.screenRect = cc.rect(0, 0, this.winSize.width, this.winSize.height);
        this.gameMode = READY;
        this.score = 0;
        this.scheduleUpdate();
        this.setTouchEnabled(true);
        return true;
    },

    onTouchesBegan:function (touches, event) {
    },

    onTouchesMoved:function (touches, event) {
    },

    onTouchesEnded:function (touches, event) {
        if (this.gameMode == OVER) {
            return;
        }
        if (this.gameMode == READY) {
            this.gameMode = START;
            this.readyLayer.setVisible(false);
            this.initBird();;
        }
        this.runBirdAction();
    },

    onTouchesCancelled:function (touches, event) {
    },

    update:function(dt) {
        if (this.gameMode != START) {
            return;
        }
        for(var i = 0; i < this.PipeSpriteList.length; ++ i) {
            var pipe = this.PipeSpriteList[i];
            pipe.setPositionX(pipe.getPositionX() - 3);
            if (pipe.getPositionX() < -pipe.getContentSize().width / 2) {
                this.PipeSpriteList.splice(i, 1);
                //cc.log("delete pipe i=" + i);
            }
        }
        this.passTime += 1;
        if(this.passTime >= this.winSize.width / 6) {
            this.addPipe();
            this.passTime = 0;
        }
        this.checkCollision();
    }
});

在update函数中更新水管的位置,如果水管出了左边的屏幕就从数组中移除,每经过一定的时间就添加一排水管。
现在看看index.html的内容,在浏览器中访问的就是它:

<!DOCTYPE HTML>
<html>
<head>
    <meta charset="utf-8">
    <title>Flappy Bird-codingnow.cn</title>
    <link rel="icon" type="image/png" href="http://codingnow.cn/favicon.ico">
    <meta name="viewport" content="user-scalable=no"/>
    <meta name="screen-orientation" content="portrait"/>
    <meta name="apple-mobile-web-app-capable" content="yes"/>
    <meta name="full-screen" content="yes"/>
    <meta name="x5-fullscreen" content="true"/>
    <style>
        body, canvas, div {
            -moz-user-select: none;
            -webkit-user-select: none;
            -ms-user-select: none;
            -khtml-user-select: none;
            -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
        }
    </style>
</head>
<body style="padding:0; margin: 0;text-align: center;background: #f2f6f8;">
    <canvas id="gameCanvas" width="720" height="1280"></canvas>
<script src="cocos2d.js"></script>
<img style="position:absolute;left:-9999px" src="http://zhoujianghai.github.io/games/flappybird/res/icon_wechat.png" onerror="this.parentNode.removeChild(this)">
</body>
</html>

在这个文件中指定了canvas的尺寸。html的内容从cocos2d-html5自带的例子中copy过来的,这里在底部添加了一个img,这个图片是分享到微信朋友圈时显示在左边的图片。
当浏览器窗口大小改变时,为了能自动调整显示游戏完整画面,需要在main.js的applicationDidFinishLaunching函数中添加:

cc.EGLView.getInstance().adjustViewPort(true);
cc.EGLView.getInstance().setDesignResolutionSize(720, 1280, cc.RESOLUTION_POLICY.SHOW_ALL);
cc.EGLView.getInstance().resizeWithBrowserSize(true);

还记得那个build.xml文件么,可以使用ant打包工具,把src目录下的js代码跟引擎代码打包成一个myApp-HelloWorld.js文件,这样我们只需要把res资源、cocos2d.js、index.html、myApp-HelloWorld.js放到网站上就可以了,也就更安全了。切换到项目build.xml所在目录,在命令符窗口执行:ant。很快就会生成myApp-HelloWorld.js文件,然后还需要修改cocos2d.js的window.addEventListener函数,修改s.src的值为myApp-HelloWorld.js,cocos2d.js文件里有详细注释的。

ok,flappy bird游戏的主要代码就完成了,感觉cocos2d-html5还是非常强大的,开发效率很高。
现在还只能在本地运行游戏,为了能在外网访问,可以把项目传到github上,创建github pages,可以参考:http://pages.github.com/

下面奉上该游戏的源码:
http://download.csdn.net/detail/zhoujianghai/7122001

cocos2d-x手游性能优化总结

近段时间在使用cocos2d-x开发2D手游,技术方案使用的是cocos2d-x+lua,因为游戏使用的是cocos2d-x 2.1.5版本,有些优化方案在最新版的cocos2d-x版本已经实现...

阅读全文

【cocos2d-x开发实战 特训99-终结篇】移植到android平台和添加admob广告

上一篇已经完成特性99在win32平台下的开发,现在把它移植到android上,首先修改Android.mk文件,内容如下: LOCAL_PATH := $(call my-dir) include $(CLEAR_...

阅读全文

【cocos2d-x开发实战 特训99-part6】完成游戏首页

现在特性99游戏的功能基本完成了,但是游戏没有首页也挺奇怪,所以这篇博客为游戏添加首页,通过首页进入游戏。 首页的元素比较简单,就是一张背景图,一个l...

阅读全文

1 条评论

欢迎留言