Afirandina Lîstika Webê ya Multiplayer .io

Afirandina Lîstika Webê ya Multiplayer .io
Di sala 2015 de derketiye Agar.io bû pêşengê celebek nû lîstikên .ioku ji wê demê ve populerbûna xwe zêde bûye. Min bixwe zêdebûna populerbûna lîstikên .io ceriband: di sê salên borî de, min ceriband di vê genre de du lîstik afirandin û firotin..

Ger we berê qet li ser van lîstikan nebihîstiye, ev lîstikên malperê yên pirlîstikvan ên belaş in ku lîstin hêsan in (hesabek hewce nake). Ew bi gelemperî di heman qadê de bi gelek lîstikvanên dijber re rû bi rû dimînin. Lîstikên din ên navdar .io: Slither.io и Diep.io.

Di vê postê de em ê fêr bibin ka çawa ji nû ve lîstikek .io biafirînin. Ji bo vê, tenê zanîna Javascript dê bes be: hûn hewce ne ku tiştên mîna hevoksaziyê fam bikin ES6, keyword this и sozên. Tewra ku hûn Javascript bi tevahî nizanin, hûn dîsa jî dikarin piraniya postê fam bikin.

Mînaka lîstika .io

Ji bo alîkariya perwerdehiyê em ê serî lê bidin mînak lîstik .io. Biceribînin ku wê bilîzin!

Afirandina Lîstika Webê ya Multiplayer .io
Lîstik pir hêsan e: hûn gemiyek li qada ku lîstikvanên din lê hene kontrol dikin. Keştiya we bixweber fîşekan dişewitîne û hûn hewl didin ku lîstikvanên din lêxin dema ku ji projeyên wan dûr dikevin.

1. Kurtî / strukturên projeyê

Ez pêşniyar dikim koda çavkaniyê dakêşin lîstika nimûne da ku hûn min bişopînin.

Nimûne jêrîn bikar tîne:

  • Îfadekirin Ji bo Node.js çarçoweya webê ya herî populer e ku servera malperê ya lîstikê birêve dibe.
  • socket.io - pirtûkxaneyek websocket ji bo danûstandina daneyan di navbera gerok û serverek de.
  • Webpack - rêveberê module. Hûn dikarin li ser çima Webpack bikar bînin bixwînin vir.

Li vir avahiya pelrêça projeyê wekî xuya dike:

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

alenî/

Her tişt di peldankê de ye public/ dê ji hêla serverê ve statîk were şandin. LI public/assets/ wêneyên ku ji hêla projeya me ve têne bikar anîn hene.

src /

Hemî koda çavkaniyê di peldankê de ye src/. Sernav client/ и server/ ji xwe re biaxivin û shared/ pelek domdar heye ku hem ji hêla xerîdar û hem jî ji hêla serverê ve tê derxistin.

2. Civînên / mîhengên projeyê

Wekî ku li jor hate gotin, em rêveberek modulê bikar tînin ku projeyê ava bikin Webpack. Ka em li veavakirina Webpackê xwe mêze bikin:

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

Li vir rêzikên herî girîng ev in:

  • src/client/index.js xala têketina xerîdar Javascript (JS) ye. Webpack dê ji vir dest pê bike û li pelên din ên îthalkirî bi dûbare bigere.
  • JS-ya derketinê ya avakirina Webpackê me dê di pelrêçê de cih bigire dist/. Ez ê vê dosyayê bi nav bikim js pakêtê.
  • Em bikar tînin Babîl, û bi taybetî veavakirinê @babel/preset-env ji bo veguheztina koda meya JS-ê ji bo gerokên kevn.
  • Em pêvekek bikar tînin da ku hemî CSS-ên ku ji hêla pelên JS ve têne referans kirin derxînin û wan li yek cîhek berhev bikin. Ez ê jê re bibêjim me pakêta css.

