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

1

parent d6fce54c
/node_modules
/dist
npm-debug.log
.DS_Store
yarn-error.log
yarn.lock
/node_modules
/dist
npm-debug.log
.DS_Store
yarn-error.log
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>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>00.前言</title>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
</style>
</head>
<body>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>00.前言</title>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
</style>
</head>
<body>
</body>
</html>
# 前言
本案例旨在使非游戏开发人员理解游戏和游戏开发的模式,不适用于实际生产
本案例使用浏览器平台,以js控制dom元素的方式讲解游戏,旨在降低平台跨度,易于理解,"游戏高手"请不要装逼
本案例并非真正的游戏开发过程,仅借助dom渲染引擎来给非游戏开发人员讲解游戏
普通的游戏开发教程更多的是在引擎API上的教学,你学完了会用,但是却不知道在这API之下究竟发生了什么
而在本案例中使用dom渲染引擎作为根本,并带大家使用最基本的js语法从0封装和游戏引擎类似的接口,从而达到让dom技术栈人员更加容易理解游戏开发的模式,因为每一个接口都是由你亲自封装,而接口的底层,恰恰是你熟悉的东西,
就好像带你实现了一个简单的基于dom渲染的游戏引擎,学习完本案例之后再去学习其他的游戏引擎,你就算不看源码也能猜出那些API的原理是什么
学习本案例需要的预备知识:`html` `javascript` `面向对象编程概念`
学习本案例前,请先记住以下概念,在之后的学习过程中,你会发现,还真是这么回事:
> **游戏可抽象为:输入 + 循环 + 输出**
>
> 输入:鼠标、键盘、陀螺仪、手柄、ar/vr、摄像头、麦克风等一切可以获取信息的设备
>
> 循环:游戏主循环, 数据更新+渲染更新
>
> 输出:渲染,对应用户看到的画面,声音等
本案例使用以下模拟器模拟
![00_1.png](../images/00_1.png)
本案例使用以下html模版
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>00.前言</title>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
</style>
</head>
<body>
</body>
</html>
```
# 前言
本案例旨在使非游戏开发人员理解游戏和游戏开发的模式,不适用于实际生产
本案例使用浏览器平台,以js控制dom元素的方式讲解游戏,旨在降低平台跨度,易于理解,"游戏高手"请不要装逼
本案例并非真正的游戏开发过程,仅借助dom渲染引擎来给非游戏开发人员讲解游戏
普通的游戏开发教程更多的是在引擎API上的教学,你学完了会用,但是却不知道在这API之下究竟发生了什么
而在本案例中使用dom渲染引擎作为根本,并带大家使用最基本的js语法从0封装和游戏引擎类似的接口,从而达到让dom技术栈人员更加容易理解游戏开发的模式,因为每一个接口都是由你亲自封装,而接口的底层,恰恰是你熟悉的东西,
就好像带你实现了一个简单的基于dom渲染的游戏引擎,学习完本案例之后再去学习其他的游戏引擎,你就算不看源码也能猜出那些API的原理是什么
学习本案例需要的预备知识:`html` `javascript` `面向对象编程概念`
学习本案例前,请先记住以下概念,在之后的学习过程中,你会发现,还真是这么回事:
> **游戏可抽象为:输入 + 循环 + 输出**
>
> 输入:鼠标、键盘、陀螺仪、手柄、ar/vr、摄像头、麦克风等一切可以获取信息的设备
>
> 循环:游戏主循环, 数据更新+渲染更新
>
> 输出:渲染,对应用户看到的画面,声音等
本案例使用以下模拟器模拟
![00_1.png](../images/00_1.png)
本案例使用以下html模版
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>00.前言</title>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
</style>
</head>
<body>
</body>
</html>
```
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>01.一只鸟</title>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
#bird {
/* 大小 */
width: 50px;
height: 50px;
/* 颜色 */
background-color: #f00;
/* 位置 */
position: absolute;
top: 50%;
left: 50%;
/* 旋转、缩放等属性 */
transform: scale(1, 1) rotate(45deg);
/* 锚点 */
transform-origin: center;
}
</style>
</head>
<body>
<div id="bird"></div>
</body>
</html>
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>01.一只鸟</title>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
#bird {
/* 大小 */
width: 50px;
height: 50px;
/* 颜色 */
background-color: #f00;
/* 位置 */
position: absolute;
top: 50%;
left: 50%;
/* 旋转、缩放等属性 */
transform: scale(1, 1) rotate(45deg);
/* 锚点 */
transform-origin: center;
}
</style>
</head>
<body>
<div id="bird"></div>
</body>
</html>
# 一只Bird
引入概念:`显示对象`
显示对象是游戏客户端渲染的基本对象,可理解为div
在body中插入一个div
```html
<div id="bird"></div>
```
把div放在你喜欢的位置,加上你喜欢的颜色
```css
#bird {
/* 大小 */
width: 100px;
height: 100px;
/* 颜色 */
background-color: #f00;
/* 位置 */
position: absolute;
top: 50%;
left: 50%;
/* 旋转、缩放等属性 */
transform: scale(1, 1) rotate(45deg);
/* 锚点 */
transform-origin: center;
}
```
经过一顿操作,我们得到了自己喜欢的一只Bird,如下图
![img.png](../images/01_1.png)
# 一只Bird
引入概念:`显示对象`
显示对象是游戏客户端渲染的基本对象,可理解为div
在body中插入一个div
```html
<div id="bird"></div>
```
把div放在你喜欢的位置,加上你喜欢的颜色
```css
#bird {
/* 大小 */
width: 100px;
height: 100px;
/* 颜色 */
background-color: #f00;
/* 位置 */
position: absolute;
top: 50%;
left: 50%;
/* 旋转、缩放等属性 */
transform: scale(1, 1) rotate(45deg);
/* 锚点 */
transform-origin: center;
}
```
经过一顿操作,我们得到了自己喜欢的一只Bird,如下图
![img.png](../images/01_1.png)
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>02.会飞的鸟</title>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
#bird {
/* 大小 */
width: 50px;
height: 50px;
/* 颜色 */
background-color: #f00;
/* 位置 */
position: absolute;
top: 50%;
left: 50%;
/* 旋转、缩放等属性 */
transform: scale(1, 1) rotate(45deg);
/* 锚点 */
transform-origin: center;
/* 动画 */
animation: bird-fly 5s infinite;
}
@keyframes bird-fly {
0% {
top: 50%;
transform: scale(1, 1) rotate(45deg)
}
50% {
top: 30%;
transform: scale(1, 1) rotate(45deg)
}
100% {
top: 50%;
transform: scale(1, 1) rotate(45deg)
}
}
</style>
</head>
<body>
<div id="bird"></div>
</body>
</html>
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>02.会飞的鸟</title>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
#bird {
/* 大小 */
width: 50px;
height: 50px;
/* 颜色 */
background-color: #f00;
/* 位置 */
position: absolute;
top: 50%;
left: 50%;
/* 旋转、缩放等属性 */
transform: scale(1, 1) rotate(45deg);
/* 锚点 */
transform-origin: center;
/* 动画 */
animation: bird-fly 5s infinite;
}
@keyframes bird-fly {
0% {
top: 50%;
transform: scale(1, 1) rotate(45deg)
}
50% {
top: 30%;
transform: scale(1, 1) rotate(45deg)
}
100% {
top: 50%;
transform: scale(1, 1) rotate(45deg)
}
}
</style>
</head>
<body>
<div id="bird"></div>
</body>
</html>
# 会飞的Bird
引入概念:使用`动画`改变显示对象属性
动画是一个游戏重要的组成部分,如果没有动画,游戏将变得枯燥无味
本小节创建一个简单keyframe动画,以此来理解游戏中的动画
在style标签中加入一段动画的代码
```css
@keyframes bird-fly {
0% {
top: 50%;
transform: scale(1, 1) rotate(45deg)
}
50% {
top: 30%;
transform: scale(1, 1) rotate(45deg)
}
100% {
top: 50%;
transform: scale(1, 1) rotate(45deg)
}
}
```
把刚刚我们写的css动画应用在我们的Bird上
```css
#bird {
[...]
/* 动画 */
animation: bird-fly 5s infinite;
}
```
经过一顿操作,我们得到了自己一只活生生的鸟
但是这样的方式似乎无法自由的操作
想想王者荣耀,你的英雄该如何送塔?
如下图:
![02_1.gif](../images/02_1.gif)
# 会飞的Bird
引入概念:使用`动画`改变显示对象属性
动画是一个游戏重要的组成部分,如果没有动画,游戏将变得枯燥无味
本小节创建一个简单keyframe动画,以此来理解游戏中的动画
在style标签中加入一段动画的代码
```css
@keyframes bird-fly {
0% {
top: 50%;
transform: scale(1, 1) rotate(45deg)
}
50% {
top: 30%;
transform: scale(1, 1) rotate(45deg)
}
100% {
top: 50%;
transform: scale(1, 1) rotate(45deg)
}
}
```
把刚刚我们写的css动画应用在我们的Bird上
```css
#bird {
[...]
/* 动画 */
animation: bird-fly 5s infinite;
}
```
经过一顿操作,我们得到了自己一只活生生的鸟
但是这样的方式似乎无法自由的操作
想想王者荣耀,你的英雄该如何送塔?
如下图:
![02_1.gif](../images/02_1.gif)
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>03.点哪飞哪</title>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
#bird {
/* 大小 */
width: 50px;
height: 50px;
/* 颜色 */
background-color: #f00;
/* 位置 */
position: absolute;
/* 旋转、缩放等属性 */
transform: scale(1, 1) rotate(45deg);
/* 锚点 */
transform-origin: center;
}
</style>
</head>
<body>
<div id="bird"></div>
</body>
<script>
const bird = document.getElementById("bird");
const speed = 10; // bird的速度 每次移动多少距离
const pos = { // bird的位置 bird当前的位置
top: 150,
left: 150,
}
const clickPos = { // 鼠标点击的位置 bird将要的到达的位置
top: 150,
left: 150,
}
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置
const mouseDown = (e) => {
// 直接改变bird的位置,没有动画
// bird.style.top = e.clientY + "px";
// bird.style.left = e.clientX + "px";
// 记录bird将要到达的位置,使用动画慢慢到达
clickPos.top = e.clientY;
clickPos.left = e.clientX;
}
document.addEventListener('mousedown', mouseDown);
setInterval(() => {
/**
* 计算Bird的位置(数据更新)
*/
// 计算新的top
if (pos.top !== clickPos.top) {
const dis = clickPos.top - pos.top; // 计算差值
const dir = dis > 0 ? 1 : -1; // 计算在top上移动的方向 1 正向移动 或 -1 反向移动;
// 如果速度过快,本次移动直接过头了(即差值<速度),就直接移动到指定top
if (Math.abs(dis) < speed) {
pos.top = clickPos.top;
} else {
pos.top = pos.top + dir * speed; // 计算新的top,新的位置 = 之前的位置 + 方向 * 速度
}
}
// 用相同的方式计算left
if (pos.left !== clickPos.left) {
const dis = clickPos.left - pos.left;
const dir = dis > 0 ? 1 : -1;
if (Math.abs(dis) < speed) {
pos.left = clickPos.left;
} else {
pos.left = pos.left + dir * speed;
}
}
/**
* 更新显示对象位置(渲染)
*/
bird.style.top = pos.top + "px";
bird.style.left = pos.left + "px";
}, 10);
</script>
</html>
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>03.点哪飞哪</title>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
#bird {
/* 大小 */
width: 50px;
height: 50px;
/* 颜色 */
background-color: #f00;
/* 位置 */
position: absolute;
/* 旋转、缩放等属性 */
transform: scale(1, 1) rotate(45deg);
/* 锚点 */
transform-origin: center;
}
</style>
</head>
<body>
<div id="bird"></div>
</body>
<script>
const bird = document.getElementById("bird");
const speed = 10; // bird的速度 每次移动多少距离
const pos = { // bird的位置 bird当前的位置
top: 150,
left: 150,
}
const clickPos = { // 鼠标点击的位置 bird将要的到达的位置
top: 150,
left: 150,
}
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置
const mouseDown = (e) => {
// 直接改变bird的位置,没有动画
// bird.style.top = e.clientY + "px";
// bird.style.left = e.clientX + "px";
// 记录bird将要到达的位置,使用动画慢慢到达
clickPos.top = e.clientY;
clickPos.left = e.clientX;
}
document.addEventListener('mousedown', mouseDown);
setInterval(() => {
/**
* 计算Bird的位置(数据更新)
*/
// 计算新的top
if (pos.top !== clickPos.top) {
const dis = clickPos.top - pos.top; // 计算差值
const dir = dis > 0 ? 1 : -1; // 计算在top上移动的方向 1 正向移动 或 -1 反向移动;
// 如果速度过快,本次移动直接过头了(即差值<速度),就直接移动到指定top
if (Math.abs(dis) < speed) {
pos.top = clickPos.top;
} else {
pos.top = pos.top + dir * speed; // 计算新的top,新的位置 = 之前的位置 + 方向 * 速度
}
}
// 用相同的方式计算left
if (pos.left !== clickPos.left) {
const dis = clickPos.left - pos.left;
const dir = dis > 0 ? 1 : -1;
if (Math.abs(dis) < speed) {
pos.left = clickPos.left;
} else {
pos.left = pos.left + dir * speed;
}
}
/**
* 更新显示对象位置(渲染)
*/
bird.style.top = pos.top + "px";
bird.style.left = pos.left + "px";
}, 10);
</script>
</html>
# 点哪飞哪
引入概念:`输入输出` `游戏主循环`
经过上一节的思考,我们已经得到了一只不听话的Bird和一份王者荣耀快速送塔的方案 那就是```输入输出```
本节的内容是训练Bird,让它可以点哪飞哪
## 一、输入
```javascript
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置
const mouseDown = (e) => {
// 直接改变bird的位置,没有动画
bird.style.top = e.clientY + "px";
bird.style.left = e.clientX + "px";
}
document.addEventListener('mousedown', mouseDown);
```
再次运行案例,点击屏幕
我们的Bird不仅听话,还学会了闪现,还没有冷却。
开挂了,一顿操作,被举报封号,这怎么行,开挂也要演一演
![03_1.gif](../images/03_1.gif)
## 二、主循环
`主循环`是游戏的基本要素之一
如果不能很好的理解主循环,将无法真正的理解游戏
如果你是个小白,某老师直接就教你一款游戏引擎的API,那么你可以直接拍屁股走人了
在循环中,主要做两件事:
- 1.数据更新
- 2.渲染更新
### 1.创建循环 - (使用`setInterval`来模拟游戏循环)
隆重的介绍:`setInterval`
```js
setInterval(handler, timeout, ...arguments);
```
我们需要做以下几件事:
- 1.给鸟一个飞行速度`speed`即每次循环飞多少距离
用一个变量`pos`记录Bird当前的位置
用一个变量`clickPos`记录鸟要飞到的位置即鼠标点击的位置
```javascript
const speed = 10; // bird的速度 每次移动多少距离
const pos = { // bird的位置 bird当前的位置
top: 0,
left: 0,
}
const clickPos = { // 鼠标点击的位置 bird将要的到达的位置
top: 0,
left: 0,
}
```
- 2.在点击鼠标后记录需要到达的位置而不是立即改变
```javascript
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置
const mouseDown = (e) => {
// 直接改变bird的位置,没有动画
// bird.style.top = e.clientY + "px";
// bird.style.left = e.clientX + "px";
// 记录bird将要到达的位置,使用动画慢慢到达
clickPos.top = e.clientY;
clickPos.left = e.clientX;
}
```
- 3.在循环中计算当前循环之后鸟到达的位置
- 4.更新鸟的位置
```javascript
setInterval(() => {
/**
* 计算Bird的位置(数据更新)
*/
// 计算新的top
if (pos.top !== clickPos.top) {
const dis = clickPos.top - pos.top; // 计算差值
const dir = dis > 0 ? 1 : -1; // 计算在top上移动的方向 1 正向移动 或 -1 反向移动;
// 如果速度过快,本次移动直接过头了(即差值<速度),就直接移动到指定top
if (Math.abs(dis) < speed) {
pos.top = clickPos.top;
} else {
pos.top = pos.top + dir * speed; // 计算新的top,新的位置 = 之前的位置 + 方向 * 速度
}
}
// 用相同的方式计算left
if (pos.left !== clickPos.left) {
const dis = clickPos.left - pos.left;
const dir = dis > 0 ? 1 : -1;
if (Math.abs(dis) < speed) {
pos.left = clickPos.left;
} else {
pos.left = pos.left + dir * speed;
}
}
/**
* 更新显示对象位置(渲染)
*/
bird.style.top = pos.top + "px";
bird.style.left = pos.left + "px";
}, 10);
```
运行案例,我们得到了演员Bird
![03_2.gif](../images/03_2.gif)
# 点哪飞哪
引入概念:`输入输出` `游戏主循环`
经过上一节的思考,我们已经得到了一只不听话的Bird和一份王者荣耀快速送塔的方案 那就是```输入输出```
本节的内容是训练Bird,让它可以点哪飞哪
## 一、输入
```javascript
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置
const mouseDown = (e) => {
// 直接改变bird的位置,没有动画
bird.style.top = e.clientY + "px";
bird.style.left = e.clientX + "px";
}
document.addEventListener('mousedown', mouseDown);
```
再次运行案例,点击屏幕
我们的Bird不仅听话,还学会了闪现,还没有冷却。
开挂了,一顿操作,被举报封号,这怎么行,开挂也要演一演
![03_1.gif](../images/03_1.gif)
## 二、主循环
`主循环`是游戏的基本要素之一
如果不能很好的理解主循环,将无法真正的理解游戏
如果你是个小白,某老师直接就教你一款游戏引擎的API,那么你可以直接拍屁股走人了
在循环中,主要做两件事:
- 1.数据更新
- 2.渲染更新
### 1.创建循环 - (使用`setInterval`来模拟游戏循环)
隆重的介绍:`setInterval`
```js
setInterval(handler, timeout, ...arguments);
```
我们需要做以下几件事:
- 1.给鸟一个飞行速度`speed`即每次循环飞多少距离
用一个变量`pos`记录Bird当前的位置
用一个变量`clickPos`记录鸟要飞到的位置即鼠标点击的位置
```javascript
const speed = 10; // bird的速度 每次移动多少距离
const pos = { // bird的位置 bird当前的位置
top: 0,
left: 0,
}
const clickPos = { // 鼠标点击的位置 bird将要的到达的位置
top: 0,
left: 0,
}
```
- 2.在点击鼠标后记录需要到达的位置而不是立即改变
```javascript
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置
const mouseDown = (e) => {
// 直接改变bird的位置,没有动画
// bird.style.top = e.clientY + "px";
// bird.style.left = e.clientX + "px";
// 记录bird将要到达的位置,使用动画慢慢到达
clickPos.top = e.clientY;
clickPos.left = e.clientX;
}
```
- 3.在循环中计算当前循环之后鸟到达的位置
- 4.更新鸟的位置
```javascript
setInterval(() => {
/**
* 计算Bird的位置(数据更新)
*/
// 计算新的top
if (pos.top !== clickPos.top) {
const dis = clickPos.top - pos.top; // 计算差值
const dir = dis > 0 ? 1 : -1; // 计算在top上移动的方向 1 正向移动 或 -1 反向移动;
// 如果速度过快,本次移动直接过头了(即差值<速度),就直接移动到指定top
if (Math.abs(dis) < speed) {
pos.top = clickPos.top;
} else {
pos.top = pos.top + dir * speed; // 计算新的top,新的位置 = 之前的位置 + 方向 * 速度
}
}
// 用相同的方式计算left
if (pos.left !== clickPos.left) {
const dis = clickPos.left - pos.left;
const dir = dis > 0 ? 1 : -1;
if (Math.abs(dis) < speed) {
pos.left = clickPos.left;
} else {
pos.left = pos.left + dir * speed;
}
}
/**
* 更新显示对象位置(渲染)
*/
bird.style.top = pos.top + "px";
bird.style.left = pos.left + "px";
}, 10);
```
运行案例,我们得到了演员Bird
![03_2.gif](../images/03_2.gif)
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>04.高性能鸟</title>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
#bird {
/* 大小 */
width: 50px;
height: 50px;
/* 颜色 */
background-color: #f00;
/* 位置 */
position: absolute;
/* 旋转、缩放等属性 */
transform: scale(1, 1) rotate(45deg);
/* 锚点 */
transform-origin: center;
}
</style>
</head>
<body>
<div id="bird"></div>
</body>
<script>
const bird = document.getElementById("bird");
const speed = 15; // bird的速度 每次移动多少距离
const pos = { // bird的位置 bird当前的位置
top: 150,
left: 150,
}
const clickPos = { // 鼠标点击的位置 bird将要的到达的位置
top: 150,
left: 150,
}
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置
const mouseDown = (e) => {
// 记录bird将要到达的位置,使用动画慢慢到达
clickPos.top = e.clientY;
clickPos.left = e.clientX;
}
document.addEventListener('mousedown', mouseDown);
/**
* 计算Bird的位置(数据更新)
*/
function update() {
// 计算新的top
if (pos.top !== clickPos.top) {
const dis = clickPos.top - pos.top; // 计算差值
const dir = dis > 0 ? 1 : -1; // 计算在top上移动的方向 1 正向移动 或 -1 反向移动;
// 如果速度过快,本次移动直接过头了(即差值<速度),就直接移动到指定top
if (Math.abs(dis) < speed) {
pos.top = clickPos.top;
} else {
pos.top = pos.top + dir * speed; // 计算新的top,新的位置 = 之前的位置 + 方向 * 速度
}
}
// 用相同的方式计算left
if (pos.left !== clickPos.left) {
const dis = clickPos.left - pos.left;
const dir = dis > 0 ? 1 : -1;
if (Math.abs(dis) < speed) {
pos.left = clickPos.left;
} else {
pos.left = pos.left + dir * speed;
}
}
}
/**
* 更新显示对象位置(渲染)
*/
function render() {
bird.style.top = pos.top + "px";
bird.style.left = pos.left + "px";
}
function loop() {
requestAnimationFrame(loop); // 循环调用requestAnimationFrame
update(); // 先数据更新
render(); // 后渲染更新
}
loop();
</script>
</html>
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>04.高性能鸟</title>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
#bird {
/* 大小 */
width: 50px;
height: 50px;
/* 颜色 */
background-color: #f00;
/* 位置 */
position: absolute;
/* 旋转、缩放等属性 */
transform: scale(1, 1) rotate(45deg);
/* 锚点 */
transform-origin: center;
}
</style>
</head>
<body>
<div id="bird"></div>
</body>
<script>
const bird = document.getElementById("bird");
const speed = 15; // bird的速度 每次移动多少距离
const pos = { // bird的位置 bird当前的位置
top: 150,
left: 150,
}
const clickPos = { // 鼠标点击的位置 bird将要的到达的位置
top: 150,
left: 150,
}
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置
const mouseDown = (e) => {
// 记录bird将要到达的位置,使用动画慢慢到达
clickPos.top = e.clientY;
clickPos.left = e.clientX;
}
document.addEventListener('mousedown', mouseDown);
/**
* 计算Bird的位置(数据更新)
*/
function update() {
// 计算新的top
if (pos.top !== clickPos.top) {
const dis = clickPos.top - pos.top; // 计算差值
const dir = dis > 0 ? 1 : -1; // 计算在top上移动的方向 1 正向移动 或 -1 反向移动;
// 如果速度过快,本次移动直接过头了(即差值<速度),就直接移动到指定top
if (Math.abs(dis) < speed) {
pos.top = clickPos.top;
} else {
pos.top = pos.top + dir * speed; // 计算新的top,新的位置 = 之前的位置 + 方向 * 速度
}
}
// 用相同的方式计算left
if (pos.left !== clickPos.left) {
const dis = clickPos.left - pos.left;
const dir = dis > 0 ? 1 : -1;
if (Math.abs(dis) < speed) {
pos.left = clickPos.left;
} else {
pos.left = pos.left + dir * speed;
}
}
}
/**
* 更新显示对象位置(渲染)
*/
function render() {
bird.style.top = pos.top + "px";
bird.style.left = pos.left + "px";
}
function loop() {
requestAnimationFrame(loop); // 循环调用requestAnimationFrame
update(); // 先数据更新
render(); // 后渲染更新
}
loop();
</script>
</html>
# 高性能Bird
引入概念:`requestAnimationFrame` `帧` `更新和渲染的抽离`
经过上一节的操作,我们已经得到了一只演员鸟
众所周知,现在的游戏玩家都鬼的很,游戏没个高帧率他都不玩。
一般人游戏30帧就感觉比较流畅,可是这群游戏玩家,非要60帧才看着舒服
## 一、概念解释,什么是帧
帧率表示视频、游戏每秒钟刷新画面的次数
60帧即每秒钟刷新60次画面
一般电影的帧率是23.97frame/s,而游戏,低于45帧,会感到明显卡顿
为什么电影只需要24帧就比较流畅呢?
是因为摄影机记录的是1/24秒的所有光线信息,可以理解电影是无数张快门时间是1/24秒的照片同步播放组成的
而游戏计算出来的只有那一瞬间的画面,没有一个时间段内的光线信息,所以需要更高的帧率
因此我们就知道了我们的游戏需要高帧率的重要性了
## 二、requestAnimationFrame
理解了什么是帧就得出来,一个游戏60帧,每帧的时间是,1/60,约等于16.667ms;
我们大可以把`setInterval`的延时改成16.667
但是,众所周知,浏览器提供的延时函数都不靠谱,我们无法通过它得到稳定了60帧画面
隆重的介绍 [requestAnimationFrame](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame)
[requestAnimationFrame详解](https://segmentfault.com/a/1190000020639465?utm_source=tag-newest)
[被誉为神器的requestAnimationFrame](https://www.cnblogs.com/xiaohuochai/p/5777186.html)
这个是一个浏览器给我们提供的API
借助这个API我们可以在浏览器这个平台很好的控制每秒60帧的帧率
接下来我们将数据更新和渲染更新拆分为两个函数
```javascript
/**
* 计算Bird的位置(数据更新)
*/
function update() {
// 计算新的top
if (pos.top !== clickPos.top) {
const dis = clickPos.top - pos.top; // 计算差值
const dir = dis > 0 ? 1 : -1; // 计算在top上移动的方向 1 正向移动 或 -1 反向移动;
// 如果速度过快,本次移动直接过头了(即差值<速度),就直接移动到指定top
if (Math.abs(dis) < speed) {
pos.top = clickPos.top;
} else {
pos.top = pos.top + dir * speed; // 计算新的top,新的位置 = 之前的位置 + 方向 * 速度
}
}
// 用相同的方式计算left
if (pos.left !== clickPos.left) {
const dis = clickPos.left - pos.left;
const dir = dis > 0 ? 1 : -1;
if (Math.abs(dis) < speed) {
pos.left = clickPos.left;
} else {
pos.left = pos.left + dir * speed;
}
}
}
/**
* 更新显示对象位置(渲染)
*/
function render() {
bird.style.top = pos.top + "px";
bird.style.left = pos.left + "px";
}
```
然后用`requestAnimationFrame`来创建一个主循环,在主循环中依次调用这两个函数
```javascript
function loop() {
requestAnimationFrame(loop); // 循环调用requestAnimationFrame
update(); // 先数据更新
render(); // 后渲染更新
}
loop();
```
再次运行案例,我们得到了更流畅,性能更高的Bird了
# 高性能Bird
引入概念:`requestAnimationFrame` `帧` `更新和渲染的抽离`
经过上一节的操作,我们已经得到了一只演员鸟
众所周知,现在的游戏玩家都鬼的很,游戏没个高帧率他都不玩。
一般人游戏30帧就感觉比较流畅,可是这群游戏玩家,非要60帧才看着舒服
## 一、概念解释,什么是帧
帧率表示视频、游戏每秒钟刷新画面的次数
60帧即每秒钟刷新60次画面
一般电影的帧率是23.97frame/s,而游戏,低于45帧,会感到明显卡顿
为什么电影只需要24帧就比较流畅呢?
是因为摄影机记录的是1/24秒的所有光线信息,可以理解电影是无数张快门时间是1/24秒的照片同步播放组成的
而游戏计算出来的只有那一瞬间的画面,没有一个时间段内的光线信息,所以需要更高的帧率
因此我们就知道了我们的游戏需要高帧率的重要性了
## 二、requestAnimationFrame
理解了什么是帧就得出来,一个游戏60帧,每帧的时间是,1/60,约等于16.667ms;
我们大可以把`setInterval`的延时改成16.667
但是,众所周知,浏览器提供的延时函数都不靠谱,我们无法通过它得到稳定了60帧画面
隆重的介绍 [requestAnimationFrame](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame)
[requestAnimationFrame详解](https://segmentfault.com/a/1190000020639465?utm_source=tag-newest)
[被誉为神器的requestAnimationFrame](https://www.cnblogs.com/xiaohuochai/p/5777186.html)
这个是一个浏览器给我们提供的API
借助这个API我们可以在浏览器这个平台很好的控制每秒60帧的帧率
接下来我们将数据更新和渲染更新拆分为两个函数
```javascript
/**
* 计算Bird的位置(数据更新)
*/
function update() {
// 计算新的top
if (pos.top !== clickPos.top) {
const dis = clickPos.top - pos.top; // 计算差值
const dir = dis > 0 ? 1 : -1; // 计算在top上移动的方向 1 正向移动 或 -1 反向移动;
// 如果速度过快,本次移动直接过头了(即差值<速度),就直接移动到指定top
if (Math.abs(dis) < speed) {
pos.top = clickPos.top;
} else {
pos.top = pos.top + dir * speed; // 计算新的top,新的位置 = 之前的位置 + 方向 * 速度
}
}
// 用相同的方式计算left
if (pos.left !== clickPos.left) {
const dis = clickPos.left - pos.left;
const dir = dis > 0 ? 1 : -1;
if (Math.abs(dis) < speed) {
pos.left = clickPos.left;
} else {
pos.left = pos.left + dir * speed;
}
}
}
/**
* 更新显示对象位置(渲染)
*/
function render() {
bird.style.top = pos.top + "px";
bird.style.left = pos.left + "px";
}
```
然后用`requestAnimationFrame`来创建一个主循环,在主循环中依次调用这两个函数
```javascript
function loop() {
requestAnimationFrame(loop); // 循环调用requestAnimationFrame
update(); // 先数据更新
render(); // 后渲染更新
}
loop();
```
再次运行案例,我们得到了更流畅,性能更高的Bird了
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>05.精灵Sprite</title>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
#bird {
/* 位置 */
position: absolute;
/* 旋转、缩放等属性 */
transform: scale(1, 1) rotate(0deg);
transform-origin: center; /* 锚点 */
}
</style>
</head>
<body>
<img id="bird" src="../images/bird/bird_01.png"/>
</body>
<script>
const bird = document.getElementById("bird");
const speed = 15; // bird的速度 每次移动多少距离
const pos = { // bird的位置 bird当前的位置
top: 150,
left: 150,
}
const clickPos = { // 鼠标点击的位置 bird将要的到达的位置
top: 150,
left: 150,
}
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置
const mouseDown = (e) => {
// 记录bird将要到达的位置,使用动画慢慢到达
clickPos.top = e.clientY;
clickPos.left = e.clientX;
}
document.addEventListener('mousedown', mouseDown);
/**
* 计算Bird的位置(数据更新)
*/
function update() {
// 计算新的top
if (pos.top !== clickPos.top) {
const dis = clickPos.top - pos.top; // 计算差值
const dir = dis > 0 ? 1 : -1; // 计算在top上移动的方向 1 正向移动 或 -1 反向移动;
// 如果速度过快,本次移动直接过头了(即差值<速度),就直接移动到指定top
if (Math.abs(dis) < speed) {
pos.top = clickPos.top;
} else {
pos.top = pos.top + dir * speed; // 计算新的top,新的位置 = 之前的位置 + 方向 * 速度
}
}
// 用相同的方式计算left
if (pos.left !== clickPos.left) {
const dis = clickPos.left - pos.left;
const dir = dis > 0 ? 1 : -1;
if (Math.abs(dis) < speed) {
pos.left = clickPos.left;
} else {
pos.left = pos.left + dir * speed;
}
}
}
/**
* 更新显示对象位置(渲染)
*/
function render() {
bird.style.top = pos.top + "px";
bird.style.left = pos.left + "px";
}
function loop() {
requestAnimationFrame(loop); // 循环调用requestAnimationFrame
update(); // 先数据更新
render(); // 后渲染更新
}
loop();
</script>
</html>
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>05.精灵Sprite</title>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
#bird {
/* 位置 */
position: absolute;
/* 旋转、缩放等属性 */
transform: scale(1, 1) rotate(0deg);
transform-origin: center; /* 锚点 */
}
</style>
</head>
<body>
<img id="bird" src="../images/bird/bird_01.png"/>
</body>
<script>
const bird = document.getElementById("bird");
const speed = 15; // bird的速度 每次移动多少距离
const pos = { // bird的位置 bird当前的位置
top: 150,
left: 150,
}
const clickPos = { // 鼠标点击的位置 bird将要的到达的位置
top: 150,
left: 150,
}
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置
const mouseDown = (e) => {
// 记录bird将要到达的位置,使用动画慢慢到达
clickPos.top = e.clientY;
clickPos.left = e.clientX;
}
document.addEventListener('mousedown', mouseDown);
/**
* 计算Bird的位置(数据更新)
*/
function update() {
// 计算新的top
if (pos.top !== clickPos.top) {
const dis = clickPos.top - pos.top; // 计算差值
const dir = dis > 0 ? 1 : -1; // 计算在top上移动的方向 1 正向移动 或 -1 反向移动;
// 如果速度过快,本次移动直接过头了(即差值<速度),就直接移动到指定top
if (Math.abs(dis) < speed) {
pos.top = clickPos.top;
} else {
pos.top = pos.top + dir * speed; // 计算新的top,新的位置 = 之前的位置 + 方向 * 速度
}
}
// 用相同的方式计算left
if (pos.left !== clickPos.left) {
const dis = clickPos.left - pos.left;
const dir = dis > 0 ? 1 : -1;
if (Math.abs(dis) < speed) {
pos.left = clickPos.left;
} else {
pos.left = pos.left + dir * speed;
}
}
}
/**
* 更新显示对象位置(渲染)
*/
function render() {
bird.style.top = pos.top + "px";
bird.style.left = pos.left + "px";
}
function loop() {
requestAnimationFrame(loop); // 循环调用requestAnimationFrame
update(); // 先数据更新
render(); // 后渲染更新
}
loop();
</script>
</html>
# 精灵 Sprite
引入概念:`精灵` `Sprite`
经过上一节的操作,我们已经得到了一只性能很高的Bird
如果一款游戏只有大红大绿的方块,那一定也是没人玩的
所以本节我们将要介绍游戏中的一个重要的概念`精灵`,即`Sprite`
各位可将它理解为图片即`<img/>`
- 1.创建一个`<img/>`模拟精灵的创建。这样将会更容易理解
你也可以使用一个`<div><img/></div>`, 因为在游戏开发中,`组合`优于`继承`,组件化游戏开发多采用此方案
```html
<img id="bird" src="../images/bird/bird_01.png"/>
```
- 2.给精灵一个样式
```css
#bird {
/* 位置 */
position: absolute;
/* 旋转、缩放等属性 */
transform: scale(1, 1) rotate(0deg);
transform-origin: center; /* 锚点 */
}
```
到此结束,我们已经有了第一个有头有脸的显示对象
![05_1.png](../images/05_1.png)
# 精灵 Sprite
引入概念:`精灵` `Sprite`
经过上一节的操作,我们已经得到了一只性能很高的Bird
如果一款游戏只有大红大绿的方块,那一定也是没人玩的
所以本节我们将要介绍游戏中的一个重要的概念`精灵`,即`Sprite`
各位可将它理解为图片即`<img/>`
- 1.创建一个`<img/>`模拟精灵的创建。这样将会更容易理解
你也可以使用一个`<div><img/></div>`, 因为在游戏开发中,`组合`优于`继承`,组件化游戏开发多采用此方案
```html
<img id="bird" src="../images/bird/bird_01.png"/>
```
- 2.给精灵一个样式
```css
#bird {
/* 位置 */
position: absolute;
/* 旋转、缩放等属性 */
transform: scale(1, 1) rotate(0deg);
transform-origin: center; /* 锚点 */
}
```
到此结束,我们已经有了第一个有头有脸的显示对象
![05_1.png](../images/05_1.png)
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>06.面向对象</title>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
</style>
<script src="../lib/lib.js"></script>
</head>
<body>
<img id="bird" src="../images/bird/bird_01.png">
</body>
<script>
class Bird extends GameObject {
speed; // bird的速度 每次移动多少距离
constructor(id, speed) {
super(id);
this.speed = speed; // 保存speed
// 给个默认位置
this.top = 150;
this.left = 150;
}
update() {
super.update();
const { top, left } = this;
const speed = this.speed;
// 计算新的top
if (top !== clickPos.top) {
const dis = clickPos.top - top; // 计算差值
const dir = dis > 0 ? 1 : -1; // 计算在top上移动的方向 1 正向移动 或 -1 反向移动;
// 如果速度过快,本次移动直接过头了(即差值<速度),就直接移动到指定top
if (Math.abs(dis) < speed) {
this.top = clickPos.top;
} else {
this.top = top + dir * speed; // 计算新的top,新的位置 = 之前的位置 + 方向 * 速度
}
}
// 用相同的方式计算left
if (left !== clickPos.left) {
const dis = clickPos.left - left;
const dir = dis > 0 ? 1 : -1;
if (Math.abs(dis) < speed) {
this.left = clickPos.left;
} else {
this.left = left + dir * speed;
}
}
}
}
</script>
<script>
const clickPos = { // 鼠标点击的位置 bird将要的到达的位置
top: 150,
left: 150,
}
const bird = new Bird("bird", 15);
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置
const mouseDown = (e) => {
// 记录bird将要到达的位置,使用动画慢慢到达
clickPos.top = e.clientY;
clickPos.left = e.clientX;
}
document.addEventListener('mousedown', mouseDown);
/**
* 计算Bird的位置(数据更新)
*/
function update() {
bird.update();
}
/**
* 更新显示对象位置(渲染)
*/
function render() {
bird.render();
}
function loop() {
requestAnimationFrame(loop); // 循环调用requestAnimationFrame
update(); // 先数据更新
render(); // 后渲染更新
}
loop();
</script>
</html>
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>06.面向对象</title>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
</style>
<script src="../lib/lib.js"></script>
</head>
<body>
<img id="bird" src="../images/bird/bird_01.png">
</body>
<script>
class Bird extends GameObject {
speed; // bird的速度 每次移动多少距离
constructor(id, speed) {
super(id);
this.speed = speed; // 保存speed
// 给个默认位置
this.top = 150;
this.left = 150;
}
update() {
super.update();
const { top, left } = this;
const speed = this.speed;
// 计算新的top
if (top !== clickPos.top) {
const dis = clickPos.top - top; // 计算差值
const dir = dis > 0 ? 1 : -1; // 计算在top上移动的方向 1 正向移动 或 -1 反向移动;
// 如果速度过快,本次移动直接过头了(即差值<速度),就直接移动到指定top
if (Math.abs(dis) < speed) {
this.top = clickPos.top;
} else {
this.top = top + dir * speed; // 计算新的top,新的位置 = 之前的位置 + 方向 * 速度
}
}
// 用相同的方式计算left
if (left !== clickPos.left) {
const dis = clickPos.left - left;
const dir = dis > 0 ? 1 : -1;
if (Math.abs(dis) < speed) {
this.left = clickPos.left;
} else {
this.left = left + dir * speed;
}
}
}
}
</script>
<script>
const clickPos = { // 鼠标点击的位置 bird将要的到达的位置
top: 150,
left: 150,
}
const bird = new Bird("bird", 15);
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置
const mouseDown = (e) => {
// 记录bird将要到达的位置,使用动画慢慢到达
clickPos.top = e.clientY;
clickPos.left = e.clientX;
}
document.addEventListener('mousedown', mouseDown);
/**
* 计算Bird的位置(数据更新)
*/
function update() {
bird.update();
}
/**
* 更新显示对象位置(渲染)
*/
function render() {
bird.render();
}
function loop() {
requestAnimationFrame(loop); // 循环调用requestAnimationFrame
update(); // 先数据更新
render(); // 后渲染更新
}
loop();
</script>
</html>
# 面向对象
引入概念:`面向对象` `游戏对象` `生命周期`
经过上一节的操作,我们已经得到了一只有头有脸的Bird
按这样的方式写代码,写一个小游戏那要多麻烦
所以本节我们要将游戏中通用的东西抽离出来抽象为一个游戏对象,之后所有对象的创建都继承于这个基本的游戏对象
## GameObject
- 1.创建一个js并引入,在这个js中写入如下代码
下面的代码抽象了一个简单的GameObject
```javascript
/**
* 抽象了一个简单的GameObject
*/
class GameObject {
id; // 绑定的dom元素的id
dom; // 绑定的dom元素
// 位置
top = 0;
left = 0;
// 缩放
scaleX = 1;
scaleY = 1;
// 旋转
rotate = 0;
/**
* 获得宽高
* @returns {{width: number, height: number}}
*/
get size() {
return {
width: this.dom.clientWidth,
height: this.dom.clientHeight,
}
}
constructor(id) {
this.id = id;
this.dom = document.getElementById(id); // 在构造函数中绑定dom元素
this.dom.style.position = "absolute";
}
/**
* 抽离数据更新部分
*/
update() {
}
/**
* 抽离渲染部分
*/
render() {
const { top, left, scaleX, scaleY, rotate } = this;
this.dom.style.top = top + "px";
this.dom.style.left = left + "px";
this.dom.style.transform = `scale(${scaleX}, ${scaleY}) rotate(${rotate}deg)`;
}
/**
* 抽离销毁部分
*/
destroy() {
}
}
```
- 2.用面向对象的方式创建鸟的class
```javascript
class Bird extends GameObject {
speed; // bird的速度 每次移动多少距离
constructor(id, speed) {
super(id);
this.speed = speed; // 保存speed
// 给个默认位置
this.top = 150;
this.left = 150;
}
update() {
super.update();
const { top, left } = this;
const speed = this.speed;
// 计算新的top
if (top !== clickPos.top) {
const dis = clickPos.top - top; // 计算差值
const dir = dis > 0 ? 1 : -1; // 计算在top上移动的方向 1 正向移动 或 -1 反向移动;
// 如果速度过快,本次移动直接过头了(即差值<速度),就直接移动到指定top
if (Math.abs(dis) < speed) {
this.top = clickPos.top;
} else {
this.top = top + dir * speed; // 计算新的top,新的位置 = 之前的位置 + 方向 * 速度
}
}
// 用相同的方式计算left
if (left !== clickPos.left) {
const dis = clickPos.left - left;
const dir = dis > 0 ? 1 : -1;
if (Math.abs(dis) < speed) {
this.left = clickPos.left;
} else {
this.left = left + dir * speed;
}
}
}
}
```
- 3.创建鸟的实例
```javascript
const bird = new Bird("bird", 15);
```
- 4.加入到循环中
```javascript
/**
* 计算Bird的位置(数据更新)
*/
function update() {
bird.update();
}
/**
* 更新显示对象位置(渲染)
*/
function render() {
bird.render();
}
```
再次运行案例,效果一样,但是代码维护更方便,更容易懂,且每个对象有独立的生命周期,更易于组件化,可视化
# 面向对象
引入概念:`面向对象` `游戏对象` `生命周期`
经过上一节的操作,我们已经得到了一只有头有脸的Bird
按这样的方式写代码,写一个小游戏那要多麻烦
所以本节我们要将游戏中通用的东西抽离出来抽象为一个游戏对象,之后所有对象的创建都继承于这个基本的游戏对象
## GameObject
- 1.创建一个js并引入,在这个js中写入如下代码
下面的代码抽象了一个简单的GameObject
```javascript
/**
* 抽象了一个简单的GameObject
*/
class GameObject {
id; // 绑定的dom元素的id
dom; // 绑定的dom元素
// 位置
top = 0;
left = 0;
// 缩放
scaleX = 1;
scaleY = 1;
// 旋转
rotate = 0;
/**
* 获得宽高
* @returns {{width: number, height: number}}
*/
get size() {
return {
width: this.dom.clientWidth,
height: this.dom.clientHeight,
}
}
constructor(id) {
this.id = id;
this.dom = document.getElementById(id); // 在构造函数中绑定dom元素
this.dom.style.position = "absolute";
}
/**
* 抽离数据更新部分
*/
update() {
}
/**
* 抽离渲染部分
*/
render() {
const { top, left, scaleX, scaleY, rotate } = this;
this.dom.style.top = top + "px";
this.dom.style.left = left + "px";
this.dom.style.transform = `scale(${scaleX}, ${scaleY}) rotate(${rotate}deg)`;
}
/**
* 抽离销毁部分
*/
destroy() {
}
}
```
- 2.用面向对象的方式创建鸟的class
```javascript
class Bird extends GameObject {
speed; // bird的速度 每次移动多少距离
constructor(id, speed) {
super(id);
this.speed = speed; // 保存speed
// 给个默认位置
this.top = 150;
this.left = 150;
}
update() {
super.update();
const { top, left } = this;
const speed = this.speed;
// 计算新的top
if (top !== clickPos.top) {
const dis = clickPos.top - top; // 计算差值
const dir = dis > 0 ? 1 : -1; // 计算在top上移动的方向 1 正向移动 或 -1 反向移动;
// 如果速度过快,本次移动直接过头了(即差值<速度),就直接移动到指定top
if (Math.abs(dis) < speed) {
this.top = clickPos.top;
} else {
this.top = top + dir * speed; // 计算新的top,新的位置 = 之前的位置 + 方向 * 速度
}
}
// 用相同的方式计算left
if (left !== clickPos.left) {
const dis = clickPos.left - left;
const dir = dis > 0 ? 1 : -1;
if (Math.abs(dis) < speed) {
this.left = clickPos.left;
} else {
this.left = left + dir * speed;
}
}
}
}
```
- 3.创建鸟的实例
```javascript
const bird = new Bird("bird", 15);
```
- 4.加入到循环中
```javascript
/**
* 计算Bird的位置(数据更新)
*/
function update() {
bird.update();
}
/**
* 更新显示对象位置(渲染)
*/
function render() {
bird.render();
}
```
再次运行案例,效果一样,但是代码维护更方便,更容易懂,且每个对象有独立的生命周期,更易于组件化,可视化
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>07.FlppyBird-背景循环滚动</title>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
</style>
<script src="../lib/lib.js"></script>
</head>
<body>
<div id="bg">
<img id="bg_1" src="../images/bird/background.png">
<img id="bg_2" src="../images/bird/background.png">
</div>
<div id="land">
<img id="land_1" src="../images/bird/land.png">
<img id="land_2" src="../images/bird/land.png">
</div>
<img id="bird" src="../images/bird/bird_01.png">
</body>
<script>
/**
* 屏幕宽高
* @type {{width: number, height: number}}
*/
const winSize = {
width: document.body.clientWidth,
height: document.body.clientHeight,
}
</script>
<script>
class Bird extends GameObject {
speed; // bird的速度 每次移动多少距离
constructor(id, speed = 15) {
super(id);
this.speed = speed; // 保存speed
// 给个默认位置
this.top = 150;
this.left = 150;
}
update() {
super.update();
const { top, left } = this;
const speed = this.speed;
// 计算新的top
if (top !== clickPos.top) {
const dis = clickPos.top - top; // 计算差值
const dir = dis > 0 ? 1 : -1; // 计算在top上移动的方向 1 正向移动 或 -1 反向移动;
// 如果速度过快,本次移动直接过头了(即差值<速度),就直接移动到指定top
if (Math.abs(dis) < speed) {
this.top = clickPos.top;
} else {
this.top = top + dir * speed; // 计算新的top,新的位置 = 之前的位置 + 方向 * 速度
}
}
// 用相同的方式计算left
if (left !== clickPos.left) {
const dis = clickPos.left - left;
const dir = dis > 0 ? 1 : -1;
if (Math.abs(dis) < speed) {
this.left = clickPos.left;
} else {
this.left = left + dir * speed;
}
}
}
}
class ScrollMgr extends GameObject {
speed; // 滚动速度
bg1; // bg1
bg2; // bg2
constructor(id, bg1, bg2, speed = 5) {
super(id);
this.bg1 = bg1;
this.bg2 = bg2;
this.speed = speed;
bg1.top = winSize.height - bg1.size.height; // 放在底部
bg2.top = winSize.height - bg2.size.height; // 放在底部
}
update() {
super.update();
// 获取一些参数
let bg1Left = this.bg1.left;
const bg1Width = this.bg1.size.width;
// 计算位置
bg1Left -= this.speed; // 计算位置
this.bg1.left = bg1Left; // 设置bg1的位置
this.bg2.left = bg1Left + this.bg1.size.width; // bg2跟在bg1后面
// 如果超出屏幕则交换bg1和bg2,为了做到循环滚动
if (bg1Left <= -bg1Width) {
const temp = this.bg1;
this.bg1 = this.bg2;
this.bg2 = temp;
}
}
}
</script>
<script>
const clickPos = { // 鼠标点击的位置 bird将要的到达的位置
top: 150,
left: 150,
}
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置
const mouseDown = (e) => {
// 记录bird将要到达的位置,使用动画慢慢到达
clickPos.top = e.clientY;
clickPos.left = e.clientX;
}
document.addEventListener('mousedown', mouseDown);
</script>
<script>
// 创建鸟
const bird = new Bird("bird", 15);
// 创建背景
const bg1 = new GameObject("bg_1");
const bg2 = new GameObject("bg_2");
const bgMgr = new ScrollMgr("bg", bg1, bg2, 2);
// 创建地面
const land1 = new GameObject("land_1");
const land2 = new GameObject("land_2");
const landMgr = new ScrollMgr("land", land1, land2, 5);
// 将背景放在地面的上面,因为默认top是0,子节点在内部定位在底部,所以只需要把背景定位在负的land的高度就可以了
bgMgr.top = -land1.size.height;
/**
* 数据更新
*/
function update() {
// bird更新
bird.update();
// 背景更新
bgMgr.update();
bg1.update();
bg2.update();
// 地面更新
landMgr.update();
land1.update();
land2.update();
}
/**
* 渲染更新
*/
function render() {
// bird渲染
bird.render();
// 背景渲染
bgMgr.render();
bg1.render();
bg2.render();
// 地面渲染
landMgr.render();
land1.render();
land2.render();
}
function loop() {
requestAnimationFrame(loop); // 循环调用requestAnimationFrame
update(); // 先数据更新
render(); // 后渲染更新
}
loop();
</script>
</html>
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>07.FlppyBird-背景循环滚动</title>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
</style>
<script src="../lib/lib.js"></script>
</head>
<body>
<div id="bg">
<img id="bg_1" src="../images/bird/background.png">
<img id="bg_2" src="../images/bird/background.png">
</div>
<div id="land">
<img id="land_1" src="../images/bird/land.png">
<img id="land_2" src="../images/bird/land.png">
</div>
<img id="bird" src="../images/bird/bird_01.png">
</body>
<script>
/**
* 屏幕宽高
* @type {{width: number, height: number}}
*/
const winSize = {
width: document.body.clientWidth,
height: document.body.clientHeight,
}
</script>
<script>
class Bird extends GameObject {
speed; // bird的速度 每次移动多少距离
constructor(id, speed = 15) {
super(id);
this.speed = speed; // 保存speed
// 给个默认位置
this.top = 150;
this.left = 150;
}
update() {
super.update();
const { top, left } = this;
const speed = this.speed;
// 计算新的top
if (top !== clickPos.top) {
const dis = clickPos.top - top; // 计算差值
const dir = dis > 0 ? 1 : -1; // 计算在top上移动的方向 1 正向移动 或 -1 反向移动;
// 如果速度过快,本次移动直接过头了(即差值<速度),就直接移动到指定top
if (Math.abs(dis) < speed) {
this.top = clickPos.top;
} else {
this.top = top + dir * speed; // 计算新的top,新的位置 = 之前的位置 + 方向 * 速度
}
}
// 用相同的方式计算left
if (left !== clickPos.left) {
const dis = clickPos.left - left;
const dir = dis > 0 ? 1 : -1;
if (Math.abs(dis) < speed) {
this.left = clickPos.left;
} else {
this.left = left + dir * speed;
}
}
}
}
class ScrollMgr extends GameObject {
speed; // 滚动速度
bg1; // bg1
bg2; // bg2
constructor(id, bg1, bg2, speed = 5) {
super(id);
this.bg1 = bg1;
this.bg2 = bg2;
this.speed = speed;
bg1.top = winSize.height - bg1.size.height; // 放在底部
bg2.top = winSize.height - bg2.size.height; // 放在底部
}
update() {
super.update();
// 获取一些参数
let bg1Left = this.bg1.left;
const bg1Width = this.bg1.size.width;
// 计算位置
bg1Left -= this.speed; // 计算位置
this.bg1.left = bg1Left; // 设置bg1的位置
this.bg2.left = bg1Left + this.bg1.size.width; // bg2跟在bg1后面
// 如果超出屏幕则交换bg1和bg2,为了做到循环滚动
if (bg1Left <= -bg1Width) {
const temp = this.bg1;
this.bg1 = this.bg2;
this.bg2 = temp;
}
}
}
</script>
<script>
const clickPos = { // 鼠标点击的位置 bird将要的到达的位置
top: 150,
left: 150,
}
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置
const mouseDown = (e) => {
// 记录bird将要到达的位置,使用动画慢慢到达
clickPos.top = e.clientY;
clickPos.left = e.clientX;
}
document.addEventListener('mousedown', mouseDown);
</script>
<script>
// 创建鸟
const bird = new Bird("bird", 15);
// 创建背景
const bg1 = new GameObject("bg_1");
const bg2 = new GameObject("bg_2");
const bgMgr = new ScrollMgr("bg", bg1, bg2, 2);
// 创建地面
const land1 = new GameObject("land_1");
const land2 = new GameObject("land_2");
const landMgr = new ScrollMgr("land", land1, land2, 5);
// 将背景放在地面的上面,因为默认top是0,子节点在内部定位在底部,所以只需要把背景定位在负的land的高度就可以了
bgMgr.top = -land1.size.height;
/**
* 数据更新
*/
function update() {
// bird更新
bird.update();
// 背景更新
bgMgr.update();
bg1.update();
bg2.update();
// 地面更新
landMgr.update();
land1.update();
land2.update();
}
/**
* 渲染更新
*/
function render() {
// bird渲染
bird.render();
// 背景渲染
bgMgr.render();
bg1.render();
bg2.render();
// 地面渲染
landMgr.render();
land1.render();
land2.render();
}
function loop() {
requestAnimationFrame(loop); // 循环调用requestAnimationFrame
update(); // 先数据更新
render(); // 后渲染更新
}
loop();
</script>
</html>
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>08.FlppyBird-点击飞起</title>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
</style>
<script src="../lib/lib.js"></script>
</head>
<body>
<div id="bg">
<img id="bg_1" src="../images/bird/background.png">
<img id="bg_2" src="../images/bird/background.png">
</div>
<div id="land">
<img id="land_1" src="../images/bird/land.png">
<img id="land_2" src="../images/bird/land.png">
</div>
<img id="bird" src="../images/bird/bird_01.png">
</body>
<script>
/**
* 屏幕宽高
* @type {{width: number, height: number}}
*/
const winSize = {
width: document.body.clientWidth,
height: document.body.clientHeight,
}
</script>
<script>
class Bird extends GameObject {
speed; // bird的速度 每次移动多少距离
gravity; // 重力加速度
constructor(id, gravity = 0.2, speed = 0) {
super(id);
this.speed = speed; // 保存speed
this.gravity = gravity; // 保存gravity
// 放到合适的位置
const { width, height } = this.size;
this.top = (winSize.height - height) / 2 - 100;
this.left = (winSize.width - width) / 2;
}
update() {
super.update();
// v = v0 + a * t²
this.speed += this.gravity; // 速度 = 速度 + 加速度 * 时间²
this.top += this.speed; // 更新位置
}
}
/**
* 滚动器
*/
class ScrollMgr extends GameObject {
speed; // 滚动速度
bg1; // bg1
bg2; // bg2
constructor(id, bg1, bg2, speed = 5) {
super(id);
this.bg1 = bg1;
this.bg2 = bg2;
this.speed = speed;
bg1.top = winSize.height - bg1.size.height; // 放在底部
bg2.top = winSize.height - bg2.size.height; // 放在底部
}
update() {
super.update();
// 获取一些参数
let bg1Left = this.bg1.left;
const bg1Width = this.bg1.size.width;
// 计算位置
bg1Left -= this.speed; // 计算位置
this.bg1.left = bg1Left; // 设置bg1的位置
this.bg2.left = bg1Left + this.bg1.size.width; // bg2跟在bg1后面
// 如果超出屏幕则交换bg1和bg2,为了做到循环滚动
if (bg1Left <= -bg1Width) {
const temp = this.bg1;
this.bg1 = this.bg2;
this.bg2 = temp;
}
}
}
</script>
<script>
// 创建鸟
const bird = new Bird("bird");
// 创建背景
const bg1 = new GameObject("bg_1");
const bg2 = new GameObject("bg_2");
const bgMgr = new ScrollMgr("bg", bg1, bg2, 2);
// 创建地面
const land1 = new GameObject("land_1");
const land2 = new GameObject("land_2");
const landMgr = new ScrollMgr("land", land1, land2, 5);
// 将背景放在地面的上面,因为默认top是0,子节点在内部定位在底部,所以只需要把背景定位在负的land的高度就可以了
bgMgr.top = -land1.size.height;
</script>
<script>
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置
const mouseDown = (e) => {
bird.speed = -8;
}
document.addEventListener('mousedown', mouseDown);
</script>
<script>
/**
* 数据更新
*/
function update() {
// bird更新
bird.update();
// 背景更新
bgMgr.update();
bg1.update();
bg2.update();
// 地面更新
landMgr.update();
land1.update();
land2.update();
}
/**
* 渲染更新
*/
function render() {
// bird渲染
bird.render();
// 背景渲染
bgMgr.render();
bg1.render();
bg2.render();
// 地面渲染
landMgr.render();
land1.render();
land2.render();
}
function loop() {
requestAnimationFrame(loop); // 循环调用requestAnimationFrame
update(); // 先数据更新
render(); // 后渲染更新
}
loop();
</script>
</html>
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>08.FlppyBird-点击飞起</title>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
</style>
<script src="../lib/lib.js"></script>
</head>
<body>
<div id="bg">
<img id="bg_1" src="../images/bird/background.png">
<img id="bg_2" src="../images/bird/background.png">
</div>
<div id="land">
<img id="land_1" src="../images/bird/land.png">
<img id="land_2" src="../images/bird/land.png">
</div>
<img id="bird" src="../images/bird/bird_01.png">
</body>
<script>
/**
* 屏幕宽高
* @type {{width: number, height: number}}
*/
const winSize = {
width: document.body.clientWidth,
height: document.body.clientHeight,
}
</script>
<script>
class Bird extends GameObject {
speed; // bird的速度 每次移动多少距离
gravity; // 重力加速度
constructor(id, gravity = 0.2, speed = 0) {
super(id);
this.speed = speed; // 保存speed
this.gravity = gravity; // 保存gravity
// 放到合适的位置
const { width, height } = this.size;
this.top = (winSize.height - height) / 2 - 100;
this.left = (winSize.width - width) / 2;
}
update() {
super.update();
// v = v0 + a * t²
this.speed += this.gravity; // 速度 = 速度 + 加速度 * 时间²
this.top += this.speed; // 更新位置
}
}
/**
* 滚动器
*/
class ScrollMgr extends GameObject {
speed; // 滚动速度
bg1; // bg1
bg2; // bg2
constructor(id, bg1, bg2, speed = 5) {
super(id);
this.bg1 = bg1;
this.bg2 = bg2;
this.speed = speed;
bg1.top = winSize.height - bg1.size.height; // 放在底部
bg2.top = winSize.height - bg2.size.height; // 放在底部
}
update() {
super.update();
// 获取一些参数
let bg1Left = this.bg1.left;
const bg1Width = this.bg1.size.width;
// 计算位置
bg1Left -= this.speed; // 计算位置
this.bg1.left = bg1Left; // 设置bg1的位置
this.bg2.left = bg1Left + this.bg1.size.width; // bg2跟在bg1后面
// 如果超出屏幕则交换bg1和bg2,为了做到循环滚动
if (bg1Left <= -bg1Width) {
const temp = this.bg1;
this.bg1 = this.bg2;
this.bg2 = temp;
}
}
}
</script>
<script>
// 创建鸟
const bird = new Bird("bird");
// 创建背景
const bg1 = new GameObject("bg_1");
const bg2 = new GameObject("bg_2");
const bgMgr = new ScrollMgr("bg", bg1, bg2, 2);
// 创建地面
const land1 = new GameObject("land_1");
const land2 = new GameObject("land_2");
const landMgr = new ScrollMgr("land", land1, land2, 5);
// 将背景放在地面的上面,因为默认top是0,子节点在内部定位在底部,所以只需要把背景定位在负的land的高度就可以了
bgMgr.top = -land1.size.height;
</script>
<script>
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置
const mouseDown = (e) => {
bird.speed = -8;
}
document.addEventListener('mousedown', mouseDown);
</script>
<script>
/**
* 数据更新
*/
function update() {
// bird更新
bird.update();
// 背景更新
bgMgr.update();
bg1.update();
bg2.update();
// 地面更新
landMgr.update();
land1.update();
land2.update();
}
/**
* 渲染更新
*/
function render() {
// bird渲染
bird.render();
// 背景渲染
bgMgr.render();
bg1.render();
bg2.render();
// 地面渲染
landMgr.render();
land1.render();
land2.render();
}
function loop() {
requestAnimationFrame(loop); // 循环调用requestAnimationFrame
update(); // 先数据更新
render(); // 后渲染更新
}
loop();
</script>
</html>
# FlppyBird - 模拟重力
引入概念:`模拟重力`
上节中我们已经完善了GameObject并且增加了一个公共变量来获取屏幕的宽高,还实现了背景的滚动和地面的滚动,并且增加了远慢近快的效果
本节将带大家来实现FlppyBird中的主角
- 简单的自由落体
- 点击屏幕飞起
## 1.改造Bird
- 1.增加`gravity`,代表重力加速度,并且把鸟的位置默认放到屏幕中间
```javascript
class Bird extends GameObject {
speed; // bird的速度 每次移动多少距离
gravity; // 重力加速度
constructor(id, gravity = 0.2, speed = 0) {
super(id);
this.speed = speed; // 保存speed
this.gravity = gravity; // 保存gravity
// 放到合适的位置
const { width, height } = this.size;
this.top = (winSize.height - height) / 2 - 100;
this.left = (winSize.width - width) / 2;
}
/* ... */
}
```
- 2.重写Bird的生命周期`update`
> 速度公式 v = v0 + a * t² 速度 = 速度 + 加速度 * 时间²
根据速度公式,我们很容易就算出了当前帧的速度,并且算出当前的位置
```javascript
class Bird extends GameObject {
/* ... */
update() {
super.update();
// v = v0 + a * t²
this.speed += this.gravity; // 速度 = 速度 + 加速度 * 时间²
this.top += this.speed; // 更新位置
}
}
```
- 3.修改Bird的创建
因为指定了默认参数,所以无需再传入新的参数
```javascript
// 创建鸟
const bird = new Bird("bird");
```
运行案例,发现Bird很快落出屏幕,一去不复返
![08_1](../images/08_1.gif)
- 3.点击屏幕飞跃
修改点击屏幕的回调函数,在点击之后修改Bird的speed属性为负就可以达到飞起的效果
```javascript
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置
const mouseDown = (e) => {
bird.speed = -8;
}
document.addEventListener('mousedown', mouseDown);
```
再次运行案例,点击屏幕发现Bird飞跃而起
![08_2](../images/08_2.gif)
# FlppyBird - 模拟重力
引入概念:`模拟重力`
上节中我们已经完善了GameObject并且增加了一个公共变量来获取屏幕的宽高,还实现了背景的滚动和地面的滚动,并且增加了远慢近快的效果
本节将带大家来实现FlppyBird中的主角
- 简单的自由落体
- 点击屏幕飞起
## 1.改造Bird
- 1.增加`gravity`,代表重力加速度,并且把鸟的位置默认放到屏幕中间
```javascript
class Bird extends GameObject {
speed; // bird的速度 每次移动多少距离
gravity; // 重力加速度
constructor(id, gravity = 0.2, speed = 0) {
super(id);
this.speed = speed; // 保存speed
this.gravity = gravity; // 保存gravity
// 放到合适的位置
const { width, height } = this.size;
this.top = (winSize.height - height) / 2 - 100;
this.left = (winSize.width - width) / 2;
}
/* ... */
}
```
- 2.重写Bird的生命周期`update`
> 速度公式 v = v0 + a * t² 速度 = 速度 + 加速度 * 时间²
根据速度公式,我们很容易就算出了当前帧的速度,并且算出当前的位置
```javascript
class Bird extends GameObject {
/* ... */
update() {
super.update();
// v = v0 + a * t²
this.speed += this.gravity; // 速度 = 速度 + 加速度 * 时间²
this.top += this.speed; // 更新位置
}
}
```
- 3.修改Bird的创建
因为指定了默认参数,所以无需再传入新的参数
```javascript
// 创建鸟
const bird = new Bird("bird");
```
运行案例,发现Bird很快落出屏幕,一去不复返
![08_1](../images/08_1.gif)
- 3.点击屏幕飞跃
修改点击屏幕的回调函数,在点击之后修改Bird的speed属性为负就可以达到飞起的效果
```javascript
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置
const mouseDown = (e) => {
bird.speed = -8;
}
document.addEventListener('mousedown', mouseDown);
```
再次运行案例,点击屏幕发现Bird飞跃而起
![08_2](../images/08_2.gif)
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>09.FlppyBird-节点树</title>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
</style>
<script src="../lib/flppyBirdLib.js"></script>
</head>
<body>
</body>
<script>
/**
* 屏幕宽高
* @type {{width: number, height: number}}
*/
const winSize = {
width: document.body.clientWidth,
height: document.body.clientHeight,
}
</script>
<script>
/**
* Bird
*/
class Bird extends Sprite {
speed; // bird的速度 每次移动多少距离
gravity; // 重力加速度
constructor(gravity = 0.2, speed = 0) {
super("../images/bird/bird_01.png");
this.speed = speed; // 保存speed
this.gravity = gravity; // 保存gravity
}
ready() {
super.ready();
// 放到合适的位置
const { width, height } = this.size;
this.top = (winSize.height - height) / 2 - 100;
this.left = (winSize.width - width) / 2;
}
update() {
super.update();
// v = v0 + a * t²
this.speed += this.gravity; // 速度 = 速度 + 加速度 * 时间²
this.top += this.speed; // 更新位置
}
}
/**
* 滚动器
*/
class ScrollMgr extends GameObject {
speed; // 滚动速度
bg1; // bg1
bg2; // bg2
constructor(bg1, bg2, speed = 5) {
super();
this.bg1 = bg1;
this.bg2 = bg2;
this.speed = speed;
this.addChild(this.bg1);
this.addChild(this.bg2);
}
ready() {
super.ready();
this.bg1.top = winSize.height - this.bg1.size.height; // 放在底部
this.bg2.top = winSize.height - this.bg2.size.height; // 放在底部
}
update() {
super.update();
// 获取一些参数
let bg1Left = this.bg1.left;
const bg1Width = this.bg1.size.width;
// 计算位置
bg1Left -= this.speed; // 计算位置
this.bg1.left = bg1Left; // 设置bg1的位置
this.bg2.left = bg1Left + this.bg1.size.width; // bg2跟在bg1后面
// 如果超出屏幕则交换bg1和bg2,为了做到循环滚动
if (bg1Left <= -bg1Width) {
const temp = this.bg1;
this.bg1 = this.bg2;
this.bg2 = temp;
}
}
}
</script>
<script>
/**
* FlppyBird
*/
class FlppyBird extends GameStage {
bird;
bgMgr;
landMgr;
async preloadRes() {
const path = "../images/bird/";
const promises = [
"bird_01.png", "bird_02.png", "bird_03.png", "pie.png",
"land.png", "background.png", "start_button.png"
].map((v) => {
return loadImgAsync(`${path}${v}`);
});
return Promise.all(promises);
}
async ready() {
// 创建鸟
const bird = this.bird = new Bird();
// 创建背景
const bg1 = new Sprite("../images/bird/background.png");
const bg2 = new Sprite("../images/bird/background.png");
const bgMgr = this.bgMgr = new ScrollMgr(bg1, bg2, 2);
// 创建地面
const land1 = new Sprite("../images/bird/land.png");
const land2 = new Sprite("../images/bird/land.png");
const landMgr = this.landMgr = new ScrollMgr(land1, land2, 5);
this.addChild(bgMgr);
this.addChild(landMgr);
this.addChild(bird);
// 将背景放在地面的上面,因为默认top是0,子节点在内部定位在底部,所以只需要把背景定位在负的land的高度就可以了
bgMgr.top = -land1.size.height;
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置
document.addEventListener('mousedown', this.mouseDown);
}
mouseDown = () => {
this.bird.speed = -8;
}
destroy() {
super.destroy();
document.removeEventListener('mousedown', this.mouseDown);
}
}
// 创建游戏实例
new FlppyBird();
</script>
</html>
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
<title>09.FlppyBird-节点树</title>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
</style>
<script src="../lib/flppyBirdLib.js"></script>
</head>
<body>
</body>
<script>
/**
* 屏幕宽高
* @type {{width: number, height: number}}
*/
const winSize = {
width: document.body.clientWidth,
height: document.body.clientHeight,
}
</script>
<script>
/**
* Bird
*/
class Bird extends Sprite {
speed; // bird的速度 每次移动多少距离
gravity; // 重力加速度
constructor(gravity = 0.2, speed = 0) {
super("../images/bird/bird_01.png");
this.speed = speed; // 保存speed
this.gravity = gravity; // 保存gravity
}
ready() {
super.ready();
// 放到合适的位置
const { width, height } = this.size;
this.top = (winSize.height - height) / 2 - 100;
this.left = (winSize.width - width) / 2;
}
update() {
super.update();
// v = v0 + a * t²
this.speed += this.gravity; // 速度 = 速度 + 加速度 * 时间²
this.top += this.speed; // 更新位置
}
}
/**
* 滚动器
*/
class ScrollMgr extends GameObject {
speed; // 滚动速度
bg1; // bg1
bg2; // bg2
constructor(bg1, bg2, speed = 5) {
super();
this.bg1 = bg1;
this.bg2 = bg2;
this.speed = speed;
this.addChild(this.bg1);
this.addChild(this.bg2);
}
ready() {
super.ready();
this.bg1.top = winSize.height - this.bg1.size.height; // 放在底部
this.bg2.top = winSize.height - this.bg2.size.height; // 放在底部
}
update() {
super.update();
// 获取一些参数
let bg1Left = this.bg1.left;
const bg1Width = this.bg1.size.width;
// 计算位置
bg1Left -= this.speed; // 计算位置
this.bg1.left = bg1Left; // 设置bg1的位置
this.bg2.left = bg1Left + this.bg1.size.width; // bg2跟在bg1后面
// 如果超出屏幕则交换bg1和bg2,为了做到循环滚动
if (bg1Left <= -bg1Width) {
const temp = this.bg1;
this.bg1 = this.bg2;
this.bg2 = temp;
}
}
}
</script>
<script>
/**
* FlppyBird
*/
class FlppyBird extends GameStage {
bird;
bgMgr;
landMgr;
async preloadRes() {
const path = "../images/bird/";
const promises = [
"bird_01.png", "bird_02.png", "bird_03.png", "pie.png",
"land.png", "background.png", "start_button.png"
].map((v) => {
return loadImgAsync(`${path}${v}`);
});
return Promise.all(promises);
}
async ready() {
// 创建鸟
const bird = this.bird = new Bird();
// 创建背景
const bg1 = new Sprite("../images/bird/background.png");
const bg2 = new Sprite("../images/bird/background.png");
const bgMgr = this.bgMgr = new ScrollMgr(bg1, bg2, 2);
// 创建地面
const land1 = new Sprite("../images/bird/land.png");
const land2 = new Sprite("../images/bird/land.png");
const landMgr = this.landMgr = new ScrollMgr(land1, land2, 5);
this.addChild(bgMgr);
this.addChild(landMgr);
this.addChild(bird);
// 将背景放在地面的上面,因为默认top是0,子节点在内部定位在底部,所以只需要把背景定位在负的land的高度就可以了
bgMgr.top = -land1.size.height;
// 使用mousedown监听鼠标按下,并获得鼠标点击的位置
document.addEventListener('mousedown', this.mouseDown);
}
mouseDown = () => {
this.bird.speed = -8;
}
destroy() {
super.destroy();
document.removeEventListener('mousedown', this.mouseDown);
}
}
// 创建游戏实例
new FlppyBird();
</script>
</html>
# FlppyBird - 创建障碍
引入概念:`动态生成障碍`
本节将在上节的内容上,实现动态添加障碍
## 1.实现`Pie`类
- 实现`Pie`类,继承`GameObject`
- `Pie`包含一个上的一个下的
- 上下两个均为`Sprite`,且中间距离随机
```javascript
/**
* Pie
*/
class Pie extends GameObject {
up; // 钢管上部分
down; // 钢管下部分
constructor() {
super();
this.up = this.addChild(new Sprite("../images/bird/pie_up.png"));
this.down = this.addChild(new Sprite("../images/bird/pie_down.png"));
}
/**
* 注意此函数非真正的ready函数,在被addChild的时候都会执行一次
*/
ready() {
super.ready();
// 随机中间的距离
const dis = Math.random() * 80 + 100;
this.down.top = this.up.size.height + dis;
}
// 这里重写一下size,因为子节点使用绝对定位的不计算入包围盒内,真尴尬
get size() {
return {
width: this.up.size.width,
height: this.down.top + this.down.size.height
}
}
}
```
尝试在`FlppyBird``ready`函数中创建,得到如下效果
![10_1.png](../images/10_1.png)
## 2.实现动态创建`Pie`且高度随机,从屏幕左边出现向左移动
- 创建`PieMgr`类管理`Pie`的创建和移动
- 实现创建`Pie`的方法,让`Pie`从屏幕左边出现且高度随机,并统一管理
- 创建定时器,固定事件创建`Pie`
-`update`函数中让所有`Pie`同时向左移动
```javascript
/**
* PieMgr
*/
class PieMgr extends GameObject {
pieArr = []; // 所有的Pie
speed; // Pie移动的速度
delay; // 添加Pie的延时
constructor(speed, delay) {
super();
this.speed = speed;
this.delay = delay;
}
ready() {
super.ready();
// 创建计时器来固定时间创建Pie
setInterval(() => this.createPie(), this.delay);
}
/**
* 创建Pie
*/
createPie() {
const pie = this.addChild(new Pie());
this.pieArr.push(pie); // 加入列表统一管理
pie.top = Math.random() * -150; // 高度随机
pie.left = winSize.width; // 从屏幕左边出现
}
update() {
super.update();
// 所有的Pie同时向左移动
const { speed, pieArr } = this;
pieArr.forEach((pie) => {
pie.left -= speed;
});
}
}
```
## 3.创建`PieMgr`实例并添加到节点树上
- 创建`PieMgr`实例
- 添加到节点树上,且层级在地面和背景的中间
```javascript
class FlppyBird extends GameStage {
pieMgr;
/* ... */
async ready() {
// 创建背景
const bg1 = new Sprite("../images/bird/background.png");
const bg2 = new Sprite("../images/bird/background.png");
const bgMgr = this.bgMgr = new ScrollMgr(bg1, bg2, 2);
// 创建地面
const land1 = new Sprite("../images/bird/land.png");
const land2 = new Sprite("../images/bird/land.png");
const landMgr = this.landMgr = new ScrollMgr(land1, land2, 4);
const pieMgr = this.pieMgr = new PieMgr(4, 1000); // 创建PieMgr
this.addChild(bgMgr);
this.addChild(pieMgr); // 加在背景和地面的中间
this.addChild(landMgr);
/* ... */
}
/* ... */
}
```
运行案例发现,每秒创建一个`Pie`,且从屏幕右边出现,不断相左移动
![10_2.gif](../images/10_2.gif)
# FlppyBird - 创建障碍
引入概念:`动态生成障碍`
本节将在上节的内容上,实现动态添加障碍
## 1.实现`Pie`类
- 实现`Pie`类,继承`GameObject`
- `Pie`包含一个上的一个下的
- 上下两个均为`Sprite`,且中间距离随机
```javascript
/**
* Pie
*/
class Pie extends GameObject {
up; // 钢管上部分
down; // 钢管下部分
constructor() {
super();
this.up = this.addChild(new Sprite("../images/bird/pie_up.png"));
this.down = this.addChild(new Sprite("../images/bird/pie_down.png"));
}
/**
* 注意此函数非真正的ready函数,在被addChild的时候都会执行一次
*/
ready() {
super.ready();
// 随机中间的距离
const dis = Math.random() * 80 + 100;
this.down.top = this.up.size.height + dis;
}
// 这里重写一下size,因为子节点使用绝对定位的不计算入包围盒内,真尴尬
get size() {
return {
width: this.up.size.width,
height: this.down.top + this.down.size.height
}
}
}
```
尝试在`FlppyBird``ready`函数中创建,得到如下效果
![10_1.png](../images/10_1.png)
## 2.实现动态创建`Pie`且高度随机,从屏幕左边出现向左移动
- 创建`PieMgr`类管理`Pie`的创建和移动
- 实现创建`Pie`的方法,让`Pie`从屏幕左边出现且高度随机,并统一管理
- 创建定时器,固定事件创建`Pie`
-`update`函数中让所有`Pie`同时向左移动
```javascript
/**
* PieMgr
*/
class PieMgr extends GameObject {
pieArr = []; // 所有的Pie
speed; // Pie移动的速度
delay; // 添加Pie的延时
constructor(speed, delay) {
super();
this.speed = speed;
this.delay = delay;
}
ready() {
super.ready();
// 创建计时器来固定时间创建Pie
setInterval(() => this.createPie(), this.delay);
}
/**
* 创建Pie
*/
createPie() {
const pie = this.addChild(new Pie());
this.pieArr.push(pie); // 加入列表统一管理
pie.top = Math.random() * -150; // 高度随机
pie.left = winSize.width; // 从屏幕左边出现
}
update() {
super.update();
// 所有的Pie同时向左移动
const { speed, pieArr } = this;
pieArr.forEach((pie) => {
pie.left -= speed;
});
}
}
```
## 3.创建`PieMgr`实例并添加到节点树上
- 创建`PieMgr`实例
- 添加到节点树上,且层级在地面和背景的中间
```javascript
class FlppyBird extends GameStage {
pieMgr;
/* ... */
async ready() {
// 创建背景
const bg1 = new Sprite("../images/bird/background.png");
const bg2 = new Sprite("../images/bird/background.png");
const bgMgr = this.bgMgr = new ScrollMgr(bg1, bg2, 2);
// 创建地面
const land1 = new Sprite("../images/bird/land.png");
const land2 = new Sprite("../images/bird/land.png");
const landMgr = this.landMgr = new ScrollMgr(land1, land2, 4);
const pieMgr = this.pieMgr = new PieMgr(4, 1000); // 创建PieMgr
this.addChild(bgMgr);
this.addChild(pieMgr); // 加在背景和地面的中间
this.addChild(landMgr);
/* ... */
}
/* ... */
}
```
运行案例发现,每秒创建一个`Pie`,且从屏幕右边出现,不断相左移动
![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