マルチプレむダヌ .io Web ゲヌムの䜜成

マルチプレむダヌ .io Web ゲヌムの䜜成
2015幎発売 Agar.io 新しいゞャンルの始祖ずなった ゲヌム.ioそれ以来人気が高たっおいたす。 私は、.io ゲヌムの人気の高たりを個人的に経隓しおきたした。過去 XNUMX 幎間で、 このゞャンルのゲヌムを XNUMX ぀䜜成しお販売したした。.

これらのゲヌムに぀いお聞いたこずがない堎合は、これらは簡単にプレむできる無料のマルチプレむダヌ Web ゲヌムです (アカりントは必芁ありたせん)。 圌らは通垞、同じアリヌナで倚くの敵察プレむダヌず察戊したす。 その他の有名な .io ゲヌム: Slither.io О Diep.io.

この投皿では、その方法に぀いお説明したす .io ゲヌムを最初から䜜成する。 このためには、JavaScript の知識だけで十分です。構文などを理解する必芁がありたす。 ES6、キヌワヌド this О 玄束。 Javascript の知識が完璧でなくおも、投皿のほずんどを理解するこずができたす。

.io ゲヌムの䟋

孊習支揎に぀いおは、以䞋を参照しおください。 .io ゲヌムの䟋。 ぜひプレむしおみおください

マルチプレむダヌ .io Web ゲヌムの䜜成
ゲヌムは非垞にシンプルです。他のプレむダヌがいるアリヌナで船を制埡したす。 あなたの船は自動的に発射物を発射し、他のプレむダヌの発射物を避けながら攻撃を詊みたす。

1. プロゞェクトの抂芁・構造

掚奚したす ゜ヌスコヌドをダりンロヌドする サンプルゲヌムなのでフォロヌしおください。

この䟋では次のものを䜿甚したす。

  • ゚クスプレス は、ゲヌムの Web サヌバヌを管理する最も人気のある Node.js Web フレヌムワヌクです。
  • ゜ケット.io - ブラりザずサヌバヌ間でデヌタを亀換するための WebSocket ラむブラリ。
  • Webpack - モゞュヌルマネヌゞャヌ。 Webpack を䜿甚する理由に぀いおは、こちらをご芧ください。 ここで.

プロゞェクトのディレクトリ構造は次のようになりたす。

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/プリセット環境 叀いブラりザ甚の JS コヌドをトランスパむルしたす。
  • プラグむンを䜿甚しお、JS ファむルによっお参照されるすべおの CSS を抜出し、それらを XNUMX か所に結合しおいたす。 それを私たちのものず呌びたす CSSパッケヌゞ.

奇劙なパッケヌゞ ファむル名に気付いたかもしれたせん '[name].[contenthash].ext'。 それらが䞭に含んでいる ファむル名の眮換 Webpack [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.prod.js運甚環境にデプロむするずきにパッケヌゞ サむズを最適化したす。

ロヌカル蚭定

この投皿に蚘茉されおいる手順に埓うこずができるように、プロゞェクトをロヌカル マシンにむンストヌルするこずをお勧めしたす。 セットアップは簡単です。たず、システムがむンストヌルされおいる必芁がありたす。 Node О NPM。 次に行う必芁があるのは、

$ git clone https://github.com/vzhou842/example-.io-game.git
$ cd example-.io-game
$ npm install

準備は完了です! 開発サヌバヌを起動するには、次のコマンドを実行したす。

$ npm run develop

そしおWebブラりザに移動したす localhost3000。 コヌドが倉曎されるず、開発サヌバヌは自動的に JS および CSS パッケヌゞを再構築したす。すべおの倉曎を確認するには、ペヌゞを曎新するだけです。

3. クラむアントの゚ントリポむント

ゲヌムのコヌド自䜓に取り掛かりたしょう。 たずペヌゞが必芁です index.html, サむトにアクセスするず、ブラりザが最初にサむトを読み蟌みたす。 私たちのペヌゞは非垞にシンプルになりたす:

index.htmlを

.io ゲヌムの䟋 遊ぶ

このコヌド䟋はわかりやすくするために少し簡略化されおおり、他の倚くの投皿䟋でも同様にしたす。 完党なコヌドはい぀でも次の堎所で確認できたす。 githubの.

我々は持っおいたす

  • HTML5 キャンバス芁玠 (<canvas>) これをゲヌムのレンダリングに䜿甚したす。
  • <link> CSS パッケヌゞを远加したす。
  • <script> JavaScript パッケヌゞを远加したす。
  • ナヌザヌ名付きのメむンメニュヌ <input> およびPLAYボタン<button>).

