- 到目前为止,已经实现了小鸟的飞行,接下来开始初始化障碍物,让障碍物不断从右到左移动
创建障碍
-
障碍是游戏中另一个非常重要的角色,整个障碍由许多成对的上下管子组成,障碍只需要显示和变换位置,所有使用容器
Container
类来创建障碍Pipe
类 -
创建
Pipe.js
文件,实现一个Pipe
类,继承自Container
,装载障碍物的容器宽度由障碍物的总数和每个障碍物的宽度决定,并且障碍物上下之间也需要固定的间隔,障碍物水平之间也需要有固定的间隔,所有这里要先声明5个常量:- 障碍物总数
COUNT
(一个障碍物由上下两部分管子组成) - 每个障碍物的宽度
WIDTH
- 障碍物的水平间隔
SPACE_X
- 上下管子之间的间隔
SPACE_Y
- 障碍物总数
-
这些障碍物是不间断从右到左移动的,并且样式不变,所以最好能重复利用已有的障碍物,可以节省很多内存
-
因此,当障碍物有一半移出屏幕左侧时,可以将其重新排布到装载容器后面,并且重新规划位置,这样就可以复用已有的管子,并且位置也会重新调整,让玩家看起来就会有源源不断的并且不重复管子
-
所以需要多添加一个常量:移出屏幕左侧的障碍物数量
NUM_OFF_PIPES
初始化障碍
import { Container, Sprite } from "pixi.js";
import { Ease } from "pixi-ease";
class Pipe extends Container {
static COUNT = 4; // 障碍物总数(一根管子分上下两部分),4个障碍物也就是8根管子
static SPACE_X = 300; // 管子之间的水平间隔
static SPACE_Y = 290; // 管子上下两部分之间的垂直间隔,即小鸟要穿越的空间大小
static WIDTH = 148 + Pipe.SPACE_X; // 每个障碍物的宽度(包括管子之间的间隔)
static NUM_OFF_PIPES = Pipe.COUNT * 0.5; // 移出屏幕左侧的障碍物数量,为障碍物总数的一半
constructor(options) {
super();
this.texture = options.texture;
this.groundY = options.groundY; // 地面高度
this.startX = options.startX; // 障碍物的起始位置
this.gameHeight = options.gameHeight; // 游戏整体高度
this.width = Pipe.WIDTH * Pipe.COUNT; // 初始化障碍容器的总宽度
this.passPipe = 0; // 记录小鸟穿越的管子数量
this.createPipe(); // 创建障碍物
this.ease = new Ease(); // 实例化一个缓动实例,后面移动障碍需要使用
}
}
export default Pipe;
- 声明
createPipe
方法,用于创建障碍物,通过确定障碍物的总数量,循环创建组成障碍物的上下部分管子,并且将其位置摆放好
createPipe() {
for (let i = 0; i < Pipe.COUNT; i++) {
// 创建上部分管子
const downPipe = this.initPipe(this.texture.down, "downPipe");
this.addChild(downPipe);
// 创建下部分管子
const upPipe = this.initPipe(this.texture.up, "upPipe");
this.addChild(upPipe);
// 摆放上下部分管子
this.placePipe(downPipe, upPipe, i);
}
}
- 声明
initPipe
方法,用于将管子的纹理创建成精灵,并将创建的精灵返回
initPipe(texture, spriteName = undefined) {
const sprite = new Sprite(texture);
sprite.scale.set(0.8, 0.8);
sprite.name = spriteName;
return sprite;
}
-
声明
placePipe
方法,用于确定上下管子的x
和y
位置,下部分管子需要保证两点- 下部分管子
Y
轴最大值: 地面Y
坐标 - 180(保证不埋没于地面) - 下部分管子
Y
轴最小值: 地面Y
坐标 - 管子高度 + 与上部分管子的间距(保证不脱离地面)
- 下部分管子
-
保证这两点后,下部分管子在最大值与最小值直接随机一个
Y
坐标即可,而X
坐标为障碍物的宽度倍数 -
确定下部分管子之后,上部分管子的
x
坐标和下部分一致,y
坐标与下部分管子相隔SPACE_Y
的距离
placePipe(down, up, index) {
// 下部分管子Y轴最大值(地面Y坐标 - 180,保证不埋没于地面)
const downMaxY =this.groundY - 180;
// 下部分管子Y轴最小值(地面Y坐标 - 管子高度 + 间距,保证不脱离地面)
const downMinY = this.groundY - down.height + Pipe.SPACE_Y;
// 随机下部分管子Y轴坐标
down.y = downMinY + (downMaxY - downMinY) * Math.random();
// 确定下部分管子X坐标
down.x = Pipe.WIDTH * index;
// 确定上部分管子Y坐标(下部分管子-间距)
up.y = down.y - up.height - Pipe.SPACE_Y;
// 确定下部分管子X坐标
up.x = down.x;
}
- 完成以上内容之后,在
Game.js
中完善initGamePipe
方法,将障碍物实例化,并添加到舞台上
initGamePipe() {
this.pipe = new Pipe({
gameHeight: this.height, // 游戏高度
startX: this.width + 200, // 起始位置
groundHeight: this.ground.height, // 地面高度
texture: this.assets.pipe, // 所需纹理
});
this.stage.addChild(this.pipe);
}
- 实例化后,将看到障碍物如下图所示
- 现在需要声明一个
reset
方法,用于初始化障碍物的起始位置,并且记录穿越障碍物的数量
reset() {
this.x = this.startX;
this.passThrough = 0; // 记录穿越障碍物的数量
}
- 在
Game.js
的gameReady
方法中,调用一下Pipe
类的reset
方法,那么管子就会在当前视口之外
gameReady() {
this.readyScene.visible = true;
// 重置障碍
this.pipe.reset(); // 添加此句
this.state = "ready";
this.bird.birdReady();
}
移动障碍
-
障碍初始化完成后,其最核心的状态就是从右至左不断的移动,管子也不断的产生
-
先声明
startMove
方法,通过缓动库Ease
让其不断移动,移动则需要确定两个因素- 目标位置: 首先确定装载障碍物的整个容器的初始位置为视口宽度+200,目标就是让整个容器的一半移出视口左边,则目标位置为 2 个障碍物宽度(包括水平间隔)
- 过渡时间: 以
1ms
移动4px
为基准,总体时间就是当前位置与目标位置的差值 * 4
startMove() {
const targetX = -Pipe.WIDTH * Pipe.NUM_OFF_PIPES; // 移动到的目标位置
const duration = (this.x - targetX) * 4; // 过渡时间
this.ease.add(this, { x: targetX }, { duration, ease: "linear" }); // 启动
}
- 当用户点击屏幕时,会调用
Game.js
中的gameStart
方法,所有要在该方法中同时调用Pipe.startMove()
让管子移动起来
gameStart() {
this.readyScene.visible = false;
this.state = "running";
this.pipe.startMove(); // 添加此句
//...
}
-
当有一半的障碍物移出视口时,则到达了目标位置,此时需要重新排布障碍物的位置
-
声明
resetPipe
方法,用于重新排布所有障碍物,从而达到重复利用管子的目的- ①首先把移出视口的障碍物,重新放到队列的最后面
- ②然后重新排布所有障碍物的位置
- ③将装载容器重新设置到视口的开始处
- ④让容器重新开始移动
// 重置管子
resetPipe() {
const total = this.children.length;
// 把已移出屏幕外的管子放到队列最后面,并重置其可穿越位置
for (let i = 0; i < Pipe.NUM_OFF_PIPES; i++) {
const downPipe = this.getChildAt(0);
const upPipe = this.getChildAt(1);
// 把移出的管子移动到队列最后
this.setChildIndex(downPipe, total - 1);
this.setChildIndex(upPipe, total - 1);
// 重新将管子摆放到视口内
this.placePipe(downPipe, upPipe, Pipe.NUM_OFF_PIPES + i);
}
// 重新确定队列中所有管子的x轴坐标;
for (let i = 0; i < total - Pipe.NUM_OFF_PIPES * 2; i++) {
const pipe = this.getChildAt(i);
pipe.x = Pipe.WIDTH * Math.floor(i * 0.5);
}
this.x = 0; // 重新确定障碍物的x轴坐标
this.passPipe += Pipe.NUM_OFF_PIPES; // 记录穿过的障碍物数量,后面会用到
this.startMove(); // 重新移动
}
- 那么什么时候调用
resetPipe
方法?当一轮移动完成时,立刻调用该方法对障碍物重置即可 - 所以需要监听
ease
实例的onComplete
事件,在回调中执行resetPipe
class Pipe extends Container {
//...
// 设置缓动
this.ease = new Ease();
this.ease.on("complete", () => {
this.resetPipe();
});
}
- 此时,障碍物即可不断从右往左移动,并且不会占用过多内存
停止障碍
- 停止移动障碍就只需停止缓动即可,需要声明一个
stopMove
方法,用于后面游戏结束时停止移动
stopMove() {
this.ease.destroy();
}