原文: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 输入

最新文章

  1. vector &amp; array
  2. c语言_判断例子
  3. (原创)openvswitch实验连载1-fedora 17下安装openvswitch
  4. 我是怎样自学日语的(太TM励志了!)
  5. C++ 常见容器
  6. Git 和 SVN之间的五个基本区别
  7. PLSQL developer登录身份证明检索失败的解决办法
  8. 模拟SPI协议时序
  9. No new migrations found. Your system is up-to-date.处理
  10. 安卓banner图片轮播
  11. 从MFQ方法到需求分析
  12. 用asp.net core 把用户访问记录优化到极致
  13. cocos2dx九宫图使用方法
  14. [转]python数据持久存储:pickle模块的基本使用
  15. Django模型层(1)
  16. 对最近java基础学习的一次小结
  17. 第1章 1.9计算机网络概述--OSI参考模型和网络安全
  18. javashop每次重新部署都要从新安装的问题
  19. oracle 使用occi方式 批量插入多条数据
  20. 20145329 《Java程序设计》第七周学习总结

热门文章

  1. PhotoSwipe用法
  2. 【Pyhton 】 装饰器
  3. 【Jmeter 压测MySql连接问题】
  4. 编译opencv4.5.0
  5. 一、eclipse配置TestNG
  6. python绘折线图
  7. 移动端 rem和flexible
  8. Qt QChart 创建图表
  9. 如何优雅地使用云原生 Prometheus 监控集群
  10. inotifywait命令如何监控文件变化?