Agar.io于2015年发布,成为
.io这类新
游戏的前身 ,此后其受欢迎程度已大大提高。 我经历了.io游戏的普及度的增长:在过去三年中,我
创作并出售了两种此类游戏。 。
如果您以前从未听说过此类游戏:这些是易于参与的免费多人网络游戏(无需帐户)。 通常他们会在同一个竞技场上推动许多对立的球员。 .io流派的其他著名游戏是:
Slither.io和
Diep.io。在本文中,我们将了解如何
从头开始创建.io游戏 。 为此,仅对Javascript的知识就足够了:您需要了解诸如
ES6语法,
this
和
Promises之类的知识 。 即使您不太了解Javascript,也仍然可以弄清楚大部分文章。
游戏示例.io
为了帮助您学习,我们将参考
一个示例.io游戏 。 尝试播放!
游戏非常简单:您可以在有其他玩家的竞技场上控制飞船。 您的飞船会自动发射炮弹,并尝试击中其他玩家,同时避开其炮弹。
1.概述/项目结构
我建议下载示例游戏的源代码,以便您关注我。
该示例使用以下内容:
这是项目目录结构的样子:
public/ assets/ ... src/ client/ css/ ... html/ index.html index.js ... server/ server.js ... shared/ constants.js
公开/
服务器将静态传输
public/
文件夹中的所有内容。
public/assets/
包含我们项目使用的图像。
src /
所有源代码都在
src/
文件夹中。
client/
server/
的名称不言自明,而
shared/
包含一个由客户和服务器导入的常量文件。
2.组件/项目参数
如上所述,我们使用
Webpack模块管理器来构建项目。 让我们看一下我们的Webpack配置:
webpack.common.js:
const path = require('path'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); module.exports = { entry: { game: './src/client/index.js', }, output: { filename: '[name].[contenthash].js', path: path.resolve(__dirname, 'dist'), }, module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: "babel-loader", options: { presets: ['@babel/preset-env'], }, }, }, { test: /\.css$/, use: [ { loader: MiniCssExtractPlugin.loader, }, 'css-loader', ], }, ], }, plugins: [ new MiniCssExtractPlugin({ filename: '[name].[contenthash].css', }), new HtmlWebpackPlugin({ filename: 'index.html', template: 'src/client/html/index.html', }), ], };
最重要的是以下几行:
src/client/index.js
是Javascript客户端(JS)的输入点。 Webpack将从此处开始,然后递归搜索其他导入的文件。- Webpack程序集的输出JS将位于
dist/
目录中。 我将此文件称为JS包 。 - 我们使用Babel ,特别是@ babel / preset-env配置来为较旧的浏览器转换我们的JS代码。
- 我们使用该插件提取JS文件引用的所有CSS,并将它们合并到一个位置。 我将其称为CSS包 。
您可能已经注意到奇怪的软件包文件名
'[name].[contenthash].ext'
。 它们包含Webpack
文件 [name]
替换:
[name]
将被输入点的名称替换(在我们的例子中,这是一个
game
),
[contenthash]
将被文件内容的哈希替换。 我们这样做是为了
优化用于哈希的项目 -您可以告诉浏览器无限期地缓存我们的JS包,因为
如果包更改,则其文件名也会更改(
contenthash
更改)。 完成的结果是
game.dbeee76e91a97d0c7207.js
格式的文件名。
webpack.common.js
文件是我们在开发和完成的项目配置中导入的基本配置文件。 例如,一个开发配置:
webpack.dev.js
const merge = require('webpack-merge'); const common = require('./webpack.common.js'); module.exports = merge(common, { mode: 'development', });
为了提高效率,我们在开发
webpack.dev.js
使用了
webpack.dev.js
,并切换到
webpack.prod.js
来优化在生产环境中部署时的包装尺寸。
本地设置
我建议将项目安装在本地计算机上,以便您可以按照本文中列出的步骤进行操作。 设置很简单:首先,必须在系统上安装
Node和
NPM 。 接下来,您需要执行
$ git clone https:
您就可以出发了! 要启动开发服务器,只需运行
$ npm run develop
并在网络浏览器中访问
localhost:3000 。 当代码更改时,开发服务器将自动重建JS和CSS包-只需刷新页面即可查看所有更改!
3.客户进入点
让我们开始研究游戏代码本身。 首先,我们需要
index.html
页面,当您访问该站点时,浏览器将首先加载它。 我们的页面将非常简单:
index.html
<!DOCTYPE html>
<html>
<头>
<title> .io游戏示例</ title>
<link type =“ text / css” rel =“ stylesheet” href =“ / game.bundle.css”>
</ head>
<身体>
<canvas id =“ game-canvas”> </ canvas>
<script异步src =“ / game.bundle.js”> </ script>
<div id =“播放菜单” class =“隐藏”>
<input type =“文本” id =“用户名-输入”占位符=“用户名” />
<button id =“ play-button”>播放</ button>
</ div>
</ body>
</ html>
为了清楚起见,此代码示例略有简化,我将在帖子的许多其他示例中进行相同的操作。 完整的代码始终可以在Github上查看。我们有:
- 我们将用来渲染游戏的HTML5 Canvas (
<canvas>
) <canvas>
。 <link>
添加我们的CSS包。<script>
添加我们的Javascript包。- 主菜单带有用户名
<input>
和“ PLAY”( <button>
) <button>
。
加载主页后,将从入口点的JS文件
src/client/index.js
开始,在浏览器中开始执行Javascript代码。
index.js
import { connect, play } from './networking'; import { startRendering, stopRendering } from './render'; import { startCapturingInput, stopCapturingInput } from './input'; import { downloadAssets } from './assets'; import { initState } from './state'; import { setLeaderboardHidden } from './leaderboard'; import './css/main.css'; const playMenu = document.getElementById('play-menu'); const playButton = document.getElementById('play-button'); const usernameInput = document.getElementById('username-input'); Promise.all([ connect(), downloadAssets(), ]).then(() => { playMenu.classList.remove('hidden'); usernameInput.focus(); playButton.onclick = () => {
这可能看起来很复杂,但实际上这里没有发生很多动作:
- 导入其他几个JS文件。
- 导入CSS(因此Webpack知道将它们包含在我们的CSS包中)。
- 运行
connect()
建立与服务器的连接,并运行downloadAssets()
下载渲染游戏所需的图像。 - 完成第3步后 ,将显示主菜单(
playMenu
)。 - 配置用于按PLAY按钮的处理程序。 当按下按钮时,代码将初始化游戏并告诉服务器我们已经准备好玩。
客户端-服务器逻辑的主要“内容”是由
index.js
文件导入的那些文件。 现在,我们将按顺序考虑它们。
4.交换客户数据
在这个游戏中,我们使用众所周知的
socket.io库与服务器进行通信。 Socket.io具有对
WebSocket的内置支持,非常适合双向通信:我们可以将消息发送到服务器
,而服务器可以通过同一连接向我们发送消息。
我们将拥有一个
src/client/networking.js
文件,该文件将处理与服务器的
所有通信:
网络.js
import io from 'socket.io-client'; import { processGameUpdate } from './state'; const Constants = require('../shared/constants'); const socket = io(`ws://${window.location.host}`); const connectedPromise = new Promise(resolve => { socket.on('connect', () => { console.log('Connected to server!'); resolve(); }); }); export const connect = onGameOver => ( connectedPromise.then(() => {
为了清楚起见,此代码也略有减少。此文件中有三个主要操作:
- 我们正在尝试连接到服务器。 仅在我们建立连接后才允许
connectedPromise
。 - 如果成功建立连接,我们将注册回调函数(
processGameUpdate()
和onGameOver()
),以接收可以从服务器接收的消息。 - 我们导出
play()
和updateDirection()
以便其他文件可以使用它们。
5.客户端渲染
现在该在屏幕上显示图片了!
...但是在我们执行此操作之前,我们需要下载为此所需的所有图像(资源)。 让我们写一个资源管理器:
asset.js
const ASSET_NAMES = ['ship.svg', 'bullet.svg']; const assets = {}; const downloadPromise = Promise.all(ASSET_NAMES.map(downloadAsset)); function downloadAsset(assetName) { return new Promise(resolve => { const asset = new Image(); asset.onload = () => { console.log(`Downloaded ${assetName}`); assets[assetName] = asset; resolve(); }; asset.src = `/assets/${assetName}`; }); } export const downloadAssets = () => downloadPromise; export const getAsset = assetName => assets[assetName];
资源管理不是那么容易实现! 要点是存储
assets
对象,它将把文件名键绑定到
Image
对象的值。 加载资源后,我们将其保存在
assets
对象中,以便将来快速检索。 当允许为每个单独的资源进行下载(即,将下载
所有资源)时,我们启用
downloadPromise
。
下载资源后,可以开始渲染。 如前所述,我们使用
HTML5 Canvas (
<canvas>
)在网页上绘制。 我们的游戏非常简单,因此仅绘制以下内容就足够了:
- 背景知识
- 玩家飞船
- 游戏中的其他玩家
- 炮弹
以下是
src/client/render.js
的重要片段,它们精确呈现了上面列出的四个点:
render.js
import { getAsset } from './assets'; import { getCurrentState } from './state'; const Constants = require('../shared/constants'); const { PLAYER_RADIUS, PLAYER_MAX_HP, BULLET_RADIUS, MAP_SIZE } = Constants;
为了清楚起见,该代码也被缩短。render()
是此文件的主要功能。
startRendering()
和
stopRendering()
以60 FPS控制渲染周期的激活。
各个辅助渲染函数(例如
renderBullet()
)的具体实现并不那么重要,但是这里有一个简单的示例:
render.js
function renderBullet(me, bullet) { const { x, y } = bullet; context.drawImage( getAsset('bullet.svg'), canvas.width / 2 + x - me.x - BULLET_RADIUS, canvas.height / 2 + y - me.y - BULLET_RADIUS, BULLET_RADIUS * 2, BULLET_RADIUS * 2, ); }
注意,我们使用先前在
asset.js
看到的
getAsset()
方法!
如果您有兴趣探索其他辅助渲染功能,请阅读src / client / render.js的其余部分。
6.客户输入
现在该使游戏变得
可玩了 ! 控制方案将非常简单:您可以使用鼠标(在计算机上)或触摸屏幕(在移动设备上)来更改移动方向。 为了实现这一点,我们将为Mouse和Touch事件注册
事件监听器 。
src/client/input.js
将完成所有这些操作:
input.js
import { updateDirection } from './networking'; function onMouseInput(e) { handleInput(e.clientX, e.clientY); } function onTouchInput(e) { const touch = e.touches[0]; handleInput(touch.clientX, touch.clientY); } function handleInput(x, y) { const dir = Math.atan2(x - window.innerWidth / 2, window.innerHeight / 2 - y); updateDirection(dir); } export function startCapturingInput() { window.addEventListener('mousemove', onMouseInput); window.addEventListener('touchmove', onTouchInput); } export function stopCapturingInput() { window.removeEventListener('mousemove', onMouseInput); window.removeEventListener('touchmove', onTouchInput); }
onMouseInput()
和
onTouchInput()
是事件侦听器,它们在发生输入事件(例如,移动鼠标)时调用
updateDirection()
(来自
networking.js
)。
updateDirection()
与服务器进行消息传递,该服务器处理输入事件并相应地更新游戏状态。
7.客户条件
这部分是文章第一部分中最困难的部分。 如果您不读一读就不要灰心! 您甚至可以跳过它,稍后再返回。
完成客户端-服务器代码所需的最后一个难题是
state 。 还记得“客户端渲染”部分的代码片段吗?
render.js
import { getCurrentState } from './state'; function render() { const { me, others, bullets } = getCurrentState();
根据从服务器接收到的更新,
getCurrentState()
应该能够
在任何给定时间为我们提供客户端
中游戏的当前状态。 这是服务器可以发送的游戏更新示例:
{ "t": 1555960373725, "me": { "x": 2213.8050880413657, "y": 1469.370893425012, "direction": 1.3082443894581433, "id": "AhzgAtklgo2FJvwWAADO", "hp": 100 }, "others": [], "bullets": [ { "id": "RUJfJ8Y18n", "x": 2354.029197099604, "y": 1431.6848318262666 }, { "id": "ctg5rht5s", "x": 2260.546457727445, "y": 1456.8088728920968 } ], "leaderboard": [ { "username": "Player", "score": 3 } ] }
每个游戏更新均包含五个相同的字段:
- t :创建此更新的时间的服务器时间戳。
- 我 :有关播放器收到此更新的信息。
- 其他 :有关参与同一游戏的其他玩家的一系列信息。
- bullets :有关游戏中的炮弹的一系列信息。
- 页首横幅 :当前的页首横幅数据。 在这篇文章中,我们将不考虑它们。
7.1客户的天真状态
getCurrentState()
实现只能直接从收到的最新游戏更新中返回数据。
naive-state.js
let lastGameUpdate = null;
美丽而清晰! 但是,如果一切都这么简单。 此实现存在问题的原因之一:
它将渲染的帧速率限制为服务器时钟的频率 。
帧率 :每秒的帧数(即, render()
调用)或FPS。 游戏通常通常至少达到60 FPS。
报价速度 :服务器向客户端发送游戏更新的频率。 通常它低于帧速率 。 在我们的游戏中,服务器以每秒30个周期的频率运行。
如果我们仅渲染游戏的最新更新,那么FPS实际上将永远不会超过30,因为
我们每秒从服务器接收的更新永远不会超过30 。 即使我们每秒调用
render()
60次,这些调用中的一半也只会重绘同一件事,实际上什么也没做。 天真的实现的另一个问题是它
容易出现延迟 。 以理想的Internet速度,客户端将每隔33毫秒(每秒30个)准确地收到一次游戏更新:
不幸的是,没有什么是完美的。 更为现实的情况是:
当涉及到延迟时,幼稚的实现实际上是最坏的情况。 如果收到游戏更新的延迟为50毫秒,则
客户端会额外降低50毫秒的速度,因为它仍会呈现前一次更新的游戏状态。 您可以想象这对玩家来说是多么的不方便:由于任意制动,游戏似乎会抽搐且不稳定。
7.2改善客户状态
我们将对天真的实现进行一些改进。 首先,我们使用100 ms的
渲染延迟 。 这意味着客户端的“当前”状态将始终落后于服务器上的游戏状态100毫秒。 例如,如果服务器上的时间为
150 ,则服务器在时间
50处的状态将在客户端上呈现:
这为我们提供了100毫秒的缓冲区,使我们能够在无法预测的时间内度过以接收游戏更新:
为此付出的代价
是 100 ms的恒定
输入延迟(输入滞后) 。 这是平稳游戏的一个小牺牲-大多数玩家(尤其是休闲玩家)甚至都不会注意到这种延迟。 人们适应100毫秒的恒定延迟比玩不可预测的延迟要容易得多。
我们可以使用另一种称为“客户端预测”的技术,该技术可以很好地减少感知到的延迟,但是本文中将不予考虑。
我们使用的另一个改进是
线性插值 。 由于渲染延迟,我们通常会至少更新一次客户端中的当前时间。 调用
getCurrentState()
,我们可以在客户端中当前时间前后立即在游戏更新之间执行
线性插值 :
这就解决了帧速率问题:现在,我们可以以所需的任何频率渲染唯一的帧!
7.3实施增强的客户端状态
src/client/state.js
的示例实现
src/client/state.js
使用了渲染延迟和线性插值,但这并不长久。 让我们将代码分为两部分。 这是第一个:
state.js第1部分
const RENDER_DELAY = 100; const gameUpdates = []; let gameStart = 0; let firstServerTimestamp = 0; export function initState() { gameStart = 0; firstServerTimestamp = 0; } export function processGameUpdate(update) { if (!firstServerTimestamp) { firstServerTimestamp = update.t; gameStart = Date.now(); } gameUpdates.push(update);
第一步是弄清楚
currentServerTime()
作用。 正如我们前面所看到的,每个游戏更新中都包含一个服务器时间戳。 我们想使用渲染延迟在服务器后100 ms处渲染图像,但是
我们永远不会知道服务器上的当前时间 ,因为我们不知道有多长时间才能获得更新。 互联网是无法预测的,其速度可能相差很大!
为了解决这个问题,您可以使用一个合理的近似值:我们将
假定第一个更新是即时到达的 。 如果这是真的,那么我们将知道特定时间的服务器时间! 我们将服务器时间戳保存在
firstServerTimestamp
同时将
本地 (客户端)时间戳保存在
gameStart
。
等等
服务器上是否应该没有时间=客户端中的时间? 为什么我们要区分“服务器时间戳”和“客户端时间戳”? 这是一个很好的问题! 事实证明,这不是同一回事。
Date.now()
将在客户端和服务器中返回不同的时间戳,这取决于这些计算机的本地因素。
永远不要假设所有机器上的时间戳都相同。现在我们了解了
currentServerTime()
作用:它返回
当前渲染时间的服务器时间戳 。
换句话说,这是当前服务器时间(firstServerTimestamp <+ (Date.now() - gameStart)
)减去渲染延迟(RENDER_DELAY
)。现在,让我们看看我们如何处理游戏更新。从服务器接收到更新时,将称为processGameUpdate()
,并将新更新保存到array gameUpdates
。然后,要检查内存使用情况,我们将所有旧的更新删除到基本更新,因为我们不再需要它们。什么是“基本更新”?这是我们发现从当前服务器时间开始向后移动的第一个更新。还记得这条电路吗?直接在“客户端渲染时间”左侧进行的游戏更新是一项基本更新。基本更新用于什么?为什么我们要放弃对基础的更新?为了理解这一点,让我们最后看一下实现getCurrentState()
:state.js第2部分
export function getCurrentState() { if (!firstServerTimestamp) { return {}; } const base = getBaseUpdate(); const serverTime = currentServerTime();
我们处理三种情况:base < 0
表示当前渲染时间没有更新(请参见上述实现getBaseUpdate()
)。由于渲染延迟,这种情况可能在游戏开始时就发生。在这种情况下,我们使用最新更新。base
这是我们的最新更新。这可能是由于网络延迟或互联网连接不佳。在这种情况下,我们还将使用我们拥有的最新更新。- 我们在当前渲染时间之前和之后都有更新,因此您可以进行插值!
剩下的state.js
就是线性插值的实现,这是一个简单(但很无聊)的数学运算。如果您想自己学习它,请state.js
在Github上打开它。第2部分。后端服务器
在这一部分中,我们将研究运行.io游戏示例的Node.js后端。1.服务器入口点
为了控制Web服务器,我们将使用流行的Node.js Web框架Express。它将由我们的服务器入口点文件配置src/server/server.js
:server.js,第1部分
const express = require('express'); const webpack = require('webpack'); const webpackDevMiddleware = require('webpack-dev-middleware'); const webpackConfig = require('../../webpack.dev.js');
还记得我们在第一部分中讨论过Webpack吗?这是我们将使用Webpack配置的地方。我们将以两种方式应用它们:另一个重要任务server.js
是配置socket.io服务器,该服务器仅连接到Express服务器:server.js,第2部分
const socketio = require('socket.io'); const Constants = require('../shared/constants');
与服务器成功建立socket.io连接后,我们为新套接字配置事件处理程序。事件处理程序通过委派给singleton对象来处理从客户端收到的消息game
:server.js,第3部分
const Game = require('./game');
我们创建了一个.io风格的游戏,因此我们只需要一个实例Game
(“游戏”)-所有玩家都在同一个竞技场上玩!在下一节中,我们将看到此类的工作原理Game
。2.游戏服务器
该类Game
包含最重要的服务器端逻辑。它有两个主要任务:玩家管理和游戏模拟。让我们从第一个任务开始-播放器管理。game.js,第1部分
const Constants = require('../shared/constants'); const Player = require('./player'); class Game { constructor() { this.sockets = {}; this.players = {}; this.bullets = []; this.lastUpdateTime = Date.now(); this.shouldSendUpdate = false; setInterval(this.update.bind(this), 1000 / 60); } addPlayer(socket, username) { this.sockets[socket.id] = socket;
在此游戏中,我们将通过id
插座socket.io 的字段来识别玩家(如果您感到困惑,请返回server.js
)。Socket.io本身为每个套接字分配一个唯一的套接字id
,因此我们不必为此担心。我将呼叫他的玩家ID。考虑到这一点,让我们检查类中的实例变量Game
:sockets
是将播放器ID绑定到与播放器关联的套接字的对象。它使我们可以在一定时间内通过其播放器ID访问套接字。players
是将玩家ID绑定到代码>玩家对象的对象
bullets
是Bullet
没有特定顺序的对象数组。lastUpdateTime
-这是游戏上次更新时间的时间戳。很快我们将看到它的用法。shouldSendUpdate
是辅助变量。我们也将很快看到它的使用。方法addPlayer()
,removePlayer()
并且handleInput()
不需要解释,他们在使用server.js
。如果您需要刷新内存,请返回更高一点。最后一行constructor()
开始游戏更新周期(频率为60次更新/秒):game.js,第2部分
const Constants = require('../shared/constants'); const applyCollisions = require('./collisions'); class Game {
该方法update()
可能包含服务器端逻辑中最重要的部分。按照顺序,我们列出了他所做的一切:- 计算
dt
自上次以来经过了多少时间update()
。 - . . ,
bullet.update()
true
, ( ). - . —
player.update()
Bullet
. applyCollisions()
, , . , ( player.onDealtDamage()
), bullets
.- .
update()
. shouldSendUpdate
. update()
60 /, 30 /. , 30 / ( ).
? . 30 – !
为什么然后不update()
每秒呼叫30次呢?改善了游戏的仿真性。调用update()
得越多,游戏的模拟就越准确。但是不要update()
因为调用数量而太费劲,因为这是一个计算量很大的任务-每秒60个就足够了。
该类的其余部分Game
由用于以下方面的辅助方法组成update()
:game.js,第3部分
class Game {
getLeaderboard()
非常简单-按得分数对玩家进行排序,获得5个最佳成绩,然后为每个用户名和得分返回。createUpdate()
用于update()
创建传递给玩家的游戏更新。它的主要任务是调用serializeForUpdate()
为Player
和类实现的方法Bullet
。请注意,他仅向每个玩家传送有关最近的玩家和炮弹的信息-无需传送与玩家距离较远的游戏对象的信息!3.服务器上的游戏对象
在我们的游戏中,炮弹和玩家实际上非常相似:它们是抽象的圆形移动游戏对象。为了利用播放器和外壳的这种相似性,让我们从实现基类开始Object
:object.js
class Object { constructor(id, x, y, dir, speed) { this.id = id; this.x = x; this.y = y; this.direction = dir; this.speed = speed; } update(dt) { this.x += dt * this.speed * Math.sin(this.direction); this.y -= dt * this.speed * Math.cos(this.direction); } distanceTo(object) { const dx = this.x - object.x; const dy = this.y - object.y; return Math.sqrt(dx * dx + dy * dy); } setDirection(dir) { this.direction = dir; } serializeForUpdate() { return { id: this.id, x: this.x, y: this.y, }; } }
这里没有复杂的事情发生。此类将是扩展的良好参考点。让我们看看该类如何Bullet
使用Object
:bullet.js
const shortid = require('shortid'); const ObjectClass = require('./object'); const Constants = require('../shared/constants'); class Bullet extends ObjectClass { constructor(parentID, x, y, dir) { super(shortid(), x, y, dir, Constants.BULLET_SPEED); this.parentID = parentID; }
实现Bullet
很短!我们Object
仅添加了以下扩展名:- 使用shortid包随机生成
id
弹丸。 - 添加一个字段,
parentID
以便您可以跟踪创建此射弹的玩家。 - 向加上返回值
update()
,true
如果射弹不在竞技场之外,则返回值相等(还记得,我们在上一节中讨论了这一点吗?)。
让我们继续Player
:player.js
const ObjectClass = require('./object'); const Bullet = require('./bullet'); const Constants = require('../shared/constants'); class Player extends ObjectClass { constructor(id, username, x, y) { super(id, x, y, Math.random() * 2 * Math.PI, Constants.PLAYER_SPEED); this.username = username; this.hp = Constants.PLAYER_MAX_HP; this.fireCooldown = 0; this.score = 0; }
播放器比shell复杂,因此应在此类中存储更多字段。他的方法需要update()
做很多工作,特别是如果不留下新创建的外壳,则返回它fireCooldown
(请记住,我们在上一节中已经讨论过了吗?)。他还扩展了方法serializeForUpdate()
,因为我们需要在游戏更新中包括玩家的其他字段。拥有基类Object
是避免代码可重复性的重要步骤。例如,如果没有类,则Object
每个游戏对象都应具有相同的实现distanceTo()
,并且将所有这些实现的复制粘贴同步到几个文件中将是一场噩梦。当扩展类的数量增加时,这对于大型项目尤为重要Object
。4.冲突识别
给我们留下的唯一一件事就是认识炮弹何时击中了玩家!请记住update()
类中某个方法的这段代码Game
:game.js
const applyCollisions = require('./collisions'); class Game {
我们需要实现一种方法applyCollisions()
,该方法返回击中玩家的所有炮弹。幸运的是,这并不是那么困难,因为- 所有碰撞对象都是圆,这是实现碰撞识别的最简单图形。
- 我们已经
distanceTo()
在类的上一节中实现了一种方法Object
。
这是我们的碰撞识别实现的样子:crashs.js
const Constants = require('../shared/constants');
这种简单的碰撞识别基于以下事实:如果两个圆的中心之间的距离小于其半径之和,则两个圆会发生碰撞。这是两个圆心之间的距离完全等于其半径之和时的情况:在这里,您需要仔细考虑以下两个方面:- 射弹不应落入创建它的玩家手中。这可以通过比较可以实现
bullet.parentID
用player.id
。 - 在与多个玩家同时碰撞的极端情况下,子弹只能命中一次。我们将在操作员的帮助下解决此问题
break
:一旦发现玩家与弹丸相撞,我们将停止搜索并继续前进至下一个弹丸。
结束
仅此而已!我们已经介绍了创建.io网络游戏所需的所有知识。接下来是什么?
建立自己的.io游戏!所有的示例代码都是开源的,并发布在Github上。