Dibe ku we navên pelên pakêtê yên xerîb dîtiye '[name].[contenthash].ext'. Ew dihewînin guhertinên navê pelê webpack: [name] dê bi navê xala têketinê were guheztin (di rewşa me de, ev game), û [contenthash] dê bi haşek naveroka pelê were guheztin. Em vê yekê dikin optîmîzekirina projeyê ji bo hashing - Em dikarin ji gerokan re bibêjin ku pakêtên me yên JS-ê bêdawî cache bikin ji ber ku heke pakêtek biguhere, wê demê navê pelê wê jî diguhere (guhertin contenthash). Encama dawî dê navê pelê dîtinê be game.dbeee76e91a97d0c7207.js.

file webpack.common.js pelê veavakirina bingehîn e ku em têxin nav mîhengên pêşkeftin û qediya projeyan. Li vir mînakek veavakirina pêşveçûnê ye:

webpack.dev.js

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

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

Ji bo karîgeriyê, em di pêvajoya pêşveçûnê de bikar tînin webpack.dev.js, û diguhere webpack.prod.js, ji bo xweşbînkirina mezinahiyên pakêtê dema ku di hilberînê de têne şandin.

mîhengê herêmî

Ez pêşniyar dikim ku projeyê li ser makîneyek herêmî saz bikin da ku hûn gavên ku di vê postê de hatine destnîşan kirin bişopînin. Sazkirin hêsan e: Pêşîn, pêdivî ye ku pergalê saz kiribe Node и NPM. Piştre divê hûn bikin

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

û hûn amade ne ku biçin! Ji bo destpêkirina servera pêşkeftinê, tenê bixebitin

$ npm run develop

û biçin geroka webê localhost: 3000. Dema ku kod diguhere dê servera pêşkeftinê bixweber pakêtên JS û CSS ji nû ve ava bike - tenê rûpelê nûve bike da ku hemî guhertinan bibîne!

3. Xalên Entry Client

Ka em werin ser koda lîstikê bixwe. Pêşî em rûpelek hewce ne index.html, dema ku serdana malperê bike, gerok dê pêşî wê bar bike. Rûpelê me dê pir hêsan be:

index.html

Mînak lîstikek .io  BAZÎ

Ev nimûneya kodê ji bo zelaliyê hinekî hêsan bûye, û ez ê bi gelek mînakên postê yên din re jî heman tiştî bikim. Koda tevahî her gav dikare li ser were dîtin Github.

Me heye:

  • Hêmana HTML5-ê (<canvas>) ya ku em ê bikar bînin da ku lîstikê pêşkêş bikin.
  • <link> ji bo ku pakêta meya CSS zêde bikin.
  • <script> ku pakêta meya Javascriptê zêde bike.
  • Menuya sereke bi navê bikarhêner <input> û bişkoka PLAY (<button>).

Piştî barkirina rûpela malê, gerok dê dest bi pêkanîna koda Javascript-ê bike, ji pelê JS xala têketinê dest pê dike: 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);
  };
});

Dibe ku ev tevlihev xuya bike, lê bi rastî li vir pir tişt diqewime:

  1. Çend pelên JS yên din îtxal dikin.
  2. CSS-ê derxînin (ji ber vê yekê Webpack dizane ku wan di pakêta meya CSS-ê de bihewîne).
  3. Berdaye connect() ji bo ku têkiliyek bi serverê re saz bikin û bimeşînin downloadAssets() da ku wêneyên ku ji bo pêşkêşkirina lîstikê hewce ne dakêşin.
  4. Piştî qedandina qonaxa 3 menuya sereke tê xuyang kirin (playMenu).
  5. Sazkirina handler ji bo tikandina bişkoka "PLAY". Dema ku bişkojk tê pêl kirin, kod lîstikê dest pê dike û ji serverê re dibêje ku em amade ne ku bilîzin.

"goşt"a sereke ya mantiqa xerîdar-servera me di wan pelan de ye ku ji hêla pelê ve hatine veguheztin index.js. Niha em ê wan hemûyan bi rêzê binirxînin.

4. Danûstandina daneyên muwekîlê

Di vê lîstikê de em pirtûkxaneyek naskirî bikar tînin ku bi serverê re têkilî daynin socket.io. Socket.io piştgiriya xwemalî heye soketên webê, ku ji bo danûstendina du-alî baş in: em dikarin ji serverê re peyaman bişînin и server dikare li ser heman girêdanê ji me re peyaman bişîne.

