02 - FlappyBird开发Day2

今天继续开发Flappy Bird游戏,同时进一步熟悉Cocos2D引擎常用操作。

今天的工作内容是:

  • 小鸟往上/下飞时的旋转;
  • 添加随机生成的管道,并用缓动系统控制平移;
  • 添加碰撞系统;
  • 添加计分功能;
  • 添加游戏结束界面(Over);

小鸟的旋转

为了让飞翔的小鸟看起来更正常,需要根据速度来调整它的“飞行姿态”。往上飞时,把在[-150, 0]内的速度映射到[-90, 0]的旋转角度中;往下飞时,把在[0, 360]内的速度映射到[0, 170]的旋转角度中。

这种映射操作需要用到cc.clamp()函数,但我用它会提示未定义(有代码补全),很是迷惑。于是在网上搜索了手动实现:

// Bird.js
// cc.clamp()未定义, 只能自己在网上整一个了
mapNumRange: function (num, inMin, inMax, outMin, outMax) {
    return ((num - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin;
}

update(dt) {
    // ...
    // 随速度更新角度
    let angle = 0;
    if (this.speed <= 0) {
        angle = this.mapNumRange(this.speed, -150, 0, -90, 0);
    } else {
        angle = this.mapNumRange(this.speed, 0, 360, 0, 170);
    }
    this.node.angle = -angle;
}

添加管道

设计单个管道

新建一个名称为pipe的空节点,并添加两个精灵控件,纹理分别为管道的上半部分和下半部分。为了方便调整两个管道的位置,将朝下的管道锚点设置为(0.5, 0),将朝上的管道锚点设置为(0.5, 1)

接下来为pipe节点编写脚本,让它生成时具有随机间距和高度,并编写move()方法,利用Cocos2D引擎的缓动系统cc.tween让它从生成的地方往左运动,运动到屏幕范围外时销毁自身以节省内存:

// Pipe.js
cc.Class({
    extends: cc.Component,

    properties: {
        pipeUp: cc.Node,
        pipeDown: cc.Node,
        maxDist: 0,
        minDist: 0,
        maxHeight: 0,
        minHeight: 0,
    },

    onLoad() {
        // 随机钢管间距
        let dist = Math.random() * (this.maxDist - this.minDist) + this.minDist;
        this.pipeUp.y += dist;
        this.pipeDown.y -= dist;

        // 随机间距高度
        let height = Math.random() * (this.maxHeight - this.minHeight) + this.minHeight;
        this.node.y = -512 + height;
        // console.log('Dist: ' + dist, ', Height: ' + height);
    },

    move: function () {
        cc.tween(this.node)
            .by(2, { position: cc.v2(-cc.winSize.width - 100, 0) })
            .removeSelf()
            .start();
    }
});

最后在资源管理器的assets文件夹中新建prefab文件夹用于存放预制体,将pipe节点直接拖到这里,作为批量实例化的预制体使用。

批量管理管道

接下来创建一个管道管理器PipeManager用于批量管理预制体pipe,它的脚本如下:

cc.Class({
    extends: cc.Component,

    properties: {
        pipePrefab: cc.Prefab,
    },

    onLoad() {
        this.schedule(this.createPipe, 2);
    },

    createPipe: function () {
        let pipeNode = cc.instantiate(this.pipePrefab);
        pipeNode.parent = this.node;

        // 调用Pipe.js的move函数
        let pipeJs = pipeNode.getComponent('Pipe');
        pipeJs.move();
    }
});

通过计时器,管道管理器将于控件加载后每2s生成一个Pipe,这个Pipe的父节点是管道管理器,并获取它的脚本,为每个Pipe执行move()方法。

添加碰撞系统

接下来为游戏添加碰撞系统,Cocos2D的碰撞系统也是基于组件设计的,用起来十分方便。

为了启用碰撞系统,需要先在Level.js中编写如下脚本:

// Level.js
onLoad() {
    // ...

    cc.director.getCollisionManager().enabled = true;
},

边界的碰撞系统

在游戏中,小鸟撞向天空/大地时游戏结束。因此添加edge空节点,并添加两个子节点用于填充上下边界。然后分别为两个边界添加矩形碰撞组件即可,分组为default

管道的碰撞系统

和边界的碰撞系统同理。此外,为了让小鸟通过管道后加一分,将整个Pipe预制体添加一个矩形碰撞组件,分组为score,这样就能区分得分和游戏结束的碰撞了。

Ps:除了这种方式,还能通过管道管理器遍历场景中的管道,通过比较管道和小鸟的x坐标判断是否通过管道。

小鸟的碰撞系统

首先为小鸟添加多边形碰撞组件,并用0.5Threshold自动生成碰撞盒,它的碰撞分组为default

然后在Bird.js中编写碰撞回调,详情API见相关文档:

// Bird.js
onCollisionEnter: function (other, self) {
    console.log('和 ' + other.node.group + ' 发生碰撞');
    if (other.node.group === 'default') {
        this.isScoring = false;
        cc.director.loadScene('Over', this.getScore);
    } else if (other.node.group === 'score') {
        this.isScoring = true;
    }
},

onCollisionExit: function (other, self) {
    console.log('和 ' + other.node.group + ' 结束碰撞');
    if (other.node.group === 'score' && this.isScoring) {
        this.isScoring = false;
        let scoreJs = cc.find('score').getComponent('Score');
        scoreJs.addScore();
    }
},

在碰撞刚刚发生时,触发onCollisionEnter回调,在这里编写碰撞逻辑,如果碰到的是普通障碍物(default分组)就结束游戏,否则就加分(加分归计分器Score管)。

跨场景传递参数

在Cocos2D引擎中,一般只能同时存在一个场景,当场景A不用时,它的所有控件会被销毁。

如果想要用A中的某个控件的数据给新加载的场景B,需要:

  1. 将该控件置于场景的 根节点 中(一个场景可有多个根节点)。

  2. 在该控件的脚本中写入如下代码:

    onLoad() {
        cc.game.addPersistRootNode(this.node);
    },

    这样在场景A销毁后,该节点依然存活,场景B可通过之前说的五种方法找到该节点,进而使用数据。

例如要使用Level场景中的积分器的当前分数作为Over场景的最终得分,可在计分器脚本中写入:

start() {
    this.curScore = 0;
},
    
onLoad() {
    cc.game.addPersistRootNode(this.node);
},

hideScore() {
    this.scoreLabel.string = "";
}

其余代码(加分等)已省略,这样Over场景就能使用Level场景中的curScore了。hideScore()是因为,由于计分器还管理一个分数的文本控件,在Over场景中依然存在,目前只能通过清除文本方式解决。如果有更好的解决方法欢迎大家在评论区中讨论。

还有一种方法是直接给cc.xxxx赋值,但我觉得这种方法可能会对游戏造成破坏。

明天要做的

可能是丰富游戏细节吧,游戏大部分内容已经完成。例如可以加排行榜什么的。