Kreye yon jwèt entènèt multijoueurs nan genre .io

Kreye yon jwèt entènèt multijoueurs nan genre .io
Lage nan 2015 Agar.io te vin zansèt yon nouvo genre games.io, ki gen popilarite te grandi anpil depi lè sa a. Mwen te fè eksperyans ogmantasyon nan popilarite nan jwèt .io tèt mwen: pandan twa ane ki sot pase yo, mwen kreye ak vann de jwèt sa a genre..

Nan ka ou pa janm tande pale de jwèt sa yo anvan, sa yo se jwèt entènèt multijoueurs gratis ki fasil pou jwe (pa gen okenn kont obligatwa). Yo anjeneral fè fas a anpil jwè opoze nan tèren an menm. Lòt pi popilè .io jwèt: Slither.io и Diep.io.

Nan pòs sa a, nou pral eksplore ki jan kreye yon jwèt .io nan grafouyen. Pou sa, sèlman konesans nan Javascript pral ase: ou bezwen konprann bagay sa yo tankou sentaks ES6, mo kle this и Pwomès. Menm si konesans ou nan Javascript pa pafè, ou ka toujou konprann pi fò nan pòs la.

.io jwèt egzanp

Pou èd fòmasyon nou pral refere a egzanp jwèt .io. Eseye jwe li!

Kreye yon jwèt entènèt multijoueurs nan genre .io
Jwèt la se byen senp: ou kontwole yon bato nan yon tèren ak lòt jwè yo. Bato ou otomatikman tire pwojektil epi ou eseye frape lòt jwè yo pandan w ap evite pwojektil yo.

1. Brèf BECA / estrikti nan pwojè a

Rekòmande telechaje kòd sous jwèt egzanp pou w ka swiv mwen.

Egzanp lan sèvi ak sa ki annapre yo:

  • Express se fondasyon entènèt ki pi popilè pou Node.js ki jere sèvè entènèt jwèt la.
  • socket.io - bibliyotèk websocket pou fè echanj done ant navigatè a ak sèvè a.
  • Webpack - manadjè modil. Ou ka li sou poukisa yo sèvi ak Webpack. isit la.

Men sa estrikti anyè pwojè a sanble:

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

piblik/

Tout bagay nan katab la public/ pral estatik transmèt pa sèvè a. NAN public/assets/ gen imaj ki itilize nan pwojè nou an.

src /

Tout kòd sous yo nan katab la src/. Tit client/ и server/ pale pou tèt yo ak shared/ gen yon dosye konstan ki enpòte pa tou de kliyan an ak sèvè a.

2. Asanble/anviwònman pwojè yo

Jan sa di pi wo a, nou itilize yon manadjè modil pou konstwi pwojè a Webpack. Ann pran yon gade nan konfigirasyon Webpack nou an:

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

Liy ki pi enpòtan yo isit la yo se sa ki annapre yo:

  • src/client/index.js se pwen antre nan kliyan an Javascript (JS). Webpack ap kòmanse soti isit la epi chèche lòt dosye enpòte yo.
  • Pwodiksyon JS Webpack nou an pral lokalize nan anyè a dist/. Mwen pral rele dosye sa a nou js pake.
  • Nou itilize Babèl, ak an patikilye konfigirasyon an @babel/prereglaj-env transpile kòd JS nou an pou ansyen navigatè yo.
  • Nou ap itilize yon plugin pou extraire tout CSS referans fichiers JS yo epi konbine yo nan yon sèl kote. Mwen pral rele l 'nou pakè css.

Ou ka remake non fichye pake etranj '[name].[contenthash].ext'. Yo genyen ladan yo sibstitisyon non fichye Webpack: [name] yo pral ranplase ak non an nan pwen an opinyon (nan ka nou an li se game), yon [contenthash] pral ranplase ak yon hash nan sa ki nan dosye a. Nou fè sa a optimize pwojè a pou hachaj - ou ka di navigatè yo kachèt pakè JS nou yo endefiniman, paske si yon pake chanje, non fichye li tou chanje (chanjman contenthash). Rezilta final la pral non dosye a gade game.dbeee76e91a97d0c7207.js.

dosye webpack.common.js se dosye konfigirasyon baz ke nou enpòte nan devlopman ak konfigirasyon pwojè fini. Men yon egzanp konfigirasyon devlopman:

webpack.dev.js

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

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

Pou efikasite, nou itilize nan pwosesis devlopman webpack.dev.js, ak chanje a webpack.prod.jspou optimize gwosè pake lè deplwaye nan pwodiksyon an.

Anviwònman lokal yo

Mwen rekòmande enstale pwojè a sou yon machin lokal pou ou ka swiv etap sa yo ki nan lis nan pòs sa a. Konfigirasyon an se senp: premye, sistèm nan dwe enstale Node и NPM. Apre sa ou bezwen fè

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

epi ou pare pou ale! Pou kòmanse sèvè devlopman an, jis kouri

$ npm run develop

epi ale nan navigatè entènèt localhost: 3000. Sèvè devlopman an pral otomatikman rebati pakè JS ak CSS yo kòm chanjman kòd yo rive - jis rafrechi paj la pou wè tout chanjman yo!

3. Pwen Antre Kliyan

Ann desann nan kòd jwèt la tèt li. Premye nou bezwen yon paj index.html, lè ou vizite sit la, navigatè a pral chaje li an premye. Paj nou an pral byen senp:

index.html

Yon egzanp .io jwèt  JWE

Egzanp kòd sa a te senplifye yon ti kras pou klè, epi mwen pral fè menm bagay la ak anpil nan lòt egzanp pòs yo. Ou ka toujou wè kòd konplè a nan Github.

Nou genyen:

  • Eleman twal HTML5 (<canvas>), ke nou pral sèvi ak rann jwèt la.
  • <link> ajoute pake CSS nou an.
  • <script> pou ajoute pakè Javascript nou an.
  • Meni prensipal ak non itilizatè <input> ak bouton PLAY (<button>).

Apre w fin chaje paj dakèy la, navigatè a pral kòmanse egzekite kòd Javascript, kòmanse nan dosye JS pwen antre: 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);
  };
});

Sa a ka sanble konplike, men pa gen anpil bagay k ap pase isit la:

  1. Enpòte plizyè lòt dosye JS.
  2. CSS enpòte (konsa Webpack konnen pou mete yo nan pakè CSS nou an).
  3. Lanse connect() etabli yon koneksyon ak sèvè a epi kouri downloadAssets() telechaje imaj ki nesesè pou rann jwèt la.
  4. Apre fini etap 3 meni prensipal la parèt (playMenu).
  5. Mete moun kap okipe a pou peze bouton "PLAY". Lè bouton an peze, kòd la inisyalize jwèt la epi li di sèvè a ke nou pare yo jwe.

Prensipal "vyann" nan lojik kliyan-sèvè nou an se nan dosye sa yo ki te enpòte pa dosye a index.js. Koulye a, nou pral konsidere yo tout nan lòd.

4. Echanj done kliyan

Nan jwèt sa a, nou itilize yon bibliyotèk byen koni pou kominike ak sèvè a socket.io. Socket.io gen sipò entegre priz entènèt, ki byen adapte pou kominikasyon de-fason: nou ka voye mesaj nan sèvè a и sèvè a ka voye mesaj ba nou sou menm koneksyon an.

Nou pral gen yon sèl dosye src/client/networking.jski moun ki pral pran swen tout moun kominikasyon ak sèvè a:

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

Kòd sa a tou te vin pi kout yon ti kras pou klè.

Gen twa bagay prensipal k ap pase nan dosye sa a:

  • Nou ap eseye konekte ak sèvè a. connectedPromise pèmèt sèlman lè nou te etabli yon koneksyon.
  • Si koneksyon an reyisi, nou anrejistre fonksyon callback (processGameUpdate() и onGameOver()) pou mesaj nou ka resevwa nan men sèvè a.
  • Nou ekspòte play() и updateDirection()pou lòt dosye ka sèvi ak yo.

5. Rann Kliyan

Li lè yo montre foto a sou ekran an!

…men anvan nou ka fè sa, nou bezwen telechaje tout imaj (resous) ki nesesè pou sa. Ann ekri yon manadjè resous:

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