Dê yek dosyayek me hebe src/client/networking.jskî dê lênêrîn her kesî pêwendiya bi serverê re:

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

Ev kod jî ji bo zelaliyê hinekî kurtkirî ye.

Di vê pelê de sê tiştên sereke hene:

  • Em hewl didin ku bi serverê ve girêdayî bibin. connectedPromise tenê dema ku me têkiliyek saz kir destûr tê dayîn.
  • Ger girêdan serketî be, em fonksiyonên vegerandina bangê tomar dikin (processGameUpdate() и onGameOver()) ji bo peyamên ku em dikarin ji serverê bistînin.
  • Em hinarde dikin play() и updateDirection()da ku pelên din bikarin wan bikar bînin.

5. Rendering Client

Wext e ku hûn wêneyê li ser ekranê nîşan bidin!

…lê berî ku em wiya bikin, pêdivî ye ku em hemî wêneyên (çavkaniyên) ku ji bo vê yekê hewce ne dakêşin. Ka em rêveberek çavkaniyê binivîsin:

hebûn.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];

Birêvebiriya çavkaniyê ne ew çend dijwar e ku were bicîh kirin! Xala sereke ew e ku tiştek hilanîn assets, ku dê mifteya pelê bi nirxa objektê ve girêbide Image. Dema ku çavkanî tê barkirin, em wê di objeyekê de hilînin assets ji bo gihîştina bilez di pêşerojê de. Kengê dê destûr were dayîn ku her çavkaniyek kesane dakêşîne (ango, hemî çavkaniyên), em destûrê didin downloadPromise.

Piştî dakêşana çavkaniyan, hûn dikarin dest bi vegotinê bikin. Wekî ku berê hate gotin, ji bo ku em li ser rûpelek malperê xêz bikin, em bikar tînin HTML5 Canvas (<canvas>). Lîstika me pir hêsan e, ji ber vê yekê em tenê hewce ne ku jêrîn bidin:

  1. Paşî
  2. Player gemiyê
  3. Di lîstikê de lîstikvanên din
  4. Shells

Li vir hûrgelên girîng hene src/client/render.js, ku tam çar hêmanên ku li jor hatine destnîşan kirin diyar dikin:

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

Ev kod jî ji bo zelalbûnê tê kurt kirin.

render() fonksiyona sereke ya vê pelê ye. startRendering() и stopRendering() aktîvkirina çerxa renderkirinê li 60 FPS kontrol bikin.

Bicîhkirina berbiçav a fonksiyonên alîkarê pêşkêşkirina kesane (mînak. renderBullet()) ne ew çend girîng in, lê li vir mînakek hêsan e:

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

Têbînî ku em rêbazê bikar tînin getAsset(), ku berê tê dîtin asset.js!

Heke hûn dixwazin li ser arîkarên din ên rendering fêr bibin, wê hingê yên mayî bixwînin. src/client/render.js.

6. input Client

Dem dema çêkirina lîstikê ye playable! Pîlana kontrolê dê pir hêsan be: ji bo guheztina rêça tevgerê, hûn dikarin mişkê (li ser komputerê) bikar bînin an jî ekranê (li ser cîhazek mobîl) bixin. Ji bo pêkanîna vê em ê qeyd bikin Guhdarên Bûyerê ji bo bûyerên Mouse û Touch.
Dê li van hemûyan xwedî derkeve 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() Guhdarên Bûyerê ne ku bang dikin updateDirection() (ji networking.js) dema ku bûyerek têketinê çêdibe (mînak, dema ku mişk tê guheztin). updateDirection() bi danûstandina peyaman bi serverê re, ku bûyera têketinê pêvajo dike û li gorî wê rewşa lîstikê nûve dike.

7. Rewşa Client

Ev beş di beşa yekem a postê de ya herî dijwar e. Ger cara yekem ku hûn wê dixwînin hûn jê fam nakin cesaret nebin! Tewra hûn dikarin wê berdin û paşê vegerin ser wê.

Parçeya paşîn a puzzle ya ku ji bo temamkirina koda xerîdar / serverê hewce ye ev e rewş. Parçeya kodê ji beşa Pêşkêşkirina Xerîdar bi bîr tîne?

render.js

import { getCurrentState } from './state';

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

  // Do the rendering
  // ...
}

getCurrentState() divê di xerîdar de rewşa lîstika heyî ji me re peyda bike di her demê de li ser bingeha nûvekirinên ku ji serverê hatine wergirtin. Li vir mînakek nûvekirina lîstikê ye ku server dikare bişîne:

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

Her nûvekirina lîstikê pênc qadên wekhev dihewîne:

  • t: Demjimêra serverê destnîşan dike ku kengê ev nûvekirin hate afirandin.
  • me: Agahdariya li ser lîstikvanê ku vê nûvekirinê distîne.
  • yên din: Komek agahdarî li ser lîstikvanên din ên ku beşdarî heman lîstikê dibin.
  • gule: komek agahdarî li ser projeyên di lîstikê de.
  • pêşeng: Daneyên lîderê yên heyî. Di vê postê de, em ê wan nehesibînin.

7.1 Dewleta xerîdar a naîf

pêkanîna Naive getCurrentState() tenê dikare rasterast daneya ji nûvekirina lîstika herî dawî hatî wergirtin vegerîne.

naive-dewlet.js

let lastGameUpdate = null;

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

export function getCurrentState() {
  return lastGameUpdate;
}

Xweş û zelal! Lê heger tenê ew qas hêsan bûya. Yek ji sedemên vê pêkanînê pirsgirêk e: ew rêjeya çarçoweya vegotinê bi leza demjimêra serverê re sînordar dike.

Rêjeya Çarçoveyê: hejmara çarçoweyan (ango bang render()) per second, an FPS. Lîstok bi gelemperî hewl didin ku bi kêmî ve 60 FPS bi dest bixin.

Rêjeya tikandinê: Frekansa ku server nûvekirinên lîstikê ji xerîdaran re dişîne. Ew pir caran ji rêjeya çarçoveyê kêmtir e. Di lîstika me de, server bi frekansa 30 çerxek di çirkeyê de dimeşe.

Ger em tenê nûvekirina lîstika herî paşîn pêşkêş bikin, wê hingê FPS bi eslê xwe dê çu carî nikaribe ji 30-an derbas bibe ji ber ku em tu carî ji serverê ji 30 nûvekirinên di çirkeyê de bêtir nabin. Ger em bang bikin jî render() Di saniyeyê de 60 caran, wê hingê nîvê van bangan dê bi tenê heman tiştî ji nû ve xêz bikin, bi rastî tiştek nakin. Pirsgirêkek din a pêkanîna naîf ew e meyla derengmayînê. Bi leza Înternetê ya îdeal, xerîdar dê tam her 33 ms (30 serê saniyeyê) nûvekirinek lîstikê bistîne:

Afirandina Lîstika Webê ya Multiplayer .io
Mixabin, tiştek bêkêmasî ye. Wêneyek rastîntir dê bibe:
Afirandina Lîstika Webê ya Multiplayer .io
Dema ku dor tê derengiyê, pêkanînek naîf pir rewşa herî xirab e. Ger nûvekirinek lîstikê bi 50 ms dereng were wergirtin, wê hingê xerîdar rawestgehan 50 ms zêde ji ber ku ew hîn jî rewşa lîstikê ji nûvekirina berê vedigire. Hûn dikarin bifikirin ku ev ji bo lîstikvan çiqas nerehet e: şikandina kêfî dê lîstikê xwe biqewitîne û ne aram bike.

7.2 Rewşa xerîdar çêtir kirin

Em ê ji bo pêkanîna naîf hin çêtirkirinan bikin. Pêşîn, em bikar tînin rendering dereng bi 100 ms. Ev tê vê wateyê ku rewşa "niha" ya xerîdar dê her gav 100ms li pişt rewşa lîstikê ya li ser serverê be. Mînakî, heke dema serverê ye 150, wê hingê xerîdar dê rewşa ku server wê demê tê de bû, bide 50:

Afirandina Lîstika Webê ya Multiplayer .io
Ev ji me re tamponek 100ms dide ku em di demên nûvekirina lîstika nediyar de sax bimînin:

