cocos creator实例--CocosCreator一步一步实现重力球游戏

  • 时间:
  • 来源:互联网

『 游戏玩法 』

  通过手机陀螺仪,调整手机,让球从上一层的间隔中落到下一层,楼层会不断上涨,如果球碰到上方或者下方的火焰,游戏结束。

 

『 游戏预览 』

 

『 开发工具 』

  1. CocosCreator 2.1.2

  2. VisualStudio Code

 

『 参考API 』

  1. PhysicsManager

  2. SystemEvent

  3. View

  4. action

  5. audioEngine

  6. ParticleSystem

 

『 游戏模块 』

  1. 游戏层

  2. 物理场景层

  3. 结算层

 

『 开发流程 』

  1. 创建项目

  选择一个HelloWorld项目,设置保存路径,点击新建项目。

2. 创建游戏场景

  我们直接将新建的helloworld场景改名为我们的游戏场景gameScene。

 

 

  3. 设计分辨率和初始场景

  游戏有横屏也有竖屏,我们在开发一款游戏时,需要先确定好设计方向,选择"项目"=>"项目设置"=>"项目预览",修改设计分辨率为:宽720,高1280,勾选上适配屏幕宽度。修改初始化预览场景为上一步创建的gameScene.fire场景。(初始化预览场景是在我们运行这个项目时,默认显示的第一个场景,如果场景比较多时,设置初始场景可以更方便的展示我们想看的界面。)设置完成后点击保存。

  4. 搭建游戏界面

  我们先将项目的目录结构做一下调整,新增一个resources文件夹,将Texture文件夹放到它的下面,并在resources文件夹下创建一个sound文件夹,用来存放游戏音效(后面用到)。

Texture文件夹:用来存放游戏图片。

sound文件夹:用来存放游戏音效。

接着将背景图放到Texture文件夹中,如下图:

打开CocosCreator,设置gameScene的Canvas属性,删除下方绑定的脚本组件和名为Label和cocos的组件,如下图:

设置background属性,删除名为Label和cocos的组件,如下图:

到这一步,我们就可以看到效果了,用浏览器运行,如下图:

 

  运行出来后,是不是发现界面太大,要滑动滑条才能显示完整,这样开发起来会比较麻烦,我们要想个办法让他变小点:

    1. 将canvas的DesignResolution改成360*640;

    2. 将background 的Scale缩放属性X,Y都改成0.5。

再次浏览器运行,就完整的显示出来了,这个设置在游戏开发完后我们需要还原,切记。

  5. 创建游戏脚本

  我们就直接将HelloWord.js改个名字,改名为gameLayer.js,删除多余代码,只保留基础框架,如下图:

 

  6. 绑定脚本

  现在界面和脚本都创建好了,我们再将他们关联起来,选中gameScene场景的Canvas组件,将gameLayer.js拖到右边属性栏中,如下图:

这样就将脚本和场景关联了。

 

  7. 绑定属性

  通过在组件脚本中声明属性,我们可以将脚本组件中的字段可视化地展示在属性检查器中,从而方便地在场景中调整属性值。

要声明属性,仅需要在 cc.Class 定义的properties字段中,填写属性名字和属性参数即可,如下图:

这时候,我们可以在属性检查器中看到上面定义的这个属性,如下图:

 

  再将background组件拖到定义的这个属性上,就将background组件和脚本中定义的bg属性关联上了,在代码中用的this.bg就是background组件,后面所有的组件绑定同上方式,如下图:

 

  8. 创建预制

  在assets下创建一个Prefab文件夹,再创建一个预制体,直接在gameScene中新建一个节点,将节点拖到Prefab文件夹下,就是一个预制体了。我们把名字改成ball,如下图:

截止这一步,基础方法已经了解了,接着我们再来实现游戏功能。

 

  9. UI和脚本绑定

  先搭建游戏ui界面,在脚本中声明属性,并将属性和ui上的组件绑定起来,如下图:

10. 重力传感系统设置

  10.1 开启重力传感系统