Jesyon resous se pa sa difisil pou aplike! Lide prensipal la se estoke yon objè assets, ki pral mare kle non fichye a ak valè objè a Image. Lè resous la chaje, nou estoke li nan yon objè assets pou resevwa rapid nan tan kap vini an. Kilè yo pral pèmèt telechaje chak resous endividyèl (sa vle di, yo pral telechaje tout resous), nou pèmèt downloadPromise.

Apre telechaje resous yo, ou ka kòmanse rann. Menm jan te di pi bonè, pou fè desen sou yon paj entènèt, nou itilize HTML5 Canvas (<canvas>). Jwèt nou an trè senp, kidonk nou sèlman bezwen trase bagay sa yo:

  1. Istorik
  2. Bato jwè
  3. Lòt jwè yo nan jwèt la
  4. Kokiy

Isit la yo se snippets enpòtan yo src/client/render.js, ki rann egzakteman kat atik ki nan lis pi wo a:

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

Kòd sa a tou vin pi kout pou klè.

render() se fonksyon prensipal dosye sa a. startRendering() и stopRendering() kontwole aktivasyon an nan bouk rann nan 60 FPS.

Enplemantasyon espesifik nan fonksyon moun k ap ede rann (pa egzanp renderBullet()) yo pa enpòtan, men isit la se yon egzanp senp:

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

Remake byen ke nou ap itilize metòd la getAsset(), ki te deja wè nan asset.js!

Si w enterese aprann lòt moun k ap ede w rann yo, li rès la. src/client/render.js.

6. D' kliyan

Li lè pou fè yon jwèt jwe! Konplo kontwòl la pral trè senp: chanje direksyon mouvman an, ou ka itilize sourit la (sou yon òdinatè) oswa manyen ekran an (sou yon aparèy mobil). Pou aplike sa a, nou pral anrejistre Evènman Koute pou evènman Mouse ak Touch.
Pral pran swen tout bagay sa yo 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() se Oditè Evènman ki rele updateDirection() (soti nan networking.js) lè yon evènman opinyon rive (pa egzanp, lè yo deplase sourit la). updateDirection() okipe messagerie ak sèvè a, ki okipe evènman an opinyon ak mete ajou eta a jwèt kòmsadwa.

7. Estati Kliyan

Seksyon sa a se pi difisil nan premye pati pòs la. Pa dekouraje si ou pa konprann li premye fwa ou li! Ou ka menm sote li epi tounen vin jwenn li pita.

Dènye moso devinèt ki nesesè pou konplete kòd kliyan-sèvè a se te. Sonje ti bout kòd ki soti nan seksyon Rann Kliyan an?

render.js

import { getCurrentState } from './state';

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

  // Do the rendering
  // ...
}

getCurrentState() ta dwe kapab ban nou eta aktyèl la nan jwèt la nan kliyan an nenpòt ki lè baze sou mizajou ki resevwa nan men sèvè a. Men yon egzanp yon aktyalizasyon jwèt ke sèvè a ka voye:

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

Chak aktyalizasyon jwèt gen senk jaden ki idantik:

  • t: Sèvè timestamp ki endike lè aktyalizasyon sa a te kreye.
  • me: Enfòmasyon sou jwè a k ap resevwa aktyalizasyon sa a.
  • lòt moun ki: Yon seri enfòmasyon sou lòt jwè k ap patisipe nan menm jwèt la.
  • bal: yon seri enfòmasyon sou pwojektil nan jwèt la.
  • Leaderboard: done klasman aktyèl yo. Nou pap pran yo an kont nan pòs sa a.

7.1 Eta nayif kliyan an

Aplikasyon nayif getCurrentState() ka sèlman dirèkteman retounen done yo nan aktyalizasyon jwèt ki pi resan te resevwa.

naive-state.js

let lastGameUpdate = null;

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

export function getCurrentState() {
  return lastGameUpdate;
}

Bèl ak klè! Men, si sèlman li te senp konsa. Youn nan rezon ki fè aplikasyon sa a se pwoblèm: li limite to a ankadreman rann to a revèy sèvè.

To Frame: kantite ankadreman (sa vle di apèl render()) pa segonn, oswa FPS. Jwèt anjeneral fè efò reyalize omwen 60 FPS.

Tik to: Frekans sèvè a voye mizajou jwèt bay kliyan yo. Li souvan pi ba pase to ankadreman an. Nan jwèt nou an, sèvè a kouri nan 30 tik pou chak segonn.