Afirandina Lîstika Webê ya Multiplayer .io
Berdêla vê yekê dê mayînde be derengiya ketinê bi 100 ms. Ev ji bo lîstika dilşikestî qurbaniyek piçûk e - pir lîstikvan (nemaze yên casual) dê vê derengiyê jî ferq nekin. Ji bo mirovan pir hêsantir e ku meriv bi derengiya domdar a 100ms veguhezîne ji lîstina bi derengiya nediyar.

Em dikarin teknîkek din bi navê xwe bikar bînin pêşbîniya alîgirê xerîdar, ku di kêmkirina derengiya têgihîştî de karekî baş dike, lê dê di vê postê de neyê nîqaş kirin.

Pêşveçûnek din ku em bikar tînin ev e interpolation linear. Ji ber derengiya renderkirinê, em bi gelemperî bi kêmanî yek nûvekirinek li pêş dema niha ya di xerîdar de ne. Dema ku tê gotin getCurrentState(), em dikarin îdam bikin interpolation linear di navbera nûvekirinên lîstikê de berî û piştî dema niha ya di xerîdar de:

Afirandina Lîstika Webê ya Multiplayer .io
Ev pirsgirêka rêjeya çarçoweyê çareser dike: em naha dikarin di her rêjeya çarçovê de ku em hewce ne re çarçoveyên bêhempa pêşkêş bikin!

7.3 Pêkanîna dewleta xerîdar a pêşkeftî

Mînaka pêkanînê di src/client/state.js hem derengiya render û hem jî interpolasyona xêzik bikar tîne, lê ne ji bo dirêj. Ka em kodê bikin du beş. Li vir ya yekem e:

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

Pêngava yekem ev e ku meriv fêm bike ka çi ye currentServerTime(). Wekî ku me berê jî dît, her nûvekirina lîstikê nîşanek demjimêrek serverê vedigire. Em dixwazin derengiya renderê bikar bînin da ku wêneyê 100ms li pişt serverê bidin, lê em ê tu carî dema niha li ser serverê nizanin, ji ber ku em nikarin zanibin ka ew çend ji nûvekirinan gihîştiye me. Înternet nayê pêşbînîkirin û leza wê dikare pir cûda bibe!

Ji bo ku em li dora vê pirsgirêkê bisekinin, em dikarin nêzîkbûnek maqûl bikar bînin: em em wisa bikin ku nûvekirina yekem tavilê hat. Ger ev rast bûya, wê hingê em ê di vê kêliya taybetî de dema serverê zanibin! Em mohra dema serverê tê de hilînin firstServerTimestamp û me biparêze herêmî (mişterî) mohra demjimêrê di heman kêliyê de gameStart.

Ax bisekine. Ma divê dem li ser serverê = dem li ser xerîdar nebe? Çima em di navbera "demjimêra server" û "demjimêra xerîdar" de ji hev cuda dikin? Ev pirsek mezin e! Derket holê ku ev ne heman tişt in. Date.now() dê di xerîdar û serverê de îşaretên cihêreng vegerîne, û ew bi faktorên herêmî yên van makîneyan ve girêdayî ye. Qet nehesibînin ku demjimêr dê li ser hemî makîneyan yek bin.

Niha em fêm dikin ku çi dike currentServerTime(): vedigere nîşana demjimêra serverê ya dema pêşkêşkirina heyî. Bi gotineke din, ev dema servera niha ye (firstServerTimestamp <+ (Date.now() - gameStart)) kêmkirina derengiya vegotinê (RENDER_DELAY).

Naha em binihêrin ka em çawa nûvekirinên lîstikê digirin. Dema ku nûvekirinek ji serverê tê wergirtin, jê re tê gotin processGameUpdate()û em nûvekirina nû li rêzek tomar dikin gameUpdates. Dûv re, ji bo kontrolkirina karanîna bîranînê, em berê hemî nûvekirinên kevn jê dikin nûvekirina bingehînji ber ku êdî hewcedariya me bi wan nîne.

"Nûvekirinek bingehîn" çi ye? Ev nûvekirina yekem em dibînin ku ji dema niha ya serverê paşde diçin. Vê diagramê bi bîr tîne?