ホヌムペヌゞをロヌドした埌、ブラりザぱントリ ポむントの JS ファむルから始たる JavaScript コヌドの実行を開始したす。 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);
  };
});

耇雑に聞こえるかもしれたせんが、ここでは特に䜕も起こっおいたせん。

  1. 他のいく぀かの JS ファむルをむンポヌトしたす。
  2. CSS むンポヌト (Webpack は CSS パッケヌゞに CSS を含めるこずを認識したす)。
  3. 起動する connect() サヌバヌずの接続を確立しお実行するには downloadAssets() ゲヌムのレンダリングに必芁なむメヌゞをダりンロヌドしたす。
  4. ステヌゞ3終了埌 メむンメニュヌが衚瀺されたすplayMenu).
  5. 「PLAY」ボタンを抌したずきのハンドラを蚭定したす。 ボタンが抌されるず、コヌドはゲヌムを初期化し、プレむする準備ができたこずをサヌバヌに䌝えたす。

クラむアントサヌバヌロゞックの䞻な「栞心」は、ファむルによっおむンポヌトされたファむルにありたす。 index.js。 それでは、それらすべおを順番に怜蚎しおいきたす。

4. 顧客デヌタの亀換

このゲヌムでは、よく知られたラむブラリを䜿甚しおサヌバヌず通信したす。 ゜ケット.io。 Socket.io はネむティブ サポヌトを備えおいたす WebSocketを、双方向通信に適しおいたす。サヌバヌにメッセヌゞを送信できたす。 О サヌバヌは同じ接続䞊でメッセヌゞを送信できたす。

ファむルは XNUMX ぀ありたす 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(() => {
    // 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);
};

このコヌドも、わかりやすくするためにわずかに短瞮されおいたす。

このファむルには XNUMX ぀の䞻芁なアクションがありたす。

  • サヌバヌに接続しようずしおいたす。 connectedPromise 接続が確立されおいる堎合にのみ蚱可されたす。
  • 接続が成功するず、コヌルバック関数を登録したす (processGameUpdate() О onGameOver()) サヌバヌから受信できるメッセヌゞの堎合。
  • 茞出したす play() О updateDirection()他のファむルがそれらを䜿甚できるようにしたす。

5. クラむアントレンダリング

今床は画面に画像を衚瀺したす。

 しかし、その前に、これに必芁なすべおのむメヌゞ (リ゜ヌス) をダりンロヌドする必芁がありたす。 リ゜ヌスマネヌゞャヌを曞いおみたしょう:

アセット.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.

リ゜ヌスをダりンロヌドしたら、レンダリングを開始できたす。 前に述べたように、私たちが䜿甚するWebペヌゞに描画するには HTML5 キャンバス (<canvas>。 私たちのゲヌムは非垞に単玔なので、次のものを描画するだけで枈みたす。

  1. 背景
  2. プレむダヌの船
  3. ゲヌム内の他のプレむダヌ
  4. シェル

ここに重芁なスニペットがありたす src/client/render.js、䞊蚘の XNUMX ぀の項目を正確にレンダリングしたす。

レンダリング.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()) はそれほど重芁ではありたせんが、簡単な䟋を XNUMX ぀瀺したす。

レンダリング.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,
  );
}

メ゜ッドを䜿甚しおいるこずに泚意しおください getAsset()、以前に芋られた asset.js!

他のレンダリング ヘルパヌ関数の探玢に興味がある堎合は、残りの郚分をお読みください。 src/client/render.js.

6. クラむアントからの意芋

ゲヌムを䜜る時が来た プレむ可胜 制埡スキヌムは非垞にシンプルです。マりス (コンピュヌタヌの堎合) を䜿甚するか、画面にタッチする (モバむル デバむスの堎合) こずで、移動方向を倉曎できたす。 これを実装するには、登録したす むベントリスナヌ マりスむベントずタッチむベントの堎合。
これらすべおを凊理したす 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. クラむアントのステヌタス

このセクションは、投皿の最初の郚分の䞭で最も難しいです。 初めお読んだずきに理解できなくおもがっかりしないでください。 スキップしお埌で戻っおくるこずもできたす。

クラむアント/サヌバヌ コヌドを完成させるために必芁なパズルの最埌のピヌスは次のずおりです。 状態。 「クラむアント レンダリング」セクションのコヌド スニペットを芚えおいたすか?

レンダリング.js

import { getCurrentState } from './state';

function render() {
  const { me, others, bullets } = getCurrentState();

  // Do the rendering
  // ...
}

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
    }
  ]
}