Si nou jis rann dènye aktyalizasyon jwèt la, Lè sa a, FPS la esansyèlman p ap janm kapab depase 30 paske nou pa janm resevwa plis pase 30 mizajou pou chak segonn nan men sèvè a. Menm si nou rele render() 60 fwa pa segonn, Lè sa a, mwatye nan apèl sa yo pral jis redesine menm bagay la, esansyèlman pa fè anyen. Yon lòt pwoblèm ak aplikasyon nayif la se ke li tendans reta. Nan vitès entènèt ideyal, kliyan an pral resevwa yon aktyalizasyon jwèt egzakteman chak 33 ms (30 pou chak segonn):

Kreye yon jwèt entènèt multijoueurs nan genre .io
Malerezman, pa gen anyen ki pafè. Yon foto ki pi reyalis ta dwe:
Kreye yon jwèt entènèt multijoueurs nan genre .io
Yon aplikasyon nayif se pi move ka a lè li rive latansi. Si yo resevwa yon aktyalizasyon jwèt ak yon reta 50ms, lè sa a se kliyan an ralanti pa yon 50ms siplemantè paske li toujou rann eta jwèt la soti nan aktyalizasyon anvan an. Ou ka imajine ki jan sa a se konvenyan pou jwè a: akòz ralentissement abitrè, jwèt la pral sanble saccadé ak enstab.

7.2 Amelyore eta kliyan an

Nou pral fè kèk amelyorasyon nan aplikasyon nayif la. Premyèman, nou itilize rann reta pa 100 ms. Sa vle di ke eta "aktyèl" kliyan an ap toujou 100ms dèyè eta jwèt la sou sèvè a. Pou egzanp, si tan an sèvè se 150, Lè sa a, kliyan an pral rann eta a nan ki sèvè a te nan moman an 50:

Kreye yon jwèt entènèt multijoueurs nan genre .io
Sa a ban nou yon tanpon 100ms pou siviv fwa aktyalizasyon jwèt enprevizib:

Kreye yon jwèt entènèt multijoueurs nan genre .io
Pri a pou sa a pral pèmanan lag antre pou 100 ms. Sa a se yon sakrifis minè pou jeu lis - pifò jwè yo (sitou jwè aksidantèl) pa pral menm remake reta sa a. Li pi fasil pou moun yo ajiste nan yon latansi konstan 100 ms pase li se jwe ak yon latansi enprevizib.

Nou kapab tou itilize yon lòt teknik ki rele prediksyon bò kliyan, ki fè yon bon travay nan diminye latansi yo konnen, men yo pa pral kouvri nan pòs sa a.

Yon lòt amelyorasyon nou ap itilize se entèpolasyon lineyè. Akòz rann lag, nou anjeneral omwen yon aktyalizasyon devan tan aktyèl la nan kliyan an. Lè yo rele getCurrentState(), nou ka egzekite entèpolasyon lineyè ant mizajou jwèt jis anvan ak apre tan aktyèl la nan kliyan an:

Kreye yon jwèt entènèt multijoueurs nan genre .io
Sa a rezoud pwoblèm nan pousantaj ankadreman: kounye a nou ka rann ankadreman inik nan nenpòt pousantaj ankadreman nou vle!

7.3 Aplike eta kliyan amelyore

Egzanp aplikasyon nan src/client/state.js itilize tou de reta rann ak entèpolasyon lineyè, men sa a pa dire lontan. Ann kraze kòd la an de pati. Men premye a:

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

Premye etap la se konnen ki sa currentServerTime(). Kòm nou te wè pi bonè, chak aktyalizasyon jwèt gen ladan yon timestamp sèvè. Nou vle sèvi ak rann latansi pou rann imaj la 100ms dèyè sèvè a, men nou p'ap janm konnen lè aktyèl la sou sèvè a, paske nou pa ka konnen konbyen tan li te pran pou nenpòt nan mizajou yo rive jwenn nou. Entènèt la se enprevizib ak vitès li yo ka varye anpil!

