如何构建一个多人(.io) Web 游戏,第 1 部分
原文:How to Build a Multiplayer (.io) Web Game, Part 1
GitHub: https://github.com/vzhou842/example-.io-game
深入探索一个 .io
游戏的 Javascript client-side(客户端)。
如果您以前从未听说过 .io
游戏:它们是免费的多人 web 游戏,易于加入(无需帐户),
并且通常在一个区域内让许多玩家相互竞争。其他著名的 .io
游戏包括 Slither.io
和 Diep.io
。
- Slither.io:http://slither.io
- Diep.io:https://diep.io
在本文中,我们将了解如何从头开始构建.io游戏。
您所需要的只是 Javascript 的实用知识:
您应该熟悉 ES6
语法,this
关键字和 Promises
之类的内容。
即使您对 Javascript 并不是最熟悉的,您仍然应该可以阅读本文的大部分内容。
一个 .io
游戏示例
为了帮助我们学习,我们将参考 https://example-io-game.victorzhou.com。
这是一款非常简单的游戏:你和其他玩家一起控制竞技场中的一艘船。
你的飞船会自动发射子弹,你会试图用自己的子弹击中其他玩家,同时避开他们。
目录
这是由两部分组成的系列文章的第 1 部分。我们将在这篇文章中介绍以下内容:
- 项目概况/结构:项目的高级视图。
- 构建/项目设置:开发工具、配置和设置。
- Client 入口:index.html 和 index.js。
- Client 网络通信:与服务器通信。
- Client 渲染:下载 image 资源 + 渲染游戏。
- Client 输入:让用户真正玩游戏。
- Client 状态:处理来自服务器的游戏更新。
1. 项目概况/结构
我建议下载示例游戏的源代码,以便您可以更好的继续阅读。
我们的示例游戏使用了:
- Express,Node.js 最受欢迎的 Web 框架,以为其 Web 服务器提供动力。
- socket.io,一个 websocket 库,用于在浏览器和服务器之间进行通信。
- Webpack,一个模块打包器。
项目目录的结构如下所示:
public/
assets/
...
src/
client/
css/
...
html/
index.html
index.js
...
server/
server.js
...
shared/
constants.js
public/
我们的服务器将静态服务 public/
文件夹中的所有内容。 public/assets/
包含我们项目使用的图片资源。
src/
所有源代码都在 src/
文件夹中。
client/
和 server/
很容易说明,shared/
包含一个由 client 和 server 导入的常量文件。
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 bundle。 - 我们正在使用 Babel,特别是
@babel/preset-env
配置,来为旧浏览器编译 JS 代码。 - 我们正在使用一个插件来提取 JS 文件引用的所有 CSS 并将其捆绑在一起。我将其称为 CSS bundle。
您可能已经注意到奇怪的 '[name].[contenthash].ext'
捆绑文件名。
它们包括 Webpack 文件名替换:[name]
将替换为入口点名称(这是game
),[contenthash]将替换为文件内容的哈希。
我们这样做是为了优化缓存 - 我们可以告诉浏览器永远缓存我们的 JS bundle,因为如果 JS bundle 更改,其文件名也将更改(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.prod.js
来优化包的大小。
本地设置
我建议在您的本地计算机上安装该项目,以便您可以按照本文的其余内容进行操作。
设置很简单:首先,确保已安装 Node
和 NPM
。 然后,
$ git clone https://github.com/vzhou842/example-.io-game.git
$ cd example-.io-game
$ npm install
您就可以出发了! 要运行开发服务器,只需
$ npm run develop
并在网络浏览器中访问 localhost:3000
。
当您编辑代码时,开发服务器将自动重建 JS 和 CSS bundles - 只需刷新即可查看更改!
3. Client 入口
让我们来看看实际的游戏代码。首先,我们需要一个 index.html
页面,
这是您的浏览器访问网站时首先加载的内容。我们的将非常简单:
index.html
<!DOCTYPE html>
<html>
<head>
<title>An example .io game</title>
<link type="text/css" rel="stylesheet" href="/game.bundle.css">
</head>
<body>
<canvas id="game-canvas"></canvas>
<script async src="/game.bundle.js"></script>
<div id="play-menu" class="hidden">
<input type="text" id="username-input" placeholder="Username" />
<button id="play-button">PLAY</button>
</div>
</body>
</html>
我们有:
- 我们将使用 HTML5 Canvas(
<canvas>
)元素来渲染游戏。 <link>
包含我们的 CSS bundle。<script>
包含我们的 Javascript bundle。- 主菜单,带有用户名
<input>
和“PLAY”
<button>
。
一旦主页加载到浏览器中,我们的 Javascript 代码就会开始执行,
从我们的 JS 入口文件 src/client/index.js
开始。
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 = () => {
// Play!
play(usernameInput.value);
playMenu.classList.add('hidden');
initState();
startCapturingInput();
startRendering();
setLeaderboardHidden(false);
};
});
这似乎很复杂,但实际上并没有那么多事情发生:
- 导入一堆其他 JS 文件。
- 导入一些 CSS(因此 Webpack 知道将其包含在我们的 CSS bundle 中)。
- 运行
connect()
来建立到服务器的连接,运行downloadAssets()
来下载渲染游戏所需的图像。 - 步骤 3 完成后,显示主菜单(
playMenu
)。 - 为 “PLAY” 按钮设置一个点击处理程序。如果点击,初始化游戏并告诉服务器我们准备好玩了。
客户端逻辑的核心驻留在由 index.js
导入的其他文件中。接下来我们将逐一讨论这些问题。
4. Client 网络通信
对于此游戏,我们将使用众所周知的 socket.io
库与服务器进行通信。
Socket.io 包含对 WebSocket
的内置支持,
这非常适合双向通讯:我们可以将消息发送到服务器,而服务器可以通过同一连接向我们发送消息。
我们将有一个文件 src/client/networking.js
,它负责所有与服务器的通信:
networking.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(() => {
// Register callbacks
socket.on(Constants.MSG_TYPES.GAME_UPDATE, processGameUpdate);
socket.on(Constants.MSG_TYPES.GAME_OVER, onGameOver);
})
);
export const play = username => {
socket.emit(Constants.MSG_TYPES.JOIN_GAME, username);
};
export const updateDirection = dir => {
socket.emit(Constants.MSG_TYPES.INPUT, dir);
};
此文件中发生3件主要事情:
- 我们尝试连接到服务器。只有建立连接后,
connectedPromise
才能解析。 - 如果连接成功,我们注册回调(
processGameUpdate()
和onGameOver()
)我们可能从服务器接收到的消息。 - 我们导出
play()
和updateDirection()
以供其他文件使用。
5. Client 渲染
是时候让东西出现在屏幕上了!
但在此之前,我们必须下载所需的所有图像(资源)。让我们写一个资源管理器:
assets.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 并不难实现!主要思想是保留一个 assets
对象,它将文件名 key 映射到一个 Image
对象值。
当一个 asset
下载完成后,我们将其保存到 assets
对象中,以便以后检索。
最后,一旦每个 asset
下载都已 resolve(意味着所有 assets 都已下载),我们就 resolve downloadPromise
。
随着资源的下载,我们可以继续进行渲染。如前所述,我们正在使用 HTML5 画布(<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;
// Get the canvas graphics context
const canvas = document.getElementById('game-canvas');
const context = canvas.getContext('2d');
// Make the canvas fullscreen
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
function render() {
const { me, others, bullets } = getCurrentState();
if (!me) {
return;
}
// Draw background
renderBackground(me.x, me.y);
// Draw all bullets
bullets.forEach(renderBullet.bind(null, me));
// Draw all players
renderPlayer(me, me);
others.forEach(renderPlayer.bind(null, me));
}
// ... Helper functions here excluded
let renderInterval = null;
export function startRendering() {
renderInterval = setInterval(render, 1000 / 60);
}
export function stopRendering() {
clearInterval(renderInterval);
}
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. Client 输入
如何构建一个多人(.io) Web 游戏,第 1 部分的更多相关文章
- 如何构建一个多人(.io) Web 游戏,第 2 部分
原文:How to Build a Multiplayer (.io) Web Game, Part 2 探索 .io 游戏背后的后端服务器. 上篇:如何构建一个多人(.io) Web 游戏,第 1 ...
- 构建一个用于产品介绍的WEB应用
为了让用户更好地了解您的产品功能,您在发布新产品或者升级产品功能的时候,不妨使用一个产品介绍的向导,引导用户熟悉产品功能和流程.本文将给您介绍一款优秀的用于产品介绍的WEB应用. 就像微博或邮箱这类W ...
- 从零构建一个简单的 Python Web框架
为什么你想要自己构建一个 web 框架呢?我想,原因有以下几点: 你有一个新奇的想法,觉得将会取代其他的框架 你想要获得一些名气 你遇到的问题很独特,以至于现有的框架不太合适 你对 web 框架是如何 ...
- 使用SignalR构建一个最基本的web聊天室
What is SignalR ASP.NET SignalR is a new library for ASP.NET developers that simplifies the process ...
- 构建一个最简单的web应用并部署及启动
第一种构建方式:不使用maven File-new-Dynamic Web Project,用这种方式构建的web项目是在web.xml文件中配置了welcome-file的,但是却没有对应的文件,所 ...
- 构建一个基于 Spring 的 RESTful Web Service
本文详细介绍了基于Spring创建一个“hello world” RESTful web service工程的步骤. 目标 构建一个service,接收如下HTTP GET请求: http://loc ...
- [译]Spring Boot 构建一个RESTful Web服务
翻译地址:https://spring.io/guides/gs/rest-service/ 构建一个RESTful Web服务 本指南将指导您完成使用spring创建一个“hello world”R ...
- Spring Boot 构建一个 RESTful Web Service
1 项目目标: 构建一个 web service,接收get 请求 http://localhost:8080/greeting 响应一个json 结果: {"id":1,&qu ...
- maven 构建一个web项目
maven已经大型的Java项目的管理工具,其功能非常强大,这里简单总结一下maven构建web项目的过程.本文介绍的是集成环境下的maven构建web项目. 一.准备 1.安装maven. 2.把m ...
随机推荐
- 世界上最快的排序算法——Timsort
前言 经过60多年的发展,科学家和工程师们发明了很多排序算法,有基本的插入算法,也有相对高效的归并排序算法等,他们各有各的特点,比如归并排序性能稳定.堆排序空间消耗小等等.但是这些算法也有自己的局限性 ...
- IO 的五种模型是什么
目录 前言 用户空间和内核空间 IO 五种模型 阻塞型 IO 非阻塞 IO IO 多路复用 信号驱动 IO 异步 IO 总结 阻塞和非阻塞 同步与异步 前言 我们经常看到阻塞/非阻塞,同步/异步这两组 ...
- Emlog整站搬家教程
之前使用的服务器空间到期,不想再续费,所以就打算更换服务器,购买新的服务器之后不知道怎么把EM程序转移到新的服务器上面,所以东查查西查查,终于找到了一个既简单又方便的搬家方法,分享给大家.步骤开始:1 ...
- 浏览器小程序(Browser Applet)闪亮登场
2017 年 1 月 9 日,微信小程序横空出世.随后,支付宝小程序.今日头条小程序.百度智能小程序.360小程序等纷纷推出,自此国内软件功能扩展领域进入到了小程序时代,小程序为丰富其宿主软件的功能和 ...
- Linux命令-2.文件属性
在Linux系统中文件的属性有:文件类型.属主属性.属组属性.其他属性,使用10个字符表示,如下图: 其中第0位表示文件类型: 当为[ d ]则是目录 当为[ - ]则是文件: 若是[ l ]则表示为 ...
- jmeter的一些知识目录
1.JDK安装及环境变量配置2.Jmeter安装及环境变量配置3.如何启动 jmeter 4.下载并安装mysql驱动5.创建JDBC连接池及配置6 .新建线程组及参数配置7.http默认请求及参数配 ...
- 关于 ReentrantLock 中锁 lock() 和解锁 unlock() 的底层原理浅析
关于 ReentrantLock 中锁 lock() 和解锁 unlock() 的底层原理浅析 如下代码,当我们在使用 ReentrantLock 进行加锁和解锁时,底层到底是如何帮助我们进行控制的啦 ...
- solidworks 2018 因动态绘制边线显示视图延迟的解决方案
每次鼠标移动到一个物体上时总是会卡顿几秒,直到完成所有边线的绘制后才可以继续进行其他操作,这体验实在是不好. 解决方案很简单,只要取消这个默认开启的动态高亮显示就可以了. 1.去 选项->系统选 ...
- 使用form表单上传文件
在使用form表单上传文件时候,input[type='file']是必然会用的,其中有一些小坑需要避免. 1.form的 enctype="multipart/form-data" ...
- Ubuntu虚拟机无网络连接的问题
记录一下: 通过dhcp动态分配ip地址. sudo dhclient -v 应该就能解决了.
原文:How to Build a Multiplayer (.io) Web Game, Part 2 探索 .io 游戏背后的后端服务器. 上篇:如何构建一个多人(.io) Web 游戏,第 1 ...
为了让用户更好地了解您的产品功能,您在发布新产品或者升级产品功能的时候,不妨使用一个产品介绍的向导,引导用户熟悉产品功能和流程.本文将给您介绍一款优秀的用于产品介绍的WEB应用. 就像微博或邮箱这类W ...
为什么你想要自己构建一个 web 框架呢?我想,原因有以下几点: 你有一个新奇的想法,觉得将会取代其他的框架 你想要获得一些名气 你遇到的问题很独特,以至于现有的框架不太合适 你对 web 框架是如何 ...
What is SignalR ASP.NET SignalR is a new library for ASP.NET developers that simplifies the process ...
第一种构建方式:不使用maven File-new-Dynamic Web Project,用这种方式构建的web项目是在web.xml文件中配置了welcome-file的,但是却没有对应的文件,所 ...
本文详细介绍了基于Spring创建一个“hello world” RESTful web service工程的步骤. 目标 构建一个service,接收如下HTTP GET请求: http://loc ...
翻译地址:https://spring.io/guides/gs/rest-service/ 构建一个RESTful Web服务 本指南将指导您完成使用spring创建一个“hello world”R ...
1 项目目标: 构建一个 web service,接收get 请求 http://localhost:8080/greeting 响应一个json 结果: {"id":1,&qu ...
maven已经大型的Java项目的管理工具,其功能非常强大,这里简单总结一下maven构建web项目的过程.本文介绍的是集成环境下的maven构建web项目. 一.准备 1.安装maven. 2.把m ...
前言 经过60多年的发展,科学家和工程师们发明了很多排序算法,有基本的插入算法,也有相对高效的归并排序算法等,他们各有各的特点,比如归并排序性能稳定.堆排序空间消耗小等等.但是这些算法也有自己的局限性 ...
目录 前言 用户空间和内核空间 IO 五种模型 阻塞型 IO 非阻塞 IO IO 多路复用 信号驱动 IO 异步 IO 总结 阻塞和非阻塞 同步与异步 前言 我们经常看到阻塞/非阻塞,同步/异步这两组 ...
之前使用的服务器空间到期,不想再续费,所以就打算更换服务器,购买新的服务器之后不知道怎么把EM程序转移到新的服务器上面,所以东查查西查查,终于找到了一个既简单又方便的搬家方法,分享给大家.步骤开始:1 ...
2017 年 1 月 9 日,微信小程序横空出世.随后,支付宝小程序.今日头条小程序.百度智能小程序.360小程序等纷纷推出,自此国内软件功能扩展领域进入到了小程序时代,小程序为丰富其宿主软件的功能和 ...
在Linux系统中文件的属性有:文件类型.属主属性.属组属性.其他属性,使用10个字符表示,如下图: 其中第0位表示文件类型: 当为[ d ]则是目录 当为[ - ]则是文件: 若是[ l ]则表示为 ...
1.JDK安装及环境变量配置2.Jmeter安装及环境变量配置3.如何启动 jmeter 4.下载并安装mysql驱动5.创建JDBC连接池及配置6 .新建线程组及参数配置7.http默认请求及参数配 ...
关于 ReentrantLock 中锁 lock() 和解锁 unlock() 的底层原理浅析 如下代码,当我们在使用 ReentrantLock 进行加锁和解锁时,底层到底是如何帮助我们进行控制的啦 ...
每次鼠标移动到一个物体上时总是会卡顿几秒,直到完成所有边线的绘制后才可以继续进行其他操作,这体验实在是不好. 解决方案很简单,只要取消这个默认开启的动态高亮显示就可以了. 1.去 选项->系统选 ...
在使用form表单上传文件时候,input[type='file']是必然会用的,其中有一些小坑需要避免. 1.form的 enctype="multipart/form-data" ...
记录一下: 通过dhcp动态分配ip地址. sudo dhclient -v 应该就能解决了.