cocos2dx-html5实现flappy bird

Home / C++ MrLee 2016-1-5 3195

博主原来是做Kjava(J2ME)游戏,就是早期那种MOTO,NOKIA,三星智能机。240*320。对于游戏还是相当的爱好。今天无意中看到一篇关于cocos2dx-html5的游戏开发文章,于是就收藏下来。后面学习下也供大家学习。学好了这个,做游戏真的是跨平台了。只要你有浏览器,你就能玩!当然,运行效率一定要非常不错才可以。不然用户体验比较槽。 我也是第一次使用cocos2d_html5,对js和html5也不熟,看引擎自带的例子和引擎源码,边学边做,如果使用过cocos2d-x的话,完成这个游戏还是十分简单的。游戏体验地址: http://zhoujianghai.github.io/games/flappybird/
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下添加
  • flappybird - Game

  •  
    刷新浏览器,会看到刚添加的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的内容,在浏览器中访问的就是它:
    
    
    
        
        Flappy Bird-codingnow.cn
        
        
        
        
        
        
    
    
    
        
    
    


    在这个文件中指定了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还是非常强大的,开发效率很高。
    项目源码:flappybird

    本文链接:https://www.it72.com/7446.htm

    推荐阅读
    最新回复 (0)
    返回