- 目前障碍物已经可以持续从右往左移动,那么接下来就是计算小鸟穿过的障碍物数量,以及判断小鸟与障碍物是否碰撞
初始化分数
- 由于分数的 0 到 9 也有各自对应的纹理,那么可以通过创建
Sprite
进行显示
- 这些分数精灵可以设置成一个精灵组合,放到一个装载容器内进行显示,最后将装载容器放置到舞台上
- 创建
Score.js
文件,定义Score
类,并让其继承于Container
import { Container, Sprite } from "pixi.js";
class Score extends Container {
constructor(options) {
super();
this.gameWidth = options.width;
this.scoreTexture = options.texture;
this.position.set(0, 170);
this.scale.set(0.8, 0.8);
}
export default Score;
- 这个分数是不断变化的,那么也就是说创建分数精灵的纹理需要在 0 到 9 之间不断变化
- 声明一个
setData
方法,接收一个score
作为参数,通过传入score
的长度决定需要显示多少个分数精灵,并且在预加载资源时,创建分数的纹理组成了一个数组,通过数组下标就能获取到对应数字
setData(score) {
this.removeChildren(); // 首先把原来的精灵移除
// 将传入的数字切割成数组,如传入12,切割后=>['1','2']
const scoreArr = score.toString().split("");
scoreArr.forEach((item, i) => {
// 通过切割出的数字,获得对应的纹理并创建精灵
const sprite = new Sprite(this.scoreTexture[item]);
// 设置精灵的位置
sprite.position.set(i * 65, 0);
// 设置装载容器的位置
this.x = (this.gameWidth - this.width) / 2 - i * 20;
// 添加到容器内
this.addChild(sprite);
});
}
- 在
Game.js
中,完善initScore
方法,对分数进行实例化
initScore() {
this.score = new Score({
width: this.width,
texture: this.assets.score,
});
this.score.zIndex = 1;
this.stage.addChild(this.score);
}
- 在
Game.js
的gameReady
方法中,调用Score
类的setData
方法,在游戏准备时初始化当前分数为0
gameReady() {
//...
this.score.visible = true; // 分数可见
this.curScore = 0; // 当前得分设置为0
this.score.setData(this.curScore);
//...
}
- 至此分数已经在屏幕中看到,并且测试改变当前分数
curScore
,看看分数的变化
碰撞检测和得分
-
得分,也就是计算出小鸟已经穿越过障碍物的数量
-
在
Pipe
类中定义calcPassPipe
方法,传入一个参数x
为小鸟的坐标,根据障碍物的宽度和已穿越的管子的数量,就可以统计出总的数量- 其实小鸟的
x
坐标是不变的,需要通过障碍物移动的相对位置去计算小鸟通过障碍物的数量 - 障碍物的
x
坐标是持续变化的,用小鸟的x
坐标减去障碍物容器的x
坐标,记为birdX
- 由于障碍物是不断左移的,其
x
坐标持续变小的,当birdX
大于 0 则表明小鸟已经开始穿越障碍物 - 当
birdX
大于 0 之后,障碍物往做移动,相当于小鸟往右移动,birdX
就是相对的一个x
坐标 - 然后用
birdX
除以障碍物宽度,就可以得到小鸟穿越的障碍物数量 - 前面重置障碍物的方法里就有一个变量
passPipe
,每次重置障碍物时,代表小鸟已经穿越了两个管子,此时计算得分时需要累加进去
- 其实小鸟的
calcPassPipe(x) {
let count = 0;
const birdX = x - this.x; // 记录小鸟是否开始穿越障碍物
if (birdX > 0) {
// 计算穿越的障碍物数量,这里需要+0.5再向下取整,是因为障碍物宽度包括管子和间隔,小鸟只需完全穿越管子后就可以累加得分
const num = Math.floor(birdX / Pipe.WIDTH + 0.5);
count += num;
}
count += this.passPipe; // 这里额外加一次count,因为障碍物会重置
return count; // 计算好的得分返回
}
计算得分
- 那么在
Game.js
中,声明一个calcScore
方法,用于不断更新当前的得分
calcScore() {
const count = this.pipe.calcPassPipe(this.bird.x);
this.curScore = count;
}
- 在
Game.js
中的gameStart
方法中使用Ticker
开启游戏监听,不断更新当前得分
gameStart() {
//...
// 开启游戏监听
this.updateTicker = Ticker.shared;
this.updateTicker.add(() => this.onUpdate());
}
- 声明
onUpdate
方法,用于不断更新当前得分,后面还要用于碰撞检测和判断小鸟是否死亡
onUpdate() {
if (this.state === "ready") return;
this.calcScore(); // 先更新当前得分
this.score.setData(this.curScore); // 改变显示的分数精灵
}
- 此时当用户点击屏幕时就能看到,随着障碍物的不断推移,得分会不断更新
碰撞检测
- 当小鸟飞行穿越障碍的时候,需要检测小鸟与障碍是否发生了碰撞,接下来封装一个碰撞检测的方法
- 新建
bump.js
文件,这里因为精灵默认是矩形,所以封装一个矩形精灵检测碰撞的方法 - 首先需要对精灵添加上碰撞检测需要的属性
const addCollisionProperties = (sprite) => {
// 全局x坐标属性
if (!sprite.gx) {
Object.defineProperty(sprite, "gx", {
get() {
return sprite.getGlobalPosition().x;
},
configureable: true,
enumerable: true,
});
}
// 全局y坐标属性
if (!sprite.gy) {
Object.defineProperty(sprite, "gy", {
get() {
return sprite.getGlobalPosition().y;
},
configureable: true,
enumerable: true,
});
}
// 精灵水平中心点属性
if (!sprite.centerX) {
Object.defineProperty(sprite, "centerX", {
get() {
return sprite.x + sprite.width / 2;
},
configureable: true,
enumerable: true,
});
}
// 精灵垂直中心点属性
if (!sprite.centerY) {
Object.defineProperty(sprite, "centerY", {
get() {
return sprite.y + sprite.height / 2;
},
configureable: true,
enumerable: true,
});
}
// 精灵半宽
if (!sprite.halfWidth) {
Object.defineProperty(sprite, "halfWidth", {
get() {
return sprite.width / 2;
},
configureable: true,
enumerable: true,
});
}
// 精灵半高
if (!sprite.halfHeight) {
Object.defineProperty(sprite, "halfHeight", {
get() {
return sprite.height / 2;
},
configureable: true,
enumerable: true,
});
}
// 水平锚点偏差
if (!sprite.xAnchorOffset) {
Object.defineProperty(sprite, "xAnchorOffset", {
get() {
return sprite.width * sprite.anchor?.x || 0;
},
configureable: true,
enumerable: true,
});
}
// 垂直锚点偏差
if (!sprite.yAnchorOffset) {
Object.defineProperty(sprite, "yAnchorOffset", {
get() {
return sprite.height * sprite.anchor?.y || 0;
},
configureable: true,
enumerable: true,
});
}
// 精灵半径
if (sprite.circular && !sprite.radius) {
Object.defineProperty(sprite, "radius", {
get() {
return sprite.width / 2;
},
enumerable: true,
configurable: true,
});
}
sprite._bumpPropertiesAdded = true; // 用于标记精灵是否已经添加以上属性
};
-
封装碰撞检测方法
- 在这个游戏中,障碍物是装在自己的容器中,在判断碰撞时不能使用障碍物的局部坐标,而是使用其全局内的坐标去判断
- 此外还要注意,小鸟的基准点是重新设置在嘴的位置,在判断距离时需要减去其基准点所带来的偏差
// 第三个参数global是否使用全局坐标
const hitTestSprite = (s1, s2, global = false) => {
// 添加检测碰撞的属性
if (!s1._bumpPropertiesAdded) addCollisionProperties(s1);
if (!s2._bumpPropertiesAdded) addCollisionProperties(s2);
// 用于确定是否有碰撞的变量
let hit = false;
let s1X, s1Y, s2X, s2Y;
// 计算精灵的相差距离
if (global) {
// 全局下精灵A中心点的位置
s1X = s1.gx + Math.abs(s1.halfWidth) - s1.xAnchorOffset;
s1Y = s1.gy + Math.abs(s1.halfHeight) - s1.yAnchorOffset;
// 全局下精灵B的位置
s2X = s2.gx + Math.abs(s2.halfWidth) - s2.xAnchorOffset;
s2Y = s2.gy + Math.abs(s2.halfHeight) - s2.yAnchorOffset;
} else {
// 局部精灵A的中心点的位置
s1X = s1.x + Math.abs(s1.halfWidth) - s1.xAnchorOffset;
s1Y = s1.y + Math.abs(s1.halfHeight) - s1.yAnchorOffset;
// 局部精灵B的中心点的位置
s2X = s2.x + Math.abs(s2.halfWidth) - s2.xAnchorOffset;
s2Y = s2.y + Math.abs(s2.halfHeight) - s2.yAnchorOffset;
}
const gapX = s1X - s2X; // 精灵水平相差的间距
const gapY = s1Y - s2Y; // 精灵垂直相差的间距
// 算出精灵半宽、半高和
const combineWidth = Math.abs(s1.halfWidth) + Math.abs(s2.halfWidth);
const combineHeight = Math.abs(s1.halfHeight) + Math.abs(s2.halfHeight);
// 判断两个精灵相差距离,是否小于精灵半宽和
// 先检查水平方向是否有碰撞
if (Math.abs(gapX) < combineWidth) {
// 检查垂直方向是否有碰撞
hit = Math.abs(gapY) < combineHeight;
} else {
hit = false;
}
return hit;
};
- 方法封装之后,在
Pipe
类中引入并使用,用于检测障碍物和小鸟是否碰撞 - 声明
checkCollision
方法,接收一个小鸟对象作为参数,使用封装的方法进行检测
checkCollision(bird) {
const len = this.children.length;
for (let i = 0; i < len; i++) {
// 判断每一根管子是否与小鸟有碰撞
if (hitTestSprite(this.children[i], bird, true)) return true;
}
return false;
}
- 最后在
Game.js
的onUpdate
方法中,进行持续的碰撞检测
onUpdate() {
const isHit = this.pipe.checkCollision(this.bird); // 持续碰撞检测
if (this.state === "ready") return;
if (this.bird.isDead) return this.gameOver(); // 如果小鸟状态为死亡,则游戏结束
if (isHit) return this.gameOver(); // 如果撞了,则游戏结束
this.calcScore();
this.saveBestScore();
this.score.setData(this.curScore);
}
- 在
Game.js
中声明gameOver
方法,用于确定游戏的结束状态
-
可以看到游戏结束需要实现以下内容
- 设置游戏状态为
over
- 隐藏顶部的分数
- 障碍物停止移动
- 销毁对游戏的持续监听
- 显示结束场景
- 设置游戏状态为
gameOver() {
if (this.state === "over") return;
this.state = "over"; // 设置状态为over
this.overScene.show(this.curScore, this.bestScore); // 显示结束场景
this.score.visible = false; // 隐藏分数
this.pipe.stopMove();// 调用pipe的stopMove方法,让停止障碍物移动
this.updateTicker.destroy(); // 移除游戏监听
}
- 由于目前结束场景还未实现,只有停止障碍物移动和隐藏分数
最高得分
- 在
Game.js
中声明saveBestScore
方法,每次游戏结束都会从浏览器缓存中拿最好分数 - 如果当前分数要比缓存中的大,则拿当前分数作为最大分数,并更新浏览器缓存中的分数
- 此方法在
onUpdate
中会不断调用
saveBestScore() {
this.bestScore = window.localStorage.getItem("bestScore") || 0;
if (this.curScore > this.bestScore) {
this.bestScore = this.curScore;
window.localStorage.setItem("bestScore", this.curScore);
}
}