Afirandina Lîstika Webê ya Multiplayer .io
Nûvekirina lîstikê rasterast li milê çepê "Client Render Time" nûvekirina bingehîn e.

Nûvekirina bingehîn ji bo çi tê bikar anîn? Çima em dikarin nûvekirinan bavêjin rêza bingehîn? Ji bo ku em vê yekê fêm bikin, werin paşan em li pêkanînê binêrin getCurrentState():

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

Em sê dozan digirin:

  1. base < 0 tê vê wateyê ku heya dema pêşkêşkirina heyî nûvekirin tune (li jor li pêkanînê binêre getBaseUpdate()). Ev dikare di destpêka lîstikê de ji ber derengiya renderkirinê rast çêbibe. Di vê rewşê de, em nûvekirina herî dawî ya ku hatî wergirtin bikar tînin.
  2. base nûvekirina herî dawî ya me ye. Ev dibe ku ji ber derengiya torê an girêdana înternetê ya qels be. Di vê rewşê de, em di heman demê de nûvekirina herî dawî ya ku me heye bikar tînin.
  3. Hem berî û hem jî piştî dema pêşkêşkirina heyî nûvekirinek me heye, ji ber vê yekê em dikarin interpolate!

Tiştê ku tê de maye state.js pêkanîna navberkirina xêzikî ye ku matematîkî hêsan (lê bêzar) e. Ger hûn bixwazin wê bi xwe vekolin, wê hingê vekin state.js li ser Github.

Part 2. Server Backend

Di vê beşê de, em ê li pişta Node.js ku me kontrol dike binihêrin mînakek lîstikek .io.

1. Xala ketina serverê

Ji bo birêvebirina servera malperê, em ê ji bo Node.js çarçoveyek webê ya populer bikar bînin ku jê re tê gotin Îfadekirin. Ew ê ji hêla pelê xala têketina servera me ve were mîheng kirin src/server/server.js:

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

Bînin bîra xwe ku di beşa yekem de me li ser Webpack nîqaş kir? Li vir em ê mîhengên Webpack-a xwe bikar bînin. Em ê wan bi du awayan bicîh bînin:

  • Bikar bînin webpack-dev-middleware ku bixweber pakêtên pêşkeftina me ji nû ve ava bikin, an
  • peldanka statîk veguherîne dist/, ku Webpack dê pelên me piştî avakirina hilberînê binivîse.

Karekî din ê girîng server.js ji sazkirina serverê pêk tê socket.ioku bi tenê bi servera Express ve girêdayî ye:

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

Piştî ku bi serfirazî pêwendiyek socket.io bi serverê re saz kir, me ji bo soketa nû rêvebirên bûyerê saz kirin. Rêvebirên bûyerê peyamên ku ji xerîdar têne wergirtin bi veguheztina tiştek yekalî digirin dest game:

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

Em lîstikek .io diafirînin, ji ber vê yekê em tenê kopiyek hewce ne Game ("Lîstik") - Hemî lîstikvan di heman qadê de dilîzin! Di beşa pêş de, em ê bibînin ka ev çîn çawa dixebite. Game.

2. Pêşkêşkerên lîstikê

Çar Game mantiqa herî girîng a li aliyê serverê dihewîne. Ew du karên sereke hene: rêveberiya player и simulasyona lîstikê.

Ka em bi karê yekem dest pê bikin - rêvebirina lîstikvanan.

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

  // ...
}

Di vê lîstikê de em ê lîstikvanan li gorî qadan nas bikin id soketa wan socket.io (heke hûn tevlihev bin, wê hingê vegerin server.js). Socket.io bixwe her soketek yekta destnîşan dike idji ber vê yekê ne hewce ye ku em li ser vê yekê xemgîn bibin. Ez ê gazî wî bikim ID Player.

Di hişê wê de, em guhêrbarên nimûneyê yên di polê de lêkolîn bikin Game:

  • sockets Tiştek e ku ID-a lîstikvan bi soketa ku bi lîstikvanê ve girêdayî ye ve girêdide. Ew dihêle ku em bi demê re bi nasnameyên lîstikvanê wan bigihîjin soketan.
  • players tiştek e ku nasnameya lîstikvanê bi koda> Tişta lîstikvan ve girêdide

