Paghimo og multiplayer nga dula sa web sa .io nga genre

Paghimo og multiplayer nga dula sa web sa .io nga genre
Gipagawas sa 2015 Agar.io nahimong katigulangan sa usa ka bag-ong genre dula.io, kansang pagkapopular miuswag pag-ayo sukad niadto. Nasinati nako ang pagsaka sa pagkapopular sa mga dula sa .io sa akong kaugalingon: sa miaging tulo ka tuig, ako gimugna ug gibaligya ang duha ka dula sa kini nga genre..

Sa kaso nga wala ka pa makadungog niini nga mga dula kaniadto, kini libre, mga multiplayer nga mga dula sa web nga sayon ​​nga dulaon (walay account nga gikinahanglan). Kasagaran sila mag-pit sa daghang kaatbang nga mga magdudula sa usa ka arena. Uban pang sikat nga .io nga mga dula: Slither.io ΠΈ Diep.io.

Niini nga post atong mahibal-an kung giunsa paghimo og .io nga dula gikan sa wala. Aron mahimo kini, igo na ang kahibalo sa Javascript: kinahanglan nimo nga masabtan ang mga butang sama sa syntax ES6, keyword this ΠΈ Saad. Bisan kung dili ka hingpit nga kahibalo sa Javascript, masabtan nimo ang kadaghanan sa post.

Pananglitan sa usa ka .io nga dula

Para sa tabang sa pagbansay atong hisgotan pananglitan nga dula .io. Sulayi sa pagdula niini!

Paghimo og multiplayer nga dula sa web sa .io nga genre
Ang dula yano ra: gikontrol nimo ang usa ka barko sa usa ka arena kauban ang ubang mga magdudula. Ang imong barko awtomatik nga nagpabuto og mga projectiles ug gisulayan nimo nga maigo ang ubang mga magdudula samtang naglikay sa ilang mga projectiles.

1. Mubo nga pagtan-aw / istruktura sa proyekto

Nagrekomendar ko download source code pananglitan nga dula para makasunod ka nako.

Ang pananglitan naggamit sa mosunod:

  • Ipahayag mao ang labing popular nga web framework alang sa Node.js nga nagdumala sa web server sa dula.
  • socket.io β€” websocket library para sa pagbayloay og data tali sa browser ug sa server.
  • Webpack - manager sa module. Mahimo nimong mabasa kung ngano nga gamiton ang Webpack dinhi.

Kini ang hitsura sa istruktura sa direktoryo sa proyekto:

public/
    assets/
        ...
src/
    client/
        css/
            ...
        html/
            index.html
        index.js
        ...
    server/
        server.js
        ...
    shared/
        constants.js

publiko/

Ang tanan naa sa folder public/ ipasa sa static sa server. SA public/assets/ adunay mga hulagway nga gigamit sa among proyekto.

src /

Ang tanan nga source code naa sa folder src/. Mga titulo client/ ΠΈ server/ pagsulti alang sa ilang kaugalingon ug shared/ naglangkob sa usa ka constants file nga gi-import sa kliyente ug server.

2. Mga parametro sa asembliya/proyekto

Sama sa giingon sa ibabaw, naggamit kami usa ka manager sa module aron matukod ang proyekto Webpack. Atong tan-awon ang among configuration sa 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',
    }),
  ],
};

Ang labing importante nga mga linya dinhi mao ang mosunod:

  • src/client/index.js mao ang entry point sa Javascript (JS) nga kliyente. Ang Webpack magsugod gikan dinhi ug balikbalik nga mangita alang sa ubang mga imported nga mga file.
  • Ang output JS sa among Webpack build mahimutang sa direktoryo dist/. Tawgon nako kini nga file nga among JS nga pakete.
  • Gigamit namon Babel, ug ilabina ang configuration @babel/preset-env aron ma-transpile ang among JS code para sa mga daan nga browser.
  • Gigamit namo ang usa ka plugin aron makuha ang tanang CSS nga gi-refer sa mga JS file ug isagol kini sa usa ka dapit. Tawgon ko kini sa ato CSS package.

