原文: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.ioDiep.io

在本文中,我们将了解如何从头开始构建.io游戏

您所需要的只是 Javascript 的实用知识:

您应该熟悉 ES6 语法,this 关键字和 Promises之类的内容。

即使您对 Javascript 并不是最熟悉的,您仍然应该可以阅读本文的大部分内容。

一个 .io 游戏示例

为了帮助我们学习,我们将参考 https://example-io-game.victorzhou.com

这是一款非常简单的游戏:你和其他玩家一起控制竞技场中的一艘船。

你的飞船会自动发射子弹,你会试图用自己的子弹击中其他玩家,同时避开他们。

目录

这是由两部分组成的系列文章的第 1 部分。我们将在这篇文章中介绍以下内容:

  1. 项目概况/结构:项目的高级视图。
  2. 构建/项目设置:开发工具、配置和设置。
  3. Client 入口:index.html 和 index.js。
  4. Client 网络通信:与服务器通信。
  5. Client 渲染:下载 image 资源 + 渲染游戏。
  6. Client 输入:让用户真正玩游戏。
  7. 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 来优化包的大小。

本地设置

我建议在您的本地计算机上安装该项目,以便您可以按照本文的其余内容进行操作。

设置很简单:首先,确保已安装 NodeNPM。 然后,

$ 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>)绘制到我们的网页上。我们的游戏非常简单,所以我们需要画的是:

  1. 背景
  2. 我们玩家的飞船
  3. 游戏中的其他玩家
  4. 子弹

这是 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 部分的更多相关文章

  1. 如何构建一个多人(.io) Web 游戏,第 2 部分

    原文:How to Build a Multiplayer (.io) Web Game, Part 2 探索 .io 游戏背后的后端服务器. 上篇:如何构建一个多人(.io) Web 游戏,第 1 ...

  2. 构建一个用于产品介绍的WEB应用

    为了让用户更好地了解您的产品功能,您在发布新产品或者升级产品功能的时候,不妨使用一个产品介绍的向导,引导用户熟悉产品功能和流程.本文将给您介绍一款优秀的用于产品介绍的WEB应用. 就像微博或邮箱这类W ...

  3. 从零构建一个简单的 Python Web框架

    为什么你想要自己构建一个 web 框架呢?我想,原因有以下几点: 你有一个新奇的想法,觉得将会取代其他的框架 你想要获得一些名气 你遇到的问题很独特,以至于现有的框架不太合适 你对 web 框架是如何 ...

  4. 使用SignalR构建一个最基本的web聊天室

    What is SignalR ASP.NET SignalR is a new library for ASP.NET developers that simplifies the process ...

  5. 构建一个最简单的web应用并部署及启动

    第一种构建方式:不使用maven File-new-Dynamic Web Project,用这种方式构建的web项目是在web.xml文件中配置了welcome-file的,但是却没有对应的文件,所 ...

  6. 构建一个基于 Spring 的 RESTful Web Service

    本文详细介绍了基于Spring创建一个“hello world” RESTful web service工程的步骤. 目标 构建一个service,接收如下HTTP GET请求: http://loc ...

  7. [译]Spring Boot 构建一个RESTful Web服务

    翻译地址:https://spring.io/guides/gs/rest-service/ 构建一个RESTful Web服务 本指南将指导您完成使用spring创建一个“hello world”R ...

  8. Spring Boot 构建一个 RESTful Web Service

    1  项目目标: 构建一个 web service,接收get 请求 http://localhost:8080/greeting 响应一个json 结果: {"id":1,&qu ...

  9. maven 构建一个web项目

    maven已经大型的Java项目的管理工具,其功能非常强大,这里简单总结一下maven构建web项目的过程.本文介绍的是集成环境下的maven构建web项目. 一.准备 1.安装maven. 2.把m ...

随机推荐

  1. 世界上最快的排序算法——Timsort

    前言 经过60多年的发展,科学家和工程师们发明了很多排序算法,有基本的插入算法,也有相对高效的归并排序算法等,他们各有各的特点,比如归并排序性能稳定.堆排序空间消耗小等等.但是这些算法也有自己的局限性 ...

  2. IO 的五种模型是什么

    目录 前言 用户空间和内核空间 IO 五种模型 阻塞型 IO 非阻塞 IO IO 多路复用 信号驱动 IO 异步 IO 总结 阻塞和非阻塞 同步与异步 前言 我们经常看到阻塞/非阻塞,同步/异步这两组 ...

  3. Emlog整站搬家教程

    之前使用的服务器空间到期,不想再续费,所以就打算更换服务器,购买新的服务器之后不知道怎么把EM程序转移到新的服务器上面,所以东查查西查查,终于找到了一个既简单又方便的搬家方法,分享给大家.步骤开始:1 ...

  4. 浏览器小程序(Browser Applet)闪亮登场

    2017 年 1 月 9 日,微信小程序横空出世.随后,支付宝小程序.今日头条小程序.百度智能小程序.360小程序等纷纷推出,自此国内软件功能扩展领域进入到了小程序时代,小程序为丰富其宿主软件的功能和 ...

  5. Linux命令-2.文件属性

    在Linux系统中文件的属性有:文件类型.属主属性.属组属性.其他属性,使用10个字符表示,如下图: 其中第0位表示文件类型: 当为[ d ]则是目录 当为[ - ]则是文件: 若是[ l ]则表示为 ...

  6. jmeter的一些知识目录

    1.JDK安装及环境变量配置2.Jmeter安装及环境变量配置3.如何启动 jmeter 4.下载并安装mysql驱动5.创建JDBC连接池及配置6 .新建线程组及参数配置7.http默认请求及参数配 ...

  7. 关于 ReentrantLock 中锁 lock() 和解锁 unlock() 的底层原理浅析

    关于 ReentrantLock 中锁 lock() 和解锁 unlock() 的底层原理浅析 如下代码,当我们在使用 ReentrantLock 进行加锁和解锁时,底层到底是如何帮助我们进行控制的啦 ...

  8. solidworks 2018 因动态绘制边线显示视图延迟的解决方案

    每次鼠标移动到一个物体上时总是会卡顿几秒,直到完成所有边线的绘制后才可以继续进行其他操作,这体验实在是不好. 解决方案很简单,只要取消这个默认开启的动态高亮显示就可以了. 1.去 选项->系统选 ...

  9. 使用form表单上传文件

    在使用form表单上传文件时候,input[type='file']是必然会用的,其中有一些小坑需要避免. 1.form的 enctype="multipart/form-data" ...

  10. Ubuntu虚拟机无网络连接的问题

    记录一下: 通过dhcp动态分配ip地址. sudo dhclient -v 应该就能解决了.