Flappy Bird 是一款前不久风靡世界的休闲小游戏,虽然难度超高,但是游戏本身却非常简单。下面使用 PIXI 来快速开发 HTML5 版的 Flappy Bird
初始化舞台
- 由于图片素材的背景大小为
720x1280,所以初始化游戏舞台的大小为720x1280 - 并在舞台上设置一个主容器,游戏将会放入到主容器中
<template>
<canvas id="flappy" ref="pixi"></canvas>
</template>
data() {
return {
app: null, // pixi应用
mainContainer: null, // 主容器
game: null, // 游戏
// pixi的宽高
appData: {
width: 720,
height: 1280,
}
};
},
initApp() {
this.app = new Application({
width: this.appData.width, //宽
height: this.appData.height, //高
backgroundAlpha: 0, // 背景透明
antialias: true, // 开启抗锯齿,使字体和图形边缘更加平滑
resolution: 1, // 像素比
view: this.$refs.pixi, // 引用渲染器的canvas元素
});
this.mainContainer = new Container(); // 初始化主容器
this.app.stage.addChild(this.mainContainer);
}
- 至此为止,浏览器中会有一个宽720,高1280的
canvas
场景分析
-
Flappy Bird的场景大致可以划分以下几个部分:-
背景和地面: 背景图和不断移动的地面是贯穿整个游戏,没有变化的
-
准备场景: 一个简单的游戏提示画面,游戏开始前和失败后重新开始都会进入此场景
-
游戏场景: 障碍物不断的从右往左移动,玩家控制小鸟的飞行,分数跟着变化
- 分数: 玩家当前的分数
- 小鸟: 玩家控制的精灵,是游戏的主角
- 障碍物: 游戏中另一个重要的角色,障碍物由许多成对的上下管子组成
-
结束场景: 游戏失败后,显示得分以及相关按钮等
-
资源: 所有资源在游戏初始化时都预先加载
-
-
通过分析,需要创建移动的地面、准备和结束场景、分数、小鸟、障碍物、以及一个预加载资源的类,最后封装成一个
Game类
Assets.js // 资源加载类
Bird.js // 小鸟类
Game.js // 游戏主类
Ground.js // 地面类
OverScene.js // 结束场景类
Pipe.js // 障碍物类
ReadyScene.js // 准备场景类
Score.js // 分数类
预加载资源
- 为了让玩家有更流畅的游戏体验,图片素材一般需要预先加载成纹理,需要使用
PIXI的Loader加载器,对纹理进行加载 - 其中需要加载地面、小鸟、准备和结束场景、障碍物、分数的纹理
- 构建
Assets类,让其继承PIXI的Loader类 - 有些纹理需要使用矩形切割其
frame(帧),则需通过new Texture(baseTexture, frame)
import { Loader } from "pixi.js";
class Assets extends Loader {
constructor() {
super();
this.ground = null; // 地面
this.bird = null; // 小鸟
this.ready = null; // 准备场景
this.score = null; // 分数
this.over = null; // 结束场景
this.pipe = null; // 障碍物
}
}
export default Assets;
- 声明
init方法,该方法主要用于加载所有资源,并接收一个回调函数,在资源加载完成之后执行相关逻辑
init(callback) {
// 定义所需资源
const resources = [
{ name: "ground", url: require("@/assets/images/bird/ground.png") },
{ name: "bird", url: require("@/assets/images/bird/bird.png") },
{ name: "ready", url: require("@/assets/images/bird/ready.png") },
{ name: "number", url: require("@/assets/images/bird/number.png") },
{ name: "over", url: require("@/assets/images/bird/over.png") },
{
name: "pipe",
url: require("@/assets/images/bird/pipe-green.png"),
},
];
// 重置loader
this.reset();
// 添加资源
this.add(resources);
// 资源加载完成时执行load方法
this.load((_, resources) => {
utils.clearTextureCache(); // 先清理纹理缓存,否则纹理会重复添加
this.loadAssets(resources); // 处理纹理
callback?.(); // 执行相应的回调
});
}
- 声明
loadAssets方法,用于在纹理加载完成后对其进行处理
loadAssets(resources) {
// 地面
this.ground = resources["ground"].texture;
// 小鸟
const birdFrames = [
[0, 120, 86, 60],
[0, 60, 86, 60],
[0, 0, 86, 60],
];
this.bird = birdFrames.map(
(item) => new Texture(resources["bird"].texture, new Rectangle(...item)));
// 准备场景
this.ready = {
tip: new Texture(resources["ready"].texture, new Rectangle(0, 0, 480, 158)),
tap: new Texture(resources["ready"].texture, new Rectangle(0, 158, 286, 246)),
};
// 障碍
this.pipe = {
up: new Texture(resources["pipe"].texture, new Rectangle(148, 0, 148, 820)),
down: new Texture(resources["pipe"].texture, new Rectangle(0, 0, 148, 820)),
};
// 得分
const scoreX = [0, 61, 121, 191, 261, 331, 401, 471, 541, 611];
this.score = scoreX.map(
(item) => new Texture(resources["number"].texture, new Rectangle(item, 0, 60, 91)));
// 结束场景
this.over = {
tip: new Texture(resources["over"].texture, new Rectangle(0, 298, 508, 158)),
board: new Texture(resources["over"].texture, new Rectangle(0, 0, 590, 298)),
startBtn: new Texture(resources["over"].texture, new Rectangle(590, 0, 290, 176)),
rankBtn: new Texture(resources["over"].texture, new Rectangle(590, 176, 290, 176)),
};
// 背景
this.bg = resources["bg"].texture;
}
初始化游戏
- 创建
Game.js文件,其作为游戏的主模块,整合了游戏的全部内容 - 在实例化
Game类时,自定义游戏的显示宽高,和游戏装载的容器,所有需要传入一个对象{width,height,stage} - 游戏中需要初始化游戏的所有元素,包括所有显示对象,以及游戏当前的状态和玩家的当前分数
import Assets from "./Assets";
import Ground from "./Ground";
import ReadyScene from "./ReadyScene";
import OverScene from "./OverScene";
import Bird from "./Bird";
import Score from "./Score";
import Pipe from "./Pipe";
class Game {
constructor(options) {
this.width = options.width; // 游戏呈现的宽度
this.height = options.height; // 游戏呈现的高度
this.stage = options.stage; // 装载游戏的容器
this.assets = null;
this.bird = null;
this.bg = null;
this.readyScene = null;
this.score = null;
this.pipe = null;
this.ground = null;
this.state = undefined; // 定义游戏状态,ready准备,running游戏中,over结束
this.curScore = 0; // 定义玩家当前分数
this.init(); // 初始化所需资源
}
}
- 声明
init方法,用于实例化Assets类加载所有资源,调用Assets类的init()方法加载纹理,并在纹理加载完成后创建游戏
init() {
this.assets = new Assets();
this.assets.init(() => this.createGame());
}
- 声明
createGame方法,用于初始化游戏的所有显示对象
createGame() {
// 初始化背景
this.initBg();
// 初始化地面
this.initGround();
// 初始化小鸟
this.initBird();
// 初始化游戏准备场景
this.initScene();
// 初始化分数
this.initScore();
// 初始化障碍物
this.initGamePipe();
// 游戏准备
this.gameReady();
}
- 在游戏主模块
Game.js中,为游戏的每个显示对象,声明属于自己的初始化方法
// 初始化背景
initBg() {}
// 初始化地面
initGround() {}
// 初始化小鸟
initBird() {}
// 初始化分数
initScore() {}
// 初始化场景
initScene() {
// 初始化游戏准备场景
//...
// 初始化游戏结束场景
//...
}
// 初始化游戏障碍物
initGamePipe() {}
游戏背景
- 由于背景是不变的,所以使用其加载好的纹理创建一个精灵实例,作为整体游戏的背景,并且初始化
pixi时设置了背景透明
initBg() {
this.bg = new Sprite(this.assets.bg);
this.stage.addChild(this.bg);
}
游戏地板
- 地面处于画面最下端,但地面是从右到左的不断循环的移动着的
- 所以地面需要使用平铺精灵,让其不断循环滚动
- 创建
Ground类,并且其继承于PIXI的平铺精灵TilingSprite类,该类需要纹理和宽高三个参数,另外需要知道舞台的高度,以确定地面的摆放位置
import { TilingSprite } from "pixi.js";
class Ground extends TilingSprite {
static DELTA_X = 0.128; // 定义每帧移动的像素数
constructor(options) {
super(options.texture, 840, 281); // 将TilingSprite的参数传入
this.gameHeight = options.height;
const baseY = this.gameHeight - this.height; // 地面位置 = 游戏高度 - 地面精灵高度
this.position.set(0, baseY);
this.tilePosition.set(0, 0); // 设置平铺精灵纹理的偏移量
this.viewportX = 0; // 记录当前视口位置
}
}
export default Ground;
- 完善
Game.js中的initGround方法,实例化一个地面并添加到容器中
initGround() {
this.ground = new Ground({
height: this.height,
texture: this.assets.ground,
});
this.ground.zIndex = 2; // 这里设置层级,需要令装载它的父容器设置sortableChildren属性为true
this.stage.addChild(this.ground); // 添加到舞台中
}
- 现在通过不断更新地面精灵的偏移量,可以在水平或垂直方向上移动纹理,并使纹理环绕这个精灵,无需实际改变精灵的位置从而模拟滚动
- 声明
setViewportX方法,接收一个新的滚动位置newViewportX作为参数
setViewportX(newViewportX) {
const scrollX = newViewportX - this.viewportX; // 记录滚动的距离差
this.viewportX = newViewportX; // 更新viewportX
this.tilePosition.x -= scrollX * Ground.DELTA_X; // 利用差值使纹理不断偏移
}
- 声明
moveViewportBy方法,用于不断更新精灵在视口的位置,从而达到改变精灵的纹理偏移量 - 接收一个滚动单位
units作为参数,使视口每帧滚动units个单位
moveViewportBy(units) {
const newViewportX = this.viewportX + units; // 更新视口位置
this.setViewportX(newViewportX);
}
- 最后通过
PIXI的Ticker钟表,不断执行moveViewportBy方法,每秒执行 60 次
import { TilingSprite, Ticker } from "pixi.js";
class Ground extends TilingSprite {
// ...
// ...
// ...
Ticker.shared.add(() => this.moveViewportBy(20)); // 每帧移动20个单位,1个单位为0.128像素
}
- 最后在主方法
initApp中将Game实例化就能看到地面在不断从右往左移动
initApp() {
//...
//...
this.mainContainer.sortableChildren = true; // 开启容器内children层级排序
// 实例化游戏
this.game = new Game({
stage: this.mainContainer,
width: this.appData.width,
height: this.appData.height,
});
//...
//...
}