cc.systemEvent.setAccelerometerEnabled(true);cc.systemEvent.on(cc.SystemEvent.EventType.DEVICEMOTION,this.onDeviceMotionEvent, this);

 

  10.2 关闭重力传感系统

cc.systemEvent.off(cc.SystemEvent.EventType.DEVICEMOTION,this.onDeviceMotionEvent, this);

 

  11. 物理系统设置

  11.1 开启物理系统:

cc.director.getPhysicsManager().enabled= true;

  11.2 开启物理调试状态:

cc.director.getPhysicsManager().debugDrawFlags= true;

  11.3 配置重力加速度:

cc.director.getPhysicsManager().gravity= cc.v2(0, -1000); //cc.v2(x方向的重力,y方向的重力)

  

12. 小球设置

  接着我们创建一个小球,给小球绑定一个刚体,它就会沿着重力加速度方向做自由落体运动了:

 

  现在运行项目,就可以看到一个小球垂直落下啦。

13. 小球挡板数值

  板子的宽度,板与板之间的间隔,板子的坐标,都需要随机获取。所以,我们先预定义一些区间范围的数据,在创建的时候用到

14.  先创建出一个挡板

  挡板的预制体上已经绑定了多边形的物理组件,(详情请看cocosCreator编辑器中board.prefab的board节点上绑定的组件属性)在设置完挡板的大小等一系列属性后,需要调用apply()方法,这个方法可以刷新挡板身上绑定的多边形组件的大小。

 

 

  15. 挡板设计思路

  接着再创建一行挡板,一行挡板的计算比较多,详细代码请参考gameLayer.js的第155~202行,设计思路如下:

先随机计算一个挡板的宽度,再判断是不是这行挡板的第一个挡板,如果是,则随机获取一个X坐标,否则通过上一个挡板的坐标和宽度计算当前挡板的坐标,直到不需要再创建为止。

16. 挡板初始化

  游戏未开始时,我们需要在界面上创建多行挡板:

17. 挡板移动控制

  游戏过程中,挡板向上运动,通过定时器无限循环调用移动函数,并判断如果挡板超出可视范围,就创建一行新的:

18. 挡板难度控制

  为了增加游戏难度,我们再实现一个升级功能,每隔10s加快挡板的运行速度,但也有上限,配置如下:

  BOARD_COLOR是配置不同等级下挡板显示的颜色。

19. 游戏升级

  升级功能比较容易,通过判断时间戳的差值可以得出间隔时长,注意时间戳的单位是毫秒。我们用到的挡板的图片颜色是白色的,在游戏中通过自己设置图片颜色,可以变换成任意我们想要的颜色,设置方法如下:

  难点是如何做成渐变色,通过初始颜色和结束颜色的RGB值,计算出一系列的中间值,再用runAction方法执行颜色变化动作就可以了,详细代码请参考computeGRB()和computeUpgrade()这两个函数。

20. 游戏结束判断

  当小球碰撞了上下燃烧的火焰时,判定游戏结束,这时需要进行碰撞监听,我们创建一个contact.js脚本,绑定在火焰刚体和小球刚体上,并将这两个刚体开启碰撞监听,将tag值设置为0(我们将除这两个以外的其他刚体的tag都设置为1)如图:

然后在脚本中实现碰撞监听函数:

通过判断碰撞物体身上的tag值来决定是否需要处理碰撞回调。

 

  21. 游戏得分记录

  当判定游戏结束后,我们展示游戏结算界面,并本地保存得分,本地存储方式如下:

  设置本地存储:

cc.sys.localStorage.setItem("ballBestScore", this.curScore);

  获取本地存储:

let ballBestScore = parseInt(cc.sys.localStorage.getItem("ballBestScore") || 0);

  基本流程就已经完成了,最后再加音效,算得分,就是一个完成的游戏啦!

//--------------contact.js----------------------