Tingali nakamatikod ka nga katingad-an nga mga ngalan sa file sa package '[name].[contenthash].ext'. Naglangkob sila pagpuli sa ngalan sa file Webpack: [name] pulihan sa ngalan sa input point (sa among kaso kini game), ug [contenthash] pulihan sa usa ka hash sa mga sulud sa file. Atong buhaton kini sa pag-optimize sa proyekto alang sa pag-hash - makasulti kami sa mga browser nga i-cache ang among JS packages hangtod sa hangtod tungod kay kung ang usa ka package mausab, ang file name niini mausab usab (pagbag-o contenthash). Ang nahuman nga resulta mao ang ngalan sa file sa pagtan-aw game.dbeee76e91a97d0c7207.js.

file webpack.common.js - Kini ang base nga file sa pag-configure nga among gi-import sa pag-uswag ug nahuman nga mga pag-configure sa proyekto. Pananglitan, ania ang pag-configure sa pag-uswag:

webpack.dev.js

const merge = require('webpack-merge');
const common = require('./webpack.common.js');

module.exports = merge(common, {
  mode: 'development',
});

Alang sa kaepektibo, gigamit namon sa proseso sa pag-uswag webpack.dev.js, ug switch sa webpack.prod.js, aron ma-optimize ang mga gidak-on sa package kung i-deploy sa produksiyon.

Lokal nga setup

Girekomenda nako ang pag-install sa proyekto sa imong lokal nga makina aron masunod nimo ang mga lakang nga gilista sa kini nga post. Ang pag-setup yano ra: una, ang sistema kinahanglan adunay binurotan, hubag ΠΈ NPM. Sunod kinahanglan nimong buhaton

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

ug andam ka nga moadto! Aron masugdan ang development server, dagan lang

$ npm run develop

ug adto sa imong web browser localhost: 3000. Ang development server awtomatik nga tukuron pag-usab ang JS ug CSS nga mga pakete samtang mahitabo ang mga kausaban sa code - i-refresh lang ang panid aron makita ang tanang kausaban!

3. Mga punto sa pagsulod sa kliyente

Atong adto sa game code mismo. Una kinahanglan namon ang usa ka panid index.html, kung mobisita ka sa site, ang browser ang mag-una niini. Ang among panid mahimong yano ra:

index.html

Usa ka pananglitan sa .io nga dula  MAGDUWA

Kini nga panig-ingnan sa code gipasimple gamay alang sa katin-awan, ug buhaton ko ang parehas sa daghang uban pang mga pananglitan sa post. Mahimo nimong tan-awon kanunay ang tibuuk nga code sa Github.

Kami adunay:

  • HTML5 Canvas nga elemento (<canvas>), nga atong gamiton sa paghubad sa dula.
  • <link> aron idugang ang among CSS package.
  • <script> aron idugang ang among Javascript package.
  • Main menu nga adunay username <input> ug ang β€œPLAY” nga buton (<button>).

Sa higayon nga ma-load ang home page, ang browser magsugod sa pagpatuman sa Javascript code, sugod sa entry point JS file: 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);
  };
});

Mahimong komplikado kini, apan sa tinuud wala’y daghang nahitabo dinhi:

  1. Pag-import og daghang uban pang mga JS file.
  2. Pag-import sa CSS (aron nahibal-an sa Webpack nga ilakip kini sa among CSS package).
  3. Lansad connect() aron makatukod og koneksyon sa server ug magsugod downloadAssets() aron ma-download ang mga imahe nga gikinahanglan aron ma-render ang dula.
  4. Pagkahuman sa yugto 3 main menu gipakita (playMenu).
  5. Pag-set up sa "PLAY" button nga tigdumala sa pag-klik. Sa diha nga ang buton gipugos, ang code magsugod sa dula ug mosulti sa server nga kita andam sa pagdula.

Ang panguna nga "karne" sa among lohika sa kliyente-server naa sa mga file nga gi-import sa file index.js. Karon atong tan-awon silang tanan sa han-ay.

4. Pagbayloay sa datos sa kliyente

Sa kini nga dula naggamit kami usa ka bantog nga librarya aron makigsulti sa server socket.io. Ang Socket.io adunay built-in nga suporta Mga WebSocket, nga haum kaayo alang sa duha ka paagi nga komunikasyon: makapadala kami og mga mensahe ngadto sa server ΠΈ ang server makapadala og mga mensahe kanamo sa samang koneksyon.

Kita adunay usa ka file src/client/networking.jskinsay bahala tanan komunikasyon uban sa server:

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

