Commit 43d0a4c1 authored by 邱旭's avatar 邱旭

07.FlppyBird-背景循环滚动

parent 7306b5d0
<!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>07.FlppyBird-背景循环滚动</title>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
</style>
<script src="../lib/lib.js"></script>
</head>
<body>
<div id="bg">
<img id="bg_1" src="../images/bird/background.png">
<img id="bg_2" src="../images/bird/background.png">
</div>
<div id="land">
<img id="land_1" src="../images/bird/land.png">
<img id="land_2" src="../images/bird/land.png">
</div>
<img id="bird" src="../images/bird/bird_01.png">
</body>
<script>
/**
* 屏幕宽高
* @type {{width: number, height: number}}
*/
const winSize = {
width: document.body.clientWidth,
height: document.body.clientHeight,
}
</script>
<script>
class Bird extends GameObject {
speed; // bird的速度 每次移动多少距离
constructor(id, speed = 15) {
super(id);
this.speed = speed; // 保存speed
this.setPosition(150, 150); // 给个默认位置
}
update() {
super.update();
const { top, left } = this.getPosition();
const speed = this.speed;
// 计算新的top
if (top !== clickPos.top) {
const dis = clickPos.top - top; // 计算差值
const dir = dis > 0 ? 1 : -1; // 计算在top上移动的方向 1 正向移动 或 -1 反向移动;
// 如果速度过快,本次移动直接过头了(即差值<速度),就直接移动到指定top
if (Math.abs(dis) < speed) {
this.transform.position.top = clickPos.top;
} else {
this.transform.position.top = top + dir * speed; // 计算新的top,新的位置 = 之前的位置 + 方向 * 速度
}
}
// 用相同的方式计算left
if (left !== clickPos.left) {
const dis = clickPos.left - left;
const dir = dis > 0 ? 1 : -1;
if (Math.abs(dis) < speed) {
this.transform.position.left = clickPos.left;
} else {
this.transform.position.left = left + dir * speed;
}
}
}
}
class ScrollMgr extends GameObject {
speed; // 滚动速度
bg1; // bg1
bg2; // bg2
constructor(id, bg1, bg2, speed = 5) {
super(id);
this.bg1 = bg1;
this.bg2 = bg2;
this.speed = speed;
bg1.setPosition(winSize.height - bg1.getSize().height); // 放在底部
bg2.setPosition(winSize.height - bg2.getSize().height); // 放在底部
}
update() {
super.update();
// 获取一些参数
let { top: bg1Top, left: bg1Left } = this.bg1.getPosition();
const bg1Width = this.bg1.getSize().width;
const bg2Top = this.bg2.getPosition().top;
// 计算位置
bg1Left -= this.speed; // 计算位置
this.bg1.setPosition(bg1Top, bg1Left); // 设置bg1的位置
this.bg2.setPosition(bg2Top, bg1Left + this.bg1.getSize().width);// bg2跟在bg1后面
// 如果超出屏幕则交换bg1和bg2,为了做到循环滚动
if (bg1Left <= -bg1Width) {
const temp = this.bg1;
this.bg1 = this.bg2;
this.bg2 = temp;
}
}
}
</script>
<script>
const clickPos = { // 鼠标点击的位置 bird将要的到达的位置
top: 150,
left: 150,
}
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置
const mouseDown = (e) => {
// 记录bird将要到达的位置,使用动画慢慢到达
clickPos.top = e.clientY;
clickPos.left = e.clientX;
}
document.addEventListener('mousedown', mouseDown);
</script>
<script>
// 创建鸟
const bird = new Bird("bird", 15);
// 创建背景
const bg1 = new GameObject("bg_1");
const bg2 = new GameObject("bg_2");
const bgMgr = new ScrollMgr("bg", bg1, bg2, 2);
// 创建地面
const land1 = new GameObject("land_1");
const land2 = new GameObject("land_2");
const landMgr = new ScrollMgr("land", land1, land2, 5);
// 将背景放在地面的上面,因为默认top是0,子节点在内部定位在底部,所以只需要把背景定位在负的land的高度就可以了
bgMgr.setPosition(-land1.getSize().height);
/**
* 数据更新
*/
function update() {
// bird更新
bird.update();
// 背景更新
bgMgr.update();
bg1.update();
bg2.update();
// 地面更新
landMgr.update();
land1.update();
land2.update();
}
/**
* 渲染更新
*/
function render() {
// bird渲染
bird.render();
// 背景渲染
bgMgr.render();
bg1.render();
bg2.render();
// 地面渲染
landMgr.render();
land1.render();
land2.render();
}
function loop() {
requestAnimationFrame(loop); // 循环调用requestAnimationFrame
update(); // 先数据更新
render(); // 后渲染更新
}
loop();
</script>
</html>
# FlppyBird - 背景循环滚动
引入概念:`无缝滚动` `组件抽象` `游戏优化-节省内存`
经过之前的内容我们已经在游戏开发中实践了面向对象
按照这样的写法,游戏开发会变得简单,代码可维护性更强,在面向对象的开发模式上,可抽象出一些通用的东西,成为组件,还可以开发可视化编辑器,提高效率
本节开始将会带大家在之前的基础上一步一步开发一款曾经举世闻名的小游戏`FlppyBird`
## 背景循环滚动
在游戏中内存是非常宝贵的,而占用内存的资源一般是图片,音频,视频,动画文件等。
FlppyBird占内存不过50MB,但是关卡背景图片却可以无限延长
> 图片占用内存的计算方式:`长 * 宽 * 每个像素的位数 / 8`
查阅图片内存计算公式,还以为百度在骗人
其实FlppyBird的背景图片只有320*640这么大,通过缩放等方式适配了你的手机,再通过两张图片循环滚动的方式达到背景图片无限长度的效果
本节将来教大家实现背景无限滚动的效果
在上一节课的基础上添加以下html代码
注意层级,bg应该在bird的下面
## 1.准备工作
```html
<div id="bg">
<img id="bg_1" src="../images/bird/background.png">
<img id="bg_2" src="../images/bird/background.png">
</div>
```
创建一个变量,让我们可以方便的拿到屏幕宽高
```javascript
/**
* 屏幕宽高
* @type {{width: number, height: number}}
*/
const winSize = {
width: document.body.clientWidth,
height: document.body.clientHeight,
}
```
在之前写好的GameObject类中添加一个方法,该方法用于获取这个游戏对象的宽高
```javascript
/**
* 抽象了一个简单的GameObject
*/
class GameObject {
/* ... */
/**
* 获得宽高
* @returns {{width: number, height: number}}
*/
getSize() {
const { x, y } = this.getScale();
return {
width: this.dom.clientWidth,
height: this.dom.clientHeight,
}
}
/* ... */
}
```
## 2.实现滚动
实现一个背景管理器,继承GameObject中通用的一些生命周期
```javascript
class BgMgr extends GameObject {
speed; // 滚动速度
bg1; // bg1
bg2; // bg2
constructor(id, bg1, bg2, speed = 5) {
super(id);
this.bg1 = bg1;
this.bg2 = bg2;
this.speed = speed;
bg1.setPosition(winSize.height - bg1.getSize().height); // 放在底部
bg2.setPosition(winSize.height - bg2.getSize().height); // 放在底部
}
update() {
super.update();
// 获取一些参数
let { top: bg1Top, left: bg1Left } = this.bg1.getPosition();
const bg1Width = this.bg1.getSize().width;
const bg2Top = this.bg2.getPosition().top;
// 计算位置
bg1Left -= this.speed; // 计算位置
this.bg1.setPosition(bg1Top, bg1Left); // 设置bg1的位置
this.bg2.setPosition(bg2Top, bg1Left + this.bg1.getSize().width);// bg2跟在bg1后面
// 如果移出屏幕则交换bg1和bg2,为了做到循环滚动
if (bg1Left <= -bg1Width) {
const temp = this.bg1;
this.bg1 = this.bg2;
this.bg2 = temp;
}
}
}
```
将两张背景图定位在最底下,bg1向左移动,bg2紧跟在背景1后面,如果bg1移出了屏幕,则交换变量bg1和bg2,那么bg1将跟在bg2后面移动,实现了循环滚动
将他们加入渲染和更新队列
```javascript
/**
* 数据更新
*/
function update() {
// bird更新
bird.update();
// 背景更新
bgMgr.update();
bg1.update();
bg2.update();
}
/**
* 渲染更新
*/
function render() {
// bird渲染
bird.render();
// 背景渲染
bgMgr.render();
bg1.render();
bg2.render();
}
```
再次运行案例,发现效果已经实现
![07_1.gif](../images/07_1.gif)
## 3.地面滚动
在FlppyBird中还有一个地面也在滚动。
因为我们已经实现了背景滚动,地面滚动当然和背景的逻辑一样
但是,这就很巧了,刚才我们实现的背景滚动功能比较完善,只需要用相同的方式创建地面就可以做到地面滚动
于是乎,我们将BgMgr的名字改成ScrollMgr
```javascript
class ScrollMgr extends GameObject {
/* ... */
}
```
并更新代码,然后创建我们的地面,同时加入渲染列表
> 为地面和背景赋予不同的滚动速度可以得到远处慢,近处快的效果
```javascript
// 创建鸟
const bird = new Bird("bird", 15);
// 创建背景
const bg1 = new GameObject("bg_1");
const bg2 = new GameObject("bg_2");
const bgMgr = new ScrollMgr("bg", bg1, bg2, 2);
// 创建地面
const land1 = new GameObject("land_1");
const land2 = new GameObject("land_2");
const landMgr = new ScrollMgr("land", land1, land2, 5);
// 将背景放在地面的上面,因为默认top是0,子节点在内部定位在底部,所以只需要把背景定位在负的land的高度就可以了
bgMgr.setPosition(-land1.getSize().height);
/**
* 数据更新
*/
function update() {
// bird更新
bird.update();
// 背景更新
bgMgr.update();
bg1.update();
bg2.update();
// 地面更新
landMgr.update();
land1.update();
land2.update();
}
/**
* 渲染更新
*/
function render() {
// bird渲染
bird.render();
// 背景渲染
bgMgr.render();
bg1.render();
bg2.render();
// 地面渲染
landMgr.render();
land1.render();
land2.render();
}
```
运行案例,发现效果已经实现
![07_2](../images/07_2.gif)
/*
* lib.js
* Created by 还有醋v on 2021/3/8.
* Copyright © 2021 haiyoucuv. All rights reserved.
*/
/**
* 抽象了一个简单的GameObject
*/
class GameObject {
id; // 绑定的dom元素的id
dom; // 绑定的dom元素
/**
* transform 表示显示对象的变换
* @type {{rotate: number, scale: {x: number, y: number}, position: {top: number, left: number}}}
*/
transform = {
position: { top: 0, left: 0 }, // 位置
scale: { x: 1, y: 1 }, // 缩放
rotate: 0, // 旋转
}
constructor(id) {
this.id = id;
this.dom = document.getElementById(id); // 在构造函数中绑定dom元素
this.dom.style.position = "absolute";
this.dom.style.transformOrigin = "center";
}
/**
* 获得宽高
* @returns {{width: number, height: number}}
*/
getSize() {
const { x, y } = this.getScale();
return {
width: this.dom.clientWidth,
height: this.dom.clientHeight,
}
}
/**
* 设置Position
* @param top
* @param left
*/
setPosition(top = this.transform.position.top, left = this.transform.position.left) {
this.transform.position.top = top;
this.transform.position.left = left;
}
/**
* 设置Scale
* @param x
* @param y
*/
setScale(x = this.transform.scale.x, y = this.transform.scale.y) {
this.transform.scale.x = x;
this.transform.scale.y = y;
}
/**
* 设置Rotate
* @param rotate
*/
setRotate(rotate = this.transform.rotate) {
this.transform.rotate = rotate;
}
/**
* 获得Position
* @returns {{top: number, left: number}}
*/
getPosition() {
return {
top: this.transform.position.top,
left: this.transform.position.left,
}
}
/**
* 获得Scale
* @returns {{x: number, y: number}}
*/
getScale() {
return {
x: this.transform.scale.x,
y: this.transform.scale.y,
}
}
/**
* 获得Rotate
* @returns {number}
*/
getRotate() {
return this.transform.rotate;
}
/**
* 抽离数据更新部分
*/
update() {
}
/**
* 抽离渲染部分
*/
render() {
const { top, left } = this.getPosition();
const { x: scaleX, y: scaleY } = this.getScale();
const rotate = this.getRotate();
this.dom.style.top = top + "px";
this.dom.style.left = left + "px";
this.dom.style.transform = `scale(${scaleX}, ${scaleY}) rotate(${rotate}deg)`;
}
/**
* 抽离销毁部分
*/
destroy() {
}
}
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