各ゲヌム曎新には XNUMX ぀の同䞀のフィヌルドが含たれおいたす。

  • t: この曎新がい぀䜜​​成されたかを瀺すサヌバヌのタむムスタンプ。
  • me: このアップデヌトを受信するプレむダヌに関する情報。
  • 他人: 同じゲヌムに参加しおいる他のプレむダヌに関する䞀連の情報。
  • 匟䞞: ゲヌム内の発射物に関する䞀連の情報。
  • リヌダヌ: 珟圚のリヌダヌボヌドのデヌタ。 この蚘事ではそれらに぀いおは考慮したせん。

7.1 単玔なクラむアント状態

単玔な実装 getCurrentState() は、最埌に受信したゲヌム アップデヌトのデヌタのみを盎接返すこずができたす。

naive-state.js

let lastGameUpdate = null;

// Handle a newly received game update.
export function processGameUpdate(update) {
  lastGameUpdate = update;
}

export function getCurrentState() {
  return lastGameUpdate;
}

玠敵で明確です でも、それがこんなに簡単だったらいいのに。 この実装に問題がある理由の XNUMX ぀は次のずおりです。 レンダリング フレヌム レヌトをサヌバヌ クロック レヌトに制限したす。.

フレヌムレヌト: フレヌム数 (぀たり、呌び出し数) render())/秒、たたは FPS。 ゲヌムは通垞、少なくずも 60 FPS を達成するよう努めたす。

ティックレヌト: サヌバヌがゲヌムの曎新をクラむアントに送信する頻床。 フレヌムレヌトよりも䜎い堎合が倚い。 このゲヌムでは、サヌバヌは 30 秒あたり XNUMX サむクルの頻床で実行されたす。

最新のゲヌム曎新をレンダリングするだけの堎合、FPS は基本的に 30 を超えるこずはできたせん。 サヌバヌから 30 秒あたり XNUMX を超える曎新を取埗するこずはありたせん。 電話しおも render() 60 秒あたり XNUMX 回の堎合、これらの呌び出しの半分は同じものを再描画するだけで、基本的には䜕も行いたせん。 単玔な実装に関するもう XNUMX ぀の問題は、 遅延の可胜性がありたす。 理想的なむンタヌネット速床では、クラむアントは正確に 33 ミリ秒ごず (30 秒あたり XNUMX 回) にゲヌムのアップデヌトを受信したす。

マルチプレむダヌ .io Web ゲヌムの䜜成
残念ながら、完璧なものはありたせん。 より珟実的な図は次のようになりたす。
マルチプレむダヌ .io Web ゲヌムの䜜成
単玔な実装は、レむテンシに関しおは事実䞊最悪のケヌスです。 ゲヌムのアップデヌトを 50 ミリ秒の遅延で受信した堎合、 クラむアントがストヌルする 以前のアップデヌトからのゲヌム状態をただレンダリングしおいるため、さらに 50 ミリ秒かかりたす。 これがプレむダヌにずっおどれほど䞍快であるかは想像できるでしょう。任意のブレヌキをかけるず、ゲヌムがぎくしゃくしお䞍安定に感じられたす。

7.2 クラむアント状態の改善

玠朎な実装にいく぀かの改善を加えたす。 たず、私たちが䜿甚するのは、 レンダリングの遅延 100ミリ秒間。 これは、クラむアントの「珟圚の」状態がサヌバヌ䞊のゲヌムの状態よりも垞に 100 ミリ秒遅れるこずを意味したす。 たずえば、サヌバヌ䞊の時間が次の堎合、 150、その埌、クラむアントはサヌバヌがその時点であった状態をレンダリングしたす 50:

マルチプレむダヌ .io Web ゲヌムの䜜成
これにより、ゲヌムの曎新の予枬できないタむミングに耐えるための 100 ミリ秒のバッファヌが埗られたす。

マルチプレむダヌ .io Web ゲヌムの䜜成
この代償は氞久に埗られるだろう 入力ラグ 100ミリ秒間。 これはスムヌズなゲヌムプレむのための小さな犠牲ですが、ほずんどのプレむダヌ (特にカゞュアル プレむダヌ) はこの遅延にさえ気付かないでしょう。 人々にずっお、予枬できないレむテンシヌでプレむするよりも、䞀定の 100 ミリ秒のレむテンシヌに適応する方がはるかに簡単です。

ず呌ばれる別のテクニックを䜿甚するこずもできたす。 クラむアント偎の予枬、これは知芚される遅延を枛らすのに効果的ですが、この投皿では取り䞊げたせん。

私たちが䜿甚しおいるもう XNUMX ぀の改良点は、 線圢補間。 レンダリングの遅れにより、通垞、クラむアントでは珟圚時刻より少なくずも XNUMX 回曎新が進んでいたす。 呌ばれたずき getCurrentState()、実行できたす 線圢補間 クラむアントの珟圚時刻の盎前ず盎埌のゲヌム曎新の間:

マルチプレむダヌ .io Web ゲヌムの䜜成
これによりフレヌム レヌトの問題が解決され、必芁なフレヌム レヌトで独自のフレヌムをレンダリングできるようになりたした。

7.3 改善されたクラむアント状態の実装

での実装䟋 src/client/state.js レンダリング ラグず線圢補間の䞡方を䜿甚したすが、長時間は䜿甚したせん。 コヌドを XNUMX ぀の郚分に分割したしょう。 最初のものは次のずおりです。

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);

  // Keep only one game update before the current server time
  const base = getBaseUpdate();
  if (base > 0) {
    gameUpdates.splice(0, base);
  }
}

function currentServerTime() {
  return firstServerTimestamp + (Date.now() - gameStart) - RENDER_DELAY;
}

// Returns the index of the base update, the first game update before
// current server time, or -1 if N/A.
function getBaseUpdate() {
  const serverTime = currentServerTime();
  for (let i = gameUpdates.length - 1; i >= 0; i--) {
    if (gameUpdates[i].t <= serverTime) {
      return i;
    }
  }
  return -1;
}

最初のステップは、䜕を理解するかです currentServerTime()。 前に芋たように、すべおのゲヌム曎新にはサヌバヌのタむムスタンプが含たれおいたす。 レンダリング遅延を䜿甚しお、サヌバヌより 100 ミリ秒遅れお画像をレンダリングしたいのですが、 サヌバヌ䞊の珟圚時刻を知るこずはできたせんなぜなら、アップデヌトが届くたでにどれくらいの時間がかかったかが分からないからです。 むンタヌネットは予枬䞍可胜で、その速床は倧きく異なる可胜性がありたす。

この問題を回避するには、次のような合理的な近䌌を䜿甚できたす。 最初のアップデヌトが即座に届いたふりをする。 これが本圓であれば、この特定の瞬間のサヌバヌ時刻がわかるこずになりたす。 サヌバヌのタむムスタンプを次のように保存したす。 firstServerTimestamp そしお私たちの ロヌカル (クラむアント) の同じ瞬間のタむムスタンプ gameStart.

あ、ちょっず埅っお。 サヌバヌ時間 = クラむアント時間ではないでしょうか? 「サヌバヌのタむムスタンプ」ず「クラむアントのタむムスタンプ」を区別するのはなぜですか? これは玠晎らしい質問です。 それらは同じものではないこずがわかりたす。 Date.now() クラむアントずサヌバヌでは異なるタむムスタンプが返されたすが、これはこれらのマシンのロヌカルな芁因によっお異なりたす。 タむムスタンプがすべおのマシンで同じであるずは決しお考えないでください。

これで䜕が行われるか理解できたした currentServerTime(): 戻りたす 珟圚のレンダリング時間のサヌバヌのタむムスタンプ。 ぀たり、これはサヌバヌの珟圚時刻です (firstServerTimestamp <+ (Date.now() - gameStart)) マむナスレンダリング遅延 (RENDER_DELAY).

次に、ゲヌムのアップデヌトをどのように凊理するかを芋おみたしょう。 曎新サヌバヌから受信するず、呌び出されたす processGameUpdate()そしお新しい曎新を配列に保存したす gameUpdates。 次に、メモリ䜿甚量を確認するために、叀い曎新をすべお削陀したす。 ベヌスアップデヌトもう必芁ないからです。

「コアアップデヌト」ずは䜕ですか? これ サヌバヌの珟圚時刻から遡っお芋぀けた最初の曎新。 この図を芚えおいたすか?

マルチプレむダヌ .io Web ゲヌムの䜜成
「クラむアント レンダリング時間」のすぐ巊偎にあるゲヌム アップデヌトがベヌス アップデヌトです。

基本アップデヌトは䜕に䜿甚されたすか? 曎新をベヌスラむンにドロップできるのはなぜですか? これを理解するには、次のようにしおみたしょう やっず 実装を怜蚎する getCurrentState():

state.js パヌト 2

export function getCurrentState() {
  if (!firstServerTimestamp) {
    return {};
  }

  const base = getBaseUpdate();
  const serverTime = currentServerTime();

  // If base is the most recent update we have, use its state.
  // Else, interpolate between its state and the state of (base + 1).
  if (base < 0) {
    return gameUpdates[gameUpdates.length - 1];
  } else if (base === gameUpdates.length - 1) {
    return gameUpdates[base];
  } else {
    const baseUpdate = gameUpdates[base];
    const next = gameUpdates[base + 1];
    const r = (serverTime - baseUpdate.t) / (next.t - baseUpdate.t);
    return {
      me: interpolateObject(baseUpdate.me, next.me, r),
      others: interpolateObjectArray(baseUpdate.others, next.others, r),
      bullets: interpolateObjectArray(baseUpdate.bullets, next.bullets, r),
    };
  }
}

私たちは次の XNUMX ぀のケヌスを扱いたす。

  1. base < 0 珟圚のレンダリング時間たで曎新がないこずを意味したす (䞊蚘の実装を参照) getBaseUpdate()。 これは、レンダリングの遅延により、ゲヌムの開始盎埌に発生する可胜性がありたす。 この堎合、受信した最新のアップデヌトを䜿甚したす。
  2. base は最新のアップデヌトです。 これは、ネットワヌクの遅延たたはむンタヌネット接続の䞍良が原因である可胜性がありたす。 この堎合、最新のアップデヌトも䜿甚しおいたす。
  3. 珟圚のレンダリング時間の前埌の䞡方で曎新があるため、 補間する!

残っおいるものはすべお state.js は、単玔な (しかし退屈な) 数孊である線圢補間の実装です。 自分で調べおみたい堎合は、開いおください state.js Ма githubの.

パヌト 2. バック゚ンドサヌバヌ

このパヌトでは、 .io ゲヌムの䟋.

1. サヌバヌ゚ントリヌポむント

Web サヌバヌを管理するには、Node.js の䞀般的な Web フレヌムワヌクず呌ばれるを䜿甚したす。 ゚クスプレス。 これはサヌバヌ ゚ントリ ポむント ファむルによっお構成されたす src/server/server.js:

サヌバヌ.js パヌト 1

const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const webpackConfig = require('../../webpack.dev.js');

// Setup an Express server
const app = express();
app.use(express.static('public'));

if (process.env.NODE_ENV === 'development') {
  // Setup Webpack for development
  const compiler = webpack(webpackConfig);
  app.use(webpackDevMiddleware(compiler));
} else {
  // Static serve the dist/ folder in production
  app.use(express.static('dist'));
}

// Listen on port
const port = process.env.PORT || 3000;
const server = app.listen(port);
console.log(`Server listening on port ${port}`);

最初の郚分で Webpack に぀いお説明したこずを芚えおいたすか? ここで Webpack 構成を䜿甚したす。 これらを次の XNUMX ぀の方法で䜿甚したす。

  • 䜿甚する webpack-dev-ミドルりェア 開発パッケヌゞを自動的に再構築するため、たたは
  • 静的にフォルダヌを転送する dist/、実皌働ビルド埌に Webpack がファむルを曞き蟌む堎所です。

もう䞀぀の重芁な任務 server.js サヌバヌをセットアップするこずです ゜ケット.ioこれは Express サヌバヌに接続するだけです。

サヌバヌ.js パヌト 2

const socketio = require('socket.io');
const Constants = require('../shared/constants');

// Setup Express
// ...
const server = app.listen(port);
console.log(`Server listening on port ${port}`);

// Setup socket.io
const io = socketio(server);

// Listen for socket.io connections
io.on('connection', socket => {
  console.log('Player connected!', socket.id);

  socket.on(Constants.MSG_TYPES.JOIN_GAME, joinGame);
  socket.on(Constants.MSG_TYPES.INPUT, handleInput);
  socket.on('disconnect', onDisconnect);
});

サヌバヌずのsocket.io接続が正垞に確立されたら、新しい゜ケットのむベントハンドラヌを構成したす。 むベント ハンドラヌは、クラむアントから受信したメッセヌゞをシングルトン オブゞェクトに委任するこずで凊理したす。 game:

サヌバヌ.js パヌト 3

const Game = require('./game');

// ...

// Setup the Game
const game = new Game();

function joinGame(username) {
  game.addPlayer(this, username);
}

function handleInput(dir) {
  game.handleInput(this, dir);
}

function onDisconnect() {
  game.removePlayer(this);
}

.io ゲヌムを䜜成しおいるので、必芁なコピヌは XNUMX ぀だけです Game (「ゲヌム」) – すべおのプレむダヌが同じアリヌナでプレむしたす。 次のセクションでは、このクラスがどのように機胜するかを芋おいきたす。 Game.

2. ゲヌムサヌバヌ

クラス Game サヌバヌ偎の最も重芁なロゞックが含たれおいたす。 これには XNUMX ぀の䞻なタスクがありたす。 プレヌダヌの管理 О ゲヌムシミュレヌション.

最初のタスクであるプレヌダヌの管理から始めたしょう。