bullets rêzek tiştan e Bullet, ku tu fermanek diyar tune.
lastUpdateTime - Ev demjimêra nûvekirina lîstika paşîn e. Em ê bibînin ka ew di demek nêzîk de çawa tê bikar anîn.
shouldSendUpdate guherbareke alîkar e. Em ê di demek nêzîk de karanîna wê jî bibînin.
Rêbaz addPlayer(), removePlayer() и handleInput() ne hewce ye ku were ravekirin, ew tê de têne bikar anîn server.js. Heke hûn hewce ne ku bîranîna xwe nûve bikin, hinekî bilindtir vegerin.

Rêza dawî constructor() dest pê dike çerxa nûvekirinê lîstikên (bi frekansa 60 nûvekirin / s):

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

  // ...
}

Rêbaz update() belkî beşa herî girîng a mantiqa server-side dihewîne. Ka em her tiştê ku ew dike bi rêz rêz bikin:

  1. Hesab dike ku dem çi ye dt ji dema dawî ve ye update().
  2. Her projeyek nû dike û heke hewce bike wan hilweşîne. Em ê paşê pêkanîna vê fonksiyonê bibînin. Ji bo niha bes e ku em zanibin bullet.update() vedigere true, heke proje divê were hilweşandin (ew ji meydanê derket).
  3. Her lîstikvanek nûve dike û heke hewce be projeyek çêdike. Em ê paşê vê pêkanînê jî bibînin - player.update() dikare tiştekî vegerîne Bullet.
  4. Ji bo pevçûnên di navbera proje û lîstikvanan de kontrol dike applyCollisions(), ku komek projeyên ku lîstikvanên lêdan vedigerîne. Ji bo her projeyek ku vedigere, em puana lîstikvanê ku ew avêtine zêde dikin (bikaranîna player.onDealtDamage()), û dûv re proje ji rêzê derxînin bullets.
  5. Hemî lîstikvanên kuştî agahdar dike û tune dike.
  6. Ji hemî lîstikvanan re nûvekirinek lîstikê dişîne her saniye demên ku tê gotin update(). Ev ji me re dibe alîkar ku guhêrbara alîkar ku li jor hatî destnîşan kirin bişopînin. shouldSendUpdate. Dema update() 60 car/s tê gotin, em nûvekirinên lîstikê 30 car/s dişînin. Ji ber vê yekê, frekansa saetê demjimêra serverê 30 demjimêr / s e (me di beşa yekem de behsa rêjeyên demjimêrê kir).

Çima tenê nûvekirinên lîstikê bişînin bi demê re ? Ji bo rizgarkirina kanalê. 30 nûvekirinên lîstikê di çirkeyê de pir in!

Wê demê çima tenê telefon nakin? update() 30 caran di çirkeyê de? Ji bo baştirkirina simulasyona lîstikê. Pir caran jê re tê gotin update(), dê simulasyona lîstikê rasttir be. Lê bi hejmara kêşeyan zêde xwe negirin. update(), ji ber ku ev ji hêla hesabkirinê ve karekî biha ye - 60 serê saniyeyê bes e.

Çîna mayî Game ji rêbazên alîkar ên ku tê de têne bikar anîn pêk tê update():

game.js part 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() pir hêsan - ew lîstikvanan li gorî pîvanê rêz dike, pêncên jorîn digire, û ji bo her yekê navê bikarhêner û puan vedigerîne.

createUpdate() de tê bikaranîn update() ji bo afirandina nûvekirinên lîstikê yên ku li lîstikvanan têne belav kirin. Karê wê yê sereke gazîkirina rêbazan e serializeForUpdate()ji bo dersan pêk tê Player и Bullet. Bala xwe bidinê ku ew tenê li ser her lîstikvanek daneyan vediguhezîne herî nêzîk lîstikvan û proje - ne hewce ye ku agahdariya li ser tiştên lîstikê yên ku ji lîstikvanê dûr in veguhezînin!

3. Tiştên lîstikê li ser serverê

