Commit aa9c2c59 authored by 邱旭's avatar 邱旭

08.FlppyBird-模拟重力

parent 5ca7642d
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>09.FlppyBird-节点树</title>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
</style>
<script src="../lib/flppyBirdLib.js"></script>
</head>
<body>
</body>
<script>
/**
* 屏幕宽高
* @type {{width: number, height: number}}
*/
const winSize = {
width: document.body.clientWidth,
height: document.body.clientHeight,
}
</script>
<script>
/**
* Bird
*/
class Bird extends Sprite {
speed; // bird的速度 每次移动多少距离
gravity; // 重力加速度
constructor(gravity = 0.2, speed = 0) {
super("../images/bird/bird_01.png");
this.speed = speed; // 保存speed
this.gravity = gravity; // 保存gravity
}
ready() {
super.ready();
// 放到合适的位置
const { width, height } = this.size;
this.top = (winSize.height - height) / 2 - 100;
this.left = (winSize.width - width) / 2;
}
update() {
super.update();
// v = v0 + a * t²
this.speed += this.gravity; // 速度 = 速度 + 加速度 * 时间²
this.top += this.speed; // 更新位置
}
}
/**
* 滚动器
*/
class ScrollMgr extends GameObject {
speed; // 滚动速度
bg1; // bg1
bg2; // bg2
constructor(bg1, bg2, speed = 5) {
super();
this.bg1 = bg1;
this.bg2 = bg2;
this.speed = speed;
}
async start() {
super.start();
this.addChild(this.bg1);
this.addChild(this.bg2);
}
ready() {
super.ready();
this.bg1.top = winSize.height - this.bg1.size.height; // 放在底部
this.bg2.top = winSize.height - this.bg2.size.height; // 放在底部
}
update() {
super.update();
// 获取一些参数
let bg1Left = this.bg1.left;
const bg1Width = this.bg1.size.width;
// 计算位置
bg1Left -= this.speed; // 计算位置
this.bg1.left = bg1Left; // 设置bg1的位置
this.bg2.left = bg1Left + this.bg1.size.width; // bg2跟在bg1后面
// 如果超出屏幕则交换bg1和bg2,为了做到循环滚动
if (bg1Left <= -bg1Width) {
const temp = this.bg1;
this.bg1 = this.bg2;
this.bg2 = temp;
}
}
}
</script>
<script>
/**
* 异步加载图片方法
* @param src 图片路径
* @returns {Promise<HTMLImageElement | null>}
*/
function loadImgAsync(src) {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => {
console.error(`加载资源${src}失败`);
resolve(null);
};
img.src = src;
});
}
/**
* FlppyBird
*/
class FlppyBird extends GameStage {
bird;
async reloadRes() {
const path = "../images/bird/";
const promises = [
"bird_01.png", "bird_02.png", "bird_03.png",
"land.png", "pie_down.png", "pie_up.png",
"background.png", "start_button.png"
].map((v) => {
return loadImgAsync(`${path}${v}`);
});
return Promise.all(promises);
}
async start() {
// 创建鸟
const bird = this.bird = new Bird();
// 创建背景
const bg1 = new Sprite("../images/bird/background.png");
const bg2 = new Sprite("../images/bird/background.png");
const bgMgr = new ScrollMgr(bg1, bg2, 2);
// 创建地面
const land1 = new Sprite("../images/bird/land.png");
const land2 = new Sprite("../images/bird/land.png");
const landMgr = new ScrollMgr(land1, land2, 5);
this.addChild(bgMgr);
this.addChild(landMgr);
this.addChild(bird);
// 将背景放在地面的上面,因为默认top是0,子节点在内部定位在底部,所以只需要把背景定位在负的land的高度就可以了
bgMgr.top = -land1.size.height;
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置
document.addEventListener('mousedown', this.mouseDown);
}
mouseDown = () => {
this.bird.speed = -8;
}
destroy() {
super.destroy();
document.removeEventListener('mousedown', this.mouseDown);
}
}
// 创建游戏实例
new FlppyBird();
</script>
</html>
# FlppyBird - 节点树
引入概念:`节点树` `封装游戏对象`
在游戏开发中可能会有千千万万个游戏对象,如果按照当前的开发模式,每个都在dom中预制,那是不可能的。
所以一般在游戏开发中,游戏有自己的节点树,统一管理节点的生命周期,数据更新和渲染。
本节点内容将要对之前的内容进行大量改造和封装,如果之前没有游戏开发经验,可能会很难理解
修改后的 `lib.js` 请看 `flppyBirdLib.js`
## 1.改造GameObject
既然叫节点树,那么每个游戏对象应该为树上的一个节点,有子节点,和父节点
改造`GameObject`
- 1.添加保存子节点的变量 `children`,和父节点的变量 `parent`
- 2.添加生命周期函数 `start` `ready`
- 3.封装添加子节点`addChild`和删除子节点函数`removeChild`
- 4.在数据更新和渲染更新中加入子节点的更新和渲染
- 5.将dom节点改为动态创建并支持在构造函数中传入类型,支持节点创建不同类型的dom元素
```javascript
class GameObject {
dom; // 绑定的dom元素
children = []; // 子节点
parent; // 父节点
/* ... */
constructor(type = "div") {
this.dom = document.createElement(type); // 基础GameObject为div,Sprite为img
this.dom.style.position = "absolute";
}
/**
* 生命周期 start 所有构造函数完成,执行此函数
*/
async start() {
}
/**
* 生命周期 start 加入显示列表执行此函数
*/
ready() {
}
/**
* 添加子节点
* @param child
*/
addChild(child) {
// 如果是别人的子节点,则先移除再添加到自己下面
if (child.parent) {
child.parent.removeChild(child);
}
// 执行添加
this.dom.appendChild(child.dom);
this.children.push(child);
child.parent = this;
// 容错:防止子类重写的start不是async函数
// TODO dom无法在节点不在渲染树的上的时候拿到clientWidth等属性,故将start和ready放在这里
(async () => {
await child.start();
child.ready();
})();
return child;
}
/**
* 删除子节点
* @param child
*/
removeChild(child) {
// 不是自己的子节点就提示错误
if (child.parent !== this) {
console.warn("移除的节点必须是其子集");
return null;
}
// 执行销毁和移除
child.destroy();
this.dom.removeChild(child.dom);
this.children.splice(this.children.indexOf(child), 1);
child.parent = null;
return child;
}
/**
* 抽离数据更新部分,并更新子节点
*/
update() {
this.children.forEach((child) => {
child.update();
});
}
/**
* 抽离渲染部分,并渲染子节点
*/
render() {
/* ... */
// 添加渲染子节点部分
this.children.forEach((child) => {
child.render();
});
}
}
```
## 2.封装`Sprite`
之前的Sprite只是简单的继承与GameObject并且绑定节点变为一个`<img/>`
因为我们的GameObject已经经过改造,dom节点动态创建,所以,先还要封装一个`Sprite`
- 创建`Sprite`类,继承`GameObject`
- 支持在构造函数中传入`src`参数,即图片的链接
- 在构造父类的时候传入`"img"`作为`type`参数,这样父类会创建一个img标签
- 其他方法暂不重写
```javascript
/**
* 抽象精灵Sprite
*/
class Sprite extends GameObject {
constructor(src = "") {
super("img");
this.dom.src = src;
}
}
```
## 3.封装`GameStage`
GameStage将作为游戏的主画布,默认创建一个`div`容器,并添加到`body`
GameStage中包含游戏的主控逻辑,比如,游戏主循环,事件冒泡,事件循环,资源预加载等
- 创建`GameStage`类,继承`GameObject`
- 在构造函数中吧创建的dom节点加入到`body`
```javascript
class GameStage extends GameObject {
constructor() {
super();
document.body.appendChild(this.dom);
this._gameStart();
this.loop();
}
async _gameStart() {
await this.reloadRes();
this.start();
}
/**
* 预加载资源
* @returns {Promise<void>}
*/
async reloadRes() {
}
/**
* 主循环
*/
loop = () => {
requestAnimationFrame(this.loop); // 循环调用requestAnimationFrame
this.update(); // 先数据更新
this.render(); // 后渲染更新
}
}
```
## 4.修改 `Bird` 和 背景 的创建
- 删除预制的dom节点
- 修改`Bird`的构造函数,在`super()`中传入图片链接并实现`ready`方法
```javascript
class Bird extends Sprite {
constructor(gravity = 0.2, speed = 0) {
super("../images/bird/bird_01.png");
this.speed = speed; // 保存speed
this.gravity = gravity; // 保存gravity
}
ready() {
super.ready();
// 放到合适的位置
const { width, height } = this.size;
this.top = (winSize.height - height) / 2 - 100;
this.left = (winSize.width - width) / 2;
}
/* ... */
}
```
- 修改`ScrollMgr`的构造函数,实现`start`,将传入的两个元素加入到自己的子节点 实现`ready`,设置两个元素的位置
```javascript
class ScrollMgr extends GameObject {
speed; // 滚动速度
bg1; // bg1
bg2; // bg2
constructor(bg1, bg2, speed = 5) {
super();
this.bg1 = bg1;
this.bg2 = bg2;
this.speed = speed;
}
async start() {
super.start();
this.addChild(this.bg1);
this.addChild(this.bg2);
}
ready() {
super.ready();
this.bg1.top = winSize.height - this.bg1.size.height; // 放在底部
this.bg2.top = winSize.height - this.bg2.size.height; // 放在底部
}
/* ... */
}
```
## 5.创建`GameStage`
- 创建`FlppyBird`类继承`GameStage`
- 实现`reloadRes`方法,预加载资源
- 实现`start`方法,将之前创建游戏对象的代码搬进来
- 创建`FlppyBird`实例
> 下面是一个异步加载图片的方法,可以用来预加载资源
```javascript
/**
* 异步加载图片方法
* @param src 图片路径
* @returns {Promise<HTMLImageElement | null>}
*/
function loadImgAsync(src) {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => {
console.error(`加载资源${src}失败`);
resolve(null);
};
img.src = src;
});
}
```
```javascript
/**
* FlppyBird
*/
class FlppyBird extends GameStage {
bird;
async reloadRes() {
const path = "../images/bird/";
const promises = [
"bird_01.png", "bird_02.png", "bird_03.png",
"land.png", "pie_down.png", "pie_up.png",
"background.png", "start_button.png"
].map((v) => {
return loadImgAsync(`${path}${v}`);
});
return Promise.all(promises);
}
async start() {
// 创建鸟
const bird = this.bird = new Bird();
// 创建背景
const bg1 = new Sprite("../images/bird/background.png");
const bg2 = new Sprite("../images/bird/background.png");
const bgMgr = new ScrollMgr(bg1, bg2, 2);
// 创建地面
const land1 = new Sprite("../images/bird/land.png");
const land2 = new Sprite("../images/bird/land.png");
const landMgr = new ScrollMgr(land1, land2, 5);
this.addChild(bgMgr);
this.addChild(landMgr);
this.addChild(bird);
// 将背景放在地面的上面,因为默认top是0,子节点在内部定位在底部,所以只需要把背景定位在负的land的高度就可以了
bgMgr.top = -land1.size.height;
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置
document.addEventListener('mousedown', this.mouseDown);
}
mouseDown = () => {
this.bird.speed = -8;
}
destroy() {
super.destroy();
document.removeEventListener('mousedown', this.mouseDown);
}
}
// 创建游戏实例
new FlppyBird();
```
# 再次运行案例,发现效果和刚才一样,牛逼!!
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment