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

画像

2015幎にリリヌスされたAgar.ioは、 ゲヌム.ioの新しいゞャンルの先駆者ずなり、その人気はその埌倧きく成長したした。 私が経隓した.ioゲヌムの人気の高たり過去3幎間で、このゞャンルの2぀のゲヌムを䜜成しお販売したした。 。

このようなゲヌムを聞いたこずがない堎合これらは、参加しやすい無料のマルチプレむダヌWebゲヌムですアカりントは䞍芁です。 通垞、圌らは同じアリヌナで倚くの敵プレむダヌに立ち向かいたす。 .ioゞャンルの他の有名なゲヌムは、 Slither.ioずDiep.ioです。

この投皿では、.ioゲヌムをれロから䜜成する方法を理解したす 。 このためには、Javascriptの知識だけで十分です。ES6構文、 this Promisesなどを理解する必芁がありたす。 Javascriptを完党に知っおいなくおも、ほずんどの投皿を理解できたす。

ゲヌム䟋.io


孊習を支揎するために、サンプルの.ioゲヌムを参照したす。 プレむしおみおください


ゲヌムは非垞に簡単です。他のプレむダヌがいるアリヌナで船を操䜜したす。 あなたの船は自動的に砲匟を発射し、他のプレむダヌを攻撃しようずしながら、圌らの砲匟を避けたす。

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


サンプルゲヌムの゜ヌスコヌドをダりンロヌドしお、フォロヌしおください。

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


プロゞェクトディレクトリの構造は次のずおりです。

 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', }), ], }; 

ここで最も重芁なのは、次の行です。


奇劙なパッケヌゞファむル名'[name].[contenthash].ext'気付いたかもしれたせん。 これらには、Webpack ファむルの名前の眮換が含たれたす 。 [name]は入力ポむントの名前に眮き換えられこの堎合、これはgame 、 [contenthash]はファむルコンテンツのハッシュに眮き換えられたす。 プロゞェクトをハッシュ甚に最適化するためにこれを行いたす- パッケヌゞが倉曎されるずファむル名が倉曎される contenthashが倉曎されるため、JSパッケヌゞを無期限にキャッシュするようブラりザに指瀺できたす。 最終的な結果は、 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.dev.jsを䜿甚し、 webpack.dev.jsに切り替えお、 webpack.prod.js展開するずきにパッケヌゞサむズを最適化したす。

ロヌカル蚭定


この投皿に蚘茉されおいる手順を実行できるように、ロヌカルマシンにプロゞェクトをむンストヌルするこずをお勧めしたす。 セットアップは簡単です。たず、 ノヌドず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

  <DOCTYPE html>
 <html>
 <head>
   <title> .ioゲヌムの䟋</ title>
   <link type = "text / css" rel = "stylesheet" href = "/ game.bundle.css">
 </ head>
 <本䜓>
   <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">プレむ</ button>
   </ div>
 </ body>
 </ html> 

このコヌド䟋は、わかりやすくするために若干簡略化されおいたす。他の倚くの投皿䟋でも同じこずをしたす。 完党なコヌドは、垞にGithubで衚瀺できたす。

私たちが持っおいたす


ホヌムペヌゞを読み蟌んだ埌、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); }; }); 

これは耇雑に思えるかもしれたせんが、実際にはここでは倚くのアクションが発生しおいたせん。

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

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

4.顧客デヌタの亀換


このゲヌムでは、よく知られおいるsocket.ioラむブラリを䜿甚しおサヌバヌず通信したす。 Socket.ioには、双方向通信に適したWebSocketの組み蟌みサポヌトがありたす。サヌバヌにメッセヌゞを送信でき、サヌバヌは同じ接続を介しおメッセヌゞを送信できたす。

サヌバヌずのすべおの通信を凊理する src/client/networking.jsファむルが1぀ありたす。

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぀の䞻芁なアクションがありたす。


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


画面に画像を衚瀺するずきが来たした

...しかし、これを行う前に、これに必芁なすべおのむメヌゞリ゜ヌスをダりンロヌドする必芁がありたす。 リ゜ヌスマネヌゞャヌを䜜成したしょう。

asset.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を有効にしdownloadPromise 。

リ゜ヌスをダりンロヌドしたら、レンダリングを開始できたす。 前述したように、 HTML5 Canvas  <canvas> を䜿甚しおWebペヌゞに描画したす。 私たちのゲヌムは非垞にシンプルなので、以䞋を描くだけです。

  1. 背景
  2. プレむダヌシップ
  3. ゲヌム内の他のプレむダヌ
  4. 貝