Kini nga kodigo gipamub-an usab gamay alang sa katin-awan.

Adunay tulo ka nag-unang mga butang nga nahitabo sa kini nga file:

  • Kami naningkamot sa pagkonektar sa server. connectedPromise gitugotan lamang kung nakatukod kami og koneksyon.
  • Kung malampuson ang koneksyon, magparehistro kami sa mga function sa callback (processGameUpdate() ΠΈ onGameOver()) alang sa mga mensahe nga mahimo natong madawat gikan sa server.
  • Nag-eksport kami play() ΠΈ updateDirection()aron magamit kini sa ubang mga file.

5. Paghubad sa kliyente

Panahon na aron ipakita ang litrato sa screen!

...apan sa dili pa nato mahimo kini, kinahanglan natong i-download ang tanang mga hulagway (mga kapanguhaan) nga gikinahanglan alang niini. Magsulat kita og resource manager:

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

Ang pagdumala sa kahinguhaan dili kaayo lisud nga ipatuman! Ang panguna nga punto mao ang pagtipig sa usa ka butang assets, nga magbugkos sa yawe sa filename sa kantidad sa butang Image. Kung ang kapanguhaan gikarga, among gitipigan kini sa usa ka butang assets para dali nga resibo sa umaabot. Kanus-a tugutan ang pag-download sa matag indibidwal nga kapanguhaan (nga mao, mag-download sa tanan nga mga mga kapanguhaan), among gitugotan downloadPromise.

Human ma-download ang mga kapanguhaan, mahimo ka magsugod sa paghubad. Sama sa giingon sa sayo pa, aron magdrowing sa usa ka web page nga among gigamit HTML5 Canvas (<canvas>). Ang among dula yano ra, mao nga kinahanglan namon nga i-render ang mga musunud:

  1. Kasaysayan
  2. Barko sa magdudula
  3. Ang ubang mga magdudula sa dula
  4. Mga kabhang

Ania ang importante nga mga tipik src/client/render.js, nga nagdrowing sa eksaktong upat ka punto nga gilista sa ibabaw:

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

Kini nga code gipamub-an usab alang sa katin-awan.

render() mao ang nag-unang gimbuhaton niini nga file. startRendering() ΠΈ stopRendering() kontrola ang pagpaaktibo sa siklo sa paghubad sa 60 FPS.

Piho nga mga pagpatuman sa indibidwal nga paghubad nga mga function sa katabang (pananglitan renderBullet()) dili kaayo importante, apan ania ang usa ka yano nga pananglitan:

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

Timan-i nga among gigamit ang pamaagi getAsset(), nga nakita kaniadto sa asset.js!

Kung interesado ka sa pagsuhid sa ubang mga function sa rendering helper, unya basaha ang nahabilin sa src/client/render.js.

6. Pag-input sa kliyente

Panahon na aron maghimo usa ka dula playable! Ang laraw sa pagkontrol mahimong yano kaayo: aron mabag-o ang direksyon sa paglihok, mahimo nimong gamiton ang mouse (sa usa ka kompyuter) o paghikap sa screen (sa usa ka mobile device). Aron ma-implementar kini magparehistro kita Mga Tigpaminaw sa Hinabo para sa Mouse ug Touch nga mga panghitabo.
Bahala na ning tanan 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() mao ang Event Listeners nga nanawag updateDirection() (sa networking.js) sa diha nga ang usa ka input nga panghitabo mahitabo (pananglitan, sa diha nga ang mouse gipalihok). updateDirection() naghisgot sa pagbayloay sa mga mensahe sa server, nga nagproseso sa input event ug nag-update sa kahimtang sa dula sumala niana.

7. kahimtang sa kliyente

Kini nga seksyon mao ang labing lisud sa unang bahin sa post. Ayaw kaluya kon dili nimo kini masabtan sa unang higayon nga imong gibasa kini! Mahimo nimong laktawan kini ug balik sa ulahi.

Ang katapusang piraso sa puzzle nga gikinahanglan aron makompleto ang client-server code mao ang estado. Hinumdomi ang code snippet gikan sa Client Rendering section?

render.js

import { getCurrentState } from './state';

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

  // Do the rendering
  // ...
}