Pou jwenn alantou pwoblèm sa a, nou ka itilize yon apwoksimasyon rezonab: nou pretann premye aktyalizasyon a te rive imedyatman. Si sa a te vre, Lè sa a, nou ta konnen lè sèvè a nan moman patikilye sa a! Nou estoke timestamp sèvè a nan firstServerTimestamp epi kenbe nou lokal (kliyan) timestamp nan menm moman an nan gameStart.

Oh tann. Èske li pa ta dwe tan sèvè = tan kliyan? Poukisa nou fè distenksyon ant "timestamp sèvè" ak "timestamp kliyan"? Sa a se yon gwo kesyon! Li sanble yo pa menm bagay la. Date.now() pral retounen timestamps diferan nan kliyan an ak sèvè, epi li depann de faktè lokal nan machin sa yo. Pa janm asime ke timestamps yo pral menm sou tout machin yo.

Koulye a, nou konprann sa li fè currentServerTime(): li retounen timestamp sèvè tan an rann aktyèl la. Nan lòt mo, sa a se tan aktyèl sèvè a (firstServerTimestamp <+ (Date.now() - gameStart)) mwens rann reta (RENDER_DELAY).

Koulye a, kite a gade nan ki jan nou jere mizajou jwèt yo. Lè yo resevwa yon aktyalizasyon nan men sèvè a, yo rele l processGameUpdate()epi nou sove nouvo aktyalizasyon a nan yon etalaj gameUpdates. Lè sa a, pou tcheke itilizasyon memwa, nou retire tout ansyen mizajou anvan baz aktyalizasyonpaske nou pa bezwen yo ankò.

Ki sa ki se yon "mizajou debaz"? Sa a premye aktyalizasyon a nou jwenn nan deplase bak soti nan tan aktyèl la sèvè. Sonje dyagram sa a?

Kreye yon jwèt entènèt multijoueurs nan genre .io
Aktyalizasyon jwèt la dirèkteman sou bò gòch la nan "Client Render Time" se aktyalizasyon debaz la.

Ki sa ki aktyalizasyon baz la itilize pou? Poukisa nou ka lage mizajou nan baz? Pou konprann sa a, ann finalman konsidere aplikasyon an getCurrentState():

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

Nou okipe twa ka:

  1. base < 0 vle di ke pa gen okenn mizajou jiskaske moman rann aktyèl la (gade aplikasyon an pi wo a getBaseUpdate()). Sa a ka rive dwa nan kòmansman an nan jwèt la akòz rann lag. Nan ka sa a, nou itilize dènye aktyalizasyon nou resevwa a.
  2. base se dènye aktyalizasyon nou genyen. Sa a ka akòz reta rezo oswa move koneksyon entènèt. Nan ka sa a, nou ap itilize tou dènye aktyalizasyon nou genyen an.
  3. Nou gen yon aktyalizasyon tou de anvan ak apre tan rann aktyèl la, pou nou kapab entèpole!

Tout sa ki rete nan state.js se yon aplikasyon entèpolasyon lineyè ki senp (men raz) matematik. Si ou vle eksplore li tèt ou, Lè sa a, louvri state.js sou Github.

Pati 2. Backend sèvè

Nan pati sa a nou pral gade nan backend Node.js ki kontwole nou an .io jwèt egzanp.

1. Pwen antre sèvè

Pou jere sèvè entènèt la, nou pral sèvi ak yon kad entènèt popilè pou Node.js yo rele Express. Li pral konfigirasyon pa dosye pwen antre sèvè nou an src/server/server.js:

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

Sonje ke nan premye pati nou te diskite Webpack? Sa a se kote nou pral itilize konfigirasyon Webpack nou an. Nou pral sèvi ak yo nan de fason:

  • Sèvi ak webpack-dev-middleware otomatikman rebati pakè devlopman nou yo, oswa
  • katab transfè statique dist/, nan ki Webpack pral ekri dosye nou yo apre pwodiksyon an bati.

Yon lòt travay enpòtan server.js se mete kanpe sèvè a socket.ioki jis konekte ak sèvè Express la:

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

Apre nou fin etabli yon koneksyon socket.io ak sèvè a avèk siksè, nou mete reskonsab evènman pou nouvo priz la. Evènman moun kap okipe mesaj yo resevwa nan men kliyan yo lè yo delege nan yon objè singleton game:

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