䞊蚘の4぀のポむントを正確にレンダリングする重芁なsrc/client/render.jsを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() の特定の実装はそれほど重芁ではありたせんが、ここに1぀の簡単な䟋を瀺したす。

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()メ゜ッドを䜿甚しおいるこずに泚意しお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()呌び出すむベントリスナヌです。 updateDirection()は入力むベントを凊理し、それに応じおゲヌムの状態を曎新するサヌバヌずのメッセヌゞングに関䞎しおいたす。

7.顧客の状態


このセクションは、投皿の最初の郚分で最も難しいです。 あなたが最初の読曞からそれを理解しおいないなら、萜胆しないでください スキップしお、埌で戻るこずもできたす。

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

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

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


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

矎しくクリア しかし、すべおがずおも簡単だった堎合。 この実装に問題がある理由の1぀は、 レンダリングのフレヌムレヌトをサヌバヌクロックの呚波数に制限するこずです。

フレヌムレヌト フレヌム数぀たり、1秒あたりのrender()呌び出し、たたはFPS。 ゲヌムは通垞、少なくずも60 FPSに達する傟向がありたす。

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

ゲヌムの最新の曎新をレンダリングするだけの堎合、FPSは実際には30を超えるこずはできたせん。 サヌバヌから毎秒30を超える曎新を受け取るこずはないからです。 1秒間に60回render()を呌び出しおも、これらの呌び出しの半分は単玔に同じものを再描画し、本質的には䜕もしたせん。 別の玠朎な実装問題はそれが遅れる傟向があるずいうこずです。 理想的なむンタヌネット速床では、クラむアントは正確に33 ms1秒あたり30ごずにゲヌムの曎新を受け取りたす。


残念ながら、完璧なものはありたせん。 より珟実的な画像は次のずおりです。

単玔な実装は、遅延に関しおはほずんど最悪のケヌスです。 ゲヌムの曎新が50ミリ秒の遅延で受信されるず、 クラむアントは以前の曎新からのゲヌムの状態をレンダリングするため、さらに50ミリ秒遅くなりたす 。 プレむダヌにずっおどれほど䞍䟿なこずか想像できたす。braking意的なブレヌキングのため、ゲヌムは䞍安定で䞍安定に芋えたす。

7.2顧客ステヌタスの改善


単玔な実装にいく぀かの改善を加えたす。 たず、100 msのレンダリング遅延を䜿甚したす 。 これは、クラむアントの「珟圚の」状態が垞にサヌバヌ䞊のゲヌムの状態より100ミリ秒遅れるこずを意味したす。 たずえば、サヌバヌの時刻が150の堎合、サヌバヌが時刻50にあった状態がクラむアントに衚瀺されたす。


これにより、100ミリ秒のバッファヌが提䟛され、ゲヌムの曎新を受信する予枬䞍可胜な時間を乗り切るこずができたす。


これに察する支払いは、 100ミリ秒の䞀定の入力遅延入力遅延です。 これはスムヌズなゲヌムプレむのための小さな犠牲です-ほずんどのプレむダヌ特にカゞュアルなプレむダヌはこの遅延に気付かないでしょう。 予枬できない遅延で遊ぶよりも、100ミリ秒の䞀定の遅延に適応する方がはるかに簡単です。

「クラむアント偎の予枬」ず呌ばれる別の手法を䜿甚するこずもできたす。これは、知芚される遅延を枛らすのに良い仕事をしたすが、この蚘事では考慮したせん。

私たちが䜿甚する別の改善点は、 線圢補間です。 レンダリングの遅延により、通垞、少なくずも1回の曎新でクラむアントの珟圚の時間を远い越したす。 getCurrentState()が呌び出されるず、クラむアントの珟圚の時刻の盎前ず盎埌にゲヌム曎新間の線圢補間を実行できたす。


これにより、フレヌムレヌトの問題が解決したす。必芁な任意の呚波数で䞀意のフレヌムをレンダリングできるようになりたした。

7.3拡匵クラむアントステヌタスの実装


src/client/state.js実装䟋では、レンダリング遅延ず線圢補間の䞡方を䜿甚しsrc/client/state.jsが、これは長くはsrc/client/state.jsたせん。 コヌドを2぀の郚分に分けたしょう。 これが最初のものです

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に保存し、同時にロヌカル クラむアントタむムスタンプをfirstServerTimestampに保存したす。

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

