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>
<!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 - 创建障碍
引入概念:`动态生成障碍` 引入概念:`动态生成障碍`
本节将在上节的内容上,实现动态添加障碍 本节将在上节的内容上,实现动态添加障碍
## 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)
This diff is collapsed.
This diff is collapsed.
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