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

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

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天前
|
数据可视化 图形学 UED
从模型托管到交互开发:DataV 如何简化三维 Web 应用构建?
从模型托管到交互开发:DataV 如何简化三维 Web 应用构建?
|
1月前
|
安全 Linux 开发工具
零基础构建开源项目OpenIM桌面应用和pc web- Electron篇
OpenIM 为开发者提供开源即时通讯 SDK,作为 Twilio、Sendbird 等云服务的替代方案。借助 OpenIM,开发者可以构建安全可靠的即时通讯应用,如 WeChat、Zoom、Slack 等。 本仓库基于开源版 OpenIM SDK 开发,提供了一款基于 Electron 的即时通讯应用。您可以使用此应用程序作为 OpenIM SDK 的参考实现。本项目同时引用了 @openim/electron-client-sdk 和 @openim/wasm-client-sdk,分别为 Electron 版本和 Web 版本的 SDK,可以同时构建 PC Web 程序和桌面应用(Wi
46 2
|
3月前
|
安全 应用服务中间件 网络安全
实战经验分享:利用免费SSL证书构建安全可靠的Web应用
本文分享了利用免费SSL证书构建安全Web应用的实战经验,涵盖选择合适的证书颁发机构、申请与获取证书、配置Web服务器、优化安全性及实际案例。帮助开发者提升应用安全性,增强用户信任。
|
4月前
|
监控 前端开发 JavaScript
使用 MERN 堆栈构建可扩展 Web 应用程序的最佳实践
使用 MERN 堆栈构建可扩展 Web 应用程序的最佳实践
73 6
|
4月前
|
存储 消息中间件 缓存
构建互联网高性能WEB系统经验总结
如何构建一个优秀的高性能、高可靠的应用系统对每一个开发者至关重要
47 2
|
4月前
|
JSON 前端开发 API
使用Python和Flask构建简易Web API
使用Python和Flask构建简易Web API
212 3
|
4月前
|
机器学习/深度学习 数据采集 Docker
Docker容器化实战:构建并部署一个简单的Web应用
Docker容器化实战:构建并部署一个简单的Web应用
|
4月前
|
JSON API 数据格式
使用Python和Flask构建简单的Web API
使用Python和Flask构建简单的Web API
|
4月前
|
消息中间件 前端开发 JavaScript
探索微前端架构:构建现代Web应用的新策略
本文探讨了微前端架构的概念、优势及实施策略,旨在解决传统单体应用难以快速迭代和团队协作的问题。微前端允许不同团队独立开发、部署应用的各部分,提升灵活性与可维护性。文中还讨论了技术栈灵活性、独立部署、团队自治等优势,并提出了定义清晰接口、使用Web组件、状态管理和样式隔离等实施策略。
|
4月前
|
缓存 安全 前端开发
构建高效Web应用的五大关键技术
【10月更文挑战第42天】在数字化浪潮中,Web应用已成为企业与用户互动的重要桥梁。本文将深入探讨提升Web应用性能和用户体验的五项核心技术,包括前端优化、后端架构设计、数据库管理、安全性增强以及API开发的最佳实践。通过这些技术的应用,开发者可以构建出更快、更稳定且更安全的Web应用,满足现代网络环境的需求。

热门文章

最新文章