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.5
的Threshold
自动生成碰撞盒,它的碰撞分组为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,需要:
将该控件置于场景的 根节点 中(一个场景可有多个根节点)。
在该控件的脚本中写入如下代码:
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
赋值,但我觉得这种方法可能会对游戏造成破坏。
明天要做的
可能是丰富游戏细节吧,游戏大部分内容已经完成。例如可以加排行榜什么的。