//碰撞监听脚本
cc.Class({
    extends: cc.Component,
    properties: {

    },
    onLoad () {

    },

    onDestroy () {

    },

    onBeginContact ( contact, selfCollider, otherCollider){
        if(selfCollider.tag == 0 && otherCollider.tag == 0){
            cc.log("onBeginContact...");  //碰撞开始
            this.gameOver();
        }
    },
  onEndContact (contact, selfCollider, otherCollider){
        //cc.log("onEndContact...");//碰撞结束 
    },
  onPreSolve(contact, selfCollider, otherCollider){
        //cc.log("onPreSolve...");//碰撞持续,接触时被调用
    },
  onPostSolve (contact, selfCollider, otherCollider){
        //cc.log("onPostSolve...");//碰撞接触更新完后调用,可以获得冲量信息
    },

    //游戏结束
    gameOver (){
        if(this.callBack){
            this.callBack();
        }
    },

    gameOverCallBack (callBack){
        this.callBack = callBack;
    },

    //隐藏动作
    hideBall (){
        this.node.runAction(cc.fadeOut(1.0));
    },

    //显示动作
    showBall(){
        this.node.opacity = 0;
        this.node.runAction(cc.fadeIn(0.5));
    }
});



















//--------------gameLayer.js----------------------

let DESIGN_WIDTH = 720;            //设计分辨率宽
let DESIGN_HEIGHT = 1280;          //设计分辨率高
let BOARD_DEFAULT_WIDTH = 98;    //挡板初始宽度
// let BOARD_BINGBOX_POS = [(-21, 15), (21, 15), (29, 10), (32, 4), 
//                         (32, -4), (29, -10), (21, -15), (-21, -15), 
//                         (-29, -10), (-32, -4), (-32, 4), (-20, 10)];

let BOARD_INTERVAL_MIN = 30;   //两个挡板之前的最小间距
let BOARD_INTERVAL_MAX = 70;   //两个挡板之前的最大间距

let HEIGHT_INTERVAL = 130;      //两挡板之间高度间隔

let BOARD_WIDTH_MIN = [198, 150, 98, 98, 98, 98, 98];       //挡板最小宽度 (7个等级)
let BOARD_WIDTH_MAX = [498, 450, 398, 350, 350, 300, 300];    //挡板最大宽度 (7个等级)

let BOARD_SPEED = [1, 1.5, 2, 2.5, 3, 4, 5];             //挡板运行速度 (7个等级)

let BOARD_COLOR = [cc.Color.GREEN, cc.Color.CYAN, cc.Color.YELLOW, cc.Color.ORANGE, cc.Color.MAGENTA, cc.Color.RED, cc.Color.GRAY];

//音效名称
let sound = {
    BG : "sound/background",      //背景
    DIE : "sound/buzz",           //死亡音效
    GAMEWIN : "sound/get_item",   //过关音效
    GAMEOVER : "sound/pass",   //游戏结束
}