これでcurrentServerTime()が䜕をするのか理解できたした 。 珟圚のレンダリング時間のサヌバヌタむムスタンプを返したす。 ぀たり、これは珟圚のサヌバヌ時間 firstServerTimestamp <+ (Date.now() - gameStart) からレンダリング遅延 RENDER_DELAY を匕いたものです。

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

「基本曎新」ずは䜕ですかこれは、珟圚のサヌバヌ時刻から逆方向に移動する最初の曎新です。この回路を芚えおいたすか


ゲヌムの曎新は、Client Render Timeのすぐ巊偎にあり、基本的な曎新です。

基本的な曎新は䜕に䜿甚されたすかなぜベヌスの曎新を砎棄できるのですかこれを理解するために、最埌に実装を芋おみたしょう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), }; } } 

3぀のケヌスを凊理したす。

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

残っおstate.jsいるのは、単玔なしかし退屈な数孊である線圢補間の実装です。自分で孊習したい堎合はstate.js、Githubで開いおください。

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


このパヌトでは、.ioゲヌムの䟋を実行するNode.jsバック゚ンドを芋おいきたす。

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


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

server.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構成を䜿甚したす。それらを2぀の方法で適甚したす。


別の重芁なタスクserver.jsは、単にExpressサヌバヌに接続するsocket.ioサヌバヌを構成するこずです。

server.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。

server.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ゞャンルのゲヌムを䜜成するため、1぀のむンスタンスGame「ゲヌム」のみが必芁です。すべおのプレむダヌが同じアリヌナでプレむしたす次のセクションでは、このクラスがどのように機胜するかを芋おいきたすGame。

2.ゲヌムサヌバヌ


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

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

game.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。


bulletsBullet特定の順序を持​​たないオブゞェクトの配列です。
lastUpdateTime-これは、ゲヌムが最埌に曎新された時刻のタむムスタンプです。すぐに䜿甚方法がわかりたす。
shouldSendUpdate補助倉数です。その䜿甚も間もなく芋られたす。
メ゜ッドaddPlayer()、removePlayer()およびhandleInput()説明する必芁はありたせん、それらはで䜿甚されserver.jsたす。メモリを曎新する必芁がある堎合は、もう少し䞊に戻りたす。

最埌の行はゲヌムの曎新サむクルをconstructor()開始したす60曎新/秒の頻床で

game.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 – !

なぜ、update()毎秒30回電話しないのですかゲヌムのシミュレヌションを改善するため。より頻繁に呌び出されるupdate()ほど、ゲヌムのシミュレヌションはより正確になりたす。ただし、呌び出し回数に倢䞭update()になりすぎないでください。これは蚈算コストの高いタスクであるため、1秒あたり60で十分です。

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

game.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()非垞に簡単です-ポむント数でプレヌダヌを゜ヌトし、ベスト5を取埗しお、各ナヌザヌ名ずスコアを返したす。プレヌダヌに枡されるゲヌムの曎新を䜜成するために

createUpdate()䜿甚さupdate()れたす。その䞻なタスクはserializeForUpdate()、Playerおよびクラスに実装されたメ゜ッドを呌び出すこずBulletです。圌は各プレむダヌに最も近いプレむダヌずシェルに関する情報のみを転送するこずに泚意しおください-プレむダヌから遠くにあるゲヌムオブゞェクトに関する情報を送信する必芁はありたせん

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


私たちのゲヌムでは、シェルずプレむダヌは実際には非垞によく䌌おいたす。それらは抜象的なラりンド移動ゲヌムオブゞェクトです。プレヌダヌずシェルのこの類䌌性を利甚するために、基本クラスの実装から始めたしょうObject。

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

bullet.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次の拡匵のみに远加したした。


に移りたしょう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

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()プレヌダヌにヒットするすべおのシェルを返すメ゜ッドを実装する必芁がありたす。幞いなこずに、これはそれほど難しくありたせん。なぜなら、


衝突認識の実装は次のようになりたす。

collisions.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; } 

この単玔な衝突認識は、䞭心間の距離が半埄の合蚈よりも小さい堎合に2぀の円が衝突するずいう事実に基づいおいたす。2぀の円の䞭心間の距離がそれらの半埄の合蚈に正確に等しい堎合は次のずおりです。


ここでは、いく぀かの偎面を慎重に怜蚎する必芁がありたす。


終わり


以䞊です .io Webゲヌムを䜜成するために知っおおく必芁のあるすべおを網矅したした。 次は 独自の.ioゲヌムをビルドしおください

サンプルコヌドはすべおオヌプン゜ヌスであり、Githubに投皿されおいたす。

Source: https://habr.com/ru/post/J450574/


All Articles