给DOM开发者的游戏开发指南-09.FlppyBird-节点树

仓库地址:github.com/haiyoucuv/W…

引入概念:节点树 封装游戏对象

在游戏开发中可能会有千千万万个游戏对象,如果按照当前的开发模式,每个都在dom中预制,那是不可能的。

所以一般在游戏开发中,游戏有自己的节点树,统一管理节点的生命周期,数据更新和渲染。

本节点内容将要对之前的内容进行大量改造和封装,如果之前没有游戏开发经验,可能会很难理解

修改后的 lib.js 请看 flppyBirdLib.js

1.改造GameObject

既然叫节点树,那么每个游戏对象应该为树上的一个节点,有子节点,和父节点

改造GameObject

  • 1.添加保存子节点的变量 children,和父节点的变量 parent
  • 2.添加生命周期函数 start ready
  • 3.封装添加子节点addChild和删除子节点函数removeChild
  • 4.在数据更新和渲染更新中加入子节点的更新和渲染
  • 5.将dom节点改为动态创建并支持在构造函数中传入类型,支持节点创建不同类型的dom元素
class GameObject {
	dom;    // 绑定的dom元素

	children = [];  // 子节点

	parent; // 父节点

	/* ... */

	constructor(type = "div") {
		this.dom = document.createElement(type); // 基础GameObject为div,Sprite为img
		this.dom.style.position = "absolute";
	}

	/**
	 * 生命周期 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等属性,故将ready放在这里
		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标签
  • 其他方法暂不重写
/**
 * 抽象精灵Sprite
 */
class Sprite extends GameObject {

	constructor(src = "") {
		super("img");

		this.dom.src = src;
	}

}
复制代码

3.封装GameStage

GameStage将作为游戏的主画布,默认创建一个div容器,并添加到body
GameStage中包含游戏的主控逻辑,比如,游戏主循环,事件冒泡,事件循环,资源预加载等

  • 创建GameStage类,继承GameObject
  • 在构造函数中吧创建的dom节点加入到body
class GameStage extends GameObject {

	constructor() {
		super();
		document.body.appendChild(this.dom);
		this._gameStart();
		this.loop();
	}

	async _gameStart() {
		await this.reloadRes();
		this.ready();
	}

	/**
	 * 预加载资源
	 * @returns {Promise<void>}
	 */
	async preloadRes() {

	}

	/**
	 * 主循环
	 */
	loop = () => {
		requestAnimationFrame(this.loop);    // 循环调用requestAnimationFrame
		this.update();   // 先数据更新
		this.render();   // 后渲染更新
	}
}
复制代码

4.修改 Bird 和 背景 的创建

  • 删除预制的dom节点
  • 修改Bird的构造函数,在super()中传入图片链接并实现ready方法
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,设置两个元素的位置
    class ScrollMgr extends GameObject {

	speed; // 滚动速度
	bg1;    // bg1
	bg2;    // bg2

	constructor(bg1, bg2, speed = 5) {
		super();
		this.bg1 = bg1;
		this.bg2 = bg2;
		this.speed = speed;
		
		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实例

下面是一个异步加载图片的方法,可以用来预加载资源

/**
 * 异步加载图片方法
 * @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;
	bgMgr;
	landMgr;

	async preloadRes() {
		const path = "../images/bird/";

		const promises = [
			"bird_01.png", "bird_02.png", "bird_03.png", "pie.png",
			"land.png", "background.png", "start_button.png"
		].map((v) => {
			return loadImgAsync(`${path}${v}`);
		});

		return Promise.all(promises);
	}

	async ready() {
		// 创建鸟
		const bird = this.bird = new Bird();

		// 创建背景
		const bg1 = new Sprite("../images/bird/background.png");
		const bg2 = new Sprite("../images/bird/background.png");
		const bgMgr = this.bgMgr = new ScrollMgr(bg1, bg2, 2);

		// 创建地面
		const land1 = new Sprite("../images/bird/land.png");
		const land2 = new Sprite("../images/bird/land.png");
		const landMgr = this.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();
复制代码

再次运行案例,发现效果和刚才一样,牛逼!!

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享