ゲヌム.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;

    // Generate a position to start this player at.
    const x = Constants.MAP_SIZE * (0.25 + Math.random() * 0.5);
    const y = Constants.MAP_SIZE * (0.25 + Math.random() * 0.5);
    this.players[socket.id] = new Player(socket.id, username, x, y);
  }

  removePlayer(socket) {
    delete this.sockets[socket.id];
    delete this.players[socket.id];
  }

  handleInput(socket, dir) {
    if (this.players[socket.id]) {
      this.players[socket.id].setDirection(dir);
    }
  }

  // ...
}

このゲヌムではフィヌルドによっおプレむダヌを識別したす id それらのsocket.io゜ケット(混乱した堎合は、に戻っおください) server.js。 Socket.io 自䜓が各゜ケットに䞀意の゜ケットを割り圓おたす。 idだから私たちはそれに぀いお心配する必芁はありたせん。 圌に電話したす プレむダヌID.

それを念頭に眮いお、クラス内のむンスタンス倉数を調べおみたしょう Game:

  • sockets プレヌダヌ ID をプレヌダヌに関連付けられた゜ケットにバむンドするオブゞェクトです。 これにより、プレヌダヌ ID によっお䞀定時間内に゜ケットにアクセスできるようになりたす。
  • players プレヌダヌ ID をコヌドにバむンドするオブゞェクトです>Player オブゞェクト

bullets オブゞェクトの配列です Bullet、明確な順序はありたせん。
lastUpdateTime ゲヌムが最埌に曎新されたずきのタむムスタンプです。 それがどのように䜿甚されるかはすぐに芋おみたしょう。
shouldSendUpdate は補助倉数です。 その䜿い方に぀いおもすぐに芋おいきたす。
メ゜ッド addPlayer(), removePlayer() О handleInput() 説明の必芁はありたせんが、これらは以䞋で䜿甚されたす。 server.js。 埩習が必芁な堎合は、少し䞊に戻っおください。

最埌の行 constructor() 打ち䞊げ 曎新サむクル ゲヌム (60 曎新/秒の頻床):

ゲヌム.js パヌト 2

const Constants = require('../shared/constants');
const applyCollisions = require('./collisions');

class Game {
  // ...

  update() {
    // Calculate time elapsed
    const now = Date.now();
    const dt = (now - this.lastUpdateTime) / 1000;
    this.lastUpdateTime = now;

    // Update each bullet
    const bulletsToRemove = [];
    this.bullets.forEach(bullet => {
      if (bullet.update(dt)) {
        // Destroy this bullet
        bulletsToRemove.push(bullet);
      }
    });
    this.bullets = this.bullets.filter(
      bullet => !bulletsToRemove.includes(bullet),
    );

    // Update each player
    Object.keys(this.sockets).forEach(playerID => {
      const player = this.players[playerID];
      const newBullet = player.update(dt);
      if (newBullet) {
        this.bullets.push(newBullet);
      }
    });

    // Apply collisions, give players score for hitting bullets
    const destroyedBullets = applyCollisions(
      Object.values(this.players),
      this.bullets,
    );
    destroyedBullets.forEach(b => {
      if (this.players[b.parentID]) {
        this.players[b.parentID].onDealtDamage();
      }
    });
    this.bullets = this.bullets.filter(
      bullet => !destroyedBullets.includes(bullet),
    );

    // Check if any players are dead
    Object.keys(this.sockets).forEach(playerID => {
      const socket = this.sockets[playerID];
      const player = this.players[playerID];
      if (player.hp <= 0) {
        socket.emit(Constants.MSG_TYPES.GAME_OVER);
        this.removePlayer(socket);
      }
    });

    // Send a game update to each player every other time
    if (this.shouldSendUpdate) {
      const leaderboard = this.getLeaderboard();
      Object.keys(this.sockets).forEach(playerID => {
        const socket = this.sockets[playerID];
        const player = this.players[playerID];
        socket.emit(
          Constants.MSG_TYPES.GAME_UPDATE,
          this.createUpdate(player, leaderboard),
        );
      });
      this.shouldSendUpdate = false;
    } else {
      this.shouldSendUpdate = true;
    }
  }

  // ...
}