Nou ap kreye yon jwèt .io, kidonk nou pral sèlman bezwen yon kopi Game ("Jwèt") - tout jwè yo jwe nan menm tèren an! Nan pwochen seksyon an nou pral wè ki jan klas sa a fonksyone Game.

2. Serveurs jwèt

Gwoup Game gen lojik ki pi enpòtan sou bò sèvè a. Li gen de travay prensipal: jesyon jwè и simulation jwèt.

Ann kòmanse ak premye travay la, jesyon jwè.

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

  // ...
}

Nan jwèt sa a nou pral idantifye jwè yo pa jaden id priz socket.io yo (si ou jwenn konfonn, Lè sa a, tounen nan server.js). Socket.io tèt li bay chak priz yon inik idkidonk nou pa bezwen enkyete sou sa. Mwen pral rele l ' ID jwè.

Avèk sa nan tèt ou, ann egzamine varyab egzanp yo nan klas la Game:

  • sockets se yon objè ki mare ID jwè a nan priz ki asosye ak jwè a. Li pèmèt nou jwenn aksè nan sipò pa ID jwè yo nan yon tan konstan.
  • players se yon objè ki mare ID jwè a ak kòd la> Objè jwè

bullets se yon seri objè Bullet, ki pa gen okenn lòd definitif.
lastUpdateTime - Sa a se timestamp nan dènye aktyalizasyon jwèt la. Nou pral wè ki jan yo itilize li byento.
shouldSendUpdate se yon varyab oksilyè. Nou pral wè tou itilizasyon li byento.
Metòd addPlayer(), removePlayer() и handleInput() pa bezwen eksplike, yo itilize nan server.js. Si ou bezwen yon rafrechisman, tounen yon ti kras pi wo.

Dènye liy constructor() kòmanse sik aktyalizasyon jwèt (ak yon frekans 60 mizajou / s):

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

  // ...
}

Metòd update() gen pwobableman pati ki pi enpòtan nan lojik bò sèvè a. Ann bay lis tout sa li fè nan lòd:

  1. Kalkile ki lè li ye dt li te depi dènye a update().
  2. Rafrechi chak pwojektil epi detwi yo si sa nesesè. Nou pral wè aplikasyon fonksyonalite sa a pita. Pou kounye a, li ase pou nou konnen sa bullet.update() retounen true, si yo dwe detwi pwojektil la (li ale deyò tèren an).
  3. Mete ajou chak jwè epi kreye yon pwojektil si sa nesesè. Nou pral wè tou aplikasyon sa a pita - player.update() ka retounen yon objè Bullet.
  4. Tcheke pou kolizyon ant pwojektil ak jwè ak applyCollisions(), ki retounen yon seri pwojektil ki frape jwè yo. Pou chak pwojektil retounen, nou ogmante nòt jwè ki te tire l (itilize player.onDealtDamage()), epi retire pwojektil la nan etalaj la bullets.
  5. Notifye epi detwi tout jwè ki te touye yo.
  6. Voye yon aktyalizasyon jwèt bay tout jwè yo chak segonn fwa lè yo rele update(). Varyab oksilyè mansyone pi wo a ede nou swiv sa a shouldSendUpdate... Kòm update() rele 60 fwa/s, nou voye mizajou jwèt 30 fwa/s. Kidonk, frekans revèy revèy sèvè se 30 revèy / s (nou te pale sou pousantaj revèy nan premye pati a).

Poukisa voye mizajou jwèt sèlman atravè tan ? Pou sove chanèl la. 30 mizajou jwèt pou chak segonn se anpil!

Poukisa pa jis rele update() 30 fwa pa segonn? Pou amelyore simulation jwèt la. Pi souvan li rele update(), simulation jwèt la pi egzat yo pral. Men, pa twò pote ale ak kantite defi yo. update(), paske sa a se yon travay enfòmatik chè - 60 pou chak segonn se byen ase.

Rès klas la Game konsiste de metòd asistan yo itilize nan update():

game.js pati 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() trè senp - li klase jwè yo pa nòt, pran senk pi wo a, epi retounen non itilizatè a ak nòt pou chak.