Di lîstika me de, proje û lîstikvan bi rastî pir dişibin hev: ew tiştên lîstikê yên razber, dor û tevger in. Ji bo ku em ji vê wekheviya di navbera lîstikvan û projelan de sûd werbigirin, em bi pêkanîna çîna bingehîn dest pê bikin 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,
    };
  }
}

Li vir tiştek tevlihev nabe. Ev çîn dê ji bo dirêjkirinê xalek lengerek baş be. Ka em bibînin ka çîn çawa ye Bullet bikar tîne Object:

gule.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 pir kurt! Me lê zêde kiriye Object tenê pêvekên jêrîn:

  • Bikaranîna pakêtê shortid ji bo nifşê random id projectile.
  • Zêdekirina zeviyek parentIDda ku hûn lîstikvanê ku ev proje afirandiye bişopînin.
  • Zêdekirina nirxek vegerê li update(), ku wekhev e trueheke proje li derveyî arene be (tê bîra we ku em di beşa paşîn de li ser vê yekê axivîn?).

Ka em herin ser 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,
    };
  }
}

Lîstik ji projelan tevlihevtir in, ji ber vê yekê divê çend zeviyên din di vê polê de werin hilanîn. Rêbaza wî update() gelek karan dike, bi taybetî, ger ku yek nemîne, projeya ku nû hatî afirandin vedigerîne fireCooldown (tê bîra we ku me di beşa berê de li ser vê yekê axivî?). Di heman demê de ew rêbazê dirêj dike serializeForUpdate(), ji ber ku em hewce ne ku di nûvekirina lîstikê de qadên zêde ji bo lîstikvanan têxin nav xwe.

Xwedî çîna bingehîn Object - gavek girîng ku ji dubarekirina kodê dûr nekevin. Ji bo nimûne, no class Object divê her tiştê lîstikê xwedî heman pêkanînê be distanceTo(), û kopî-paskirina van hemî pêkanînan di nav gelek pelan de dê bibe kabûsek. Ev bi taybetî ji bo projeyên mezin girîng dibe., dema ku hejmara berfireh dibe Object ders mezin dibin.

4. Vedîtina pevçûnê

Tiştê ku ji me re maye ev e ku em nas bikin dema ku proje li lîstikvanan dikevin! Vê perçeya kodê ji rêbazê bîr bînin update() di polê de 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),
    );

    // ...
  }
}

Divê em rêbazê pêk bînin applyCollisions(), ku hemû projeyên ku lîstikvanên lêdan vedigere. Xweşbextane, ev ne ew çend dijwar e ku meriv bike ji ber ku

  • Hemî tiştên ku li hev diqelibin çember in, û ev şeklê herî hêsan e ji bo pêkanîna tespîtkirina pevçûnê.
  • Jixwe rêbazek me heye distanceTo(), ku me di beşa berê de di polê de bicîh kir Object.

Li vir pêkanîna me ya tespîtkirina pevçûnê çawa xuya dike:

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

Ev tespîta pevçûnê ya hêsan li ser vê rastiyê ye ku du derdor li hev dikevin ger mesafeya navendên wan ji berhevoka tîrêjên wan kêmtir be. Li vir rewşek heye ku dûrahiya di navbera navendên du çemberan de tam bi berhevoka tîrêjên wan re wekhev e:

Afirandina Lîstika Webê ya Multiplayer .io
Li vir çend aliyên din jî hene ku meriv li vir bifikire:

  • Divê proje li lîstikvanê ku ew afirandiye nekeve. Ev dikare bi danberhevkirinê pêk were bullet.parentID с player.id.
  • Di rewşek sînordar a ku di heman demê de pir lîstikvan li hev dikevin de divê proje tenê carekê lêxe. Em ê vê pirsgirêkê bi karanîna operatorê çareser bikin break: Hema ku lîstikvanê ku bi mûşekê diqelibe tê dîtin, em lêgerînê disekinînin û diçin projeya din.

The End

Navê pêger! Me her tiştê ku hûn hewce ne ku ji bo afirandina lîstikek webê .io zanibin veşartiye. Pêşî çi ye? Lîstika xweya .io ava bikin!

Hemî koda nimûne çavkaniya vekirî ye û li ser hatî şandin Github.

Source: www.habr.com

Add a comment