getCurrentState() kinahanglan nga makahatag kanamo sa karon nga kahimtang sa dula sa kliyente bisan unsang orasa base sa mga update nga nadawat gikan sa server. Ania ang usa ka pananglitan sa usa ka update sa dula nga mahimong ipadala sa server:

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

Ang matag update sa dula adunay lima ka parehas nga mga natad:

  • t: Server timestamp nga nagpaila kung kanus-a kini nga update gihimo.
  • me: Impormasyon mahitungod sa player nga nakadawat niini nga update.
  • sa uban: Usa ka han-ay sa impormasyon bahin sa ubang mga magdudula nga miapil sa samang duwa.
  • bala: han-ay sa impormasyon bahin sa mga projectiles sa dula.
  • liderato: Kasamtangang datos sa leaderboard. Dili namo sila tagdon sa kini nga post.

7.1 Naive nga kahimtang sa kliyente

Wala'y pulos nga pagpatuman getCurrentState() mahimo ra nga direkta nga ibalik ang datos gikan sa labing bag-o nga nadawat nga update sa dula.

naive-state.js

let lastGameUpdate = null;

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

export function getCurrentState() {
  return lastGameUpdate;
}

Nindot ug klaro! Pero kung ingon ana lang ka simple. Usa sa mga hinungdan nga kini nga pagpatuman adunay problema: kini naglimite sa rendering frame rate sa server clock speed.

Frame Rate: gidaghanon sa mga frame (ie mga tawag render()) kada segundo, o FPS. Ang mga dula kasagaran naningkamot nga makab-ot ang labing menos 60 FPS.

Tick ​​Rate: Ang kasubsob diin ang server nagpadala sa mga update sa dula ngadto sa mga kliyente. Kini kasagaran mas ubos kay sa frame rate. Sa among dula, ang server nagdagan sa 30 ticks matag segundo.

Kung i-render ra naton ang pinakabag-o nga pag-update sa dula, nan ang FPS sa tinuud dili gyud molapas sa 30 tungod kay wala kami makadawat labaw pa sa 30 nga mga update matag segundo gikan sa server. Bisag tawagan ta render() 60 ka beses matag segundo, unya ang katunga sa kini nga mga tawag mag-redraw lang sa parehas nga butang, nga wala’y mahimo. Ang laing problema sa usa ka walay pulos nga pagpatuman mao nga kini ubos sa mga paglangan. Sa sulundon nga katulin sa Internet, ang kliyente makadawat usa ka update sa dula nga eksakto matag 33 ms (30 matag segundo):

Paghimo og multiplayer nga dula sa web sa .io nga genre
Ikasubo, walay perpekto. Ang mas realistiko nga hulagway mao ang:
Paghimo og multiplayer nga dula sa web sa .io nga genre
Ang usa ka walay hinungdan nga pagpatuman mao ang labing grabe nga kaso kung bahin sa latency. Kung ang usa ka update sa dula madawat nga adunay 50ms nga paglangan, nan ang kliyente naghinayhinay sa usa ka dugang nga 50ms tungod kay kini naghubad pa sa kahimtang sa dula gikan sa miaging update. Imong mahanduraw kung unsa kini ka dili kombenyente alang sa magdudula: tungod sa arbitraryong mga paghinay, ang dula ingon og jerky ug dili lig-on.

7.2 Gipauswag nga kahimtang sa kliyente

Maghimo kami og pipila ka mga kalamboan sa walay pulos nga pagpatuman. Una, atong gamiton paglangan sa paghubad sa 100 ms. Kini nagpasabut nga ang "kasamtangan" nga kahimtang sa kliyente kanunay nga 100ms sa luyo sa kahimtang sa dula sa server. Pananglitan, kung ang oras sa server 150, unya ang kliyente mohatag sa kahimtang diin ang server niadtong panahona 50:

Paghimo og multiplayer nga dula sa web sa .io nga genre
Naghatag kini kanamo usa ka 100ms buffer aron mabuhi ang dili matag-an nga oras sa mga pag-update sa dula:

Paghimo og multiplayer nga dula sa web sa .io nga genre
Ang bili niini mahimong permanente input lag sa 100 ms. Kini usa ka gamay nga sakripisyo alang sa hapsay nga dula - kadaghanan sa mga magdudula (ilabi na ang mga kaswal) dili gani makamatikod niini nga paglangan. Mas dali alang sa mga tawo nga mag-adjust sa kanunay nga 100ms latency kaysa magdula nga wala matag-an nga latency.