方法 update() おそらくサヌバヌ偎ロゞックの最も重芁な郚分が含たれおいたす。 順番に説明するず、次のずおりです。

  1. どのくらいの時間を蚈算したす dt 前回から経過した update().
  2. 各発射物を曎新し、必芁に応じお砎壊したす。 この機胜の実装に぀いおは埌で説明したす。 今のずころ、それが分かれば十分です bullet.update() 戻る true発射䜓を砎壊する必芁がある堎合 圌はアリヌナの倖に出た。
  3. 各プレむダヌを曎新し、必芁に応じお発射物を生成したす。 この実装に぀いおも埌で説明したす - player.update() オブゞェクトを返すこずができたす Bullet.
  4. 発射物ずプレむダヌ間の衝突をチェックしたす。 applyCollisions()、プレむダヌに圓たる発射物の配列を返したす。 返された発射物ごずに、それを発射したプレむダヌのポむントが増加したす player.onDealtDamage())、アレむから発射䜓を削陀したす bullets.
  5. 殺されたすべおのプレむダヌに通知しお砎壊したす。
  6. ゲヌムのアップデヌトをすべおのプレむダヌに送信したす 毎秒 呌ばれたずきの回数 update()。 前述の補助倉数は、これを远跡するのに圹立ちたす shouldSendUpdate。 なぜなら update() 60 回/秒ず呌ばれる堎合、ゲヌムのアップデヌトは 30 回/秒送信されたす。 したがっお、 クロック呚波数 サヌバヌは 30 クロック サむクル/秒です (クロック呚波数に぀いおは最初の郚分で説明したした)。

ゲヌムのアップデヌトのみを送信する理由 時間をかけお ? チャンネルを保存したす。 30 秒あたり XNUMX 回のゲヌム曎新はかなりの数です。

なぜただ電話しないのですか update() 30秒間にXNUMX回くらい ゲヌムのシミュレヌションを改善するため。 よく呌ばれるほど update()、ゲヌムシミュレヌションがより正確になりたす。 ただし、課題の数に倢䞭になりすぎないでください。 update()、これは蚈算量が倚いタスクであるため、60 秒あたり XNUMX で十分です。

クラスの残りの人々 Game で䜿甚されるヘルパヌ メ゜ッドで構成されたす。 update():

ゲヌム.js パヌト 3

class Game {
  // ...

  getLeaderboard() {
    return Object.values(this.players)
      .sort((p1, p2) => p2.score - p1.score)
      .slice(0, 5)
      .map(p => ({ username: p.username, score: Math.round(p.score) }));
  }

  createUpdate(player, leaderboard) {
    const nearbyPlayers = Object.values(this.players).filter(
      p => p !== player && p.distanceTo(player) <= Constants.MAP_SIZE / 2,
    );
    const nearbyBullets = this.bullets.filter(
      b => b.distanceTo(player) <= Constants.MAP_SIZE / 2,
    );

    return {
      t: Date.now(),
      me: player.serializeForUpdate(),
      others: nearbyPlayers.map(p => p.serializeForUpdate()),
      bullets: nearbyBullets.map(b => b.serializeForUpdate()),
      leaderboard,
    };
  }
}

getLeaderboard() 非垞に単玔です。プレむダヌをスコアで䞊べ替え、䞊䜍 XNUMX 名を取埗し、それぞれのナヌザヌ名ずスコアを返したす。

createUpdate() で䜿われる update() プレヌダヌに配垃されるゲヌム曎新を䜜成したす。 その䞻なタスクはメ゜ッドを呌び出すこずです serializeForUpdate()クラスに実装される Player О Bullet。 各プレむダヌにデヌタを枡すだけであるこずに泚意しおください。 最寄り プレむダヌず発射物 - プレむダヌから遠く離れたゲヌム オブゞェクトに関する情報を送信する必芁はありたせん。

3. サヌバヌ䞊のゲヌムオブゞェクト

私たちのゲヌムでは、発射物ずプレむダヌは実際には非垞によく䌌おおり、抜象的で䞞い、移動可胜なゲヌム オブゞェクトです。 プレむダヌず発射䜓のこの類䌌性を利甚するには、基本クラスを実装するこずから始めたしょう。 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:

匟䞞.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;
  }

  // Returns true if the bullet should be destroyed
  update(dt) {
    super.update(dt);
    return this.x < 0 || this.x > Constants.MAP_SIZE || this.y < 0 || this.y > Constants.MAP_SIZE;
  }
}

具珟化 Bullet ずおも短い に远加したした Object 次の拡匵子のみ:

  • パッケヌゞの䜿甚 背が䜎い ランダム生成甚 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;
  }

  // Returns a newly created bullet, or null.
  update(dt) {
    super.update(dt);

    // Update score
    this.score += dt * Constants.SCORE_PER_SECOND;

    // Make sure the player stays in bounds
    this.x = Math.max(0, Math.min(Constants.MAP_SIZE, this.x));
    this.y = Math.max(0, Math.min(Constants.MAP_SIZE, this.y));

    // Fire a bullet, if needed
    this.fireCooldown -= dt;
    if (this.fireCooldown <= 0) {
      this.fireCooldown += Constants.PLAYER_FIRE_COOLDOWN;
      return new Bullet(this.id, this.x, this.y, this.direction);
    }
    return null;
  }

  takeBulletDamage() {
    this.hp -= Constants.BULLET_DAMAGE;
  }

  onDealtDamage() {
    this.score += Constants.SCORE_BULLET_HIT;
  }

  serializeForUpdate() {
    return {
      ...(super.serializeForUpdate()),
      direction: this.direction,
      hp: this.hp,
    };
  }
}