cc.Class({
    extends: cc.Component,

    properties: {
        bg : cc.Node,
        gameBgImg : cc.Node,
        physicsLayer : cc.Node,
        boardLayer : cc.Node,
        upgradeImg : cc.Node,
        gameScoreText : cc.Label,
        fireEffect0 : cc.Node,
        fireEffect1 : cc.Node,
        fireBody0 : cc.Node,
        fireBody1 : cc.Node,

        //游戏结束界面
        bgSp : cc.Node,
        gameOverLayer : cc.Node,
        scoreTxt : cc.Node,
        bestScoreTxt : cc.Node,
        bestScoreImg : cc.Node,
        newRecordImg : cc.Node,
        
        ballAtlas : cc.SpriteAtlas,
        ballPrefab : cc.Prefab,
        boardPrefab : cc.Prefab,
    },

    onLoad () {
        this.allBoards = [];  //所有挡板
        this.curLevel = 0;    //记录当前等级
        this.curColorIdx = 0; //记录当前颜色索引
        this.curTimeStamp = new Date().getTime(); //获取当前时间戳
        this.curScore = 0;    //记录当前得分
        this.isGameOver = false; //是否有戏结束

        this.playMusic(sound.BG);

        //打开重力传感系统
        this.openDeviceMotion();

        //打开物理系统
        cc.director.getPhysicsManager().enabled = true;
        //cc.director.getPhysicsManager().debugDrawFlags = true;
        
        // 重力加速度的配置
        cc.director.getPhysicsManager().gravity = cc.v2(0, -1000);

        //创建多行挡板
        let curH = 0;
        while(curH >= (-DESIGN_HEIGHT / 2 - HEIGHT_INTERVAL)){
            this.createALineBoard(curH);
            curH -= HEIGHT_INTERVAL;
        }

        //创建小球
        this.createBall();

        //适配
        this.fitNode(this.bg);
        this.fitNode(this.gameBgImg);
        this.fitNode(this.bgSp);

        //上下火粒子动画适配
        let h = this.gameBgImg.height;
        this.fireEffect0.position = cc.v2(0, -h / 2);
        this.fireEffect1.position = cc.v2(0, h / 2);
        this.fireBody0.position = cc.v2(0, -h / 2);
        this.fireBody1.position = cc.v2(0, h / 2);

    },

    //创建小球
    createBall (){
        if(this.ballImg == null){
            this.ballImg = cc.instantiate(this.ballPrefab);
            this.ballImg.parent = this.physicsLayer;
        }
        this.ballImg.position = cc.v2(200, 300);
        let ballNode = this.ballImg.getComponent("contact");
        ballNode.gameOverCallBack(() => {
            if(!this.isGameOver){
                cc.log("game over...gameLayer");
                this.playSound(sound.DIE);
                //停止所有动作
                this.isGameOver = true;
                this.closeDeviceMotion();
                //小球逐渐隐藏
                ballNode.hideBall();
                this.gameBgImg.runAction(cc.sequence(cc.delayTime(0.5), cc.callFunc(()=>{
                    this.showGameOverLayer();
                })));
            }
        });
        
        //小球显示
        ballNode.showBall();

        let rand = Math.floor(Math.random() * 9) + 1;
        ballNode.getComponent(cc.Sprite).spriteFrame = this.ballAtlas.getSpriteFrame("ball_" + rand);
    },

    // 适配结点
    fitNode: function (obj) {
        let canvasSize = cc.view.getCanvasSize();
        let canvasScale = canvasSize.width / canvasSize.height;
        let designScale = DESIGN_WIDTH / DESIGN_HEIGHT;
        obj.height = DESIGN_HEIGHT * (designScale / canvasScale);
    },

    onDestroy () {
        this.closeDeviceMotion();
    },

    openDeviceMotion(){
        cc.systemEvent.setAccelerometerEnabled(true);
        cc.systemEvent.on(cc.SystemEvent.EventType.DEVICEMOTION, this.onDeviceMotionEvent, this);
    },

    closeDeviceMotion(){
        cc.systemEvent.off(cc.SystemEvent.EventType.DEVICEMOTION, this.onDeviceMotionEvent, this);
    },

    onDeviceMotionEvent (event) {
        if(this.ballImg){
            this.ballImg.getComponent(cc.RigidBody).applyForceToCenter(cc.v2(event.acc.x * 400000 , 0));
        }
    },

    //创建一排挡板
    createALineBoard : function(posH){
        let isNeed = true;
        let boards = [];
        while(isNeed){
            //随机一个宽度
            let randomW = Math.random() * (BOARD_WIDTH_MAX[this.curLevel] - BOARD_WIDTH_MIN[this.curLevel]) + BOARD_WIDTH_MIN[this.curLevel];  

            let posX = 0;
            if(boards.length == 0){//第一个挡板
                //随机一个坐标
                let minPosX = -DESIGN_WIDTH / 2 - randomW / 2;
                let maxPosX = -DESIGN_WIDTH / 2 + randomW / 2;
                posX = Math.random() * (maxPosX - minPosX) + minPosX;

                let board = {};
                board.w = randomW;
                board.x = posX;
                boards.push(board);
                this.createBoard(cc.v2(posX, posH), board.w);
            } 
            else{//不是第一个,根据前一个挡板的坐标进行计算位置
                let lastBoard = boards[boards.length - 1];
                let randomInterval = Math.random() * (BOARD_INTERVAL_MAX - BOARD_INTERVAL_MIN) + BOARD_INTERVAL_MIN;
                posX = lastBoard.w / 2 + randomW / 2 + randomInterval + lastBoard.x;

                //校验这个挡板坐标是否会影响下一个挡板的创建
                //计算这个挡板到右边距的距离
                let rightDis = DESIGN_WIDTH / 2 - posX - randomW / 2;
                if((rightDis < BOARD_INTERVAL_MIN) && (rightDis > 0)){  //如果到右边距离小于最小间距且大于0,则当前挡板大小不合适,需要重新创建
                    isNeed = true;
                }
                else {
                    let board = {};
                    board.w = randomW;
                    board.x = posX;
                    boards.push(board);
                    this.createBoard(cc.v2(board.x, posH), board.w);
                    if(rightDis > BOARD_INTERVAL_MAX){//如果到右边距离大于最大间距,则还需要继续创建
                        isNeed = true;
                    }
                    else {
                        isNeed = false;
                    }
                }
            }
        }
        return boards;
    },

    //创建挡板
    createBoard : function(pos, width){
        let board = cc.instantiate(this.boardPrefab);
        board.parent = this.boardLayer;
        board.position = pos;
        board.width = width;
        board.color = BOARD_COLOR[this.curColorIdx];

        let boxP = board.getComponent(cc.PhysicsPolygonCollider);
        let points = boxP.points;
        for(let i = 0; i < points.length; i++){
            let pos = points[i];
            if(pos.x > 0){  //大于0的向右扩展
                board.getComponent(cc.PhysicsPolygonCollider).points[i] = cc.v2(pos.x + (width - BOARD_DEFAULT_WIDTH) / 2, pos.y);
            }
            else{
                board.getComponent(cc.PhysicsPolygonCollider).points[i] = cc.v2(pos.x - (width - BOARD_DEFAULT_WIDTH) / 2, pos.y);
            }
        }
        boxP.apply();
        this.allBoards.push(board);
        return board;
    },

    //移动挡板
    moveBoard : function(){
        for(let i = 0; i < this.allBoards.length; i++){
            let board = this.allBoards[i];
            let curPosY = board.position.y;

            //移动挡板
            board.position = cc.v2(board.position.x, curPosY + BOARD_SPEED[this.curLevel]);
            //判断挡板是否已经完全移除屏幕
            if(board.position.y > (DESIGN_HEIGHT / 2 + 150)){
                this.allBoards.splice(i, 1);
                board.removeFromParent();
            }
        }
    },

    //计算变化后的RGB值
    computeGRB : function(oldValue, newValue, tmpValue){
        if(tmpValue != newValue){
            if(newValue > oldValue){
                tmpValue += 8;
                if(tmpValue > newValue){
                    tmpValue = newValue;
                }
            }
            else{
                tmpValue -= 8;
                if(tmpValue < newValue){
                    tmpValue = newValue;
                }
            }   
        }
        return tmpValue;
    },

    //计算升级
    computeUpgrade : function(){
        //获取时间戳,判断是否需需要升级
        let timeStamp = new Date().getTime();
        if((timeStamp - this.curTimeStamp) >= 10000){ //每10s调整一次难度
            if(this.curLevel < 6){
                //显示升级提示图片
                this.upgradeImg.active = true;
                this.upgradeImg.opacity = 255;
                this.upgradeImg.runAction(cc.sequence(cc.delayTime(1.0), cc.blink(1, 5), cc.fadeOut(1, 0)));
        
                this.curLevel += 1;
            }
            this.curTimeStamp = timeStamp;

            //保存现在的颜色
            let oldColor = BOARD_COLOR[this.curColorIdx];
            let tmpRedValue = oldColor.getR();
            let tmpGreenValue = oldColor.getG();
            let tmpBlueValue = oldColor.getB();

            this.curColorIdx = (this.curColorIdx + 1) % 7;
            //更换所有挡板颜色
            let array = [];
            for(let num = 0; num < 32; num++){ //32是最多变化次数  255除以8向上取整所得
                array.push(cc.callFunc(() =>{
                    tmpRedValue = this.computeGRB(oldColor.getR(), BOARD_COLOR[this.curColorIdx].getR(), tmpRedValue);
                    tmpGreenValue = this.computeGRB(oldColor.getG(), BOARD_COLOR[this.curColorIdx].getG(), tmpGreenValue);
                    tmpBlueValue = this.computeGRB(oldColor.getB(), BOARD_COLOR[this.curColorIdx].getB(), tmpBlueValue);
                    
                    for(let i = 0; i < this.allBoards.length; i++){
                        let board = this.allBoards[i];
                        board.color = cc.color(tmpRedValue, tmpGreenValue, tmpBlueValue, 255);
                    }
                }));
                array.push(cc.delayTime(0.05));
            }
            this.physicsLayer.runAction(cc.sequence(array));
        }
    },

    //计算得分
    computeSocre : function(){
        this.gameScoreText.string = this.curScore;
    },

    //显示结算界面
    showGameOverLayer (){
        this.playSound(sound.GAMEOVER);
        this.gameOverLayer.active = true;
        this.gameOverLayer.position = cc.v2(0, 2000);
        this.gameOverLayer.runAction(cc.moveTo(0.8, cc.v2(0, 0)));

        //当前得分
        this.scoreTxt.getComponent(cc.Label).string = this.curScore;

        //最高分
        let ballBestScore = parseInt(cc.sys.localStorage.getItem("ballBestScore") || 0);
        this.bestScoreTxt.getComponent(cc.Label).string = ballBestScore;
        if(ballBestScore >= this.curScore){
            this.bestScoreTxt.active = true;
            this.bestScoreImg.active= true;
            this.newRecordImg.active = false;
        }
        else{
            this.bestScoreTxt.active = false;
            this.bestScoreImg.active= false;
            this.newRecordImg.active = true;
            cc.sys.localStorage.setItem("ballBestScore", this.curScore);
        }
    },

    //再来一局按钮回调
    againBtnCallBack(){
        //清理上局的挡板
        this.boardLayer.removeAllChildren();
        //刷新界面,重新开始游戏
        this.gameOverLayer.runAction(cc.sequence(cc.moveTo(0.3, cc.v2(1500, 0)), cc.callFunc(()=>{
            //重置得分
            this.curScore = 0;
            this.computeSocre();
            this.isGameOver = false;
            this.curLevel = 0;
            this.curTimeStamp = new Date().getTime();
            this.curColorIdx = 0;
            //重置小球
            this.createBall();

            //创建多行挡板
            let curH = 0;
            while(curH >= (-DESIGN_HEIGHT / 2 - HEIGHT_INTERVAL)){
                this.createALineBoard(curH);
                curH -= HEIGHT_INTERVAL;
            }

            //开启传感系统
            this.openDeviceMotion();
        })));
    },

    //播放音乐
    playMusic : function(name){
        cc.loader.loadRes(name, cc.AudioClip, function (err, clip) {
            var audioID = cc.audioEngine.playEffect(clip, true);
        });
    },

    //播放音效
    playSound : function(name){
        cc.loader.loadRes(name, cc.AudioClip, function (err, clip) {
            var audioID = cc.audioEngine.playEffect(clip, false);
        });
    },

    update: function (dt) {
        if(!this.isGameOver){
            this.computeUpgrade();
            this.moveBoard();
    
            //判断是否要新建一行挡板
            let allCount = this.allBoards.length;
            if((allCount > 0) && this.allBoards[allCount - 1]){
                let pos = this.allBoards[allCount - 1].position;
                if(pos.y >= -DESIGN_HEIGHT / 2){
                    this.createALineBoard(pos.y - HEIGHT_INTERVAL);
                    this.curScore += 1;
                    this.computeSocre();
                    this.playSound(sound.GAMEWIN);
                }
            }
        }
    },
});






 

 


感谢:

本文参考自https://mp.weixin.qq.com/s/Y6vYG1gV7J6ecD38lp0RfA这篇文章,这里感谢原作者对于技术的分享。

下载:

本文章源码和资源下载地址

^随风~~
发布了300 篇原创文章 · 获赞 12 · 访问量 1万+
私信 关注

本文链接http://element-ui.cn/news/show-1342.aspx