Mahimo natong gamiton ang laing teknik nga gitawag "pagtagna sa bahin sa kliyente", nga usa ka maayong trabaho sa pagpakunhod sa gituohang latency, apan dili hisgotan niini nga post.

Ang laing kalamboan nga atong gigamit mao ang linear interpolation. Tungod sa pagkalangan sa pag-render, kasagaran kami labing menos usa ka update sa unahan sa kasamtangan nga oras sa kliyente. Sa dihang gitawag getCurrentState(), matuman nato linear interpolation tali sa mga update sa dula sa wala pa ug pagkahuman sa karon nga oras sa kliyente:

Paghimo og multiplayer nga dula sa web sa .io nga genre
Nasulbad niini ang problema sa frame rate: mahimo na namong maghatag ug talagsaon nga mga frame sa bisan unsang frame rate nga among gikinahanglan!

7.3 Pagpatuman sa usa ka gipaayo nga kahimtang sa kliyente

Pananglitan sa pagpatuman sa src/client/state.js naggamit sa paglangan sa paghubad ug linear interpolation, apan dili kini magdugay. Atong putlon ang code sa duha ka bahin. Ania ang una:

state.js, bahin 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;
}

Ang una nga butang nga kinahanglan nimong buhaton mao ang mahibal-an kung unsa ang mahimo niini currentServerTime(). Sama sa among nakita sa sayo pa, ang matag update sa dula naglakip sa usa ka timestamp sa server. Gusto namon nga gamiton ang render latency aron mahatag ang imahe nga 100ms sa luyo sa server, apan dili gayud nato mahibal-an ang kasamtangan nga oras sa server, tungod kay dili namo mahibal-an kung unsa ka dugay ang bisan unsang mga update nga nakaabot kanamo. Ang Internet dili matag-an ug ang katulin niini mahimong lahi kaayo!

Aron masulbad kini nga problema, mahimo natong gamiton ang usa ka makatarunganon nga pagbanabana: kita magpakaaron-ingnon ta nga ang unang update miabot dayon. Kung kini tinuod, nan mahibal-an namon ang oras sa server nianang partikular nga higayon! Gitipigan namon ang timestamp sa server firstServerTimestamp ug luwasa ang among lokal (kliyente) timestamp sa samang higayon sa gameStart.

Oh, paghulat kadiyot. Dili ba kinahanglan adunay oras sa server = oras sa kliyente? Ngano nga kita magkalainlain tali sa "server timestamp" ug "kliyente timestamp"? Nindot ni nga pangutana! Kini nahimo nga kini dili parehas nga butang. Date.now() ibalik ang lainlaing mga timestamp sa kliyente ug server ug kini nagdepende sa mga hinungdan nga lokal sa kini nga mga makina. Ayaw gyud hunahunaa nga ang mga timestamp parehas sa tanan nga makina.

Karon nasabtan na nato kung unsa ang gibuhat niini currentServerTime(): nibalik timestamp sa server sa kasamtangan nga oras sa pag-render. Sa laing pagkasulti, kini ang kasamtangan nga oras sa server (firstServerTimestamp <+ (Date.now() - gameStart)) minus ang paglangan sa paghubad (RENDER_DELAY).

Karon tan-awon nato kung giunsa nato pagdumala ang mga update sa dula. Kung ang usa ka update nadawat gikan sa server, kini gitawag processGameUpdate(), ug among gitipigan ang bag-ong update sa usa ka laray gameUpdates. Dayon, aron masusi ang paggamit sa memorya, among tangtangon ang tanang daan nga mga update sa base nga updatekay dili na nato sila kinahanglan.

Unsa ang "core update"? Kini ang una nga pag-update nga among nakit-an pinaagi sa pagbalhin pabalik gikan sa karon nga oras sa server. Hinumdomi kini nga diagram?

Paghimo og multiplayer nga dula sa web sa .io nga genre
Ang pag-update sa dula direkta sa wala sa "Oras sa Pag-render sa Kliyente" mao ang sukaranan nga pag-update.

Para sa unsa ang base update? Ngano nga mahimo naton ihulog ang mga update sa base? Aron masabtan kini, ato sa katapusan tan-awon nato ang implementasyon getCurrentState():