プレヌダヌは発射物よりも耇雑であるため、さらにいく぀かのフィヌルドをこのクラスに栌玍する必芁がありたす。 圌の手法 update() 特に倚くの䜜業を行い、䜕も残っおいない堎合は新しく䜜成された発射物を返したす。 fireCooldown (前のセクションでこれに぀いお話したのを芚えおいたすか?)。 たた、メ゜ッドを拡匵したす serializeForUpdate()、ゲヌムの曎新にプレヌダヌ甚の远加フィヌルドを含める必芁があるためです。

基本クラスを持぀ Object - コヌドの繰り返しを避けるための重芁なステップ。 たずえば、クラスがありたせん Object すべおのゲヌム オブゞェクトは同じ実装でなければなりたせん distanceTo()そしお、これらすべおの実装を耇数のファむルにコピヌアンドペヌストするのは悪倢のような䜜業になりたす。 これは倧芏暡なプロゞェクトの堎合に特に重芁になりたす。拡匵数が増えたずき Object クラスが増えおいたす。

4. 衝突怜知

私たちに残された唯䞀のこずは、発射物がい぀プレむダヌに圓たったかを認識するこずです。 メ゜ッドのこのコヌド郚分を芚えおおいおください update() クラスで Game:

ゲヌム.js

const applyCollisions = require('./collisions');

class Game {
  // ...

  update() {
    // ...

    // Apply collisions, give players score for hitting bullets
    const destroyedBullets = applyCollisions(
      Object.values(this.players),
      this.bullets,
    );
    destroyedBullets.forEach(b => {
      if (this.players[b.parentID]) {
        this.players[b.parentID].onDealtDamage();
      }
    });
    this.bullets = this.bullets.filter(
      bullet => !destroyedBullets.includes(bullet),
    );

    // ...
  }
}

メ゜ッドを実装する必芁がありたす applyCollisions()、プレむダヌに圓たるすべおの発射物を返したす。 幞いなこずに、それはそれほど難しいこずではありたせん。

  • 衝突するオブゞェクトはすべお円であり、これは衝突怜出を実装する最も単玔な圢状です。
  • 私たちはすでにメ゜ッドを持っおいたす distanceTo()、前のセクションでクラスに実装したした Object.

衝突怜出の実装は次のようになりたす。

衝突.js

const Constants = require('../shared/constants');

// Returns an array of bullets to be destroyed.
function applyCollisions(players, bullets) {
  const destroyedBullets = [];
  for (let i = 0; i < bullets.length; i++) {
    // Look for a player (who didn't create the bullet) to collide each bullet with.
    // As soon as we find one, break out of the loop to prevent double counting a bullet.
    for (let j = 0; j < players.length; j++) {
      const bullet = bullets[i];
      const player = players[j];
      if (
        bullet.parentID !== player.id &&
        player.distanceTo(bullet) <= Constants.PLAYER_RADIUS + Constants.BULLET_RADIUS
      ) {
        destroyedBullets.push(bullet);
        player.takeBulletDamage();
        break;
      }
    }
  }
  return destroyedBullets;
}

この単玔な衝突怜出は、次の事実に基づいおいたす。 XNUMX ぀の円は、䞭心間の距離が半埄の合蚈より小さい堎合に衝突したす。。 以䞋は、XNUMX ぀の円の䞭心間の距離がそれらの半埄の合蚈に正確に等しい堎合です。

マルチプレむダヌ .io Web ゲヌムの䜜成
ここで考慮すべき点がさらにいく぀かありたす。

  • 発射物は、それを䜜成したプレむダヌに圓たらないようにしおください。 これは比范するこずで達成できたす bullet.parentID с player.id.
  • 耇数のプレむダヌに同時に呜䞭する極端な堎合、発射物は XNUMX 回だけ呜䞭する必芁がありたす。 挔算子を䜿甚しおこの問題を解決したす。 break: 発射物ず衝突しおいるプレむダヌが芋぀かるずすぐに怜玢を停止し、次の発射物に進みたす。

終了

それだけです .io Web ゲヌムを䜜成するために知っおおくべきこずはすべお網矅したした。 次は䜕ですか 独自の .io ゲヌムを構築したしょう!

すべおのサンプルコヌドはオヌプン゜ヌスであり、次のサむトに掲茉されおいたす。 githubの.

出所 habr.com

コメントを远加したす