createUpdate() itilize nan update() pou kreye mizajou jwèt ke yo distribye bay jwè yo. Travay prensipal li se rele metòd serializeForUpdate()aplike pou klas yo Player и Bullet. Remake byen ke li sèlman pase done bay chak jwè sou ki pi pre jwè yo ak pwojektil - pa gen okenn nesesite transmèt enfòmasyon sou objè jwèt ki sitiye lwen jwè a!

3. Objè jwèt sou sèvè a

Nan jwèt nou an, pwojektil ak jwè yo aktyèlman trè menm jan an: yo se abstrè, wonn, objè mobil jwèt. Pou pran avantaj de resanblans sa a ant jwè yo ak pwojektil, ann kòmanse pa aplike klas debaz la 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,
    };
  }
}

Pa gen anyen konplike k ap pase isit la. Klas sa a pral yon bon pwen jete lank pou ekstansyon an. Ann wè ki jan klas la Bullet itilizasyon Object:

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

Aplikasyon Bullet trè kout! Nou te ajoute nan Object sèlman ekstansyon sa yo:

  • Sèvi ak yon pake shortid pou jenerasyon o aza id pwojektil.
  • Ajoute yon jaden parentIDpou ou ka swiv jwè ki te kreye pwojektil sa a.
  • Ajoute yon valè retounen nan update(), ki egal true, si pwojektil la deyò tèren an (sonje nou te pale sou sa a nan dènye seksyon an?).

Ann ale nan 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,
    };
  }
}

Jwè yo pi konplèks pase pwojektil, kidonk klas sa a ta dwe estoke kèk jaden plis. Metòd li update() fè anpil travay, an patikilye, retounen pwojektil ki fèk kreye si pa gen okenn kite fireCooldown (Sonje nou te pale sou sa a nan seksyon anvan an?). Li pwolonje tou metòd la serializeForUpdate(), paske nou bezwen mete lòt jaden pou jwè a nan aktyalizasyon jwèt la.

Disponibilite yon klas de baz Object - yon etap enpòtan pou evite repete kòd. Pa egzanp, pa gen klas Object chak objè jwèt dwe gen menm aplikasyon an distanceTo(), ak kopye-kole tout aplikasyon sa yo atravè plizyè dosye ta yon kochma. Sa a vin espesyalman enpòtan pou gwo pwojè.lè kantite agrandi Object klas yo ap grandi.

4. Deteksyon kolizyon

Sèl bagay ki rete pou nou se rekonèt lè pwojektil yo frape jwè yo! Sonje moso kòd sa a ki soti nan metòd la update() nan klas la Game:

jwèt.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),
    );

    // ...
  }
}

Nou bezwen aplike metòd la applyCollisions(), ki retounen tout pwojektil ki frape jwè yo. Erezman, sa pa difisil pou fè paske

  • Tout objè kolizyon yo se ti sèk, ki se fòm ki pi senp pou aplike deteksyon kolizyon.
  • Nou deja gen yon metòd distanceTo(), ke nou aplike nan klas la nan seksyon anvan an Object.

Men ki jan aplikasyon nou an nan deteksyon kolizyon sanble:

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

Deteksyon kolizyon senp sa a baze sou lefèt ke de sèk fè kolizyon si distans ant sant yo pi piti pase sòm reyon yo. Isit la se ka a kote distans ki genyen ant sant yo nan de sèk se egzakteman egal a sòm total reyon yo:

Kreye yon jwèt entènèt multijoueurs nan genre .io
Isit la ou bezwen peye anpil atansyon sou yon koup plis aspè:

  • Pwojektil la pa dwe frape jwè ki te kreye li a. Sa a ka reyalize lè w konpare bullet.parentID с player.id.
  • Pwojektil la ta dwe sèlman frape yon fwa nan ka ekstrèm nan frape plizyè jwè an menm tan an. Nou pral rezoud pwoblèm sa a lè l sèvi avèk operatè a break: Yon fwa yo jwenn yon jwè k ap fè kolizyon ak yon pwojektil, nou sispann chèche epi ale nan pwochen pwojektil la.

Fen an

Se tout! Nou te kouvri tout sa ou bezwen konnen pou kreye yon jwèt entènèt .io. Ki sa kap vini? Bati pwòp jwèt .io ou!

Tout kòd egzanp se sous louvri epi yo afiche sou Github.

Sous: www.habr.com

Add nouvo kòmantè