state.js, bahin 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),
    };
  }
}

Gidumala namo ang tulo ka mga kaso:

  1. base < 0 nagpasabot nga walay mga update hangtud sa kasamtangan nga panahon sa paghubad (tan-awa ang pagpatuman sa ibabaw getBaseUpdate()). Mahimo kini nga mahitabo sa pagsugod sa dula tungod sa pagkalangan sa pag-render. Sa kini nga kaso, among gigamit ang labing bag-ong update nga nadawat.
  2. base mao ang pinakabag-o nga update nga naa namo. Mahimong mahitabo kini tungod sa latency sa network o dili maayo nga koneksyon sa internet. Sa kini nga kaso usab gigamit namon ang pinakabag-o nga update nga naa namo.
  3. Kami adunay usa ka update sa wala pa ug pagkahuman sa karon nga oras sa pag-render, aron mahimo namon interpolate!

Ang tanan nga nahabilin sa state.js usa ka pagpatuman sa linear interpolation nga yano (apan boring) nga matematika. Kung gusto nimo nga susihon kini sa imong kaugalingon, dayon ablihi state.js sa Github.

Bahin 2. Backend server

Niini nga bahin atong tan-awon ang Node.js backend nga nagkontrol sa atong pananglitan sa usa ka .io nga dula.

1. Server entry point

Aron madumala ang web server mogamit kami og popular nga web framework alang sa Node.js nga gitawag Ipahayag. Kini ma-configure sa among server entry point file src/server/server.js:

server.js, bahin 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}`);

Hinumdomi nga sa unang bahin atong gihisgutan ang Webpack? Dinhi atong gamiton ang atong mga configuration sa Webpack. Atong gamiton kini sa duha ka paagi:

  • Paggamit webpack-dev-middleware sa awtomatik nga pagtukod pag-usab sa atong mga development packages, o
  • Statically pagbalhin sa usa ka folder dist/, diin isulat sa Webpack ang among mga file pagkahuman sa paghimo sa produksiyon.

Laing importante nga buluhaton server.js naglangkob sa pag-set up sa server socket.ionga nagkonektar lang sa Express server:

server.js, bahin 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);
});

Human sa malampuson nga pag-establisar og koneksyon sa socket.io sa server, among gi-configure ang mga event handler alang sa bag-ong socket. Ang mga tigdumala sa panghitabo nagproseso sa mga mensahe nga nadawat gikan sa mga kliyente pinaagi sa pagdelegar sa usa ka butang nga singleton game:

server.js, bahin 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);
}

Naghimo kami og .io nga dula, mao nga magkinahanglan lang kami og usa ka kopya Game (β€œGame”) – ang tanang magdudula magdula sa samang arena! Sa sunod nga seksyon atong tan-awon kung giunsa kini nga klase molihok Game.

2. Mga server sa dula

Класс Game naglangkob sa labing importante nga server-side lohika. Kini adunay duha ka nag-unang buluhaton: pagdumala sa magdudula и simulation sa dula.

Magsugod ta sa una nga buluhaton - pagdumala sa mga magdudula.

game.js, bahin 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);
    }
  }

  // ...
}

Niini nga dula atong mailhan ang mga magdudula pinaagi sa uma id ilang socket socket.io (kung naglibog ka, balik sa server.js). Ang Socket.io mismo nag-assign sa matag socket nga usa ka talagsaon id, mao nga dili kita kinahanglan nga mabalaka mahitungod niini. Tawgon ko siya Player ID.

Uban niana sa hunahuna, atong susihon ang mga variable nga pananglitan sa klase Game:

  • sockets mao ang usa ka butang nga nagbugkos sa player ID sa socket nga nakig-uban sa player. Kini nagtugot kanato sa pag-access sa mga socket pinaagi sa ilang mga player ID sa paglabay sa panahon.
  • players mao ang usa ka butang nga nagbugkos sa player ID sa code> Player butang

bullets usa ka han-ay sa mga butang Bullet, walay piho nga han-ay.
lastUpdateTime - Kini ang timestamp sa katapusang pag-update sa dula. Atong tan-awon kung giunsa kini gigamit sa dili madugay.
shouldSendUpdate usa ka auxiliary variable. Makita usab nato ang paggamit niini sa dili madugay.
Mga pamaagi addPlayer(), removePlayer() ΠΈ handleInput() dili kinahanglan nga ipasabut, sila gigamit sa server.js. Kung kinahanglan nimo ang usa ka refresher, balik nga mas taas og gamay.

Katapusan nga linya constructor() nagsugod sa update cycle mga dula (nga adunay kasubsob nga 60 nga pag-update/s):

game.js, bahin 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;
    }
  }

  // ...
}

Paagi update() naglangkob tingali sa labing importante nga bahin sa server-side lohika. Atong ilista ang tanan nga gibuhat niini sa han-ay:

  1. Nagkuwenta kung unsang orasa na dt kini sukad sa katapusan update().
  2. I-refresh ang matag projectile ug gub-on kini kung gikinahanglan. Atong makita ang pagpatuman niini nga pagpaandar sa ulahi. Sa pagkakaron igo na alang kanato ang pagkahibalo niana bullet.update() ningbalik true, kung ang projectile kinahanglan nga gub-on (niadto siya sa gawas sa arena).
  3. Pag-update sa matag magdudula ug maghimo usa ka projectile kung kinahanglan. Makita usab nato kini nga pagpatuman sa ulahi - player.update() makabalik ug butang Bullet.
  4. Pagsusi sa mga bangga tali sa mga projectiles ug gigamit sa mga magdudula applyCollisions(), nga nagbalik sa usa ka han-ay sa mga projectile nga naigo sa mga magdudula. Sa matag projectile nga gibalik, among dugangan ang score sa magdudula nga nagpabuto niini (gamit player.onDealtDamage()), ug dayon kuhaa ang projectile gikan sa laray bullets.
  5. Gipahibalo ug gilaglag ang tanan nga gipatay nga mga magdudula.
  6. Nagpadala usa ka update sa dula sa tanan nga mga magdudula matag segundo mga panahon nga gitawag update(). Ang auxiliary variable nga gihisgutan sa ibabaw makatabang kanato sa pagsubay niini shouldSendUpdate. Ingon update() gitawag nga 60 nga mga panahon / s, kami nagpadala sa mga update sa dula 30 ka beses / s. Sa ingon, frequency sa orasan Ang server mao ang 30 nga mga siklo sa orasan / s (naghisgot kami bahin sa frequency sa orasan sa una nga bahin).

Ngano magpadala ra ug mga update sa dula pinaagi sa panahon ? Aron sa pagluwas sa channel. Ang 30 nga pag-update sa dula matag segundo daghan!

Ngano nga dili na lang tawagan? update() 30 ka beses kada segundo? Aron mapaayo ang simulation sa dula. Mas kanunay kini gitawag update(), mas tukma ang simulation sa dula. Apan ayaw kaayo madala sa gidaghanon sa mga hagit update(), tungod kay kini usa ka computationally mahal nga buluhaton - 60 matag segundo igo na.

Ang nahabilin sa klase Game naglangkob sa mga pamaagi sa katabang nga gigamit sa update():

game.js, bahin 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() Kini yano ra - kini naghan-ay sa mga magdudula pinaagi sa iskor, gikuha ang nag-una nga lima, ug gibalik ang username ug puntos alang sa matag usa.

createUpdate() gigamit sa update() sa paghimo sa mga update sa dula nga gipang-apod-apod sa mga magdudula. Ang nag-unang tahas niini mao ang pagtawag sa mga pamaagi serializeForUpdate(), gipatuman alang sa mga klase Player ΠΈ Bullet. Timan-i nga kini nagpadala lamang sa data ngadto sa matag player mahitungod sa labing duol mga magdudula ug mga projectiles - dili kinahanglan nga ipadala ang kasayuran bahin sa mga butang sa dula nga nahimutang layo sa magdudula!

3. Mga butang sa dula sa server

Sa among dula, ang mga projectiles ug mga magdudula parehas kaayo: sila mga abstract round nga naglihok nga mga butang sa dula. Aron mapahimuslan kini nga pagkaparehas tali sa mga magdudula ug mga projectiles, magsugod kita pinaagi sa pagpatuman sa usa ka base nga klase Object:

butang.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,
    };
  }
}

Wala’y komplikado nga nahitabo dinhi. Kini nga klase mahimong usa ka maayong pagsugod nga punto alang sa pagpalapad. Atong tan-awon kung giunsa ang klase Bullet naggamit 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;
  }
}

Pagpatuman Bullet mubo kaayo! Among gidugang sa Object lamang ang mosunod nga mga extension:

  • Paggamit sa putos shortid alang sa random nga henerasyon id projectile.
  • Pagdugang og field parentID, aron masubay nimo ang magdudula nga naghimo niini nga projectile.
  • Pagdugang sa bili sa pagbalik sa update(), nga managsama true, kung ang projectile anaa sa gawas sa arena (hinumdomi nga atong gihisgutan kini sa katapusang seksyon?).

Mopadayon ta sa 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,
    };
  }
}

Ang mga magdudula mas komplikado kay sa mga projectiles, mao nga kini nga klase kinahanglan nga magtipig og pipila pa nga mga natad. Iyang pamaagi update() naghimo ug dugang nga trabaho, ilabina ang pag-uli sa bag-ong nabuhat nga projectile kung wala nay nahabilin fireCooldown (hinumdomi nga atong gihisgutan kini sa miaging seksyon?). Gipadako usab niini ang pamaagi serializeForUpdate(), tungod kay kinahanglan namon nga ilakip ang dugang nga mga natad alang sa magdudula sa pag-update sa dula.

Ang pagkaanaa sa usa ka base nga klase Object - usa ka importante nga lakang aron malikayan ang pagsubli sa code. Pananglitan, walay klase Object matag dula nga butang kinahanglan adunay parehas nga pagpatuman distanceTo(), ug ang pagkopya-paste sa tanan niini nga mga pagpatuman sa daghang mga file mahimong usa ka damgo. Kini labi ka hinungdanon alang sa dagkong mga proyekto, sa diha nga ang gidaghanon sa pagpalapad Object nagkadaghan ang mga klase.

4. Pagsusi sa bangga

Ang nahabilin nga butang nga among buhaton mao ang pag-ila kung ang mga projectiles naigo sa mga magdudula! Hinumdumi kini nga code snippet gikan sa pamaagi update() sa klase Game:

duwa.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),
    );

    // ...
  }
}

Kinahanglan natong ipatuman ang pamaagi applyCollisions(), nga nagbalik sa tanan nga mga projectiles nga naigo sa mga magdudula. Maayo na lang, dili kini lisud nga buhaton tungod kay

  • Ang tanan nga nagbangga nga mga butang mga lingin, ug kini ang pinakasimple nga porma aron ipatuman ang pag-ila sa pagbangga.
  • Naa na tay pamaagi distanceTo(), nga among gipatuman sa klase sa miaging seksyon Object.

Mao kini ang hitsura sa among pagpatuman sa pagtuki sa pagbangga:

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

Kining yano nga pag-ila sa bangga gibase sa kamatuoran nga duha ka lingin magbangga kon ang gilay-on tali sa ilang mga sentro mas ubos kay sa sumada sa ilang radii. Ania ang usa ka kaso diin ang gilay-on tali sa mga sentro sa duha ka lingin mao ang eksaktong katumbas sa gidaghanon sa ilang radii:

Paghimo og multiplayer nga dula sa web sa .io nga genre
Dinhi kinahanglan nimo nga hatagan og maayo nga pagtagad ang pipila pa nga mga aspeto:

  • Ang projectile kinahanglang dili maigo sa magdudula nga nagbuhat niini. Kini makab-ot pinaagi sa pagtandi bullet.parentID с player.id.
  • Ang projectile kinahanglan nga makausa ra maigo sa grabe nga kaso sa pag-igo sa daghang mga magdudula sa parehas nga oras. Atong sulbaron kini nga problema gamit ang operator break: Kung makit-an ang usa ka magdudula nga nabangga sa usa ka projectile, mohunong kami sa pagpangita ug mopadayon sa sunod nga projectile.

Ang katapusan

Mao ra na! Gitabonan namo ang tanan nga kinahanglan nimong masayran aron makahimo og .io nga dula sa web. Unsay sunod? Paghimo sa imong kaugalingon nga .io nga dula!

Ang tanan nga pananglitan nga code bukas nga gigikanan ug gi-post sa Github.

Source: www.habr.com

Idugang sa usa ka comment