Commit 714aa5c8 authored by 谌继荃's avatar 谌继荃

1

parent d6fce54c
/node_modules /node_modules
/dist /dist
npm-debug.log npm-debug.log
.DS_Store .DS_Store
yarn-error.log yarn-error.log
yarn.lock yarn.lock
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="HtmlRequiredAltAttribute" enabled="false" level="WARNING" enabled_by_default="false" />
</profile>
</component>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/../What's The Game/!/.idea/What's The Game!.iml" filepath="$PROJECT_DIR$/../What's The Game/!/.idea/What's The Game!.iml" />
</modules>
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ChangeListManager">
<list default="true" id="6c702afd-70dc-432d-9640-09d64ceaafcb" name="默认变更列表" comment="">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/04.高性能鸟/04.高性能鸟.md" beforeDir="false" afterPath="$PROJECT_DIR$/04.高性能鸟/04.高性能鸟.md" afterDir="false" />
<change beforePath="$PROJECT_DIR$/08.FlppyBird-模拟重力/08.FlppyBird-模拟重力.md" beforeDir="false" afterPath="$PROJECT_DIR$/08.FlppyBird-模拟重力/08.FlppyBird-模拟重力.md" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="ProjectId" id="1tevL8vGrxpUhS81FYTYujNuILU" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">
<property name="RunOnceActivity.OpenProjectViewOnStart" value="true" />
<property name="RunOnceActivity.ShowReadmeOnStart" value="true" />
<property name="WebServerToolWindowFactoryState" value="false" />
<property name="dart.analysis.tool.window.visible" value="false" />
<property name="settings.editor.selected.configurable" value="preferences.pluginManager" />
<property name="vue.rearranger.settings.migration" value="true" />
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="默认任务">
<changelist id="6c702afd-70dc-432d-9640-09d64ceaafcb" name="默认变更列表" comment="" />
<created>1623142310502</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1623142310502</updated>
<workItem from="1623142311711" duration="1628000" />
</task>
<task id="LOCAL-00001" summary="错别字一个,谢强哥">
<created>1623142375379</created>
<option name="number" value="00001" />
<option name="presentableId" value="LOCAL-00001" />
<option name="project" value="LOCAL" />
<updated>1623142375379</updated>
</task>
<option name="localTasksCounter" value="2" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value="错别字一个,谢强哥" />
<option name="LAST_COMMIT_MESSAGE" value="错别字一个,谢强哥" />
</component>
</project>
\ No newline at end of file
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>00.前言</title> <title>00.前言</title>
<style> <style>
html, body { html, body {
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
} }
</style> </style>
</head> </head>
<body> <body>
</body> </body>
</html> </html>
# 前言 # 前言
本案例旨在使非游戏开发人员理解游戏和游戏开发的模式,不适用于实际生产 本案例旨在使非游戏开发人员理解游戏和游戏开发的模式,不适用于实际生产
本案例使用浏览器平台,以js控制dom元素的方式讲解游戏,旨在降低平台跨度,易于理解,"游戏高手"请不要装逼 本案例使用浏览器平台,以js控制dom元素的方式讲解游戏,旨在降低平台跨度,易于理解,"游戏高手"请不要装逼
本案例并非真正的游戏开发过程,仅借助dom渲染引擎来给非游戏开发人员讲解游戏 本案例并非真正的游戏开发过程,仅借助dom渲染引擎来给非游戏开发人员讲解游戏
普通的游戏开发教程更多的是在引擎API上的教学,你学完了会用,但是却不知道在这API之下究竟发生了什么 普通的游戏开发教程更多的是在引擎API上的教学,你学完了会用,但是却不知道在这API之下究竟发生了什么
而在本案例中使用dom渲染引擎作为根本,并带大家使用最基本的js语法从0封装和游戏引擎类似的接口,从而达到让dom技术栈人员更加容易理解游戏开发的模式,因为每一个接口都是由你亲自封装,而接口的底层,恰恰是你熟悉的东西, 而在本案例中使用dom渲染引擎作为根本,并带大家使用最基本的js语法从0封装和游戏引擎类似的接口,从而达到让dom技术栈人员更加容易理解游戏开发的模式,因为每一个接口都是由你亲自封装,而接口的底层,恰恰是你熟悉的东西,
就好像带你实现了一个简单的基于dom渲染的游戏引擎,学习完本案例之后再去学习其他的游戏引擎,你就算不看源码也能猜出那些API的原理是什么 就好像带你实现了一个简单的基于dom渲染的游戏引擎,学习完本案例之后再去学习其他的游戏引擎,你就算不看源码也能猜出那些API的原理是什么
学习本案例需要的预备知识:`html` `javascript` `面向对象编程概念` 学习本案例需要的预备知识:`html` `javascript` `面向对象编程概念`
学习本案例前,请先记住以下概念,在之后的学习过程中,你会发现,还真是这么回事: 学习本案例前,请先记住以下概念,在之后的学习过程中,你会发现,还真是这么回事:
> **游戏可抽象为:输入 + 循环 + 输出** > **游戏可抽象为:输入 + 循环 + 输出**
> >
> 输入:鼠标、键盘、陀螺仪、手柄、ar/vr、摄像头、麦克风等一切可以获取信息的设备 > 输入:鼠标、键盘、陀螺仪、手柄、ar/vr、摄像头、麦克风等一切可以获取信息的设备
> >
> 循环:游戏主循环, 数据更新+渲染更新 > 循环:游戏主循环, 数据更新+渲染更新
> >
> 输出:渲染,对应用户看到的画面,声音等 > 输出:渲染,对应用户看到的画面,声音等
本案例使用以下模拟器模拟 本案例使用以下模拟器模拟
![00_1.png](../images/00_1.png) ![00_1.png](../images/00_1.png)
本案例使用以下html模版 本案例使用以下html模版
```html ```html
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>00.前言</title> <title>00.前言</title>
<style> <style>
html, body { html, body {
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
} }
</style> </style>
</head> </head>
<body> <body>
</body> </body>
</html> </html>
``` ```
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh"> <html lang="zh">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>01.一只鸟</title> <title>01.一只鸟</title>
<style> <style>
html, body { html, body {
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
} }
#bird { #bird {
/* 大小 */ /* 大小 */
width: 50px; width: 50px;
height: 50px; height: 50px;
/* 颜色 */ /* 颜色 */
background-color: #f00; background-color: #f00;
/* 位置 */ /* 位置 */
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
/* 旋转、缩放等属性 */ /* 旋转、缩放等属性 */
transform: scale(1, 1) rotate(45deg); transform: scale(1, 1) rotate(45deg);
/* 锚点 */ /* 锚点 */
transform-origin: center; transform-origin: center;
} }
</style> </style>
</head> </head>
<body> <body>
<div id="bird"></div> <div id="bird"></div>
</body> </body>
</html> </html>
# 一只Bird # 一只Bird
引入概念:`显示对象` 引入概念:`显示对象`
显示对象是游戏客户端渲染的基本对象,可理解为div 显示对象是游戏客户端渲染的基本对象,可理解为div
在body中插入一个div 在body中插入一个div
```html ```html
<div id="bird"></div> <div id="bird"></div>
``` ```
把div放在你喜欢的位置,加上你喜欢的颜色 把div放在你喜欢的位置,加上你喜欢的颜色
```css ```css
#bird { #bird {
/* 大小 */ /* 大小 */
width: 100px; width: 100px;
height: 100px; height: 100px;
/* 颜色 */ /* 颜色 */
background-color: #f00; background-color: #f00;
/* 位置 */ /* 位置 */
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
/* 旋转、缩放等属性 */ /* 旋转、缩放等属性 */
transform: scale(1, 1) rotate(45deg); transform: scale(1, 1) rotate(45deg);
/* 锚点 */ /* 锚点 */
transform-origin: center; transform-origin: center;
} }
``` ```
经过一顿操作,我们得到了自己喜欢的一只Bird,如下图 经过一顿操作,我们得到了自己喜欢的一只Bird,如下图
![img.png](../images/01_1.png) ![img.png](../images/01_1.png)
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh"> <html lang="zh">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>02.会飞的鸟</title> <title>02.会飞的鸟</title>
<style> <style>
html, body { html, body {
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
} }
#bird { #bird {
/* 大小 */ /* 大小 */
width: 50px; width: 50px;
height: 50px; height: 50px;
/* 颜色 */ /* 颜色 */
background-color: #f00; background-color: #f00;
/* 位置 */ /* 位置 */
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
/* 旋转、缩放等属性 */ /* 旋转、缩放等属性 */
transform: scale(1, 1) rotate(45deg); transform: scale(1, 1) rotate(45deg);
/* 锚点 */ /* 锚点 */
transform-origin: center; transform-origin: center;
/* 动画 */ /* 动画 */
animation: bird-fly 5s infinite; animation: bird-fly 5s infinite;
} }
@keyframes bird-fly { @keyframes bird-fly {
0% { 0% {
top: 50%; top: 50%;
transform: scale(1, 1) rotate(45deg) transform: scale(1, 1) rotate(45deg)
} }
50% { 50% {
top: 30%; top: 30%;
transform: scale(1, 1) rotate(45deg) transform: scale(1, 1) rotate(45deg)
} }
100% { 100% {
top: 50%; top: 50%;
transform: scale(1, 1) rotate(45deg) transform: scale(1, 1) rotate(45deg)
} }
} }
</style> </style>
</head> </head>
<body> <body>
<div id="bird"></div> <div id="bird"></div>
</body> </body>
</html> </html>
# 会飞的Bird # 会飞的Bird
引入概念:使用`动画`改变显示对象属性 引入概念:使用`动画`改变显示对象属性
动画是一个游戏重要的组成部分,如果没有动画,游戏将变得枯燥无味 动画是一个游戏重要的组成部分,如果没有动画,游戏将变得枯燥无味
本小节创建一个简单keyframe动画,以此来理解游戏中的动画 本小节创建一个简单keyframe动画,以此来理解游戏中的动画
在style标签中加入一段动画的代码 在style标签中加入一段动画的代码
```css ```css
@keyframes bird-fly { @keyframes bird-fly {
0% { 0% {
top: 50%; top: 50%;
transform: scale(1, 1) rotate(45deg) transform: scale(1, 1) rotate(45deg)
} }
50% { 50% {
top: 30%; top: 30%;
transform: scale(1, 1) rotate(45deg) transform: scale(1, 1) rotate(45deg)
} }
100% { 100% {
top: 50%; top: 50%;
transform: scale(1, 1) rotate(45deg) transform: scale(1, 1) rotate(45deg)
} }
} }
``` ```
把刚刚我们写的css动画应用在我们的Bird上 把刚刚我们写的css动画应用在我们的Bird上
```css ```css
#bird { #bird {
[...] [...]
/* 动画 */ /* 动画 */
animation: bird-fly 5s infinite; animation: bird-fly 5s infinite;
} }
``` ```
经过一顿操作,我们得到了自己一只活生生的鸟 经过一顿操作,我们得到了自己一只活生生的鸟
但是这样的方式似乎无法自由的操作 但是这样的方式似乎无法自由的操作
想想王者荣耀,你的英雄该如何送塔? 想想王者荣耀,你的英雄该如何送塔?
如下图: 如下图:
![02_1.gif](../images/02_1.gif) ![02_1.gif](../images/02_1.gif)
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh"> <html lang="zh">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>03.点哪飞哪</title> <title>03.点哪飞哪</title>
<style> <style>
html, body { html, body {
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
} }
#bird { #bird {
/* 大小 */ /* 大小 */
width: 50px; width: 50px;
height: 50px; height: 50px;
/* 颜色 */ /* 颜色 */
background-color: #f00; background-color: #f00;
/* 位置 */ /* 位置 */
position: absolute; position: absolute;
/* 旋转、缩放等属性 */ /* 旋转、缩放等属性 */
transform: scale(1, 1) rotate(45deg); transform: scale(1, 1) rotate(45deg);
/* 锚点 */ /* 锚点 */
transform-origin: center; transform-origin: center;
} }
</style> </style>
</head> </head>
<body> <body>
<div id="bird"></div> <div id="bird"></div>
</body> </body>
<script> <script>
const bird = document.getElementById("bird"); const bird = document.getElementById("bird");
const speed = 10; // bird的速度 每次移动多少距离 const speed = 10; // bird的速度 每次移动多少距离
const pos = { // bird的位置 bird当前的位置 const pos = { // bird的位置 bird当前的位置
top: 150, top: 150,
left: 150, left: 150,
} }
const clickPos = { // 鼠标点击的位置 bird将要的到达的位置 const clickPos = { // 鼠标点击的位置 bird将要的到达的位置
top: 150, top: 150,
left: 150, left: 150,
} }
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置 // 使用mousedown监听鼠标按下,并获得鼠标点击的位置
const mouseDown = (e) => { const mouseDown = (e) => {
// 直接改变bird的位置,没有动画 // 直接改变bird的位置,没有动画
// bird.style.top = e.clientY + "px"; // bird.style.top = e.clientY + "px";
// bird.style.left = e.clientX + "px"; // bird.style.left = e.clientX + "px";
// 记录bird将要到达的位置,使用动画慢慢到达 // 记录bird将要到达的位置,使用动画慢慢到达
clickPos.top = e.clientY; clickPos.top = e.clientY;
clickPos.left = e.clientX; clickPos.left = e.clientX;
} }
document.addEventListener('mousedown', mouseDown); document.addEventListener('mousedown', mouseDown);
setInterval(() => { setInterval(() => {
/** /**
* 计算Bird的位置(数据更新) * 计算Bird的位置(数据更新)
*/ */
// 计算新的top // 计算新的top
if (pos.top !== clickPos.top) { if (pos.top !== clickPos.top) {
const dis = clickPos.top - pos.top; // 计算差值 const dis = clickPos.top - pos.top; // 计算差值
const dir = dis > 0 ? 1 : -1; // 计算在top上移动的方向 1 正向移动 或 -1 反向移动; const dir = dis > 0 ? 1 : -1; // 计算在top上移动的方向 1 正向移动 或 -1 反向移动;
// 如果速度过快,本次移动直接过头了(即差值<速度),就直接移动到指定top // 如果速度过快,本次移动直接过头了(即差值<速度),就直接移动到指定top
if (Math.abs(dis) < speed) { if (Math.abs(dis) < speed) {
pos.top = clickPos.top; pos.top = clickPos.top;
} else { } else {
pos.top = pos.top + dir * speed; // 计算新的top,新的位置 = 之前的位置 + 方向 * 速度 pos.top = pos.top + dir * speed; // 计算新的top,新的位置 = 之前的位置 + 方向 * 速度
} }
} }
// 用相同的方式计算left // 用相同的方式计算left
if (pos.left !== clickPos.left) { if (pos.left !== clickPos.left) {
const dis = clickPos.left - pos.left; const dis = clickPos.left - pos.left;
const dir = dis > 0 ? 1 : -1; const dir = dis > 0 ? 1 : -1;
if (Math.abs(dis) < speed) { if (Math.abs(dis) < speed) {
pos.left = clickPos.left; pos.left = clickPos.left;
} else { } else {
pos.left = pos.left + dir * speed; pos.left = pos.left + dir * speed;
} }
} }
/** /**
* 更新显示对象位置(渲染) * 更新显示对象位置(渲染)
*/ */
bird.style.top = pos.top + "px"; bird.style.top = pos.top + "px";
bird.style.left = pos.left + "px"; bird.style.left = pos.left + "px";
}, 10); }, 10);
</script> </script>
</html> </html>
# 点哪飞哪 # 点哪飞哪
引入概念:`输入输出` `游戏主循环` 引入概念:`输入输出` `游戏主循环`
经过上一节的思考,我们已经得到了一只不听话的Bird和一份王者荣耀快速送塔的方案 那就是```输入输出``` 经过上一节的思考,我们已经得到了一只不听话的Bird和一份王者荣耀快速送塔的方案 那就是```输入输出```
本节的内容是训练Bird,让它可以点哪飞哪 本节的内容是训练Bird,让它可以点哪飞哪
## 一、输入 ## 一、输入
```javascript ```javascript
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置 // 使用mousedown监听鼠标按下,并获得鼠标点击的位置
const mouseDown = (e) => { const mouseDown = (e) => {
// 直接改变bird的位置,没有动画 // 直接改变bird的位置,没有动画
bird.style.top = e.clientY + "px"; bird.style.top = e.clientY + "px";
bird.style.left = e.clientX + "px"; bird.style.left = e.clientX + "px";
} }
document.addEventListener('mousedown', mouseDown); document.addEventListener('mousedown', mouseDown);
``` ```
再次运行案例,点击屏幕 再次运行案例,点击屏幕
我们的Bird不仅听话,还学会了闪现,还没有冷却。 我们的Bird不仅听话,还学会了闪现,还没有冷却。
开挂了,一顿操作,被举报封号,这怎么行,开挂也要演一演 开挂了,一顿操作,被举报封号,这怎么行,开挂也要演一演
![03_1.gif](../images/03_1.gif) ![03_1.gif](../images/03_1.gif)
## 二、主循环 ## 二、主循环
`主循环`是游戏的基本要素之一 `主循环`是游戏的基本要素之一
如果不能很好的理解主循环,将无法真正的理解游戏 如果不能很好的理解主循环,将无法真正的理解游戏
如果你是个小白,某老师直接就教你一款游戏引擎的API,那么你可以直接拍屁股走人了 如果你是个小白,某老师直接就教你一款游戏引擎的API,那么你可以直接拍屁股走人了
在循环中,主要做两件事: 在循环中,主要做两件事:
- 1.数据更新 - 1.数据更新
- 2.渲染更新 - 2.渲染更新
### 1.创建循环 - (使用`setInterval`来模拟游戏循环) ### 1.创建循环 - (使用`setInterval`来模拟游戏循环)
隆重的介绍:`setInterval` 隆重的介绍:`setInterval`
```js ```js
setInterval(handler, timeout, ...arguments); setInterval(handler, timeout, ...arguments);
``` ```
我们需要做以下几件事: 我们需要做以下几件事:
- 1.给鸟一个飞行速度`speed`即每次循环飞多少距离 - 1.给鸟一个飞行速度`speed`即每次循环飞多少距离
用一个变量`pos`记录Bird当前的位置 用一个变量`pos`记录Bird当前的位置
用一个变量`clickPos`记录鸟要飞到的位置即鼠标点击的位置 用一个变量`clickPos`记录鸟要飞到的位置即鼠标点击的位置
```javascript ```javascript
const speed = 10; // bird的速度 每次移动多少距离 const speed = 10; // bird的速度 每次移动多少距离
const pos = { // bird的位置 bird当前的位置 const pos = { // bird的位置 bird当前的位置
top: 0, top: 0,
left: 0, left: 0,
} }
const clickPos = { // 鼠标点击的位置 bird将要的到达的位置 const clickPos = { // 鼠标点击的位置 bird将要的到达的位置
top: 0, top: 0,
left: 0, left: 0,
} }
``` ```
- 2.在点击鼠标后记录需要到达的位置而不是立即改变 - 2.在点击鼠标后记录需要到达的位置而不是立即改变
```javascript ```javascript
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置 // 使用mousedown监听鼠标按下,并获得鼠标点击的位置
const mouseDown = (e) => { const mouseDown = (e) => {
// 直接改变bird的位置,没有动画 // 直接改变bird的位置,没有动画
// bird.style.top = e.clientY + "px"; // bird.style.top = e.clientY + "px";
// bird.style.left = e.clientX + "px"; // bird.style.left = e.clientX + "px";
// 记录bird将要到达的位置,使用动画慢慢到达 // 记录bird将要到达的位置,使用动画慢慢到达
clickPos.top = e.clientY; clickPos.top = e.clientY;
clickPos.left = e.clientX; clickPos.left = e.clientX;
} }
``` ```
- 3.在循环中计算当前循环之后鸟到达的位置 - 3.在循环中计算当前循环之后鸟到达的位置
- 4.更新鸟的位置 - 4.更新鸟的位置
```javascript ```javascript
setInterval(() => { setInterval(() => {
/** /**
* 计算Bird的位置(数据更新) * 计算Bird的位置(数据更新)
*/ */
// 计算新的top // 计算新的top
if (pos.top !== clickPos.top) { if (pos.top !== clickPos.top) {
const dis = clickPos.top - pos.top; // 计算差值 const dis = clickPos.top - pos.top; // 计算差值
const dir = dis > 0 ? 1 : -1; // 计算在top上移动的方向 1 正向移动 或 -1 反向移动; const dir = dis > 0 ? 1 : -1; // 计算在top上移动的方向 1 正向移动 或 -1 反向移动;
// 如果速度过快,本次移动直接过头了(即差值<速度),就直接移动到指定top // 如果速度过快,本次移动直接过头了(即差值<速度),就直接移动到指定top
if (Math.abs(dis) < speed) { if (Math.abs(dis) < speed) {
pos.top = clickPos.top; pos.top = clickPos.top;
} else { } else {
pos.top = pos.top + dir * speed; // 计算新的top,新的位置 = 之前的位置 + 方向 * 速度 pos.top = pos.top + dir * speed; // 计算新的top,新的位置 = 之前的位置 + 方向 * 速度
} }
} }
// 用相同的方式计算left // 用相同的方式计算left
if (pos.left !== clickPos.left) { if (pos.left !== clickPos.left) {
const dis = clickPos.left - pos.left; const dis = clickPos.left - pos.left;
const dir = dis > 0 ? 1 : -1; const dir = dis > 0 ? 1 : -1;
if (Math.abs(dis) < speed) { if (Math.abs(dis) < speed) {
pos.left = clickPos.left; pos.left = clickPos.left;
} else { } else {
pos.left = pos.left + dir * speed; pos.left = pos.left + dir * speed;
} }
} }
/** /**
* 更新显示对象位置(渲染) * 更新显示对象位置(渲染)
*/ */
bird.style.top = pos.top + "px"; bird.style.top = pos.top + "px";
bird.style.left = pos.left + "px"; bird.style.left = pos.left + "px";
}, 10); }, 10);
``` ```
运行案例,我们得到了演员Bird 运行案例,我们得到了演员Bird
![03_2.gif](../images/03_2.gif) ![03_2.gif](../images/03_2.gif)
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh"> <html lang="zh">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>04.高性能鸟</title> <title>04.高性能鸟</title>
<style> <style>
html, body { html, body {
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
} }
#bird { #bird {
/* 大小 */ /* 大小 */
width: 50px; width: 50px;
height: 50px; height: 50px;
/* 颜色 */ /* 颜色 */
background-color: #f00; background-color: #f00;
/* 位置 */ /* 位置 */
position: absolute; position: absolute;
/* 旋转、缩放等属性 */ /* 旋转、缩放等属性 */
transform: scale(1, 1) rotate(45deg); transform: scale(1, 1) rotate(45deg);
/* 锚点 */ /* 锚点 */
transform-origin: center; transform-origin: center;
} }
</style> </style>
</head> </head>
<body> <body>
<div id="bird"></div> <div id="bird"></div>
</body> </body>
<script> <script>
const bird = document.getElementById("bird"); const bird = document.getElementById("bird");
const speed = 15; // bird的速度 每次移动多少距离 const speed = 15; // bird的速度 每次移动多少距离
const pos = { // bird的位置 bird当前的位置 const pos = { // bird的位置 bird当前的位置
top: 150, top: 150,
left: 150, left: 150,
} }
const clickPos = { // 鼠标点击的位置 bird将要的到达的位置 const clickPos = { // 鼠标点击的位置 bird将要的到达的位置
top: 150, top: 150,
left: 150, left: 150,
} }
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置 // 使用mousedown监听鼠标按下,并获得鼠标点击的位置
const mouseDown = (e) => { const mouseDown = (e) => {
// 记录bird将要到达的位置,使用动画慢慢到达 // 记录bird将要到达的位置,使用动画慢慢到达
clickPos.top = e.clientY; clickPos.top = e.clientY;
clickPos.left = e.clientX; clickPos.left = e.clientX;
} }
document.addEventListener('mousedown', mouseDown); document.addEventListener('mousedown', mouseDown);
/** /**
* 计算Bird的位置(数据更新) * 计算Bird的位置(数据更新)
*/ */
function update() { function update() {
// 计算新的top // 计算新的top
if (pos.top !== clickPos.top) { if (pos.top !== clickPos.top) {
const dis = clickPos.top - pos.top; // 计算差值 const dis = clickPos.top - pos.top; // 计算差值
const dir = dis > 0 ? 1 : -1; // 计算在top上移动的方向 1 正向移动 或 -1 反向移动; const dir = dis > 0 ? 1 : -1; // 计算在top上移动的方向 1 正向移动 或 -1 反向移动;
// 如果速度过快,本次移动直接过头了(即差值<速度),就直接移动到指定top // 如果速度过快,本次移动直接过头了(即差值<速度),就直接移动到指定top
if (Math.abs(dis) < speed) { if (Math.abs(dis) < speed) {
pos.top = clickPos.top; pos.top = clickPos.top;
} else { } else {
pos.top = pos.top + dir * speed; // 计算新的top,新的位置 = 之前的位置 + 方向 * 速度 pos.top = pos.top + dir * speed; // 计算新的top,新的位置 = 之前的位置 + 方向 * 速度
} }
} }
// 用相同的方式计算left // 用相同的方式计算left
if (pos.left !== clickPos.left) { if (pos.left !== clickPos.left) {
const dis = clickPos.left - pos.left; const dis = clickPos.left - pos.left;
const dir = dis > 0 ? 1 : -1; const dir = dis > 0 ? 1 : -1;
if (Math.abs(dis) < speed) { if (Math.abs(dis) < speed) {
pos.left = clickPos.left; pos.left = clickPos.left;
} else { } else {
pos.left = pos.left + dir * speed; pos.left = pos.left + dir * speed;
} }
} }
} }
/** /**
* 更新显示对象位置(渲染) * 更新显示对象位置(渲染)
*/ */
function render() { function render() {
bird.style.top = pos.top + "px"; bird.style.top = pos.top + "px";
bird.style.left = pos.left + "px"; bird.style.left = pos.left + "px";
} }
function loop() { function loop() {
requestAnimationFrame(loop); // 循环调用requestAnimationFrame requestAnimationFrame(loop); // 循环调用requestAnimationFrame
update(); // 先数据更新 update(); // 先数据更新
render(); // 后渲染更新 render(); // 后渲染更新
} }
loop(); loop();
</script> </script>
</html> </html>
# 高性能Bird # 高性能Bird
引入概念:`requestAnimationFrame` `帧` `更新和渲染的抽离` 引入概念:`requestAnimationFrame` `帧` `更新和渲染的抽离`
经过上一节的操作,我们已经得到了一只演员鸟 经过上一节的操作,我们已经得到了一只演员鸟
众所周知,现在的游戏玩家都鬼的很,游戏没个高帧率他都不玩。 众所周知,现在的游戏玩家都鬼的很,游戏没个高帧率他都不玩。
一般人游戏30帧就感觉比较流畅,可是这群游戏玩家,非要60帧才看着舒服 一般人游戏30帧就感觉比较流畅,可是这群游戏玩家,非要60帧才看着舒服
## 一、概念解释,什么是帧 ## 一、概念解释,什么是帧
帧率表示视频、游戏每秒钟刷新画面的次数 帧率表示视频、游戏每秒钟刷新画面的次数
60帧即每秒钟刷新60次画面 60帧即每秒钟刷新60次画面
一般电影的帧率是23.97frame/s,而游戏,低于45帧,会感到明显卡顿 一般电影的帧率是23.97frame/s,而游戏,低于45帧,会感到明显卡顿
为什么电影只需要24帧就比较流畅呢? 为什么电影只需要24帧就比较流畅呢?
是因为摄影机记录的是1/24秒的所有光线信息,可以理解电影是无数张快门时间是1/24秒的照片同步播放组成的 是因为摄影机记录的是1/24秒的所有光线信息,可以理解电影是无数张快门时间是1/24秒的照片同步播放组成的
而游戏计算出来的只有那一瞬间的画面,没有一个时间段内的光线信息,所以需要更高的帧率 而游戏计算出来的只有那一瞬间的画面,没有一个时间段内的光线信息,所以需要更高的帧率
因此我们就知道了我们的游戏需要高帧率的重要性了 因此我们就知道了我们的游戏需要高帧率的重要性了
## 二、requestAnimationFrame ## 二、requestAnimationFrame
理解了什么是帧就得出来,一个游戏60帧,每帧的时间是,1/60,约等于16.667ms; 理解了什么是帧就得出来,一个游戏60帧,每帧的时间是,1/60,约等于16.667ms;
我们大可以把`setInterval`的延时改成16.667 我们大可以把`setInterval`的延时改成16.667
但是,众所周知,浏览器提供的延时函数都不靠谱,我们无法通过它得到稳定了60帧画面 但是,众所周知,浏览器提供的延时函数都不靠谱,我们无法通过它得到稳定了60帧画面
隆重的介绍 [requestAnimationFrame](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame) 隆重的介绍 [requestAnimationFrame](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame)
[requestAnimationFrame详解](https://segmentfault.com/a/1190000020639465?utm_source=tag-newest) [requestAnimationFrame详解](https://segmentfault.com/a/1190000020639465?utm_source=tag-newest)
[被誉为神器的requestAnimationFrame](https://www.cnblogs.com/xiaohuochai/p/5777186.html) [被誉为神器的requestAnimationFrame](https://www.cnblogs.com/xiaohuochai/p/5777186.html)
这个是一个浏览器给我们提供的API 这个是一个浏览器给我们提供的API
借助这个API我们可以在浏览器这个平台很好的控制每秒60帧的帧率 借助这个API我们可以在浏览器这个平台很好的控制每秒60帧的帧率
接下来我们将数据更新和渲染更新拆分为两个函数 接下来我们将数据更新和渲染更新拆分为两个函数
```javascript ```javascript
/** /**
* 计算Bird的位置(数据更新) * 计算Bird的位置(数据更新)
*/ */
function update() { function update() {
// 计算新的top // 计算新的top
if (pos.top !== clickPos.top) { if (pos.top !== clickPos.top) {
const dis = clickPos.top - pos.top; // 计算差值 const dis = clickPos.top - pos.top; // 计算差值
const dir = dis > 0 ? 1 : -1; // 计算在top上移动的方向 1 正向移动 或 -1 反向移动; const dir = dis > 0 ? 1 : -1; // 计算在top上移动的方向 1 正向移动 或 -1 反向移动;
// 如果速度过快,本次移动直接过头了(即差值<速度),就直接移动到指定top // 如果速度过快,本次移动直接过头了(即差值<速度),就直接移动到指定top
if (Math.abs(dis) < speed) { if (Math.abs(dis) < speed) {
pos.top = clickPos.top; pos.top = clickPos.top;
} else { } else {
pos.top = pos.top + dir * speed; // 计算新的top,新的位置 = 之前的位置 + 方向 * 速度 pos.top = pos.top + dir * speed; // 计算新的top,新的位置 = 之前的位置 + 方向 * 速度
} }
} }
// 用相同的方式计算left // 用相同的方式计算left
if (pos.left !== clickPos.left) { if (pos.left !== clickPos.left) {
const dis = clickPos.left - pos.left; const dis = clickPos.left - pos.left;
const dir = dis > 0 ? 1 : -1; const dir = dis > 0 ? 1 : -1;
if (Math.abs(dis) < speed) { if (Math.abs(dis) < speed) {
pos.left = clickPos.left; pos.left = clickPos.left;
} else { } else {
pos.left = pos.left + dir * speed; pos.left = pos.left + dir * speed;
} }
} }
} }
/** /**
* 更新显示对象位置(渲染) * 更新显示对象位置(渲染)
*/ */
function render() { function render() {
bird.style.top = pos.top + "px"; bird.style.top = pos.top + "px";
bird.style.left = pos.left + "px"; bird.style.left = pos.left + "px";
} }
``` ```
然后用`requestAnimationFrame`来创建一个主循环,在主循环中依次调用这两个函数 然后用`requestAnimationFrame`来创建一个主循环,在主循环中依次调用这两个函数
```javascript ```javascript
function loop() { function loop() {
requestAnimationFrame(loop); // 循环调用requestAnimationFrame requestAnimationFrame(loop); // 循环调用requestAnimationFrame
update(); // 先数据更新 update(); // 先数据更新
render(); // 后渲染更新 render(); // 后渲染更新
} }
loop(); loop();
``` ```
再次运行案例,我们得到了更流畅,性能更高的Bird了 再次运行案例,我们得到了更流畅,性能更高的Bird了
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh"> <html lang="zh">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>05.精灵Sprite</title> <title>05.精灵Sprite</title>
<style> <style>
html, body { html, body {
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
} }
#bird { #bird {
/* 位置 */ /* 位置 */
position: absolute; position: absolute;
/* 旋转、缩放等属性 */ /* 旋转、缩放等属性 */
transform: scale(1, 1) rotate(0deg); transform: scale(1, 1) rotate(0deg);
transform-origin: center; /* 锚点 */ transform-origin: center; /* 锚点 */
} }
</style> </style>
</head> </head>
<body> <body>
<img id="bird" src="../images/bird/bird_01.png"/> <img id="bird" src="../images/bird/bird_01.png"/>
</body> </body>
<script> <script>
const bird = document.getElementById("bird"); const bird = document.getElementById("bird");
const speed = 15; // bird的速度 每次移动多少距离 const speed = 15; // bird的速度 每次移动多少距离
const pos = { // bird的位置 bird当前的位置 const pos = { // bird的位置 bird当前的位置
top: 150, top: 150,
left: 150, left: 150,
} }
const clickPos = { // 鼠标点击的位置 bird将要的到达的位置 const clickPos = { // 鼠标点击的位置 bird将要的到达的位置
top: 150, top: 150,
left: 150, left: 150,
} }
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置 // 使用mousedown监听鼠标按下,并获得鼠标点击的位置
const mouseDown = (e) => { const mouseDown = (e) => {
// 记录bird将要到达的位置,使用动画慢慢到达 // 记录bird将要到达的位置,使用动画慢慢到达
clickPos.top = e.clientY; clickPos.top = e.clientY;
clickPos.left = e.clientX; clickPos.left = e.clientX;
} }
document.addEventListener('mousedown', mouseDown); document.addEventListener('mousedown', mouseDown);
/** /**
* 计算Bird的位置(数据更新) * 计算Bird的位置(数据更新)
*/ */
function update() { function update() {
// 计算新的top // 计算新的top
if (pos.top !== clickPos.top) { if (pos.top !== clickPos.top) {
const dis = clickPos.top - pos.top; // 计算差值 const dis = clickPos.top - pos.top; // 计算差值
const dir = dis > 0 ? 1 : -1; // 计算在top上移动的方向 1 正向移动 或 -1 反向移动; const dir = dis > 0 ? 1 : -1; // 计算在top上移动的方向 1 正向移动 或 -1 反向移动;
// 如果速度过快,本次移动直接过头了(即差值<速度),就直接移动到指定top // 如果速度过快,本次移动直接过头了(即差值<速度),就直接移动到指定top
if (Math.abs(dis) < speed) { if (Math.abs(dis) < speed) {
pos.top = clickPos.top; pos.top = clickPos.top;
} else { } else {
pos.top = pos.top + dir * speed; // 计算新的top,新的位置 = 之前的位置 + 方向 * 速度 pos.top = pos.top + dir * speed; // 计算新的top,新的位置 = 之前的位置 + 方向 * 速度
} }
} }
// 用相同的方式计算left // 用相同的方式计算left
if (pos.left !== clickPos.left) { if (pos.left !== clickPos.left) {
const dis = clickPos.left - pos.left; const dis = clickPos.left - pos.left;
const dir = dis > 0 ? 1 : -1; const dir = dis > 0 ? 1 : -1;
if (Math.abs(dis) < speed) { if (Math.abs(dis) < speed) {
pos.left = clickPos.left; pos.left = clickPos.left;
} else { } else {
pos.left = pos.left + dir * speed; pos.left = pos.left + dir * speed;
} }
} }
} }
/** /**
* 更新显示对象位置(渲染) * 更新显示对象位置(渲染)
*/ */
function render() { function render() {
bird.style.top = pos.top + "px"; bird.style.top = pos.top + "px";
bird.style.left = pos.left + "px"; bird.style.left = pos.left + "px";
} }
function loop() { function loop() {
requestAnimationFrame(loop); // 循环调用requestAnimationFrame requestAnimationFrame(loop); // 循环调用requestAnimationFrame
update(); // 先数据更新 update(); // 先数据更新
render(); // 后渲染更新 render(); // 后渲染更新
} }
loop(); loop();
</script> </script>
</html> </html>
# 精灵 Sprite # 精灵 Sprite
引入概念:`精灵` `Sprite` 引入概念:`精灵` `Sprite`
经过上一节的操作,我们已经得到了一只性能很高的Bird 经过上一节的操作,我们已经得到了一只性能很高的Bird
如果一款游戏只有大红大绿的方块,那一定也是没人玩的 如果一款游戏只有大红大绿的方块,那一定也是没人玩的
所以本节我们将要介绍游戏中的一个重要的概念`精灵`,即`Sprite` 所以本节我们将要介绍游戏中的一个重要的概念`精灵`,即`Sprite`
各位可将它理解为图片即`<img/>` 各位可将它理解为图片即`<img/>`
- 1.创建一个`<img/>`模拟精灵的创建。这样将会更容易理解 - 1.创建一个`<img/>`模拟精灵的创建。这样将会更容易理解
你也可以使用一个`<div><img/></div>`, 因为在游戏开发中,`组合`优于`继承`,组件化游戏开发多采用此方案 你也可以使用一个`<div><img/></div>`, 因为在游戏开发中,`组合`优于`继承`,组件化游戏开发多采用此方案
```html ```html
<img id="bird" src="../images/bird/bird_01.png"/> <img id="bird" src="../images/bird/bird_01.png"/>
``` ```
- 2.给精灵一个样式 - 2.给精灵一个样式
```css ```css
#bird { #bird {
/* 位置 */ /* 位置 */
position: absolute; position: absolute;
/* 旋转、缩放等属性 */ /* 旋转、缩放等属性 */
transform: scale(1, 1) rotate(0deg); transform: scale(1, 1) rotate(0deg);
transform-origin: center; /* 锚点 */ transform-origin: center; /* 锚点 */
} }
``` ```
到此结束,我们已经有了第一个有头有脸的显示对象 到此结束,我们已经有了第一个有头有脸的显示对象
![05_1.png](../images/05_1.png) ![05_1.png](../images/05_1.png)
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh"> <html lang="zh">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>06.面向对象</title> <title>06.面向对象</title>
<style> <style>
html, body { html, body {
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
} }
</style> </style>
<script src="../lib/lib.js"></script> <script src="../lib/lib.js"></script>
</head> </head>
<body> <body>
<img id="bird" src="../images/bird/bird_01.png"> <img id="bird" src="../images/bird/bird_01.png">
</body> </body>
<script> <script>
class Bird extends GameObject { class Bird extends GameObject {
speed; // bird的速度 每次移动多少距离 speed; // bird的速度 每次移动多少距离
constructor(id, speed) { constructor(id, speed) {
super(id); super(id);
this.speed = speed; // 保存speed this.speed = speed; // 保存speed
// 给个默认位置 // 给个默认位置
this.top = 150; this.top = 150;
this.left = 150; this.left = 150;
} }
update() { update() {
super.update(); super.update();
const { top, left } = this; const { top, left } = this;
const speed = this.speed; const speed = this.speed;
// 计算新的top // 计算新的top
if (top !== clickPos.top) { if (top !== clickPos.top) {
const dis = clickPos.top - top; // 计算差值 const dis = clickPos.top - top; // 计算差值
const dir = dis > 0 ? 1 : -1; // 计算在top上移动的方向 1 正向移动 或 -1 反向移动; const dir = dis > 0 ? 1 : -1; // 计算在top上移动的方向 1 正向移动 或 -1 反向移动;
// 如果速度过快,本次移动直接过头了(即差值<速度),就直接移动到指定top // 如果速度过快,本次移动直接过头了(即差值<速度),就直接移动到指定top
if (Math.abs(dis) < speed) { if (Math.abs(dis) < speed) {
this.top = clickPos.top; this.top = clickPos.top;
} else { } else {
this.top = top + dir * speed; // 计算新的top,新的位置 = 之前的位置 + 方向 * 速度 this.top = top + dir * speed; // 计算新的top,新的位置 = 之前的位置 + 方向 * 速度
} }
} }
// 用相同的方式计算left // 用相同的方式计算left
if (left !== clickPos.left) { if (left !== clickPos.left) {
const dis = clickPos.left - left; const dis = clickPos.left - left;
const dir = dis > 0 ? 1 : -1; const dir = dis > 0 ? 1 : -1;
if (Math.abs(dis) < speed) { if (Math.abs(dis) < speed) {
this.left = clickPos.left; this.left = clickPos.left;
} else { } else {
this.left = left + dir * speed; this.left = left + dir * speed;
} }
} }
} }
} }
</script> </script>
<script> <script>
const clickPos = { // 鼠标点击的位置 bird将要的到达的位置 const clickPos = { // 鼠标点击的位置 bird将要的到达的位置
top: 150, top: 150,
left: 150, left: 150,
} }
const bird = new Bird("bird", 15); const bird = new Bird("bird", 15);
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置 // 使用mousedown监听鼠标按下,并获得鼠标点击的位置
const mouseDown = (e) => { const mouseDown = (e) => {
// 记录bird将要到达的位置,使用动画慢慢到达 // 记录bird将要到达的位置,使用动画慢慢到达
clickPos.top = e.clientY; clickPos.top = e.clientY;
clickPos.left = e.clientX; clickPos.left = e.clientX;
} }
document.addEventListener('mousedown', mouseDown); document.addEventListener('mousedown', mouseDown);
/** /**
* 计算Bird的位置(数据更新) * 计算Bird的位置(数据更新)
*/ */
function update() { function update() {
bird.update(); bird.update();
} }
/** /**
* 更新显示对象位置(渲染) * 更新显示对象位置(渲染)
*/ */
function render() { function render() {
bird.render(); bird.render();
} }
function loop() { function loop() {
requestAnimationFrame(loop); // 循环调用requestAnimationFrame requestAnimationFrame(loop); // 循环调用requestAnimationFrame
update(); // 先数据更新 update(); // 先数据更新
render(); // 后渲染更新 render(); // 后渲染更新
} }
loop(); loop();
</script> </script>
</html> </html>
# 面向对象 # 面向对象
引入概念:`面向对象` `游戏对象` `生命周期` 引入概念:`面向对象` `游戏对象` `生命周期`
经过上一节的操作,我们已经得到了一只有头有脸的Bird 经过上一节的操作,我们已经得到了一只有头有脸的Bird
按这样的方式写代码,写一个小游戏那要多麻烦 按这样的方式写代码,写一个小游戏那要多麻烦
所以本节我们要将游戏中通用的东西抽离出来抽象为一个游戏对象,之后所有对象的创建都继承于这个基本的游戏对象 所以本节我们要将游戏中通用的东西抽离出来抽象为一个游戏对象,之后所有对象的创建都继承于这个基本的游戏对象
## GameObject ## GameObject
- 1.创建一个js并引入,在这个js中写入如下代码 - 1.创建一个js并引入,在这个js中写入如下代码
下面的代码抽象了一个简单的GameObject 下面的代码抽象了一个简单的GameObject
```javascript ```javascript
/** /**
* 抽象了一个简单的GameObject * 抽象了一个简单的GameObject
*/ */
class GameObject { class GameObject {
id; // 绑定的dom元素的id id; // 绑定的dom元素的id
dom; // 绑定的dom元素 dom; // 绑定的dom元素
// 位置 // 位置
top = 0; top = 0;
left = 0; left = 0;
// 缩放 // 缩放
scaleX = 1; scaleX = 1;
scaleY = 1; scaleY = 1;
// 旋转 // 旋转
rotate = 0; rotate = 0;
/** /**
* 获得宽高 * 获得宽高
* @returns {{width: number, height: number}} * @returns {{width: number, height: number}}
*/ */
get size() { get size() {
return { return {
width: this.dom.clientWidth, width: this.dom.clientWidth,
height: this.dom.clientHeight, height: this.dom.clientHeight,
} }
} }
constructor(id) { constructor(id) {
this.id = id; this.id = id;
this.dom = document.getElementById(id); // 在构造函数中绑定dom元素 this.dom = document.getElementById(id); // 在构造函数中绑定dom元素
this.dom.style.position = "absolute"; this.dom.style.position = "absolute";
} }
/** /**
* 抽离数据更新部分 * 抽离数据更新部分
*/ */
update() { update() {
} }
/** /**
* 抽离渲染部分 * 抽离渲染部分
*/ */
render() { render() {
const { top, left, scaleX, scaleY, rotate } = this; const { top, left, scaleX, scaleY, rotate } = this;
this.dom.style.top = top + "px"; this.dom.style.top = top + "px";
this.dom.style.left = left + "px"; this.dom.style.left = left + "px";
this.dom.style.transform = `scale(${scaleX}, ${scaleY}) rotate(${rotate}deg)`; this.dom.style.transform = `scale(${scaleX}, ${scaleY}) rotate(${rotate}deg)`;
} }
/** /**
* 抽离销毁部分 * 抽离销毁部分
*/ */
destroy() { destroy() {
} }
} }
``` ```
- 2.用面向对象的方式创建鸟的class - 2.用面向对象的方式创建鸟的class
```javascript ```javascript
class Bird extends GameObject { class Bird extends GameObject {
speed; // bird的速度 每次移动多少距离 speed; // bird的速度 每次移动多少距离
constructor(id, speed) { constructor(id, speed) {
super(id); super(id);
this.speed = speed; // 保存speed this.speed = speed; // 保存speed
// 给个默认位置 // 给个默认位置
this.top = 150; this.top = 150;
this.left = 150; this.left = 150;
} }
update() { update() {
super.update(); super.update();
const { top, left } = this; const { top, left } = this;
const speed = this.speed; const speed = this.speed;
// 计算新的top // 计算新的top
if (top !== clickPos.top) { if (top !== clickPos.top) {
const dis = clickPos.top - top; // 计算差值 const dis = clickPos.top - top; // 计算差值
const dir = dis > 0 ? 1 : -1; // 计算在top上移动的方向 1 正向移动 或 -1 反向移动; const dir = dis > 0 ? 1 : -1; // 计算在top上移动的方向 1 正向移动 或 -1 反向移动;
// 如果速度过快,本次移动直接过头了(即差值<速度),就直接移动到指定top // 如果速度过快,本次移动直接过头了(即差值<速度),就直接移动到指定top
if (Math.abs(dis) < speed) { if (Math.abs(dis) < speed) {
this.top = clickPos.top; this.top = clickPos.top;
} else { } else {
this.top = top + dir * speed; // 计算新的top,新的位置 = 之前的位置 + 方向 * 速度 this.top = top + dir * speed; // 计算新的top,新的位置 = 之前的位置 + 方向 * 速度
} }
} }
// 用相同的方式计算left // 用相同的方式计算left
if (left !== clickPos.left) { if (left !== clickPos.left) {
const dis = clickPos.left - left; const dis = clickPos.left - left;
const dir = dis > 0 ? 1 : -1; const dir = dis > 0 ? 1 : -1;
if (Math.abs(dis) < speed) { if (Math.abs(dis) < speed) {
this.left = clickPos.left; this.left = clickPos.left;
} else { } else {
this.left = left + dir * speed; this.left = left + dir * speed;
} }
} }
} }
} }
``` ```
- 3.创建鸟的实例 - 3.创建鸟的实例
```javascript ```javascript
const bird = new Bird("bird", 15); const bird = new Bird("bird", 15);
``` ```
- 4.加入到循环中 - 4.加入到循环中
```javascript ```javascript
/** /**
* 计算Bird的位置(数据更新) * 计算Bird的位置(数据更新)
*/ */
function update() { function update() {
bird.update(); bird.update();
} }
/** /**
* 更新显示对象位置(渲染) * 更新显示对象位置(渲染)
*/ */
function render() { function render() {
bird.render(); bird.render();
} }
``` ```
再次运行案例,效果一样,但是代码维护更方便,更容易懂,且每个对象有独立的生命周期,更易于组件化,可视化 再次运行案例,效果一样,但是代码维护更方便,更容易懂,且每个对象有独立的生命周期,更易于组件化,可视化
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh"> <html lang="zh">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>07.FlppyBird-背景循环滚动</title> <title>07.FlppyBird-背景循环滚动</title>
<style> <style>
html, body { html, body {
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
} }
</style> </style>
<script src="../lib/lib.js"></script> <script src="../lib/lib.js"></script>
</head> </head>
<body> <body>
<div id="bg"> <div id="bg">
<img id="bg_1" src="../images/bird/background.png"> <img id="bg_1" src="../images/bird/background.png">
<img id="bg_2" src="../images/bird/background.png"> <img id="bg_2" src="../images/bird/background.png">
</div> </div>
<div id="land"> <div id="land">
<img id="land_1" src="../images/bird/land.png"> <img id="land_1" src="../images/bird/land.png">
<img id="land_2" src="../images/bird/land.png"> <img id="land_2" src="../images/bird/land.png">
</div> </div>
<img id="bird" src="../images/bird/bird_01.png"> <img id="bird" src="../images/bird/bird_01.png">
</body> </body>
<script> <script>
/** /**
* 屏幕宽高 * 屏幕宽高
* @type {{width: number, height: number}} * @type {{width: number, height: number}}
*/ */
const winSize = { const winSize = {
width: document.body.clientWidth, width: document.body.clientWidth,
height: document.body.clientHeight, height: document.body.clientHeight,
} }
</script> </script>
<script> <script>
class Bird extends GameObject { class Bird extends GameObject {
speed; // bird的速度 每次移动多少距离 speed; // bird的速度 每次移动多少距离
constructor(id, speed = 15) { constructor(id, speed = 15) {
super(id); super(id);
this.speed = speed; // 保存speed this.speed = speed; // 保存speed
// 给个默认位置 // 给个默认位置
this.top = 150; this.top = 150;
this.left = 150; this.left = 150;
} }
update() { update() {
super.update(); super.update();
const { top, left } = this; const { top, left } = this;
const speed = this.speed; const speed = this.speed;
// 计算新的top // 计算新的top
if (top !== clickPos.top) { if (top !== clickPos.top) {
const dis = clickPos.top - top; // 计算差值 const dis = clickPos.top - top; // 计算差值
const dir = dis > 0 ? 1 : -1; // 计算在top上移动的方向 1 正向移动 或 -1 反向移动; const dir = dis > 0 ? 1 : -1; // 计算在top上移动的方向 1 正向移动 或 -1 反向移动;
// 如果速度过快,本次移动直接过头了(即差值<速度),就直接移动到指定top // 如果速度过快,本次移动直接过头了(即差值<速度),就直接移动到指定top
if (Math.abs(dis) < speed) { if (Math.abs(dis) < speed) {
this.top = clickPos.top; this.top = clickPos.top;
} else { } else {
this.top = top + dir * speed; // 计算新的top,新的位置 = 之前的位置 + 方向 * 速度 this.top = top + dir * speed; // 计算新的top,新的位置 = 之前的位置 + 方向 * 速度
} }
} }
// 用相同的方式计算left // 用相同的方式计算left
if (left !== clickPos.left) { if (left !== clickPos.left) {
const dis = clickPos.left - left; const dis = clickPos.left - left;
const dir = dis > 0 ? 1 : -1; const dir = dis > 0 ? 1 : -1;
if (Math.abs(dis) < speed) { if (Math.abs(dis) < speed) {
this.left = clickPos.left; this.left = clickPos.left;
} else { } else {
this.left = left + dir * speed; this.left = left + dir * speed;
} }
} }
} }
} }
class ScrollMgr extends GameObject { class ScrollMgr extends GameObject {
speed; // 滚动速度 speed; // 滚动速度
bg1; // bg1 bg1; // bg1
bg2; // bg2 bg2; // bg2
constructor(id, bg1, bg2, speed = 5) { constructor(id, bg1, bg2, speed = 5) {
super(id); super(id);
this.bg1 = bg1; this.bg1 = bg1;
this.bg2 = bg2; this.bg2 = bg2;
this.speed = speed; this.speed = speed;
bg1.top = winSize.height - bg1.size.height; // 放在底部 bg1.top = winSize.height - bg1.size.height; // 放在底部
bg2.top = winSize.height - bg2.size.height; // 放在底部 bg2.top = winSize.height - bg2.size.height; // 放在底部
} }
update() { update() {
super.update(); super.update();
// 获取一些参数 // 获取一些参数
let bg1Left = this.bg1.left; let bg1Left = this.bg1.left;
const bg1Width = this.bg1.size.width; const bg1Width = this.bg1.size.width;
// 计算位置 // 计算位置
bg1Left -= this.speed; // 计算位置 bg1Left -= this.speed; // 计算位置
this.bg1.left = bg1Left; // 设置bg1的位置 this.bg1.left = bg1Left; // 设置bg1的位置
this.bg2.left = bg1Left + this.bg1.size.width; // bg2跟在bg1后面 this.bg2.left = bg1Left + this.bg1.size.width; // bg2跟在bg1后面
// 如果超出屏幕则交换bg1和bg2,为了做到循环滚动 // 如果超出屏幕则交换bg1和bg2,为了做到循环滚动
if (bg1Left <= -bg1Width) { if (bg1Left <= -bg1Width) {
const temp = this.bg1; const temp = this.bg1;
this.bg1 = this.bg2; this.bg1 = this.bg2;
this.bg2 = temp; this.bg2 = temp;
} }
} }
} }
</script> </script>
<script> <script>
const clickPos = { // 鼠标点击的位置 bird将要的到达的位置 const clickPos = { // 鼠标点击的位置 bird将要的到达的位置
top: 150, top: 150,
left: 150, left: 150,
} }
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置 // 使用mousedown监听鼠标按下,并获得鼠标点击的位置
const mouseDown = (e) => { const mouseDown = (e) => {
// 记录bird将要到达的位置,使用动画慢慢到达 // 记录bird将要到达的位置,使用动画慢慢到达
clickPos.top = e.clientY; clickPos.top = e.clientY;
clickPos.left = e.clientX; clickPos.left = e.clientX;
} }
document.addEventListener('mousedown', mouseDown); document.addEventListener('mousedown', mouseDown);
</script> </script>
<script> <script>
// 创建鸟 // 创建鸟
const bird = new Bird("bird", 15); const bird = new Bird("bird", 15);
// 创建背景 // 创建背景
const bg1 = new GameObject("bg_1"); const bg1 = new GameObject("bg_1");
const bg2 = new GameObject("bg_2"); const bg2 = new GameObject("bg_2");
const bgMgr = new ScrollMgr("bg", bg1, bg2, 2); const bgMgr = new ScrollMgr("bg", bg1, bg2, 2);
// 创建地面 // 创建地面
const land1 = new GameObject("land_1"); const land1 = new GameObject("land_1");
const land2 = new GameObject("land_2"); const land2 = new GameObject("land_2");
const landMgr = new ScrollMgr("land", land1, land2, 5); const landMgr = new ScrollMgr("land", land1, land2, 5);
// 将背景放在地面的上面,因为默认top是0,子节点在内部定位在底部,所以只需要把背景定位在负的land的高度就可以了 // 将背景放在地面的上面,因为默认top是0,子节点在内部定位在底部,所以只需要把背景定位在负的land的高度就可以了
bgMgr.top = -land1.size.height; bgMgr.top = -land1.size.height;
/** /**
* 数据更新 * 数据更新
*/ */
function update() { function update() {
// bird更新 // bird更新
bird.update(); bird.update();
// 背景更新 // 背景更新
bgMgr.update(); bgMgr.update();
bg1.update(); bg1.update();
bg2.update(); bg2.update();
// 地面更新 // 地面更新
landMgr.update(); landMgr.update();
land1.update(); land1.update();
land2.update(); land2.update();
} }
/** /**
* 渲染更新 * 渲染更新
*/ */
function render() { function render() {
// bird渲染 // bird渲染
bird.render(); bird.render();
// 背景渲染 // 背景渲染
bgMgr.render(); bgMgr.render();
bg1.render(); bg1.render();
bg2.render(); bg2.render();
// 地面渲染 // 地面渲染
landMgr.render(); landMgr.render();
land1.render(); land1.render();
land2.render(); land2.render();
} }
function loop() { function loop() {
requestAnimationFrame(loop); // 循环调用requestAnimationFrame requestAnimationFrame(loop); // 循环调用requestAnimationFrame
update(); // 先数据更新 update(); // 先数据更新
render(); // 后渲染更新 render(); // 后渲染更新
} }
loop(); loop();
</script> </script>
</html> </html>
# FlppyBird - 背景循环滚动 # FlppyBird - 背景循环滚动
引入概念:`无缝滚动` `组件抽象` `游戏优化-节省内存` 引入概念:`无缝滚动` `组件抽象` `游戏优化-节省内存`
经过之前的内容我们已经在游戏开发中实践了面向对象 经过之前的内容我们已经在游戏开发中实践了面向对象
按照这样的写法,游戏开发会变得简单,代码可维护性更强,在面向对象的开发模式上,可抽象出一些通用的东西,成为组件,还可以开发可视化编辑器,提高效率 按照这样的写法,游戏开发会变得简单,代码可维护性更强,在面向对象的开发模式上,可抽象出一些通用的东西,成为组件,还可以开发可视化编辑器,提高效率
本节开始将会带大家在之前的基础上一步一步开发一款曾经举世闻名的小游戏`FlppyBird` 本节开始将会带大家在之前的基础上一步一步开发一款曾经举世闻名的小游戏`FlppyBird`
## 背景循环滚动 ## 背景循环滚动
在游戏中内存是非常宝贵的,而占用内存的资源一般是图片,音频,视频,动画文件等。 在游戏中内存是非常宝贵的,而占用内存的资源一般是图片,音频,视频,动画文件等。
FlppyBird占内存不过50MB,但是关卡背景图片却可以无限延长 FlppyBird占内存不过50MB,但是关卡背景图片却可以无限延长
> 图片占用内存的计算方式:`长 * 宽 * 每个像素的位数 / 8` > 图片占用内存的计算方式:`长 * 宽 * 每个像素的位数 / 8`
查阅图片内存计算公式,还以为百度在骗人 查阅图片内存计算公式,还以为百度在骗人
其实FlppyBird的背景图片只有320*640这么大,通过缩放等方式适配了你的手机,再通过两张图片循环滚动的方式达到背景图片无限长度的效果 其实FlppyBird的背景图片只有320*640这么大,通过缩放等方式适配了你的手机,再通过两张图片循环滚动的方式达到背景图片无限长度的效果
本节将来教大家实现背景无限滚动的效果 本节将来教大家实现背景无限滚动的效果
在上一节课的基础上添加以下html代码 在上一节课的基础上添加以下html代码
注意层级,bg应该在bird的下面 注意层级,bg应该在bird的下面
## 1.准备工作 ## 1.准备工作
```html ```html
<div id="bg"> <div id="bg">
<img id="bg_1" src="../images/bird/background.png"> <img id="bg_1" src="../images/bird/background.png">
<img id="bg_2" src="../images/bird/background.png"> <img id="bg_2" src="../images/bird/background.png">
</div> </div>
``` ```
创建一个变量,让我们可以方便的拿到屏幕宽高 创建一个变量,让我们可以方便的拿到屏幕宽高
```javascript ```javascript
/** /**
* 屏幕宽高 * 屏幕宽高
* @type {{width: number, height: number}} * @type {{width: number, height: number}}
*/ */
const winSize = { const winSize = {
width: document.body.clientWidth, width: document.body.clientWidth,
height: document.body.clientHeight, height: document.body.clientHeight,
} }
``` ```
在之前写好的GameObject类中添加一个方法,该方法用于获取这个游戏对象的宽高 在之前写好的GameObject类中添加一个方法,该方法用于获取这个游戏对象的宽高
```javascript ```javascript
/** /**
* 抽象了一个简单的GameObject * 抽象了一个简单的GameObject
*/ */
class GameObject { class GameObject {
/* ... */ /* ... */
/** /**
* 获得宽高 * 获得宽高
* @returns {{width: number, height: number}} * @returns {{width: number, height: number}}
*/ */
get size() { get size() {
return { return {
width: this.dom.clientWidth, width: this.dom.clientWidth,
height: this.dom.clientHeight, height: this.dom.clientHeight,
} }
} }
/* ... */ /* ... */
} }
``` ```
## 2.实现滚动 ## 2.实现滚动
实现一个背景管理器,继承GameObject中通用的一些生命周期 实现一个背景管理器,继承GameObject中通用的一些生命周期
```javascript ```javascript
class BgMgr extends GameObject { class BgMgr extends GameObject {
speed; // 滚动速度 speed; // 滚动速度
bg1; // bg1 bg1; // bg1
bg2; // bg2 bg2; // bg2
constructor(id, bg1, bg2, speed = 5) { constructor(id, bg1, bg2, speed = 5) {
super(id); super(id);
this.bg1 = bg1; this.bg1 = bg1;
this.bg2 = bg2; this.bg2 = bg2;
this.speed = speed; this.speed = speed;
bg1.top = winSize.height - bg1.size.height; // 放在底部 bg1.top = winSize.height - bg1.size.height; // 放在底部
bg2.top = winSize.height - bg2.size.height; // 放在底部 bg2.top = winSize.height - bg2.size.height; // 放在底部
} }
update() { update() {
super.update(); super.update();
// 获取一些参数 // 获取一些参数
let bg1Left = this.bg1.left; let bg1Left = this.bg1.left;
const bg1Width = this.bg1.size.width; const bg1Width = this.bg1.size.width;
// 计算位置 // 计算位置
bg1Left -= this.speed; // 计算位置 bg1Left -= this.speed; // 计算位置
this.bg1.left = bg1Left; // 设置bg1的位置 this.bg1.left = bg1Left; // 设置bg1的位置
this.bg2.left = bg1Left + this.bg1.size.width; // bg2跟在bg1后面 this.bg2.left = bg1Left + this.bg1.size.width; // bg2跟在bg1后面
// 如果超出屏幕则交换bg1和bg2,为了做到循环滚动 // 如果超出屏幕则交换bg1和bg2,为了做到循环滚动
if (bg1Left <= -bg1Width) { if (bg1Left <= -bg1Width) {
const temp = this.bg1; const temp = this.bg1;
this.bg1 = this.bg2; this.bg1 = this.bg2;
this.bg2 = temp; this.bg2 = temp;
} }
} }
} }
``` ```
将两张背景图定位在最底下,bg1向左移动,bg2紧跟在背景1后面,如果bg1移出了屏幕,则交换变量bg1和bg2,那么bg1将跟在bg2后面移动,实现了循环滚动 将两张背景图定位在最底下,bg1向左移动,bg2紧跟在背景1后面,如果bg1移出了屏幕,则交换变量bg1和bg2,那么bg1将跟在bg2后面移动,实现了循环滚动
将他们加入渲染和更新队列 将他们加入渲染和更新队列
```javascript ```javascript
/** /**
* 数据更新 * 数据更新
*/ */
function update() { function update() {
// bird更新 // bird更新
bird.update(); bird.update();
// 背景更新 // 背景更新
bgMgr.update(); bgMgr.update();
bg1.update(); bg1.update();
bg2.update(); bg2.update();
} }
/** /**
* 渲染更新 * 渲染更新
*/ */
function render() { function render() {
// bird渲染 // bird渲染
bird.render(); bird.render();
// 背景渲染 // 背景渲染
bgMgr.render(); bgMgr.render();
bg1.render(); bg1.render();
bg2.render(); bg2.render();
} }
``` ```
再次运行案例,发现效果已经实现 再次运行案例,发现效果已经实现
![07_1.gif](../images/07_1.gif) ![07_1.gif](../images/07_1.gif)
## 3.地面滚动 ## 3.地面滚动
在FlppyBird中还有一个地面也在滚动。 在FlppyBird中还有一个地面也在滚动。
因为我们已经实现了背景滚动,地面滚动当然和背景的逻辑一样 因为我们已经实现了背景滚动,地面滚动当然和背景的逻辑一样
但是,这就很巧了,刚才我们实现的背景滚动功能比较完善,只需要用相同的方式创建地面就可以做到地面滚动 但是,这就很巧了,刚才我们实现的背景滚动功能比较完善,只需要用相同的方式创建地面就可以做到地面滚动
于是乎,我们将BgMgr的名字改成ScrollMgr 于是乎,我们将BgMgr的名字改成ScrollMgr
```javascript ```javascript
class ScrollMgr extends GameObject { class ScrollMgr extends GameObject {
/* ... */ /* ... */
} }
``` ```
并更新代码,然后创建我们的地面,同时加入渲染列表 并更新代码,然后创建我们的地面,同时加入渲染列表
> 为地面和背景赋予不同的滚动速度可以得到远处慢,近处快的效果 > 为地面和背景赋予不同的滚动速度可以得到远处慢,近处快的效果
```javascript ```javascript
// 创建鸟 // 创建鸟
const bird = new Bird("bird", 15); const bird = new Bird("bird", 15);
// 创建背景 // 创建背景
const bg1 = new GameObject("bg_1"); const bg1 = new GameObject("bg_1");
const bg2 = new GameObject("bg_2"); const bg2 = new GameObject("bg_2");
const bgMgr = new ScrollMgr("bg", bg1, bg2, 2); const bgMgr = new ScrollMgr("bg", bg1, bg2, 2);
// 创建地面 // 创建地面
const land1 = new GameObject("land_1"); const land1 = new GameObject("land_1");
const land2 = new GameObject("land_2"); const land2 = new GameObject("land_2");
const landMgr = new ScrollMgr("land", land1, land2, 5); const landMgr = new ScrollMgr("land", land1, land2, 5);
// 将背景放在地面的上面,因为默认top是0,子节点在内部定位在底部,所以只需要把背景定位在负的land的高度就可以了 // 将背景放在地面的上面,因为默认top是0,子节点在内部定位在底部,所以只需要把背景定位在负的land的高度就可以了
bgMgr.top = -land1.size.height; bgMgr.top = -land1.size.height;
/** /**
* 数据更新 * 数据更新
*/ */
function update() { function update() {
// bird更新 // bird更新
bird.update(); bird.update();
// 背景更新 // 背景更新
bgMgr.update(); bgMgr.update();
bg1.update(); bg1.update();
bg2.update(); bg2.update();
// 地面更新 // 地面更新
landMgr.update(); landMgr.update();
land1.update(); land1.update();
land2.update(); land2.update();
} }
/** /**
* 渲染更新 * 渲染更新
*/ */
function render() { function render() {
// bird渲染 // bird渲染
bird.render(); bird.render();
// 背景渲染 // 背景渲染
bgMgr.render(); bgMgr.render();
bg1.render(); bg1.render();
bg2.render(); bg2.render();
// 地面渲染 // 地面渲染
landMgr.render(); landMgr.render();
land1.render(); land1.render();
land2.render(); land2.render();
} }
``` ```
运行案例,发现效果已经实现 运行案例,发现效果已经实现
![07_2](../images/07_2.gif) ![07_2](../images/07_2.gif)
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh"> <html lang="zh">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>08.FlppyBird-点击飞起</title> <title>08.FlppyBird-点击飞起</title>
<style> <style>
html, body { html, body {
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
} }
</style> </style>
<script src="../lib/lib.js"></script> <script src="../lib/lib.js"></script>
</head> </head>
<body> <body>
<div id="bg"> <div id="bg">
<img id="bg_1" src="../images/bird/background.png"> <img id="bg_1" src="../images/bird/background.png">
<img id="bg_2" src="../images/bird/background.png"> <img id="bg_2" src="../images/bird/background.png">
</div> </div>
<div id="land"> <div id="land">
<img id="land_1" src="../images/bird/land.png"> <img id="land_1" src="../images/bird/land.png">
<img id="land_2" src="../images/bird/land.png"> <img id="land_2" src="../images/bird/land.png">
</div> </div>
<img id="bird" src="../images/bird/bird_01.png"> <img id="bird" src="../images/bird/bird_01.png">
</body> </body>
<script> <script>
/** /**
* 屏幕宽高 * 屏幕宽高
* @type {{width: number, height: number}} * @type {{width: number, height: number}}
*/ */
const winSize = { const winSize = {
width: document.body.clientWidth, width: document.body.clientWidth,
height: document.body.clientHeight, height: document.body.clientHeight,
} }
</script> </script>
<script> <script>
class Bird extends GameObject { class Bird extends GameObject {
speed; // bird的速度 每次移动多少距离 speed; // bird的速度 每次移动多少距离
gravity; // 重力加速度 gravity; // 重力加速度
constructor(id, gravity = 0.2, speed = 0) { constructor(id, gravity = 0.2, speed = 0) {
super(id); super(id);
this.speed = speed; // 保存speed this.speed = speed; // 保存speed
this.gravity = gravity; // 保存gravity this.gravity = gravity; // 保存gravity
// 放到合适的位置 // 放到合适的位置
const { width, height } = this.size; const { width, height } = this.size;
this.top = (winSize.height - height) / 2 - 100; this.top = (winSize.height - height) / 2 - 100;
this.left = (winSize.width - width) / 2; this.left = (winSize.width - width) / 2;
} }
update() { update() {
super.update(); super.update();
// v = v0 + a * t² // v = v0 + a * t²
this.speed += this.gravity; // 速度 = 速度 + 加速度 * 时间² this.speed += this.gravity; // 速度 = 速度 + 加速度 * 时间²
this.top += this.speed; // 更新位置 this.top += this.speed; // 更新位置
} }
} }
/** /**
* 滚动器 * 滚动器
*/ */
class ScrollMgr extends GameObject { class ScrollMgr extends GameObject {
speed; // 滚动速度 speed; // 滚动速度
bg1; // bg1 bg1; // bg1
bg2; // bg2 bg2; // bg2
constructor(id, bg1, bg2, speed = 5) { constructor(id, bg1, bg2, speed = 5) {
super(id); super(id);
this.bg1 = bg1; this.bg1 = bg1;
this.bg2 = bg2; this.bg2 = bg2;
this.speed = speed; this.speed = speed;
bg1.top = winSize.height - bg1.size.height; // 放在底部 bg1.top = winSize.height - bg1.size.height; // 放在底部
bg2.top = winSize.height - bg2.size.height; // 放在底部 bg2.top = winSize.height - bg2.size.height; // 放在底部
} }
update() { update() {
super.update(); super.update();
// 获取一些参数 // 获取一些参数
let bg1Left = this.bg1.left; let bg1Left = this.bg1.left;
const bg1Width = this.bg1.size.width; const bg1Width = this.bg1.size.width;
// 计算位置 // 计算位置
bg1Left -= this.speed; // 计算位置 bg1Left -= this.speed; // 计算位置
this.bg1.left = bg1Left; // 设置bg1的位置 this.bg1.left = bg1Left; // 设置bg1的位置
this.bg2.left = bg1Left + this.bg1.size.width; // bg2跟在bg1后面 this.bg2.left = bg1Left + this.bg1.size.width; // bg2跟在bg1后面
// 如果超出屏幕则交换bg1和bg2,为了做到循环滚动 // 如果超出屏幕则交换bg1和bg2,为了做到循环滚动
if (bg1Left <= -bg1Width) { if (bg1Left <= -bg1Width) {
const temp = this.bg1; const temp = this.bg1;
this.bg1 = this.bg2; this.bg1 = this.bg2;
this.bg2 = temp; this.bg2 = temp;
} }
} }
} }
</script> </script>
<script> <script>
// 创建鸟 // 创建鸟
const bird = new Bird("bird"); const bird = new Bird("bird");
// 创建背景 // 创建背景
const bg1 = new GameObject("bg_1"); const bg1 = new GameObject("bg_1");
const bg2 = new GameObject("bg_2"); const bg2 = new GameObject("bg_2");
const bgMgr = new ScrollMgr("bg", bg1, bg2, 2); const bgMgr = new ScrollMgr("bg", bg1, bg2, 2);
// 创建地面 // 创建地面
const land1 = new GameObject("land_1"); const land1 = new GameObject("land_1");
const land2 = new GameObject("land_2"); const land2 = new GameObject("land_2");
const landMgr = new ScrollMgr("land", land1, land2, 5); const landMgr = new ScrollMgr("land", land1, land2, 5);
// 将背景放在地面的上面,因为默认top是0,子节点在内部定位在底部,所以只需要把背景定位在负的land的高度就可以了 // 将背景放在地面的上面,因为默认top是0,子节点在内部定位在底部,所以只需要把背景定位在负的land的高度就可以了
bgMgr.top = -land1.size.height; bgMgr.top = -land1.size.height;
</script> </script>
<script> <script>
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置 // 使用mousedown监听鼠标按下,并获得鼠标点击的位置
const mouseDown = (e) => { const mouseDown = (e) => {
bird.speed = -8; bird.speed = -8;
} }
document.addEventListener('mousedown', mouseDown); document.addEventListener('mousedown', mouseDown);
</script> </script>
<script> <script>
/** /**
* 数据更新 * 数据更新
*/ */
function update() { function update() {
// bird更新 // bird更新
bird.update(); bird.update();
// 背景更新 // 背景更新
bgMgr.update(); bgMgr.update();
bg1.update(); bg1.update();
bg2.update(); bg2.update();
// 地面更新 // 地面更新
landMgr.update(); landMgr.update();
land1.update(); land1.update();
land2.update(); land2.update();
} }
/** /**
* 渲染更新 * 渲染更新
*/ */
function render() { function render() {
// bird渲染 // bird渲染
bird.render(); bird.render();
// 背景渲染 // 背景渲染
bgMgr.render(); bgMgr.render();
bg1.render(); bg1.render();
bg2.render(); bg2.render();
// 地面渲染 // 地面渲染
landMgr.render(); landMgr.render();
land1.render(); land1.render();
land2.render(); land2.render();
} }
function loop() { function loop() {
requestAnimationFrame(loop); // 循环调用requestAnimationFrame requestAnimationFrame(loop); // 循环调用requestAnimationFrame
update(); // 先数据更新 update(); // 先数据更新
render(); // 后渲染更新 render(); // 后渲染更新
} }
loop(); loop();
</script> </script>
</html> </html>
# FlppyBird - 模拟重力 # FlppyBird - 模拟重力
引入概念:`模拟重力` 引入概念:`模拟重力`
上节中我们已经完善了GameObject并且增加了一个公共变量来获取屏幕的宽高,还实现了背景的滚动和地面的滚动,并且增加了远慢近快的效果 上节中我们已经完善了GameObject并且增加了一个公共变量来获取屏幕的宽高,还实现了背景的滚动和地面的滚动,并且增加了远慢近快的效果
本节将带大家来实现FlppyBird中的主角 本节将带大家来实现FlppyBird中的主角
- 简单的自由落体 - 简单的自由落体
- 点击屏幕飞起 - 点击屏幕飞起
## 1.改造Bird ## 1.改造Bird
- 1.增加`gravity`,代表重力加速度,并且把鸟的位置默认放到屏幕中间 - 1.增加`gravity`,代表重力加速度,并且把鸟的位置默认放到屏幕中间
```javascript ```javascript
class Bird extends GameObject { class Bird extends GameObject {
speed; // bird的速度 每次移动多少距离 speed; // bird的速度 每次移动多少距离
gravity; // 重力加速度 gravity; // 重力加速度
constructor(id, gravity = 0.2, speed = 0) { constructor(id, gravity = 0.2, speed = 0) {
super(id); super(id);
this.speed = speed; // 保存speed this.speed = speed; // 保存speed
this.gravity = gravity; // 保存gravity this.gravity = gravity; // 保存gravity
// 放到合适的位置 // 放到合适的位置
const { width, height } = this.size; const { width, height } = this.size;
this.top = (winSize.height - height) / 2 - 100; this.top = (winSize.height - height) / 2 - 100;
this.left = (winSize.width - width) / 2; this.left = (winSize.width - width) / 2;
} }
/* ... */ /* ... */
} }
``` ```
- 2.重写Bird的生命周期`update` - 2.重写Bird的生命周期`update`
> 速度公式 v = v0 + a * t² 速度 = 速度 + 加速度 * 时间² > 速度公式 v = v0 + a * t² 速度 = 速度 + 加速度 * 时间²
根据速度公式,我们很容易就算出了当前帧的速度,并且算出当前的位置 根据速度公式,我们很容易就算出了当前帧的速度,并且算出当前的位置
```javascript ```javascript
class Bird extends GameObject { class Bird extends GameObject {
/* ... */ /* ... */
update() { update() {
super.update(); super.update();
// v = v0 + a * t² // v = v0 + a * t²
this.speed += this.gravity; // 速度 = 速度 + 加速度 * 时间² this.speed += this.gravity; // 速度 = 速度 + 加速度 * 时间²
this.top += this.speed; // 更新位置 this.top += this.speed; // 更新位置
} }
} }
``` ```
- 3.修改Bird的创建 - 3.修改Bird的创建
因为指定了默认参数,所以无需再传入新的参数 因为指定了默认参数,所以无需再传入新的参数
```javascript ```javascript
// 创建鸟 // 创建鸟
const bird = new Bird("bird"); const bird = new Bird("bird");
``` ```
运行案例,发现Bird很快落出屏幕,一去不复返 运行案例,发现Bird很快落出屏幕,一去不复返
![08_1](../images/08_1.gif) ![08_1](../images/08_1.gif)
- 3.点击屏幕飞跃 - 3.点击屏幕飞跃
修改点击屏幕的回调函数,在点击之后修改Bird的speed属性为负就可以达到飞起的效果 修改点击屏幕的回调函数,在点击之后修改Bird的speed属性为负就可以达到飞起的效果
```javascript ```javascript
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置 // 使用mousedown监听鼠标按下,并获得鼠标点击的位置
const mouseDown = (e) => { const mouseDown = (e) => {
bird.speed = -8; bird.speed = -8;
} }
document.addEventListener('mousedown', mouseDown); document.addEventListener('mousedown', mouseDown);
``` ```
再次运行案例,点击屏幕发现Bird飞跃而起 再次运行案例,点击屏幕发现Bird飞跃而起
![08_2](../images/08_2.gif) ![08_2](../images/08_2.gif)
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh"> <html lang="zh">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>09.FlppyBird-节点树</title> <title>09.FlppyBird-节点树</title>
<style> <style>
html, body { html, body {
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
} }
</style> </style>
<script src="../lib/flppyBirdLib.js"></script> <script src="../lib/flppyBirdLib.js"></script>
</head> </head>
<body> <body>
</body> </body>
<script> <script>
/** /**
* 屏幕宽高 * 屏幕宽高
* @type {{width: number, height: number}} * @type {{width: number, height: number}}
*/ */
const winSize = { const winSize = {
width: document.body.clientWidth, width: document.body.clientWidth,
height: document.body.clientHeight, height: document.body.clientHeight,
} }
</script> </script>
<script> <script>
/** /**
* Bird * Bird
*/ */
class Bird extends Sprite { class Bird extends Sprite {
speed; // bird的速度 每次移动多少距离 speed; // bird的速度 每次移动多少距离
gravity; // 重力加速度 gravity; // 重力加速度
constructor(gravity = 0.2, speed = 0) { constructor(gravity = 0.2, speed = 0) {
super("../images/bird/bird_01.png"); super("../images/bird/bird_01.png");
this.speed = speed; // 保存speed this.speed = speed; // 保存speed
this.gravity = gravity; // 保存gravity this.gravity = gravity; // 保存gravity
} }
ready() { ready() {
super.ready(); super.ready();
// 放到合适的位置 // 放到合适的位置
const { width, height } = this.size; const { width, height } = this.size;
this.top = (winSize.height - height) / 2 - 100; this.top = (winSize.height - height) / 2 - 100;
this.left = (winSize.width - width) / 2; this.left = (winSize.width - width) / 2;
} }
update() { update() {
super.update(); super.update();
// v = v0 + a * t² // v = v0 + a * t²
this.speed += this.gravity; // 速度 = 速度 + 加速度 * 时间² this.speed += this.gravity; // 速度 = 速度 + 加速度 * 时间²
this.top += this.speed; // 更新位置 this.top += this.speed; // 更新位置
} }
} }
/** /**
* 滚动器 * 滚动器
*/ */
class ScrollMgr extends GameObject { class ScrollMgr extends GameObject {
speed; // 滚动速度 speed; // 滚动速度
bg1; // bg1 bg1; // bg1
bg2; // bg2 bg2; // bg2
constructor(bg1, bg2, speed = 5) { constructor(bg1, bg2, speed = 5) {
super(); super();
this.bg1 = bg1; this.bg1 = bg1;
this.bg2 = bg2; this.bg2 = bg2;
this.speed = speed; this.speed = speed;
this.addChild(this.bg1); this.addChild(this.bg1);
this.addChild(this.bg2); this.addChild(this.bg2);
} }
ready() { ready() {
super.ready(); super.ready();
this.bg1.top = winSize.height - this.bg1.size.height; // 放在底部 this.bg1.top = winSize.height - this.bg1.size.height; // 放在底部
this.bg2.top = winSize.height - this.bg2.size.height; // 放在底部 this.bg2.top = winSize.height - this.bg2.size.height; // 放在底部
} }
update() { update() {
super.update(); super.update();
// 获取一些参数 // 获取一些参数
let bg1Left = this.bg1.left; let bg1Left = this.bg1.left;
const bg1Width = this.bg1.size.width; const bg1Width = this.bg1.size.width;
// 计算位置 // 计算位置
bg1Left -= this.speed; // 计算位置 bg1Left -= this.speed; // 计算位置
this.bg1.left = bg1Left; // 设置bg1的位置 this.bg1.left = bg1Left; // 设置bg1的位置
this.bg2.left = bg1Left + this.bg1.size.width; // bg2跟在bg1后面 this.bg2.left = bg1Left + this.bg1.size.width; // bg2跟在bg1后面
// 如果超出屏幕则交换bg1和bg2,为了做到循环滚动 // 如果超出屏幕则交换bg1和bg2,为了做到循环滚动
if (bg1Left <= -bg1Width) { if (bg1Left <= -bg1Width) {
const temp = this.bg1; const temp = this.bg1;
this.bg1 = this.bg2; this.bg1 = this.bg2;
this.bg2 = temp; this.bg2 = temp;
} }
} }
} }
</script> </script>
<script> <script>
/** /**
* FlppyBird * FlppyBird
*/ */
class FlppyBird extends GameStage { class FlppyBird extends GameStage {
bird; bird;
bgMgr; bgMgr;
landMgr; landMgr;
async preloadRes() { async preloadRes() {
const path = "../images/bird/"; const path = "../images/bird/";
const promises = [ const promises = [
"bird_01.png", "bird_02.png", "bird_03.png", "pie.png", "bird_01.png", "bird_02.png", "bird_03.png", "pie.png",
"land.png", "background.png", "start_button.png" "land.png", "background.png", "start_button.png"
].map((v) => { ].map((v) => {
return loadImgAsync(`${path}${v}`); return loadImgAsync(`${path}${v}`);
}); });
return Promise.all(promises); return Promise.all(promises);
} }
async ready() { async ready() {
// 创建鸟 // 创建鸟
const bird = this.bird = new Bird(); const bird = this.bird = new Bird();
// 创建背景 // 创建背景
const bg1 = new Sprite("../images/bird/background.png"); const bg1 = new Sprite("../images/bird/background.png");
const bg2 = new Sprite("../images/bird/background.png"); const bg2 = new Sprite("../images/bird/background.png");
const bgMgr = this.bgMgr = new ScrollMgr(bg1, bg2, 2); const bgMgr = this.bgMgr = new ScrollMgr(bg1, bg2, 2);
// 创建地面 // 创建地面
const land1 = new Sprite("../images/bird/land.png"); const land1 = new Sprite("../images/bird/land.png");
const land2 = new Sprite("../images/bird/land.png"); const land2 = new Sprite("../images/bird/land.png");
const landMgr = this.landMgr = new ScrollMgr(land1, land2, 5); const landMgr = this.landMgr = new ScrollMgr(land1, land2, 5);
this.addChild(bgMgr); this.addChild(bgMgr);
this.addChild(landMgr); this.addChild(landMgr);
this.addChild(bird); this.addChild(bird);
// 将背景放在地面的上面,因为默认top是0,子节点在内部定位在底部,所以只需要把背景定位在负的land的高度就可以了 // 将背景放在地面的上面,因为默认top是0,子节点在内部定位在底部,所以只需要把背景定位在负的land的高度就可以了
bgMgr.top = -land1.size.height; bgMgr.top = -land1.size.height;
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置 // 使用mousedown监听鼠标按下,并获得鼠标点击的位置
document.addEventListener('mousedown', this.mouseDown); document.addEventListener('mousedown', this.mouseDown);
} }
mouseDown = () => { mouseDown = () => {
this.bird.speed = -8; this.bird.speed = -8;
} }
destroy() { destroy() {
super.destroy(); super.destroy();
document.removeEventListener('mousedown', this.mouseDown); document.removeEventListener('mousedown', this.mouseDown);
} }
} }
// 创建游戏实例 // 创建游戏实例
new FlppyBird(); new FlppyBird();
</script> </script>
</html> </html>
# FlppyBird - 节点树 # FlppyBird - 节点树
引入概念:`节点树` `封装游戏对象` 引入概念:`节点树` `封装游戏对象`
在游戏开发中可能会有千千万万个游戏对象,如果按照当前的开发模式,每个都在dom中预制,那是不可能的。 在游戏开发中可能会有千千万万个游戏对象,如果按照当前的开发模式,每个都在dom中预制,那是不可能的。
所以一般在游戏开发中,游戏有自己的节点树,统一管理节点的生命周期,数据更新和渲染。 所以一般在游戏开发中,游戏有自己的节点树,统一管理节点的生命周期,数据更新和渲染。
本节点内容将要对之前的内容进行大量改造和封装,如果之前没有游戏开发经验,可能会很难理解 本节点内容将要对之前的内容进行大量改造和封装,如果之前没有游戏开发经验,可能会很难理解
修改后的 `lib.js` 请看 `flppyBirdLib.js` 修改后的 `lib.js` 请看 `flppyBirdLib.js`
## 1.改造GameObject ## 1.改造GameObject
既然叫节点树,那么每个游戏对象应该为树上的一个节点,有子节点,和父节点 既然叫节点树,那么每个游戏对象应该为树上的一个节点,有子节点,和父节点
改造`GameObject` 改造`GameObject`
- 1.添加保存子节点的变量 `children`,和父节点的变量 `parent` - 1.添加保存子节点的变量 `children`,和父节点的变量 `parent`
- 2.添加生命周期函数 `start` `ready` - 2.添加生命周期函数 `start` `ready`
- 3.封装添加子节点`addChild`和删除子节点函数`removeChild` - 3.封装添加子节点`addChild`和删除子节点函数`removeChild`
- 4.在数据更新和渲染更新中加入子节点的更新和渲染 - 4.在数据更新和渲染更新中加入子节点的更新和渲染
- 5.将dom节点改为动态创建并支持在构造函数中传入类型,支持节点创建不同类型的dom元素 - 5.将dom节点改为动态创建并支持在构造函数中传入类型,支持节点创建不同类型的dom元素
```javascript ```javascript
class GameObject { class GameObject {
dom; // 绑定的dom元素 dom; // 绑定的dom元素
children = []; // 子节点 children = []; // 子节点
parent; // 父节点 parent; // 父节点
/* ... */ /* ... */
constructor(type = "div") { constructor(type = "div") {
this.dom = document.createElement(type); // 基础GameObject为div,Sprite为img this.dom = document.createElement(type); // 基础GameObject为div,Sprite为img
this.dom.style.position = "absolute"; this.dom.style.position = "absolute";
} }
/** /**
* 生命周期 start 加入显示列表执行此函数 * 生命周期 start 加入显示列表执行此函数
*/ */
ready() { ready() {
} }
/** /**
* 添加子节点 * 添加子节点
* @param child * @param child
*/ */
addChild(child) { addChild(child) {
// 如果是别人的子节点,则先移除再添加到自己下面 // 如果是别人的子节点,则先移除再添加到自己下面
if (child.parent) { if (child.parent) {
child.parent.removeChild(child); child.parent.removeChild(child);
} }
// 执行添加 // 执行添加
this.dom.appendChild(child.dom); this.dom.appendChild(child.dom);
this.children.push(child); this.children.push(child);
child.parent = this; child.parent = this;
// 容错:防止子类重写的start不是async函数 // 容错:防止子类重写的start不是async函数
// TODO dom无法在节点不在渲染树的上的时候拿到clientWidth等属性,故将ready放在这里 // TODO dom无法在节点不在渲染树的上的时候拿到clientWidth等属性,故将ready放在这里
child.ready(); child.ready();
return child; return child;
} }
/** /**
* 删除子节点 * 删除子节点
* @param child * @param child
*/ */
removeChild(child) { removeChild(child) {
// 不是自己的子节点就提示错误 // 不是自己的子节点就提示错误
if (child.parent !== this) { if (child.parent !== this) {
console.warn("移除的节点必须是其子集"); console.warn("移除的节点必须是其子集");
return null; return null;
} }
// 执行销毁和移除 // 执行销毁和移除
child.destroy(); child.destroy();
this.dom.removeChild(child.dom); this.dom.removeChild(child.dom);
this.children.splice(this.children.indexOf(child), 1); this.children.splice(this.children.indexOf(child), 1);
child.parent = null; child.parent = null;
return child; return child;
} }
/** /**
* 抽离数据更新部分,并更新子节点 * 抽离数据更新部分,并更新子节点
*/ */
update() { update() {
this.children.forEach((child) => { this.children.forEach((child) => {
child.update(); child.update();
}); });
} }
/** /**
* 抽离渲染部分,并渲染子节点 * 抽离渲染部分,并渲染子节点
*/ */
render() { render() {
/* ... */ /* ... */
// 添加渲染子节点部分 // 添加渲染子节点部分
this.children.forEach((child) => { this.children.forEach((child) => {
child.render(); child.render();
}); });
} }
} }
``` ```
## 2.封装`Sprite` ## 2.封装`Sprite`
之前的Sprite只是简单的继承与GameObject并且绑定节点变为一个`<img/>` 之前的Sprite只是简单的继承与GameObject并且绑定节点变为一个`<img/>`
因为我们的GameObject已经经过改造,dom节点动态创建,所以,先还要封装一个`Sprite` 因为我们的GameObject已经经过改造,dom节点动态创建,所以,先还要封装一个`Sprite`
- 创建`Sprite`类,继承`GameObject` - 创建`Sprite`类,继承`GameObject`
- 支持在构造函数中传入`src`参数,即图片的链接 - 支持在构造函数中传入`src`参数,即图片的链接
- 在构造父类的时候传入`"img"`作为`type`参数,这样父类会创建一个img标签 - 在构造父类的时候传入`"img"`作为`type`参数,这样父类会创建一个img标签
- 其他方法暂不重写 - 其他方法暂不重写
```javascript ```javascript
/** /**
* 抽象精灵Sprite * 抽象精灵Sprite
*/ */
class Sprite extends GameObject { class Sprite extends GameObject {
constructor(src = "") { constructor(src = "") {
super("img"); super("img");
this.dom.src = src; this.dom.src = src;
} }
} }
``` ```
## 3.封装`GameStage` ## 3.封装`GameStage`
GameStage将作为游戏的主画布,默认创建一个`div`容器,并添加到`body` GameStage将作为游戏的主画布,默认创建一个`div`容器,并添加到`body`
GameStage中包含游戏的主控逻辑,比如,游戏主循环,事件冒泡,事件循环,资源预加载等 GameStage中包含游戏的主控逻辑,比如,游戏主循环,事件冒泡,事件循环,资源预加载等
- 创建`GameStage`类,继承`GameObject` - 创建`GameStage`类,继承`GameObject`
- 在构造函数中吧创建的dom节点加入到`body` - 在构造函数中吧创建的dom节点加入到`body`
```javascript ```javascript
class GameStage extends GameObject { class GameStage extends GameObject {
constructor() { constructor() {
super(); super();
document.body.appendChild(this.dom); document.body.appendChild(this.dom);
this._gameStart(); this._gameStart();
this.loop(); this.loop();
} }
async _gameStart() { async _gameStart() {
await this.reloadRes(); await this.reloadRes();
this.ready(); this.ready();
} }
/** /**
* 预加载资源 * 预加载资源
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async preloadRes() { async preloadRes() {
} }
/** /**
* 主循环 * 主循环
*/ */
loop = () => { loop = () => {
requestAnimationFrame(this.loop); // 循环调用requestAnimationFrame requestAnimationFrame(this.loop); // 循环调用requestAnimationFrame
this.update(); // 先数据更新 this.update(); // 先数据更新
this.render(); // 后渲染更新 this.render(); // 后渲染更新
} }
} }
``` ```
## 4.修改 `Bird` 和 背景 的创建 ## 4.修改 `Bird` 和 背景 的创建
- 删除预制的dom节点 - 删除预制的dom节点
- 修改`Bird`的构造函数,在`super()`中传入图片链接并实现`ready`方法 - 修改`Bird`的构造函数,在`super()`中传入图片链接并实现`ready`方法
```javascript ```javascript
class Bird extends Sprite { class Bird extends Sprite {
constructor(gravity = 0.2, speed = 0) { constructor(gravity = 0.2, speed = 0) {
super("../images/bird/bird_01.png"); super("../images/bird/bird_01.png");
this.speed = speed; // 保存speed this.speed = speed; // 保存speed
this.gravity = gravity; // 保存gravity this.gravity = gravity; // 保存gravity
} }
ready() { ready() {
super.ready(); super.ready();
// 放到合适的位置 // 放到合适的位置
const { width, height } = this.size; const { width, height } = this.size;
this.top = (winSize.height - height) / 2 - 100; this.top = (winSize.height - height) / 2 - 100;
this.left = (winSize.width - width) / 2; this.left = (winSize.width - width) / 2;
} }
/* ... */ /* ... */
} }
``` ```
- 修改`ScrollMgr`的构造函数,实现`start`,将传入的两个元素加入到自己的子节点 实现`ready`,设置两个元素的位置 - 修改`ScrollMgr`的构造函数,实现`start`,将传入的两个元素加入到自己的子节点 实现`ready`,设置两个元素的位置
```javascript ```javascript
class ScrollMgr extends GameObject { class ScrollMgr extends GameObject {
speed; // 滚动速度 speed; // 滚动速度
bg1; // bg1 bg1; // bg1
bg2; // bg2 bg2; // bg2
constructor(bg1, bg2, speed = 5) { constructor(bg1, bg2, speed = 5) {
super(); super();
this.bg1 = bg1; this.bg1 = bg1;
this.bg2 = bg2; this.bg2 = bg2;
this.speed = speed; this.speed = speed;
this.addChild(this.bg1); this.addChild(this.bg1);
this.addChild(this.bg2); this.addChild(this.bg2);
} }
ready() { ready() {
super.ready(); super.ready();
this.bg1.top = winSize.height - this.bg1.size.height; // 放在底部 this.bg1.top = winSize.height - this.bg1.size.height; // 放在底部
this.bg2.top = winSize.height - this.bg2.size.height; // 放在底部 this.bg2.top = winSize.height - this.bg2.size.height; // 放在底部
} }
/* ... */ /* ... */
} }
``` ```
## 5.创建`GameStage` ## 5.创建`GameStage`
- 创建`FlppyBird`类继承`GameStage` - 创建`FlppyBird`类继承`GameStage`
- 实现`reloadRes`方法,预加载资源 - 实现`reloadRes`方法,预加载资源
- 实现`start`方法,将之前创建游戏对象的代码搬进来 - 实现`start`方法,将之前创建游戏对象的代码搬进来
- 创建`FlppyBird`实例 - 创建`FlppyBird`实例
> 下面是一个异步加载图片的方法,可以用来预加载资源 > 下面是一个异步加载图片的方法,可以用来预加载资源
```javascript ```javascript
/** /**
* 异步加载图片方法 * 异步加载图片方法
* @param src 图片路径 * @param src 图片路径
* @returns {Promise<HTMLImageElement | null>} * @returns {Promise<HTMLImageElement | null>}
*/ */
function loadImgAsync(src) { function loadImgAsync(src) {
return new Promise((resolve) => { return new Promise((resolve) => {
const img = new Image(); const img = new Image();
img.onload = () => resolve(img); img.onload = () => resolve(img);
img.onerror = () => { img.onerror = () => {
console.error(`加载资源${src}失败`); console.error(`加载资源${src}失败`);
resolve(null); resolve(null);
}; };
img.src = src; img.src = src;
}); });
} }
``` ```
```javascript ```javascript
/** /**
* FlppyBird * FlppyBird
*/ */
class FlppyBird extends GameStage { class FlppyBird extends GameStage {
bird; bird;
bgMgr; bgMgr;
landMgr; landMgr;
async preloadRes() { async preloadRes() {
const path = "../images/bird/"; const path = "../images/bird/";
const promises = [ const promises = [
"bird_01.png", "bird_02.png", "bird_03.png", "pie.png", "bird_01.png", "bird_02.png", "bird_03.png", "pie.png",
"land.png", "background.png", "start_button.png" "land.png", "background.png", "start_button.png"
].map((v) => { ].map((v) => {
return loadImgAsync(`${path}${v}`); return loadImgAsync(`${path}${v}`);
}); });
return Promise.all(promises); return Promise.all(promises);
} }
async ready() { async ready() {
// 创建鸟 // 创建鸟
const bird = this.bird = new Bird(); const bird = this.bird = new Bird();
// 创建背景 // 创建背景
const bg1 = new Sprite("../images/bird/background.png"); const bg1 = new Sprite("../images/bird/background.png");
const bg2 = new Sprite("../images/bird/background.png"); const bg2 = new Sprite("../images/bird/background.png");
const bgMgr = this.bgMgr = new ScrollMgr(bg1, bg2, 2); const bgMgr = this.bgMgr = new ScrollMgr(bg1, bg2, 2);
// 创建地面 // 创建地面
const land1 = new Sprite("../images/bird/land.png"); const land1 = new Sprite("../images/bird/land.png");
const land2 = new Sprite("../images/bird/land.png"); const land2 = new Sprite("../images/bird/land.png");
const landMgr = this.landMgr = new ScrollMgr(land1, land2, 5); const landMgr = this.landMgr = new ScrollMgr(land1, land2, 5);
this.addChild(bgMgr); this.addChild(bgMgr);
this.addChild(landMgr); this.addChild(landMgr);
this.addChild(bird); this.addChild(bird);
// 将背景放在地面的上面,因为默认top是0,子节点在内部定位在底部,所以只需要把背景定位在负的land的高度就可以了 // 将背景放在地面的上面,因为默认top是0,子节点在内部定位在底部,所以只需要把背景定位在负的land的高度就可以了
bgMgr.top = -land1.size.height; bgMgr.top = -land1.size.height;
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置 // 使用mousedown监听鼠标按下,并获得鼠标点击的位置
document.addEventListener('mousedown', this.mouseDown); document.addEventListener('mousedown', this.mouseDown);
} }
mouseDown = () => { mouseDown = () => {
this.bird.speed = -8; this.bird.speed = -8;
} }
destroy() { destroy() {
super.destroy(); super.destroy();
document.removeEventListener('mousedown', this.mouseDown); document.removeEventListener('mousedown', this.mouseDown);
} }
} }
// 创建游戏实例 // 创建游戏实例
new FlppyBird(); new FlppyBird();
``` ```
# 再次运行案例,发现效果和刚才一样,牛逼!! # 再次运行案例,发现效果和刚才一样,牛逼!!
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh"> <html lang="zh">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>10.FlppyBird-创建障碍</title> <title>10.FlppyBird-创建障碍</title>
<style> <style>
html, body { html, body {
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
} }
</style> </style>
<script src="../lib/flppyBirdLib.js"></script> <script src="../lib/flppyBirdLib.js"></script>
</head> </head>
<body> <body>
</body> </body>
<script> <script>
/** /**
* 屏幕宽高 * 屏幕宽高
* @type {{width: number, height: number}} * @type {{width: number, height: number}}
*/ */
const winSize = { const winSize = {
width: document.body.clientWidth, width: document.body.clientWidth,
height: document.body.clientHeight, height: document.body.clientHeight,
} }
</script> </script>
<script> <script>
/** /**
* Bird * Bird
*/ */
class Bird extends Sprite { class Bird extends Sprite {
speed; // bird的速度 每次移动多少距离 speed; // bird的速度 每次移动多少距离
gravity; // 重力加速度 gravity; // 重力加速度
constructor(gravity = 0.2, speed = 0) { constructor(gravity = 0.2, speed = 0) {
super("../images/bird/bird_01.png"); super("../images/bird/bird_01.png");
this.speed = speed; // 保存speed this.speed = speed; // 保存speed
this.gravity = gravity; // 保存gravity this.gravity = gravity; // 保存gravity
} }
ready() { ready() {
super.ready(); super.ready();
// 放到合适的位置 // 放到合适的位置
const { width, height } = this.size; const { width, height } = this.size;
this.top = (winSize.height - height) / 2 - 100; this.top = (winSize.height - height) / 2 - 100;
this.left = (winSize.width - width) / 2; this.left = (winSize.width - width) / 2;
} }
update() { update() {
super.update(); super.update();
// v = v0 + a * t² // v = v0 + a * t²
this.speed += this.gravity; // 速度 = 速度 + 加速度 * 时间² this.speed += this.gravity; // 速度 = 速度 + 加速度 * 时间²
this.top += this.speed; // 更新位置 this.top += this.speed; // 更新位置
} }
} }
/** /**
* 滚动器 * 滚动器
*/ */
class ScrollMgr extends GameObject { class ScrollMgr extends GameObject {
speed; // 滚动速度 speed; // 滚动速度
bg1; // bg1 bg1; // bg1
bg2; // bg2 bg2; // bg2
constructor(bg1, bg2, speed = 5) { constructor(bg1, bg2, speed = 5) {
super(); super();
this.bg1 = bg1; this.bg1 = bg1;
this.bg2 = bg2; this.bg2 = bg2;
this.speed = speed; this.speed = speed;
this.addChild(this.bg1); this.addChild(this.bg1);
this.addChild(this.bg2); this.addChild(this.bg2);
} }
ready() { ready() {
super.ready(); super.ready();
this.bg1.top = winSize.height - this.bg1.size.height; // 放在底部 this.bg1.top = winSize.height - this.bg1.size.height; // 放在底部
this.bg2.top = winSize.height - this.bg2.size.height; // 放在底部 this.bg2.top = winSize.height - this.bg2.size.height; // 放在底部
} }
update() { update() {
super.update(); super.update();
// 获取一些参数 // 获取一些参数
let bg1Left = this.bg1.left; let bg1Left = this.bg1.left;
const bg1Width = this.bg1.size.width; const bg1Width = this.bg1.size.width;
// 计算位置 // 计算位置
bg1Left -= this.speed; // 计算位置 bg1Left -= this.speed; // 计算位置
this.bg1.left = bg1Left; // 设置bg1的位置 this.bg1.left = bg1Left; // 设置bg1的位置
this.bg2.left = bg1Left + this.bg1.size.width; // bg2跟在bg1后面 this.bg2.left = bg1Left + this.bg1.size.width; // bg2跟在bg1后面
// 如果超出屏幕则交换bg1和bg2,为了做到循环滚动 // 如果超出屏幕则交换bg1和bg2,为了做到循环滚动
if (bg1Left <= -bg1Width) { if (bg1Left <= -bg1Width) {
const temp = this.bg1; const temp = this.bg1;
this.bg1 = this.bg2; this.bg1 = this.bg2;
this.bg2 = temp; this.bg2 = temp;
} }
} }
} }
</script> </script>
<script> <script>
/** /**
* Pie * Pie
*/ */
class Pie extends GameObject { class Pie extends GameObject {
up; // 钢管上部分 up; // 钢管上部分
down; // 钢管下部分 down; // 钢管下部分
constructor() { constructor() {
super(); super();
this.up = this.addChild(new Sprite("../images/bird/pie.png")); this.up = this.addChild(new Sprite("../images/bird/pie.png"));
this.down = this.addChild(new Sprite("../images/bird/pie.png")); this.down = this.addChild(new Sprite("../images/bird/pie.png"));
} }
/** /**
* 注意此函数非真正的ready函数,在被addChild的时候都会执行一次 * 注意此函数非真正的ready函数,在被addChild的时候都会执行一次
*/ */
ready() { ready() {
super.ready(); super.ready();
// 随机中间的距离 // 随机中间的距离
const dis = Math.random() * 80 + 100; const dis = Math.random() * 80 + 100;
this.down.top = this.up.size.height + dis; this.down.top = this.up.size.height + dis;
} }
// 这里重写一下size,因为子节点使用绝对定位的不计算入包围盒内,真尴尬 // 这里重写一下size,因为子节点使用绝对定位的不计算入包围盒内,真尴尬
get size() { get size() {
return { return {
width: this.up.size.width, width: this.up.size.width,
height: this.down.top + this.down.size.height height: this.down.top + this.down.size.height
} }
} }
} }
/** /**
* PieMgr * PieMgr
*/ */
class PieMgr extends GameObject { class PieMgr extends GameObject {
pieArr = []; // 所有的Pie pieArr = []; // 所有的Pie
speed; // Pie移动的速度 speed; // Pie移动的速度
delay; // 添加Pie的延时 delay; // 添加Pie的延时
constructor(speed, delay) { constructor(speed, delay) {
super(); super();
this.speed = speed; this.speed = speed;
this.delay = delay; this.delay = delay;
} }
ready() { ready() {
super.ready(); super.ready();
// 创建计时器来固定时间创建Pie // 创建计时器来固定时间创建Pie
setInterval(() => this.createPie(), this.delay); setInterval(() => this.createPie(), this.delay);
} }
/** /**
* 创建Pie * 创建Pie
*/ */
createPie() { createPie() {
const pie = this.addChild(new Pie()); const pie = this.addChild(new Pie());
this.pieArr.push(pie); // 加入列表统一管理 this.pieArr.push(pie); // 加入列表统一管理
pie.top = Math.random() * -150; // 高度随机 pie.top = Math.random() * -150; // 高度随机
pie.left = winSize.width; // 从屏幕左边出现 pie.left = winSize.width; // 从屏幕左边出现
} }
update() { update() {
super.update(); super.update();
// 所有的Pie同时向左移动 // 所有的Pie同时向左移动
const { speed, pieArr } = this; const { speed, pieArr } = this;
pieArr.forEach((pie) => { pieArr.forEach((pie) => {
pie.left -= speed; pie.left -= speed;
}); });
} }
} }
</script> </script>
<script> <script>
/** /**
* FlppyBird * FlppyBird
*/ */
class FlppyBird extends GameStage { class FlppyBird extends GameStage {
bird; bird;
bgMgr; bgMgr;
landMgr; landMgr;
pieMgr; pieMgr;
async preloadRes() { async preloadRes() {
const path = "../images/bird/"; const path = "../images/bird/";
const promises = [ const promises = [
"bird_01.png", "bird_02.png", "bird_03.png", "pie.png", "bird_01.png", "bird_02.png", "bird_03.png", "pie.png",
"land.png", "background.png", "start_button.png" "land.png", "background.png", "start_button.png"
].map((v) => { ].map((v) => {
return loadImgAsync(`${path}${v}`); return loadImgAsync(`${path}${v}`);
}); });
return Promise.all(promises); return Promise.all(promises);
} }
async ready() { async ready() {
// 创建鸟 // 创建鸟
const bird = this.bird = new Bird(); const bird = this.bird = new Bird();
// 创建背景 // 创建背景
const bg1 = new Sprite("../images/bird/background.png"); const bg1 = new Sprite("../images/bird/background.png");
const bg2 = new Sprite("../images/bird/background.png"); const bg2 = new Sprite("../images/bird/background.png");
const bgMgr = this.bgMgr = new ScrollMgr(bg1, bg2, 2); const bgMgr = this.bgMgr = new ScrollMgr(bg1, bg2, 2);
// 创建地面 // 创建地面
const land1 = new Sprite("../images/bird/land.png"); const land1 = new Sprite("../images/bird/land.png");
const land2 = new Sprite("../images/bird/land.png"); const land2 = new Sprite("../images/bird/land.png");
const landMgr = this.landMgr = new ScrollMgr(land1, land2, 4); const landMgr = this.landMgr = new ScrollMgr(land1, land2, 4);
const pieMgr = this.pieMgr = new PieMgr(4, 1000); // 创建PieMgr const pieMgr = this.pieMgr = new PieMgr(4, 1000); // 创建PieMgr
this.addChild(bgMgr); this.addChild(bgMgr);
this.addChild(pieMgr); // 加在背景和地面的中间 this.addChild(pieMgr); // 加在背景和地面的中间
this.addChild(landMgr); this.addChild(landMgr);
this.addChild(bird); this.addChild(bird);
// 将背景放在地面的上面,因为默认top是0,子节点在内部定位在底部,所以只需要把背景定位在负的land的高度就可以了 // 将背景放在地面的上面,因为默认top是0,子节点在内部定位在底部,所以只需要把背景定位在负的land的高度就可以了
bgMgr.top = -land1.size.height; bgMgr.top = -land1.size.height;
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置 // 使用mousedown监听鼠标按下,并获得鼠标点击的位置
document.addEventListener('mousedown', this.mouseDown); document.addEventListener('mousedown', this.mouseDown);
} }
mouseDown = () => { mouseDown = () => {
this.bird.speed = -8; this.bird.speed = -8;
} }
destroy() { destroy() {
super.destroy(); super.destroy();
document.removeEventListener('mousedown', this.mouseDown); document.removeEventListener('mousedown', this.mouseDown);
} }
} }
// 创建游戏实例 // 创建游戏实例
new FlppyBird(); new FlppyBird();
</script> </script>
</html> </html>
# FlppyBird - 创建障碍 # FlppyBird - 创建障碍
引入概念:`动态生成障碍` 引入概念:`动态生成障碍`
本节将在上节的内容上,实现动态添加障碍 本节将在上节的内容上,实现动态添加障碍
## 1.实现`Pie`类 ## 1.实现`Pie`类
- 实现`Pie`类,继承`GameObject` - 实现`Pie`类,继承`GameObject`
- `Pie`包含一个上的一个下的 - `Pie`包含一个上的一个下的
- 上下两个均为`Sprite`,且中间距离随机 - 上下两个均为`Sprite`,且中间距离随机
```javascript ```javascript
/** /**
* Pie * Pie
*/ */
class Pie extends GameObject { class Pie extends GameObject {
up; // 钢管上部分 up; // 钢管上部分
down; // 钢管下部分 down; // 钢管下部分
constructor() { constructor() {
super(); super();
this.up = this.addChild(new Sprite("../images/bird/pie_up.png")); this.up = this.addChild(new Sprite("../images/bird/pie_up.png"));
this.down = this.addChild(new Sprite("../images/bird/pie_down.png")); this.down = this.addChild(new Sprite("../images/bird/pie_down.png"));
} }
/** /**
* 注意此函数非真正的ready函数,在被addChild的时候都会执行一次 * 注意此函数非真正的ready函数,在被addChild的时候都会执行一次
*/ */
ready() { ready() {
super.ready(); super.ready();
// 随机中间的距离 // 随机中间的距离
const dis = Math.random() * 80 + 100; const dis = Math.random() * 80 + 100;
this.down.top = this.up.size.height + dis; this.down.top = this.up.size.height + dis;
} }
// 这里重写一下size,因为子节点使用绝对定位的不计算入包围盒内,真尴尬 // 这里重写一下size,因为子节点使用绝对定位的不计算入包围盒内,真尴尬
get size() { get size() {
return { return {
width: this.up.size.width, width: this.up.size.width,
height: this.down.top + this.down.size.height height: this.down.top + this.down.size.height
} }
} }
} }
``` ```
尝试在`FlppyBird``ready`函数中创建,得到如下效果 尝试在`FlppyBird``ready`函数中创建,得到如下效果
![10_1.png](../images/10_1.png) ![10_1.png](../images/10_1.png)
## 2.实现动态创建`Pie`且高度随机,从屏幕左边出现向左移动 ## 2.实现动态创建`Pie`且高度随机,从屏幕左边出现向左移动
- 创建`PieMgr`类管理`Pie`的创建和移动 - 创建`PieMgr`类管理`Pie`的创建和移动
- 实现创建`Pie`的方法,让`Pie`从屏幕左边出现且高度随机,并统一管理 - 实现创建`Pie`的方法,让`Pie`从屏幕左边出现且高度随机,并统一管理
- 创建定时器,固定事件创建`Pie` - 创建定时器,固定事件创建`Pie`
-`update`函数中让所有`Pie`同时向左移动 -`update`函数中让所有`Pie`同时向左移动
```javascript ```javascript
/** /**
* PieMgr * PieMgr
*/ */
class PieMgr extends GameObject { class PieMgr extends GameObject {
pieArr = []; // 所有的Pie pieArr = []; // 所有的Pie
speed; // Pie移动的速度 speed; // Pie移动的速度
delay; // 添加Pie的延时 delay; // 添加Pie的延时
constructor(speed, delay) { constructor(speed, delay) {
super(); super();
this.speed = speed; this.speed = speed;
this.delay = delay; this.delay = delay;
} }
ready() { ready() {
super.ready(); super.ready();
// 创建计时器来固定时间创建Pie // 创建计时器来固定时间创建Pie
setInterval(() => this.createPie(), this.delay); setInterval(() => this.createPie(), this.delay);
} }
/** /**
* 创建Pie * 创建Pie
*/ */
createPie() { createPie() {
const pie = this.addChild(new Pie()); const pie = this.addChild(new Pie());
this.pieArr.push(pie); // 加入列表统一管理 this.pieArr.push(pie); // 加入列表统一管理
pie.top = Math.random() * -150; // 高度随机 pie.top = Math.random() * -150; // 高度随机
pie.left = winSize.width; // 从屏幕左边出现 pie.left = winSize.width; // 从屏幕左边出现
} }
update() { update() {
super.update(); super.update();
// 所有的Pie同时向左移动 // 所有的Pie同时向左移动
const { speed, pieArr } = this; const { speed, pieArr } = this;
pieArr.forEach((pie) => { pieArr.forEach((pie) => {
pie.left -= speed; pie.left -= speed;
}); });
} }
} }
``` ```
## 3.创建`PieMgr`实例并添加到节点树上 ## 3.创建`PieMgr`实例并添加到节点树上
- 创建`PieMgr`实例 - 创建`PieMgr`实例
- 添加到节点树上,且层级在地面和背景的中间 - 添加到节点树上,且层级在地面和背景的中间
```javascript ```javascript
class FlppyBird extends GameStage { class FlppyBird extends GameStage {
pieMgr; pieMgr;
/* ... */ /* ... */
async ready() { async ready() {
// 创建背景 // 创建背景
const bg1 = new Sprite("../images/bird/background.png"); const bg1 = new Sprite("../images/bird/background.png");
const bg2 = new Sprite("../images/bird/background.png"); const bg2 = new Sprite("../images/bird/background.png");
const bgMgr = this.bgMgr = new ScrollMgr(bg1, bg2, 2); const bgMgr = this.bgMgr = new ScrollMgr(bg1, bg2, 2);
// 创建地面 // 创建地面
const land1 = new Sprite("../images/bird/land.png"); const land1 = new Sprite("../images/bird/land.png");
const land2 = new Sprite("../images/bird/land.png"); const land2 = new Sprite("../images/bird/land.png");
const landMgr = this.landMgr = new ScrollMgr(land1, land2, 4); const landMgr = this.landMgr = new ScrollMgr(land1, land2, 4);
const pieMgr = this.pieMgr = new PieMgr(4, 1000); // 创建PieMgr const pieMgr = this.pieMgr = new PieMgr(4, 1000); // 创建PieMgr
this.addChild(bgMgr); this.addChild(bgMgr);
this.addChild(pieMgr); // 加在背景和地面的中间 this.addChild(pieMgr); // 加在背景和地面的中间
this.addChild(landMgr); this.addChild(landMgr);
/* ... */ /* ... */
} }
/* ... */ /* ... */
} }
``` ```
运行案例发现,每秒创建一个`Pie`,且从屏幕右边出现,不断相左移动 运行案例发现,每秒创建一个`Pie`,且从屏幕右边出现,不断相左移动
![10_2.gif](../images/10_2.gif) ![10_2.gif](../images/10_2.gif)
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh"> <html lang="zh">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>11.FlppyBird-对象池</title> <title>11.FlppyBird-对象池</title>
<style> <style>
html, body { html, body {
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
} }
</style> </style>
<script src="../lib/flppyBirdLib.js"></script> <script src="../lib/flppyBirdLib.js"></script>
</head> </head>
<body> <body>
</body> </body>
<script> <script>
/** /**
* 屏幕宽高 * 屏幕宽高
* @type {{width: number, height: number}} * @type {{width: number, height: number}}
*/ */
const winSize = { const winSize = {
width: document.body.clientWidth, width: document.body.clientWidth,
height: document.body.clientHeight, height: document.body.clientHeight,
} }
</script> </script>
<script> <script>
/** /**
* Bird * Bird
*/ */
class Bird extends Sprite { class Bird extends Sprite {
speed; // bird的速度 每次移动多少距离 speed; // bird的速度 每次移动多少距离
gravity; // 重力加速度 gravity; // 重力加速度
constructor(gravity = 0.2, speed = 0) { constructor(gravity = 0.2, speed = 0) {
super("../images/bird/bird_01.png"); super("../images/bird/bird_01.png");
this.speed = speed; // 保存speed this.speed = speed; // 保存speed
this.gravity = gravity; // 保存gravity this.gravity = gravity; // 保存gravity
} }
ready() { ready() {
super.ready(); super.ready();
// 放到合适的位置 // 放到合适的位置
const { width, height } = this.size; const { width, height } = this.size;
this.top = (winSize.height - height) / 2 - 100; this.top = (winSize.height - height) / 2 - 100;
this.left = (winSize.width - width) / 2; this.left = (winSize.width - width) / 2;
} }
update() { update() {
super.update(); super.update();
// v = v0 + a * t² // v = v0 + a * t²
this.speed += this.gravity; // 速度 = 速度 + 加速度 * 时间² this.speed += this.gravity; // 速度 = 速度 + 加速度 * 时间²
this.top += this.speed; // 更新位置 this.top += this.speed; // 更新位置
} }
} }
/** /**
* 滚动器 * 滚动器
*/ */
class ScrollMgr extends GameObject { class ScrollMgr extends GameObject {
speed; // 滚动速度 speed; // 滚动速度
bg1; // bg1 bg1; // bg1
bg2; // bg2 bg2; // bg2
constructor(bg1, bg2, speed = 5) { constructor(bg1, bg2, speed = 5) {
super(); super();
this.bg1 = bg1; this.bg1 = bg1;
this.bg2 = bg2; this.bg2 = bg2;
this.speed = speed; this.speed = speed;
this.addChild(this.bg1); this.addChild(this.bg1);
this.addChild(this.bg2); this.addChild(this.bg2);
} }
ready() { ready() {
super.ready(); super.ready();
this.bg1.top = winSize.height - this.bg1.size.height; // 放在底部 this.bg1.top = winSize.height - this.bg1.size.height; // 放在底部
this.bg2.top = winSize.height - this.bg2.size.height; // 放在底部 this.bg2.top = winSize.height - this.bg2.size.height; // 放在底部
} }
update() { update() {
super.update(); super.update();
// 获取一些参数 // 获取一些参数
let bg1Left = this.bg1.left; let bg1Left = this.bg1.left;
const bg1Width = this.bg1.size.width; const bg1Width = this.bg1.size.width;
// 计算位置 // 计算位置
bg1Left -= this.speed; // 计算位置 bg1Left -= this.speed; // 计算位置
this.bg1.left = bg1Left; // 设置bg1的位置 this.bg1.left = bg1Left; // 设置bg1的位置
this.bg2.left = bg1Left + this.bg1.size.width; // bg2跟在bg1后面 this.bg2.left = bg1Left + this.bg1.size.width; // bg2跟在bg1后面
// 如果超出屏幕则交换bg1和bg2,为了做到循环滚动 // 如果超出屏幕则交换bg1和bg2,为了做到循环滚动
if (bg1Left <= -bg1Width) { if (bg1Left <= -bg1Width) {
const temp = this.bg1; const temp = this.bg1;
this.bg1 = this.bg2; this.bg1 = this.bg2;
this.bg2 = temp; this.bg2 = temp;
} }
} }
} }
</script> </script>
<script> <script>
/** /**
* Pie * Pie
*/ */
class Pie extends GameObject { class Pie extends GameObject {
up; // 钢管上部分 up; // 钢管上部分
down; // 钢管下部分 down; // 钢管下部分
constructor() { constructor() {
super(); super();
this.up = this.addChild(new Sprite("../images/bird/pie.png")); this.up = this.addChild(new Sprite("../images/bird/pie.png"));
this.down = this.addChild(new Sprite("../images/bird/pie.png")); this.down = this.addChild(new Sprite("../images/bird/pie.png"));
} }
/** /**
* 注意此函数非真正的ready函数,在被addChild的时候都会执行一次 * 注意此函数非真正的ready函数,在被addChild的时候都会执行一次
*/ */
ready() { ready() {
super.ready(); super.ready();
// 随机中间的距离 // 随机中间的距离
const dis = Math.random() * 80 + 100; const dis = Math.random() * 80 + 100;
this.down.top = this.up.size.height + dis; this.down.top = this.up.size.height + dis;
} }
// 这里重写一下size,因为子节点使用绝对定位的不计算入包围盒内,真尴尬 // 这里重写一下size,因为子节点使用绝对定位的不计算入包围盒内,真尴尬
get size() { get size() {
return { return {
width: this.up.size.width, width: this.up.size.width,
height: this.down.top + this.down.size.height height: this.down.top + this.down.size.height
} }
} }
} }
/** /**
* PieMgr * PieMgr
*/ */
class PieMgr extends GameObject { class PieMgr extends GameObject {
pieArr = []; // 所有的Pie pieArr = []; // 所有的Pie
speed; // Pie移动的速度 speed; // Pie移动的速度
delay; // 添加Pie的延时 delay; // 添加Pie的延时
constructor(speed, delay) { constructor(speed, delay) {
super(); super();
this.speed = speed; this.speed = speed;
this.delay = delay; this.delay = delay;
} }
ready() { ready() {
super.ready(); super.ready();
// 创建计时器来固定时间创建Pie // 创建计时器来固定时间创建Pie
setInterval(() => this.createPie(), this.delay); setInterval(() => this.createPie(), this.delay);
} }
/** /**
* 创建Pie * 创建Pie
*/ */
createPie() { createPie() {
// 使用对象池 如果对象池中取不到,说明对象池空了,需要新创建 // 使用对象池 如果对象池中取不到,说明对象池空了,需要新创建
const pie = ObjectPool.get("pie") || new Pie(); const pie = ObjectPool.get("pie") || new Pie();
this.addChild(pie); this.addChild(pie);
this.pieArr.push(pie); // 加入列表统一管理 this.pieArr.push(pie); // 加入列表统一管理
pie.top = Math.random() * -150; // 高度随机 pie.top = Math.random() * -150; // 高度随机
pie.left = winSize.width; // 从屏幕左边出现 pie.left = winSize.width; // 从屏幕左边出现
} }
update() { update() {
super.update(); super.update();
// 所有的Pie同时向左移动 // 所有的Pie同时向左移动
const { speed, pieArr } = this; const { speed, pieArr } = this;
pieArr.forEach((pie) => { pieArr.forEach((pie) => {
pie.left -= speed; pie.left -= speed;
if (pie.left <= -pie.size.width) { // 如果移出屏幕 if (pie.left <= -pie.size.width) { // 如果移出屏幕
this.pieArr.splice(this.pieArr.indexOf(pie), 1); // 从托管列表里移除 this.pieArr.splice(this.pieArr.indexOf(pie), 1); // 从托管列表里移除
this.removeChild(pie); // 从子节点移除 this.removeChild(pie); // 从子节点移除
ObjectPool.put("pie", pie); // 加入对象池 ObjectPool.put("pie", pie); // 加入对象池
} }
}); });
} }
} }
</script> </script>
<script> <script>
/** /**
* FlppyBird * FlppyBird
*/ */
class FlppyBird extends GameStage { class FlppyBird extends GameStage {
bird; bird;
bgMgr; bgMgr;
landMgr; landMgr;
pieMgr; pieMgr;
async preloadRes() { async preloadRes() {
const path = "../images/bird/"; const path = "../images/bird/";
const promises = [ const promises = [
"bird_01.png", "bird_02.png", "bird_03.png", "pie.png", "bird_01.png", "bird_02.png", "bird_03.png", "pie.png",
"land.png", "background.png", "start_button.png" "land.png", "background.png", "start_button.png"
].map((v) => { ].map((v) => {
return loadImgAsync(`${path}${v}`); return loadImgAsync(`${path}${v}`);
}); });
return Promise.all(promises); return Promise.all(promises);
} }
async ready() { async ready() {
// 创建鸟 // 创建鸟
const bird = this.bird = new Bird(); const bird = this.bird = new Bird();
// 创建背景 // 创建背景
const bg1 = new Sprite("../images/bird/background.png"); const bg1 = new Sprite("../images/bird/background.png");
const bg2 = new Sprite("../images/bird/background.png"); const bg2 = new Sprite("../images/bird/background.png");
const bgMgr = this.bgMgr = new ScrollMgr(bg1, bg2, 2); const bgMgr = this.bgMgr = new ScrollMgr(bg1, bg2, 2);
// 创建地面 // 创建地面
const land1 = new Sprite("../images/bird/land.png"); const land1 = new Sprite("../images/bird/land.png");
const land2 = new Sprite("../images/bird/land.png"); const land2 = new Sprite("../images/bird/land.png");
const landMgr = this.landMgr = new ScrollMgr(land1, land2, 4); const landMgr = this.landMgr = new ScrollMgr(land1, land2, 4);
const pieMgr = this.pieMgr = new PieMgr(4, 1000); // 创建PieMgr const pieMgr = this.pieMgr = new PieMgr(4, 1000); // 创建PieMgr
this.addChild(bgMgr); this.addChild(bgMgr);
this.addChild(pieMgr); // 加在背景和地面的中间 this.addChild(pieMgr); // 加在背景和地面的中间
this.addChild(landMgr); this.addChild(landMgr);
this.addChild(bird); this.addChild(bird);
// 将背景放在地面的上面,因为默认top是0,子节点在内部定位在底部,所以只需要把背景定位在负的land的高度就可以了 // 将背景放在地面的上面,因为默认top是0,子节点在内部定位在底部,所以只需要把背景定位在负的land的高度就可以了
bgMgr.top = -land1.size.height; bgMgr.top = -land1.size.height;
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置 // 使用mousedown监听鼠标按下,并获得鼠标点击的位置
document.addEventListener('mousedown', this.mouseDown); document.addEventListener('mousedown', this.mouseDown);
} }
mouseDown = () => { mouseDown = () => {
this.bird.speed = -8; this.bird.speed = -8;
} }
destroy() { destroy() {
super.destroy(); super.destroy();
document.removeEventListener('mousedown', this.mouseDown); document.removeEventListener('mousedown', this.mouseDown);
} }
} }
// 创建游戏实例 // 创建游戏实例
new FlppyBird(); new FlppyBird();
</script> </script>
</html> </html>
# FlppyBird - 对象池 # FlppyBird - 对象池
引入概念:`对象池` 引入概念:`对象池`
上节我们完成了障碍的动态创建 上节我们完成了障碍的动态创建
但是运行一段时间发现,dom节点树上有n个障碍,卡得一批 但是运行一段时间发现,dom节点树上有n个障碍,卡得一批
这些移出屏幕的障碍,不仅占了大量内存,而且他们其实完全不需要更新和渲染 这些移出屏幕的障碍,不仅占了大量内存,而且他们其实完全不需要更新和渲染
![11_1.png](../images/11_1.png) ![11_1.png](../images/11_1.png)
## 对象池 ## 对象池
使用对象池,回收对象,保存在池中,需要的时候不必再重新创建,只需要从池中获取就可以 使用对象池,回收对象,保存在池中,需要的时候不必再重新创建,只需要从池中获取就可以
对象移除显示列表的时候,放回池中就可以了 对象移除显示列表的时候,放回池中就可以了
- 创建`ObjectPool` - 创建`ObjectPool`
- 用一个静态变量来保存对象 - 用一个静态变量来保存对象
- 实现静态方法`put`接口,传入`name`来区分保存对象的类型,这样可以保存不同的类型的对象 - 实现静态方法`put`接口,传入`name`来区分保存对象的类型,这样可以保存不同的类型的对象
- 实现静态方法`get`接口,传入`name`来获取相应的类型的对象 - 实现静态方法`get`接口,传入`name`来获取相应的类型的对象
```javascript ```javascript
/** /**
* 一个简单的通用对象池 * 一个简单的通用对象池
*/ */
class ObjectPool { class ObjectPool {
static objs = {}; static objs = {};
static put(name, obj) { static put(name, obj) {
const pool = ObjectPool.objs[name] || (ObjectPool.objs[name] = []); const pool = ObjectPool.objs[name] || (ObjectPool.objs[name] = []);
pool.push(obj); pool.push(obj);
} }
static get(name) { static get(name) {
const pool = ObjectPool.objs[name] || (ObjectPool.objs[name] = []); const pool = ObjectPool.objs[name] || (ObjectPool.objs[name] = []);
if (pool.length <= 0) { if (pool.length <= 0) {
return null; return null;
} }
return pool.shift(); return pool.shift();
} }
} }
``` ```
## 改造`PieMgr` ## 改造`PieMgr`
- 创建`Pie`时先从对象池中获取,如果没有,则新创建 - 创建`Pie`时先从对象池中获取,如果没有,则新创建
-`Pie`移出屏幕后,从托管列表中移除,从子节点移除,并且放回对象池 -`Pie`移出屏幕后,从托管列表中移除,从子节点移除,并且放回对象池
```javascript ```javascript
class PieMgr extends GameObject { class PieMgr extends GameObject {
/* ... */ /* ... */
/** /**
* 创建Pie * 创建Pie
*/ */
createPie() { createPie() {
// 使用对象池 如果对象池中取不到,说明对象池空了,需要新创建 // 使用对象池 如果对象池中取不到,说明对象池空了,需要新创建
const pie = ObjectPool.get("pie") || new Pie(); const pie = ObjectPool.get("pie") || new Pie();
this.addChild(pie); this.addChild(pie);
this.pieArr.push(pie); // 加入列表统一管理 this.pieArr.push(pie); // 加入列表统一管理
pie.top = Math.random() * -150; // 高度随机 pie.top = Math.random() * -150; // 高度随机
pie.left = winSize.width; // 从屏幕左边出现 pie.left = winSize.width; // 从屏幕左边出现
} }
update() { update() {
super.update(); super.update();
// 所有的Pie同时向左移动 // 所有的Pie同时向左移动
const { speed, pieArr } = this; const { speed, pieArr } = this;
pieArr.forEach((pie) => { pieArr.forEach((pie) => {
pie.left -= speed; pie.left -= speed;
if (pie.left <= -pie.size.width) { // 如果移出屏幕 if (pie.left <= -pie.size.width) { // 如果移出屏幕
this.pieArr.splice(this.pieArr.indexOf(pie), 1); // 从托管列表里移除 this.pieArr.splice(this.pieArr.indexOf(pie), 1); // 从托管列表里移除
this.removeChild(pie); // 从子节点移除 this.removeChild(pie); // 从子节点移除
ObjectPool.put("pie", pie); // 加入对象池 ObjectPool.put("pie", pie); // 加入对象池
} }
}); });
} }
} }
``` ```
运行案例,挂机10分钟,一点也不卡,显示列表最多也只有两个`Pie`同时存在 运行案例,挂机10分钟,一点也不卡,显示列表最多也只有两个`Pie`同时存在
# 挂机一天也不卡,牛逼 # 挂机一天也不卡,牛逼
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh"> <html lang="zh">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>12.FlppyBird-死亡判断</title> <title>12.FlppyBird-死亡判断</title>
<style> <style>
html, body { html, body {
margin: 0; margin: 0;
padding: 0; padding: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
} }
</style> </style>
<script src="../lib/flppyBirdLib.js"></script> <script src="../lib/flppyBirdLib.js"></script>
</head> </head>
<body> <body>
</body> </body>
<script> <script>
/** /**
* 屏幕宽高 * 屏幕宽高
* @type {{width: number, height: number}} * @type {{width: number, height: number}}
*/ */
const winSize = { const winSize = {
width: document.body.clientWidth, width: document.body.clientWidth,
height: document.body.clientHeight, height: document.body.clientHeight,
} }
</script> </script>
<script> <script>
/** /**
* Bird * Bird
*/ */
class Bird extends Sprite { class Bird extends Sprite {
speed; // bird的速度 每次移动多少距离 speed; // bird的速度 每次移动多少距离
gravity; // 重力加速度 gravity; // 重力加速度
dieLine; // 死亡线 dieLine; // 死亡线
constructor(gravity = 0.2, speed = 0) { constructor(gravity = 0.2, speed = 0) {
super("../images/bird/bird_01.png"); super("../images/bird/bird_01.png");
this.speed = speed; // 保存speed this.speed = speed; // 保存speed
this.gravity = gravity; // 保存gravity this.gravity = gravity; // 保存gravity
} }
ready() { ready() {
super.ready(); super.ready();
// 放到合适的位置 // 放到合适的位置
const { width, height } = this.size; const { width, height } = this.size;
this.top = (winSize.height - height) / 2 - 100; this.top = (winSize.height - height) / 2 - 100;
this.left = (winSize.width - width) / 2; this.left = (winSize.width - width) / 2;
} }
update() { update() {
super.update(); super.update();
// v = v0 + a * t² // v = v0 + a * t²
this.speed += this.gravity; // 速度 = 速度 + 加速度 * 时间² this.speed += this.gravity; // 速度 = 速度 + 加速度 * 时间²
let top = this.top + this.speed; // 更新位置 let top = this.top + this.speed; // 更新位置
// dieLine 因为锚点在左上角所以dieLine应该减去自己的高度 // dieLine 因为锚点在左上角所以dieLine应该减去自己的高度
const dieLine = this.dieLine - this.size.height; const dieLine = this.dieLine - this.size.height;
// 如果大于dieLine了就停在dieLine // 如果大于dieLine了就停在dieLine
if (top > dieLine) { if (top > dieLine) {
top = dieLine; top = dieLine;
// 发送死亡事件 // 发送死亡事件
const event = new Event("playerDie"); const event = new Event("playerDie");
event.data = "坠机了,你死了"; event.data = "坠机了,你死了";
document.dispatchEvent(event); document.dispatchEvent(event);
} }
this.top = top; this.top = top;
} }
} }
/** /**
* 滚动器 * 滚动器
*/ */
class ScrollMgr extends GameObject { class ScrollMgr extends GameObject {
speed; // 滚动速度 speed; // 滚动速度
bg1; // bg1 bg1; // bg1
bg2; // bg2 bg2; // bg2
constructor(bg1, bg2, speed = 5) { constructor(bg1, bg2, speed = 5) {
super(); super();
this.bg1 = bg1; this.bg1 = bg1;
this.bg2 = bg2; this.bg2 = bg2;
this.speed = speed; this.speed = speed;
this.addChild(this.bg1); this.addChild(this.bg1);
this.addChild(this.bg2); this.addChild(this.bg2);
} }
ready() { ready() {
super.ready(); super.ready();
this.bg1.top = winSize.height - this.bg1.size.height; // 放在底部 this.bg1.top = winSize.height - this.bg1.size.height; // 放在底部
this.bg2.top = winSize.height - this.bg2.size.height; // 放在底部 this.bg2.top = winSize.height - this.bg2.size.height; // 放在底部
} }
update() { update() {
super.update(); super.update();
// 获取一些参数 // 获取一些参数
let bg1Left = this.bg1.left; let bg1Left = this.bg1.left;
const bg1Width = this.bg1.size.width; const bg1Width = this.bg1.size.width;
// 计算位置 // 计算位置
bg1Left -= this.speed; // 计算位置 bg1Left -= this.speed; // 计算位置
this.bg1.left = bg1Left; // 设置bg1的位置 this.bg1.left = bg1Left; // 设置bg1的位置
this.bg2.left = bg1Left + this.bg1.size.width; // bg2跟在bg1后面 this.bg2.left = bg1Left + this.bg1.size.width; // bg2跟在bg1后面
// 如果超出屏幕则交换bg1和bg2,为了做到循环滚动 // 如果超出屏幕则交换bg1和bg2,为了做到循环滚动
if (bg1Left <= -bg1Width) { if (bg1Left <= -bg1Width) {
const temp = this.bg1; const temp = this.bg1;
this.bg1 = this.bg2; this.bg1 = this.bg2;
this.bg2 = temp; this.bg2 = temp;
} }
} }
} }
</script> </script>
<script> <script>
/** /**
* Pie * Pie
*/ */
class Pie extends GameObject { class Pie extends GameObject {
up; // 钢管上部分 up; // 钢管上部分
down; // 钢管下部分 down; // 钢管下部分
constructor() { constructor() {
super(); super();
this.up = this.addChild(new Sprite("../images/bird/pie.png")); this.up = this.addChild(new Sprite("../images/bird/pie.png"));
this.down = this.addChild(new Sprite("../images/bird/pie.png")); this.down = this.addChild(new Sprite("../images/bird/pie.png"));
} }
/** /**
* 注意此函数非真正的ready函数,在被addChild的时候都会执行一次 * 注意此函数非真正的ready函数,在被addChild的时候都会执行一次
*/ */
ready() { ready() {
super.ready(); super.ready();
// 随机中间的距离 // 随机中间的距离
const dis = Math.random() * 80 + 100; const dis = Math.random() * 80 + 100;
this.down.top = this.up.size.height + dis; this.down.top = this.up.size.height + dis;
} }
// 这里重写一下size,因为子节点使用绝对定位的不计算入包围盒内,真尴尬 // 这里重写一下size,因为子节点使用绝对定位的不计算入包围盒内,真尴尬
get size() { get size() {
return { return {
width: this.up.size.width, width: this.up.size.width,
height: this.down.top + this.down.size.height height: this.down.top + this.down.size.height
} }
} }
} }
/** /**
* PieMgr * PieMgr
*/ */
class PieMgr extends GameObject { class PieMgr extends GameObject {
pieArr = []; // 所有的Pie pieArr = []; // 所有的Pie
speed; // Pie移动的速度 speed; // Pie移动的速度
delay; // 添加Pie的延时 delay; // 添加Pie的延时
bird; // 玩家 bird; // 玩家
constructor(speed, delay, bird) { constructor(speed, delay, bird) {
super(); super();
this.speed = speed; this.speed = speed;
this.delay = delay; this.delay = delay;
this.bird = bird; this.bird = bird;
} }
ready() { ready() {
super.ready(); super.ready();
// 创建计时器来固定时间创建Pie // 创建计时器来固定时间创建Pie
setInterval(() => this.createPie(), this.delay); setInterval(() => this.createPie(), this.delay);
} }
/** /**
* 创建Pie * 创建Pie
*/ */
createPie() { createPie() {
// 使用对象池 如果对象池中取不到,说明对象池空了,需要新创建 // 使用对象池 如果对象池中取不到,说明对象池空了,需要新创建
const pie = ObjectPool.get("pie") || new Pie(); const pie = ObjectPool.get("pie") || new Pie();
this.addChild(pie); this.addChild(pie);
this.pieArr.push(pie); // 加入列表统一管理 this.pieArr.push(pie); // 加入列表统一管理
pie.top = Math.random() * -150; // 高度随机 pie.top = Math.random() * -150; // 高度随机
pie.left = winSize.width; // 从屏幕左边出现 pie.left = winSize.width; // 从屏幕左边出现
} }
update() { update() {
super.update(); super.update();
// 移动 // 移动
// 所有的Pie同时向左移动 // 所有的Pie同时向左移动
const { speed, pieArr } = this; const { speed, pieArr } = this;
pieArr.forEach((pie) => { pieArr.forEach((pie) => {
pie.left -= speed; pie.left -= speed;
if (pie.left <= -pie.size.width) { // 如果移出屏幕 if (pie.left <= -pie.size.width) { // 如果移出屏幕
this.pieArr.splice(this.pieArr.indexOf(pie), 1); // 从托管列表里移除 this.pieArr.splice(this.pieArr.indexOf(pie), 1); // 从托管列表里移除
this.removeChild(pie); // 从子节点移除 this.removeChild(pie); // 从子节点移除
ObjectPool.put("pie", pie); // 加入对象池 ObjectPool.put("pie", pie); // 加入对象池
return; return;
} }
// 检查碰撞 // 检查碰撞
// 重构盒子,因为pie.up和pie.down的坐标是相对于pie的,所以要重构正方形的left和top // 重构盒子,因为pie.up和pie.down的坐标是相对于pie的,所以要重构正方形的left和top
const pieUpBox = { const pieUpBox = {
left: pie.left, left: pie.left,
top: pie.top + pie.up.top, top: pie.top + pie.up.top,
size: pie.up.size, size: pie.up.size,
} }
const pieDownBox = { const pieDownBox = {
left: pie.left, left: pie.left,
top: pie.top + pie.down.top, top: pie.top + pie.down.top,
size: pie.down.size, size: pie.down.size,
} }
if (boxCollisionTest(this.bird, pieUpBox) || boxCollisionTest(this.bird, pieDownBox)) { if (boxCollisionTest(this.bird, pieUpBox) || boxCollisionTest(this.bird, pieDownBox)) {
// 发送死亡事件 // 发送死亡事件
const event = new Event("playerDie"); const event = new Event("playerDie");
event.data = "撞到障碍,你死了"; event.data = "撞到障碍,你死了";
document.dispatchEvent(event); document.dispatchEvent(event);
} }
}); });
} }
} }
</script> </script>
<script> <script>
/** /**
* FlppyBird * FlppyBird
*/ */
class FlppyBird extends GameStage { class FlppyBird extends GameStage {
bird; bird;
bgMgr; bgMgr;
landMgr; landMgr;
pieMgr; pieMgr;
pause = false; pause = false;
async preloadRes() { async preloadRes() {
const path = "../images/bird/"; const path = "../images/bird/";
const promises = [ const promises = [
"bird_01.png", "bird_02.png", "bird_03.png", "pie.png", "bird_01.png", "bird_02.png", "bird_03.png", "pie.png",
"land.png", "background.png", "start_button.png" "land.png", "background.png", "start_button.png"
].map((v) => { ].map((v) => {
return loadImgAsync(`${path}${v}`); return loadImgAsync(`${path}${v}`);
}); });
return Promise.all(promises); return Promise.all(promises);
} }
async ready() { async ready() {
// 创建背景 // 创建背景
const bg1 = new Sprite("../images/bird/background.png"); const bg1 = new Sprite("../images/bird/background.png");
const bg2 = new Sprite("../images/bird/background.png"); const bg2 = new Sprite("../images/bird/background.png");
const bgMgr = this.bgMgr = new ScrollMgr(bg1, bg2, 2); const bgMgr = this.bgMgr = new ScrollMgr(bg1, bg2, 2);
// 创建地面 // 创建地面
const land1 = new Sprite("../images/bird/land.png"); const land1 = new Sprite("../images/bird/land.png");
const land2 = new Sprite("../images/bird/land.png"); const land2 = new Sprite("../images/bird/land.png");
const landMgr = this.landMgr = new ScrollMgr(land1, land2, 4); const landMgr = this.landMgr = new ScrollMgr(land1, land2, 4);
// 创建鸟 // 创建鸟
const bird = this.bird = new Bird(); const bird = this.bird = new Bird();
// 创建PieMgr // 创建PieMgr
const pieMgr = this.pieMgr = new PieMgr(4, 1000, bird); const pieMgr = this.pieMgr = new PieMgr(4, 1000, bird);
this.addChild(bgMgr); this.addChild(bgMgr);
this.addChild(pieMgr); // 加在背景和地面的中间 this.addChild(pieMgr); // 加在背景和地面的中间
this.addChild(landMgr); this.addChild(landMgr);
this.addChild(bird); this.addChild(bird);
// 死亡线 // 死亡线
bird.dieLine = winSize.height - land1.size.height; bird.dieLine = winSize.height - land1.size.height;
// 将背景放在地面的上面,因为默认top是0,子节点在内部定位在底部,所以只需要把背景定位在负的land的高度就可以了 // 将背景放在地面的上面,因为默认top是0,子节点在内部定位在底部,所以只需要把背景定位在负的land的高度就可以了
bgMgr.top = -land1.size.height; bgMgr.top = -land1.size.height;
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置 // 使用mousedown监听鼠标按下,并获得鼠标点击的位置
document.addEventListener("mousedown", this.mouseDown); document.addEventListener("mousedown", this.mouseDown);
document.addEventListener("playerDie", this.pauseGame); document.addEventListener("playerDie", this.pauseGame);
} }
update() { update() {
if (this.pause) return; if (this.pause) return;
super.update(); super.update();
} }
pauseGame = (e) => { pauseGame = (e) => {
console.error(e.data); console.error(e.data);
this.pause = true; this.pause = true;
} }
mouseDown = () => { mouseDown = () => {
this.bird.speed = -8; this.bird.speed = -8;
} }
destroy() { destroy() {
super.destroy(); super.destroy();
document.removeEventListener("playerDie", this.mouseDown); document.removeEventListener("playerDie", this.mouseDown);
document.removeEventListener("playerDie", this.pauseGame); document.removeEventListener("playerDie", this.pauseGame);
} }
} }
// 创建游戏实例 // 创建游戏实例
new FlppyBird(); new FlppyBird();
</script> </script>
</html> </html>
# FlppyBird - 死亡判断 # FlppyBird - 死亡判断
引入概念:`碰撞` `盒式碰撞` 引入概念:`碰撞` `盒式碰撞`
上节我们完成了障碍创建的优化,性能提升一大截 上节我们完成了障碍创建的优化,性能提升一大截
本节将完成死亡条件的判断, 本案例的碰撞检测的方式包含一些强引用,并不适用于大型项目的开发 本节将完成死亡条件的判断, 本案例的碰撞检测的方式包含一些强引用,并不适用于大型项目的开发
- ### 1.落到地面死亡 - ### 1.落到地面死亡
- ### 2.撞到障碍死亡 - ### 2.撞到障碍死亡
- ### 3.死亡暂停游戏 - ### 3.死亡暂停游戏
## 落地死亡 ## 落地死亡
首先先修改一下`Bird`类, 增加一个`dieLine`的参数,表示为死亡线,如果我们的玩家碰到这根线则判定为死亡 首先先修改一下`Bird`类, 增加一个`dieLine`的参数,表示为死亡线,如果我们的玩家碰到这根线则判定为死亡
```javascript ```javascript
class Bird extends Sprite { class Bird extends Sprite {
/* ... */ /* ... */
dieLine; // 死亡线 dieLine; // 死亡线
/* ... */ /* ... */
} }
``` ```
修改`FlppyStage`中的`ready`,在所有节点都`addChild`之后设置鸟的死亡线 修改`FlppyStage`中的`ready`,在所有节点都`addChild`之后设置鸟的死亡线
> 因为dom限制,无法在节点不在渲染的时候拿到clientWidth等属性,故要在在所有节点都`addChild`之后才设置鸟的死亡线 > 因为dom限制,无法在节点不在渲染的时候拿到clientWidth等属性,故要在在所有节点都`addChild`之后才设置鸟的死亡线
因为地面放在`landMgr`的最底下,且`landMgr`的大小和`body`一样,故`land``Bird`在同一空间下 因为地面放在`landMgr`的最底下,且`landMgr`的大小和`body`一样,故`land``Bird`在同一空间下
得到死亡线高度为`winSize.height - land1.size.height` 得到死亡线高度为`winSize.height - land1.size.height`
```javascript ```javascript
class FlppyBird extends GameStage { class FlppyBird extends GameStage {
/* ... */ /* ... */
async ready() { async ready() {
/* ... */ /* ... */
// 死亡线 // 死亡线
bird.dieLine = winSize.height - land1.size.height; bird.dieLine = winSize.height - land1.size.height;
/* ... */ /* ... */
} }
/* ... */ /* ... */
} }
``` ```
修改`Bird``update`函数 修改`Bird``update`函数
- 撞到死亡线即停留在死亡线 - 撞到死亡线即停留在死亡线
- 打印 `坠机了,你死了` - 打印 `坠机了,你死了`
> 因为锚点在左上角的关系,故在计算是否死亡时,应将dieLine减去自己的高度 > 因为锚点在左上角的关系,故在计算是否死亡时,应将dieLine减去自己的高度
```javascript ```javascript
class Bird extends Sprite { class Bird extends Sprite {
/* ... */ /* ... */
dieLine; // 死亡线 dieLine; // 死亡线
/* ... */ /* ... */
update() { update() {
super.update(); super.update();
// v = v0 + a * t² // v = v0 + a * t²
this.speed += this.gravity; // 速度 = 速度 + 加速度 * 时间² this.speed += this.gravity; // 速度 = 速度 + 加速度 * 时间²
let top = this.top + this.speed; // 更新位置 let top = this.top + this.speed; // 更新位置
// dieLine 因为锚点在左上角所以dieLine应该减去自己的高度 // dieLine 因为锚点在左上角所以dieLine应该减去自己的高度
const dieLine = this.dieLine - this.size.height; const dieLine = this.dieLine - this.size.height;
// 如果大于dieLine了就停在dieLine // 如果大于dieLine了就停在dieLine
if (top > dieLine) { if (top > dieLine) {
top = dieLine; top = dieLine;
console.log("坠机了,你死了"); console.log("坠机了,你死了");
} }
this.top = top; this.top = top;
} }
} }
``` ```
运行代码发现`bird`会停在地面,并且控制台打印 `坠机了,你死了` 运行代码发现`bird`会停在地面,并且控制台打印 `坠机了,你死了`
![12_1.png](../images/12_1.png) ![12_1.png](../images/12_1.png)
## 盒子碰撞 ## 盒子碰撞
盒子碰撞是使用游戏对象的包围盒检测是否有交集来判断是否碰撞 盒子碰撞是使用游戏对象的包围盒检测是否有交集来判断是否碰撞
将盒子映射到x轴和y轴,得到线段A1->A2, B1->B2, A3->A4, B3->B4 将盒子映射到x轴和y轴,得到线段A1->A2, B1->B2, A3->A4, B3->B4
如果A1->A2和B1->B2有交集,且A3->A4和B3->B4也有交集,则可认定两个盒子相交,即两个对象碰撞 如果A1->A2和B1->B2有交集,且A3->A4和B3->B4也有交集,则可认定两个盒子相交,即两个对象碰撞
如图 无交集 判定为无碰撞 如图 无交集 判定为无碰撞
![12_2.png](../images/12_2.png) ![12_2.png](../images/12_2.png)
如同 只有一条轴上的映射有交集 判定为无碰撞 如同 只有一条轴上的映射有交集 判定为无碰撞
![12_3.png](../images/12_3.png) ![12_3.png](../images/12_3.png)
如图 两条轴上的映射都有交集 判定为碰撞 如图 两条轴上的映射都有交集 判定为碰撞
![12_4.png](../images/12_4.png) ![12_4.png](../images/12_4.png)
代码实现 代码实现
```javascript ```javascript
/** /**
* 盒子碰撞 * 盒子碰撞
* @param rect1 {top, left, size:{width, height}} * @param rect1 {top, left, size:{width, height}}
* @param rect2 {top, left, size:{width, height}} * @param rect2 {top, left, size:{width, height}}
* @returns {boolean} * @returns {boolean}
*/ */
function boxCollisionTest(rect1, rect2) { function boxCollisionTest(rect1, rect2) {
const { top: t1, left: l1 } = rect1; const { top: t1, left: l1 } = rect1;
const { width: w1, height: h1 } = rect1.size; const { width: w1, height: h1 } = rect1.size;
const { top: t2, left: l2 } = rect2; const { top: t2, left: l2 } = rect2;
const { width: w2, height: h2 } = rect2.size; const { width: w2, height: h2 } = rect2.size;
const b1 = t1 + h1, const b1 = t1 + h1,
b2 = t2 + h2, b2 = t2 + h2,
r1 = l1 + w1, r1 = l1 + w1,
r2 = l2 + w2; r2 = l2 + w2;
return ((t1 > t2 && t1 < b2) || (t2 > t1 && t2 < b1)) // 检查 t1->b1 和 t2->b2 的交集 return ((t1 > t2 && t1 < b2) || (t2 > t1 && t2 < b1)) // 检查 t1->b1 和 t2->b2 的交集
&& ((l1 > l2 && l1 < r2) || (l2 > l1 && l2 < r1)); // 检查 l1->r1 和 l2->r2 的交集 && ((l1 > l2 && l1 < r2) || (l2 > l1 && l2 < r1)); // 检查 l1->r1 和 l2->r2 的交集
} }
``` ```
## 改造`PieMgr` ## 改造`PieMgr`
- 传入我们的主角`bird` - 传入我们的主角`bird`
- 在移动`pie`之后检查碰撞 - 在移动`pie`之后检查碰撞
```javascript ```javascript
/** /**
* PieMgr * PieMgr
*/ */
class PieMgr extends GameObject { class PieMgr extends GameObject {
/* ... */ /* ... */
bird; // 玩家 bird; // 玩家
/* ... */ /* ... */
update() { update() {
super.update(); super.update();
// 所有的Pie同时向左移动 // 所有的Pie同时向左移动
const { speed, pieArr } = this; const { speed, pieArr } = this;
pieArr.forEach((pie) => { pieArr.forEach((pie) => {
// 移动 // 移动
pie.left -= speed; pie.left -= speed;
if (pie.left <= -pie.size.width) { // 如果移出屏幕 if (pie.left <= -pie.size.width) { // 如果移出屏幕
this.pieArr.splice(this.pieArr.indexOf(pie), 1); // 从托管列表里移除 this.pieArr.splice(this.pieArr.indexOf(pie), 1); // 从托管列表里移除
this.removeChild(pie); // 从子节点移除 this.removeChild(pie); // 从子节点移除
ObjectPool.put("pie", pie); // 加入对象池 ObjectPool.put("pie", pie); // 加入对象池
return; return;
} }
// 检查碰撞 // 检查碰撞
// 重构盒子,因为pie.up和pie.down的坐标是相对于pie的,所以要重构正方形的left和top // 重构盒子,因为pie.up和pie.down的坐标是相对于pie的,所以要重构正方形的left和top
const pieUpBox = { const pieUpBox = {
left: pie.left, left: pie.left,
top: pie.top + pie.up.top, top: pie.top + pie.up.top,
size: pie.up.size, size: pie.up.size,
} }
const pieDownBox = { const pieDownBox = {
left: pie.left, left: pie.left,
top: pie.top + pie.down.top, top: pie.top + pie.down.top,
size: pie.down.size, size: pie.down.size,
} }
if (boxCollisionTest(this.bird, pieUpBox) || boxCollisionTest(this.bird, pieDownBox)) { if (boxCollisionTest(this.bird, pieUpBox) || boxCollisionTest(this.bird, pieDownBox)) {
console.log("撞到障碍,你死了"); console.log("撞到障碍,你死了");
} }
}); });
} }
} }
const pieMgr = this.pieMgr = new PieMgr(4, 1000, bird); const pieMgr = this.pieMgr = new PieMgr(4, 1000, bird);
``` ```
运行案例得到效果 运行案例得到效果
![12_5.png](../images/12_5.png) ![12_5.png](../images/12_5.png)
## 死亡暂停游戏 ## 死亡暂停游戏
在本案例中我们可以简单的将暂停游戏简单的理解为暂停循环 在本案例中我们可以简单的将暂停游戏简单的理解为暂停循环
- 在FlppyBird的`update`中插入控制变量`pause` - 在FlppyBird的`update`中插入控制变量`pause`
- 如果`pause`为真则不执行`super.update` - 如果`pause`为真则不执行`super.update`
```javascript ```javascript
class FlppyBird extends GameStage { class FlppyBird extends GameStage {
pause = false; pause = false;
/* ... */ /* ... */
update() { update() {
if (this.pause) return; if (this.pause) return;
super.update(); super.update();
} }
} }
``` ```
使用事件通知游戏暂停 使用事件通知游戏暂停
本案例不带大家实现自己的事件收发器 本案例不带大家实现自己的事件收发器
借用强大的`document`来实现事件收发 借用强大的`document`来实现事件收发
每一个dom节点其实都继承了一个事件收发器,所以可以直接利用dom节点做事件收发 每一个dom节点其实都继承了一个事件收发器,所以可以直接利用dom节点做事件收发
-`FlppyBird``ready`中监听`playerDie`事件,并在`destroy`中移除 -`FlppyBird``ready`中监听`playerDie`事件,并在`destroy`中移除
- 将之前的死亡打印,变为发送`playerDie`事件 - 将之前的死亡打印,变为发送`playerDie`事件
```javascript ```javascript
class FlppyBird extends GameStage { class FlppyBird extends GameStage {
/* ... */ /* ... */
pause = false; pause = false;
async ready() { async ready() {
document.addEventListener("playerDie", this.pauseGame); document.addEventListener("playerDie", this.pauseGame);
} }
update() { update() {
if (this.pause) return; if (this.pause) return;
super.update(); super.update();
} }
pauseGame = (e) => { pauseGame = (e) => {
console.error(e.data); console.error(e.data);
this.pause = true; this.pause = true;
} }
} }
class Bird extends Sprite { class Bird extends Sprite {
/* ... */ /* ... */
update() { update() {
super.update(); super.update();
// v = v0 + a * t² // v = v0 + a * t²
this.speed += this.gravity; // 速度 = 速度 + 加速度 * 时间² this.speed += this.gravity; // 速度 = 速度 + 加速度 * 时间²
let top = this.top + this.speed; // 更新位置 let top = this.top + this.speed; // 更新位置
// dieLine 因为锚点在左上角所以dieLine应该减去自己的高度 // dieLine 因为锚点在左上角所以dieLine应该减去自己的高度
const dieLine = this.dieLine - this.size.height; const dieLine = this.dieLine - this.size.height;
// 如果大于dieLine了就停在dieLine // 如果大于dieLine了就停在dieLine
if (top > dieLine) { if (top > dieLine) {
top = dieLine; top = dieLine;
// 发送死亡事件 // 发送死亡事件
const event = new Event("playerDie"); const event = new Event("playerDie");
event.data = "坠机了,你死了"; event.data = "坠机了,你死了";
document.dispatchEvent(event); document.dispatchEvent(event);
} }
this.top = top; this.top = top;
} }
} }
class PieMgr extends GameObject { class PieMgr extends GameObject {
/* ... */ /* ... */
bird; // 玩家 bird; // 玩家
update() { update() {
super.update(); super.update();
// 移动 // 移动
// 所有的Pie同时向左移动 // 所有的Pie同时向左移动
const { speed, pieArr } = this; const { speed, pieArr } = this;
pieArr.forEach((pie) => { pieArr.forEach((pie) => {
pie.left -= speed; pie.left -= speed;
if (pie.left <= -pie.size.width) { // 如果移出屏幕 if (pie.left <= -pie.size.width) { // 如果移出屏幕
this.pieArr.splice(this.pieArr.indexOf(pie), 1); // 从托管列表里移除 this.pieArr.splice(this.pieArr.indexOf(pie), 1); // 从托管列表里移除
this.removeChild(pie); // 从子节点移除 this.removeChild(pie); // 从子节点移除
ObjectPool.put("pie", pie); // 加入对象池 ObjectPool.put("pie", pie); // 加入对象池
return; return;
} }
// 检查碰撞 // 检查碰撞
// 重构盒子,因为pie.up和pie.down的坐标是相对于pie的,所以要重构正方形的left和top // 重构盒子,因为pie.up和pie.down的坐标是相对于pie的,所以要重构正方形的left和top
const pieUpBox = { const pieUpBox = {
left: pie.left, left: pie.left,
top: pie.top + pie.up.top, top: pie.top + pie.up.top,
size: pie.up.size, size: pie.up.size,
} }
const pieDownBox = { const pieDownBox = {
left: pie.left, left: pie.left,
top: pie.top + pie.down.top, top: pie.top + pie.down.top,
size: pie.down.size, size: pie.down.size,
} }
if (boxCollisionTest(this.bird, pieUpBox) || boxCollisionTest(this.bird, pieDownBox)) { if (boxCollisionTest(this.bird, pieUpBox) || boxCollisionTest(this.bird, pieDownBox)) {
// 发送死亡事件 // 发送死亡事件
const event = new Event("playerDie"); const event = new Event("playerDie");
event.data = "撞到障碍,你死了"; event.data = "撞到障碍,你死了";
document.dispatchEvent(event); document.dispatchEvent(event);
} }
}); });
} }
} }
``` ```
运行案例,发现两种死亡方式都可以正常暂停游戏 运行案例,发现两种死亡方式都可以正常暂停游戏
![12_6.png](../images/12_6.png) ![12_6.png](../images/12_6.png)
![12_6.png](../images/12_7.png) ![12_6.png](../images/12_7.png)
/* /*
* flppyBirdLib.js * flppyBirdLib.js
* Created by 还有醋v on 2021/3/10. * Created by 还有醋v on 2021/3/10.
* Copyright © 2021 haiyoucuv. All rights reserved. * Copyright © 2021 haiyoucuv. All rights reserved.
*/ */
/** /**
* 抽象了一个简单的GameObject * 抽象了一个简单的GameObject
*/ */
class GameObject { class GameObject {
dom; // 绑定的dom元素 dom; // 绑定的dom元素
children = []; // 子节点 children = []; // 子节点
parent; // 父节点 parent; // 父节点
// 位置 // 位置
top = 0; top = 0;
left = 0; left = 0;
// 缩放 // 缩放
scaleX = 1; scaleX = 1;
scaleY = 1; scaleY = 1;
// 旋转 // 旋转
rotate = 0; rotate = 0;
/** /**
* 获得宽高 * 获得宽高
* @returns {{width: number, height: number}} * @returns {{width: number, height: number}}
*/ */
get size() { get size() {
return { return {
width: this.dom.clientWidth, width: this.dom.clientWidth,
height: this.dom.clientHeight, height: this.dom.clientHeight,
} }
} }
constructor(type = "div") { constructor(type = "div") {
this.dom = document.createElement(type); // 基础GameObject为div,Sprite为img this.dom = document.createElement(type); // 基础GameObject为div,Sprite为img
this.dom.style.position = "absolute"; this.dom.style.position = "absolute";
} }
/** /**
* 生命周期 start 加入显示列表执行此函数 * 生命周期 start 加入显示列表执行此函数
*/ */
ready() { ready() {
} }
/** /**
* 添加子节点 * 添加子节点
* @param child * @param child
*/ */
addChild(child) { addChild(child) {
// 如果是别人的子节点,则先移除再添加到自己下面 // 如果是别人的子节点,则先移除再添加到自己下面
if (child.parent) { if (child.parent) {
child.parent.removeChild(child); child.parent.removeChild(child);
} }
// 执行添加 // 执行添加
this.dom.appendChild(child.dom); this.dom.appendChild(child.dom);
this.children.push(child); this.children.push(child);
child.parent = this; child.parent = this;
// 容错:防止子类重写的start不是async函数 // 容错:防止子类重写的start不是async函数
// TODO dom无法在节点不在渲染树的上的时候拿到clientWidth等属性,故将ready放在这里 // TODO dom无法在节点不在渲染树的上的时候拿到clientWidth等属性,故将ready放在这里
child.ready(); child.ready();
return child; return child;
} }
/** /**
* 删除子节点 * 删除子节点
* @param child * @param child
*/ */
removeChild(child) { removeChild(child) {
// 不是自己的子节点就提示错误 // 不是自己的子节点就提示错误
if (child.parent !== this) { if (child.parent !== this) {
console.warn("移除的节点必须是其子集"); console.warn("移除的节点必须是其子集");
return null; return null;
} }
// 执行销毁和移除 // 执行销毁和移除
child.destroy(); child.destroy();
this.dom.removeChild(child.dom); this.dom.removeChild(child.dom);
this.children.splice(this.children.indexOf(child), 1); this.children.splice(this.children.indexOf(child), 1);
child.parent = null; child.parent = null;
return child; return child;
} }
/** /**
* 抽离数据更新部分,并更新子节点 * 抽离数据更新部分,并更新子节点
*/ */
update() { update() {
this.children.forEach((child) => { this.children.forEach((child) => {
child.update(); child.update();
}); });
} }
/** /**
* 抽离渲染部分,并渲染子节点 * 抽离渲染部分,并渲染子节点
*/ */
render() { render() {
const { top, left, scaleX, scaleY, rotate } = this; const { top, left, scaleX, scaleY, rotate } = this;
this.dom.style.top = top + "px"; this.dom.style.top = top + "px";
this.dom.style.left = left + "px"; this.dom.style.left = left + "px";
this.dom.style.transform = `scale(${scaleX}, ${scaleY}) rotate(${rotate}deg)`; this.dom.style.transform = `scale(${scaleX}, ${scaleY}) rotate(${rotate}deg)`;
// 添加渲染子节点部分 // 添加渲染子节点部分
this.children.forEach((child) => { this.children.forEach((child) => {
child.render(); child.render();
}); });
} }
/** /**
* 抽离销毁部分 * 抽离销毁部分
*/ */
destroy() { destroy() {
} }
} }
/** /**
* 抽象精灵Sprite * 抽象精灵Sprite
*/ */
class Sprite extends GameObject { class Sprite extends GameObject {
constructor(src = "") { constructor(src = "") {
super("img"); super("img");
this.dom.src = src; this.dom.src = src;
} }
} }
class GameStage extends GameObject { class GameStage extends GameObject {
constructor() { constructor() {
super(); super();
document.body.appendChild(this.dom); document.body.appendChild(this.dom);
this._gameStart(); this._gameStart();
this.loop(); this.loop();
} }
async _gameStart() { async _gameStart() {
await this.preloadRes(); await this.preloadRes();
this.ready(); this.ready();
} }
/** /**
* 预加载资源 * 预加载资源
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async preloadRes() { async preloadRes() {
} }
/** /**
* 主循环 * 主循环
*/ */
loop = () => { loop = () => {
requestAnimationFrame(this.loop); // 循环调用requestAnimationFrame requestAnimationFrame(this.loop); // 循环调用requestAnimationFrame
this.update(); // 先数据更新 this.update(); // 先数据更新
this.render(); // 后渲染更新 this.render(); // 后渲染更新
} }
} }
/** /**
* 异步加载图片方法 * 异步加载图片方法
* @param src 图片路径 * @param src 图片路径
* @returns {Promise<HTMLImageElement | null>} * @returns {Promise<HTMLImageElement | null>}
*/ */
function loadImgAsync(src) { function loadImgAsync(src) {
return new Promise((resolve) => { return new Promise((resolve) => {
const img = new Image(); const img = new Image();
img.onload = () => resolve(img); img.onload = () => resolve(img);
img.onerror = () => { img.onerror = () => {
console.error(`加载资源${src}失败`); console.error(`加载资源${src}失败`);
resolve(null); resolve(null);
}; };
img.src = src; img.src = src;
}); });
} }
/** /**
* 一个简单的通用对象池 * 一个简单的通用对象池
*/ */
class ObjectPool { class ObjectPool {
static objs = {}; static objs = {};
static put(name, obj) { static put(name, obj) {
const pool = ObjectPool.objs[name] || (ObjectPool.objs[name] = []); const pool = ObjectPool.objs[name] || (ObjectPool.objs[name] = []);
pool.push(obj); pool.push(obj);
} }
static get(name) { static get(name) {
const pool = ObjectPool.objs[name] || (ObjectPool.objs[name] = []); const pool = ObjectPool.objs[name] || (ObjectPool.objs[name] = []);
if (pool.length <= 0) { if (pool.length <= 0) {
return null; return null;
} }
return pool.shift(); return pool.shift();
} }
} }
/** /**
* 盒子碰撞 * 盒子碰撞
* @param rect1 {top, left, size:{width, height}} * @param rect1 {top, left, size:{width, height}}
* @param rect2 {top, left, size:{width, height}} * @param rect2 {top, left, size:{width, height}}
* @returns {boolean} * @returns {boolean}
*/ */
function boxCollisionTest(rect1, rect2) { function boxCollisionTest(rect1, rect2) {
const { top: t1, left: l1 } = rect1; const { top: t1, left: l1 } = rect1;
const { width: w1, height: h1 } = rect1.size; const { width: w1, height: h1 } = rect1.size;
const { top: t2, left: l2 } = rect2; const { top: t2, left: l2 } = rect2;
const { width: w2, height: h2 } = rect2.size; const { width: w2, height: h2 } = rect2.size;
const b1 = t1 + h1, const b1 = t1 + h1,
b2 = t2 + h2, b2 = t2 + h2,
r1 = l1 + w1, r1 = l1 + w1,
r2 = l2 + w2; r2 = l2 + w2;
return ((t1 > t2 && t1 < b2) || (t2 > t1 && t2 < b1)) // 检查 t1->b1 和 t2->b2 的交集 return ((t1 > t2 && t1 < b2) || (t2 > t1 && t2 < b1)) // 检查 t1->b1 和 t2->b2 的交集
&& ((l1 > l2 && l1 < r2) || (l2 > l1 && l2 < r1)); // 检查 l1->r1 和 l2->r2 的交集 && ((l1 > l2 && l1 < r2) || (l2 > l1 && l2 < r1)); // 检查 l1->r1 和 l2->r2 的交集
} }
/* /*
* lib.js * lib.js
* Created by 还有醋v on 2021/3/8. * Created by 还有醋v on 2021/3/8.
* Copyright © 2021 haiyoucuv. All rights reserved. * Copyright © 2021 haiyoucuv. All rights reserved.
*/ */
/** /**
* 抽象了一个简单的GameObject * 抽象了一个简单的GameObject
*/ */
class GameObject { class GameObject {
id; // 绑定的dom元素的id id; // 绑定的dom元素的id
dom; // 绑定的dom元素 dom; // 绑定的dom元素
// 位置 // 位置
top = 0; top = 0;
left = 0; left = 0;
// 缩放 // 缩放
scaleX = 1; scaleX = 1;
scaleY = 1; scaleY = 1;
// 旋转 // 旋转
rotate = 0; rotate = 0;
/** /**
* 获得宽高 * 获得宽高
* @returns {{width: number, height: number}} * @returns {{width: number, height: number}}
*/ */
get size() { get size() {
return { return {
width: this.dom.clientWidth, width: this.dom.clientWidth,
height: this.dom.clientHeight, height: this.dom.clientHeight,
} }
} }
constructor(id) { constructor(id) {
this.id = id; this.id = id;
this.dom = document.getElementById(id); // 在构造函数中绑定dom元素 this.dom = document.getElementById(id); // 在构造函数中绑定dom元素
this.dom.style.position = "absolute"; this.dom.style.position = "absolute";
} }
/** /**
* 抽离数据更新部分 * 抽离数据更新部分
*/ */
update() { update() {
} }
/** /**
* 抽离渲染部分 * 抽离渲染部分
*/ */
render() { render() {
const { top, left, scaleX, scaleY, rotate } = this; const { top, left, scaleX, scaleY, rotate } = this;
this.dom.style.top = top + "px"; this.dom.style.top = top + "px";
this.dom.style.left = left + "px"; this.dom.style.left = left + "px";
this.dom.style.transform = `scale(${scaleX}, ${scaleY}) rotate(${rotate}deg)`; this.dom.style.transform = `scale(${scaleX}, ${scaleY}) rotate(${rotate}deg)`;
} }
/** /**
* 抽离销毁部分 * 抽离销毁部分
*/ */
destroy() { 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