Mitme kasutaja veebimĂ€ngu loomine .io ĆŸanris

Mitme kasutaja veebimĂ€ngu loomine .io ĆŸanris
Ilmunud 2015. aastal Agar.io sai uue ĆŸanri alguseks mĂ€ngude .io, mille populaarsus on alates sellest ajast tĂ”usnud. Olen ise kogenud mĂ€ngude .io populaarsuse kasvu: viimase kolme aasta jooksul olen loonud ja mĂŒĂŒnud kaks mĂ€ngu sellest ĆŸanrist..

Kui te pole selliseid mĂ€nge kunagi varem kuulnud: need on tasuta mitmikmĂ€ngud, kus on lihtne osaleda (konto loomine ei ole vajalik). TĂŒĂŒpiliselt mĂ€ngitakse mitmeid vastandlikke mĂ€ngijaid ĂŒhel areenil. Teised tuntud mĂ€ngud ĆŸanris .io: Slither.io ja Diep.io.

Selles postituses kĂ€sitleme, kuidas nullist luua .io mĂ€ng. Selleks vajate vaid Javascripti teadmisi: peate mĂ”istma selliseid asju nagu sĂŒntaks ES6, vĂ”tmesĂ”na this ja Promises. Isegi kui te ei tunne Javascripti tĂ€iuslikult, suudate siiski enamikust postitusest aru saada.

.io mÀngu nÀidis

Koolituse abil viitame me .io mÀngu nÀidisele. Proovige selle mÀngida!

Mitme kasutaja veebimĂ€ngu loomine .io ĆŸanris
MĂ€ng on ĂŒsna lihtne: te juhtite laeva areenil, kus on teised mĂ€ngijad. Teie laev tulistab automaatselt projekte ja te ĂŒritate tabada teisi mĂ€ngijaid, samal ajal vĂ€ltides nende progete.

1. Projekti ĂŒlevaade/struktuur

Soovitan alla laadida lÀhtekood mÀngu nÀidisele, et saaksite mind jÀlgida.

NÀidis kasutab jÀrgmist:

  • Express — kĂ”ige populaarsem veebiraamistik Node.js jaoks, mis haldab mĂ€ngu veebiserverit.
  • socket.io — Websocketi teek, mis edastab andmeid brauseri ja serveri vahel.
  • Webpack — moodulite haldur. Selle kohta, miks kasutada Webpacki, saate lugeda siin.

NÀidisprojekt struktuur nÀeb vÀlja selline:

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

public/

KÔik kaustas public/ edastatakse serveri poolt staatiliselt. Kaustas public/assets/ on meie projekti kasutatavad pildid.

src/

Kogu lÀhtekood asub kaustas src/. Nime client/ ja server/ rÀÀgib enda eest, ja shared/ sisaldab konstandifaili, mida imporditakse nii kliendi kui serveri poolt.

2. Kogumid/projekti seaded

Nagu eespool mainitud, kasutame projekti kogumiseks moodulihaldurit Webpack. Vaatame meie Webpacki konfiguratsiooni:

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

Siin on kÔige olulisemad read:

  • src/client/index.js — see on Javascript (JS) kliendi sisenemispunkt. Webpack alustab siit ja otsib rekurssiivselt teisi imporditud faile.
  • Meie Webpack'i vĂ€ljund- JS asub kaustas dist\/. Ma nimetan seda faili meie JS paketiks.
  • Me kasutame Babel, ja eriti konfigureerimist @babel\/preset-env kliendi JS koodi transpileerimiseks vanade brauserite jaoks.
  • Kasutame pluginat, et eraldada kĂ”ik CSS-failid, millele JS-failid viitavad, ja koguda need ĂŒhte kohta. Ma nimetaksin seda meie CSS paketiks.

Olete vĂ”ib-olla mĂ€rganud kummalisi pakettide failinimesid '[name].[contenthash].ext'. Need sisaldavad failinime asendusi Webpack: [name] asendatakse sisenemispunkti nimega (meie puhul on see game), ja [contenthash] asendatakse faili sisu hash'iga. Teeme seda, et optimeerida projekti hash'imiseks — saame kĂ€skida brauseritel meie JS pakette lĂ”putult vahemĂ€llu salvestada, sest kui pakett muutub, muutub ka faili nimi (muutub contenthash). Valmis tulemus on faili nimi kujul game.dbeee76e91a97d0c7207.js.

Fail webpack.common.js — see on pĂ”hikonfiguratsioonifail, mille impordime arenduse ja valmisprojekti seadistustesse. NĂ€iteks siin on arenduskonfiguratsioon:

webpack.dev.js

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

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

TĂ”hususe huvides kasutame arenduse protsessis webpack.dev.js, ja lĂŒlitub webpack.prod.js, et optimeerida pakettide suurusi tootmisversioonis.

Kohalik seadistus

Soovitan paigaldada projekti oma kohalikku masinasse, et saaksite jĂ€rgida selles postituses loetletud etappe. Seadistus on lihtne: kĂ”igepealt peavad sĂŒsteemis olema paigaldatud Node ja NPM. SeejĂ€rel tuleb kĂ€ivitada

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

ja olete valmis tööle! Arendusserveri kÀivitamiseks piisab, kui teha

$ npm run develop

ja minna veebibrauseris aadressile localhost:3000. Arendusserver koostab JS ja CSS paketid automaatselt uuesti, kui koodi muudetakse — lihtsalt vĂ€rskendage lehte, et nĂ€ha kĂ”iki muudatusi!

3. Kliendipunktid

Alustame mĂ€ngu koodiga. Esiteks vajame lehte index.html, mille brauser laadib saidi kĂŒlastamisel esimesena. Meie leht on ĂŒsna lihtne:

index.html

<!DOCTYPE html>
<html>
<head>
  <title>Näide .io mängust</title>
  <link type="text/css" rel="stylesheet" href="/game.bundle.css">
</head>
<body>
  <canvas id="game-canvas"></canvas>
  <script async src="/game.bundle.js"></script>
  <div id="play-menu" class="hidden">
    <input type="text" id="username-input" placeholder="Kasutajanimi" />
    <button id="play-button">MÄNGI</button>
  </div>
</body>
</html>

See koodinÀide on natuke lihtsustatud arusaadavuse huvides, sarnaseid muudatusi teen ka paljude muude nÀidete puhul. TÀielikku koodi saab alati vaadata aadressil Github.

Meil on:

  • HTML5 Canvas element (<canvas>), mida kasutame mĂ€ngu renderdamiseks.
  • <link> CSS paketi lisamiseks.
  • <script> Javascript paketi lisamiseks.
  • PeamenĂŒĂŒ koos kasutajanimega <input> ja nupuga „PLAY“ (

PÀrast kodulehe laadimist alustab brauser Javascript-koodi tÀitmist, alustades sissepÀÀsu JS-failist: 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 = () => {
    // MĂ€ngi!
    play(usernameInput.value);
    playMenu.classList.add('hidden');
    initState();
    startCapturingInput();
    startRendering();
    setLeaderboardHidden(false);
  };
});

See vÔib tunduda keeruline, kuid tegelikult ei toimu siin mitte nii palju toiminguid:

  1. MÔnede teiste JS-failide importimine.
  2. CSS importimine (et Webpack teaks, et tuleb see meie CSS paketti lisada).
  3. KĂ€ivitamine connect() serveriga ĂŒhenduse loomiseks ja kĂ€ivitamiseks downloadAssets() mĂ€ngu renderdamiseks vajalike piltide allalaadimiseks.
  4. PĂ€rast 3. etapi lĂ”petamist kuvatakse peamenĂŒĂŒ (playMenu).
  5. Seadistame „PLAY“ nupu klĂ”psamisĂŒrituse. Nupu vajutamisel kĂ€ivitab kood mĂ€ngu ja teatab serverile, et oleme mĂ€ngimiseks valmis.

Meie kliendiserveri loogika peamine sisu asub nendes failides, mis imporditi faili index.js. NĂŒĂŒd vaatame neid kĂ”iki jĂ€rjest.

4. Kliendi andmevahetus

Selles mĂ€ngus suhtleme serveriga tuntud teegi kaudu socket.io. Socket.io-l on sisseehitatud tugi WebSockets, mis sobivad suurepĂ€raselt kahepoolseks suhtlemiseks: saame saata sĂ”numeid serverile ja server vĂ”ib saata meile sĂ”numeid sama ĂŒhenduse kaudu.

Meil on ĂŒks fail src/client/networking.js, mis tegeleb kĂ”igi serveriga suhtlemisega:

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

See kood on arusaadavuse nimel ka pisut lĂŒhendatud.

Selles failis toimuvad kolm peamist tegevust:

  • Proovime ĂŒhendust serveriga. connectedPromise lahendatakse alles siis, kui me oleme ĂŒhenduse sĂ”lminud.
  • Kui ĂŒhendus on Ă”nnestunud, registreerime callback-funktsioonid (processGameUpdate() ja onGameOver()) sĂ”numite jaoks, mida me serverilt vastu saame.
  • Ekspordime play() ja updateDirection(), et teised failid saaksid neid kasutada.

5. Klientide renderdamine

On aeg ekraanile pilt kuvada!


aga enne, kui saame seda teha, peame alla laadima kÔik vajalikud pildid (ressursid). Kirjutame ressursihalduri:

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

Ressursside haldamine ei ole nii keeruline! Peamine mĂ”te on hoida objekti assets, mis seob failinime vĂ”tme objekti vÀÀrtusega Pilt. Kui ressurss on alla laaditud, salvestame selle objekti assets kiireks juurdepÀÀsuks tulevikus. Kui iga ĂŒksiku ressursi allalaadimine on lubatud (st ressursid on alla laaditud), lubame kĂ”ik downloadPromise Ressursid alla laadides, saame minna renderdamise juurde. Nagu öeldud, kasutame veebilehe joonistamiseks.

HTML5 Canvas ). Meie mĂ€ng on ĂŒsna lihtne, seega piisab, kui joonistame vaid jĂ€rgmise: (<canvas>). Наша огра ĐŽĐŸĐČĐŸĐ»ŃŒĐœĐŸ ĐżŃ€ĐŸŃŃ‚Đ°, ĐżĐŸŃŃ‚ĐŸĐŒŃƒ ĐœĐ°ĐŒ ĐŽĐŸŃŃ‚Đ°Ń‚ĐŸŃ‡ĐœĐŸ ĐŸŃ‚Ń€ĐžŃĐŸĐČыĐČать Ń‚ĐŸĐ»ŃŒĐșĐŸ ŃĐ»Đ”ĐŽŃƒŃŽŃ‰Đ”Đ”:

  1. Taust
  2. MĂ€ngija laev
  3. Teised mÀngijad, kes mÀngus viibivad
  4. Kuulid

Siin on olulised osad src/client/render.js, mis kujutavad endast just neid nelja punkti:

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;

// Saame kangas graafika konteksti
const canvas = document.getElementById('game-canvas');
const context = canvas.getContext('2d');

// Teeme kangast tÀisekraaniks
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

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

  // Joonista taust
  renderBackground(me.x, me.y);

  // Joonista kÔik kuulid
  bullets.forEach(renderBullet.bind(null, me));

  // Joonista kÔik mÀngijad
  renderPlayer(me, me);
  others.forEach(renderPlayer.bind(null, me));
}

// ... Abifunktsioonid siin vÀlja jÀetud

let renderInterval = null;
export function startRendering() {
  renderInterval = setInterval(render, 1000 / 60);
}
export function stopRendering() {
  clearInterval(renderInterval);
}

See kood on samuti lĂŒhendatud arusaadavuse huvides.

render() — selle faili peamine funktsioon. startRendering() ja stopRendering() haldad renderdus tsĂŒkli aktiveerimist 60 FPS sagedusega.

Konkreetsete abifunktsioonide rakendused (nĂ€iteks renderBullet()) ei ole nii olulised, kuid siin on ĂŒks lihtne nĂ€ide:

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

Pange tÀhele, et kasutame meetodit getAsset(), mida me varem nÀgime asset.js!

Kui teid huvitab teiste renderdusabifunktsioonide uurimine, siis lugege jÀrelejÀÀnud osa src/client/render.js.

6. Kliendi sisend

On aeg muuta mĂ€ng mĂ€ngitavaks! Juhtimis skeem on vĂ€ga lihtne: liikumise suuna muutmiseks saab kasutada hiirt (arvutis) vĂ”i ekraani puudutamist (mobiilseadmes). Selle rakendamiseks registreerime Event Listeners hiire ja puudutuse sĂŒndmuste jaoks.
Sellega tegeleb 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() ja onTouchInput() — on Event Listeners, mis kutsuvad updateDirection() (koopia networking.js) sisse kasutuse sĂŒndmuse toimumisel (nĂ€iteks hiire liigutamisel). updateDirection() tegeleb suhtluse loomisega serveriga, mis töötleb sisendi sĂŒndmusi ja uuendab mĂ€ngu seisundit vastavalt.

7. Kliendi seisund

See jaotis on esimese osa kĂ”ige keerulisem. Ärge muretsege, kui te ei saa sellest esimesel lugemisel aru! VĂ”ite isegi selle vahele jĂ€tta ja tagasi tulla hiljem.

Viimane pusletĂŒkk, mis on vajalik kliendi-serveri koodi lĂ”petamiseks, on state. Kas mĂ€letate koodifragmenti jaotises "Kliendi renderdamine"?

render.js

import { getCurrentState } from './state';

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

  // Teeme renderdamise
  // ...
}

getCurrentState() peab suutma anda meile mÀngu praeguse seisundi kliendis igaks hetkeks serverilt saadud uuenduste pÔhjal. Siin on nÀide mÀngu uuendusest, mida server vÔib saata:

{
  "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": "MĂ€ngija",
      "score": 3
    }
  ]
}

Iga mÀngu uuendus sisaldab viit sama vÀlja:

  • t: serveri ajatempleid, mis nĂ€itab, millal see uuendus loodi.
  • me: info mĂ€ngija kohta, kes selle uuenduse saab.
  • others: massiiv teisi mĂ€ngijaid puudutavat teavet, kes osalevad samas mĂ€ngus.
  • bullets: massiiv mĂ€ngus olevate projektide kohta.
  • leaderboard: praegused tabeli juhtide andmed. Me ei arvestata neid selles postituses.

7.1 Lihtne kliendi seisund

Lihtne teostus getCurrentState() saab lihtsalt otse tagastada kÔige vÀrskema saadud mÀngu uuenduse andmed.

naive-state.js

let lastGameUpdate = null;

// KÀsitleme uut mÀngu uuendust.
export function processGameUpdate(update) {
  lastGameUpdate = update;
}

export function getCurrentState() {
  return lastGameUpdate;
}

Ilus ja arusaadav! Aga kui ainult kĂ”ik oleks nii lihtne. Üks pĂ”hjus, miks selline teostus on problemaatiline: see piirab renderdamise kaadrisagedust serveri taktsageduse kaudu.

Kaadrisagedus (Frame Rate): kaadrite (st kutsete render()) arv sekundis, ehk FPS. MĂ€ngudes pĂŒĂŒeldakse tavaliselt 60 FPS saavutamise poole.

Taktsagedus (Tick Rate): sagedus, millega server saadab mÀngu uuendusi klientidele. Sageli on see madalam kui kaadrisagedus. Meie mÀngus töötab server 30 takti sekundis.

Kui me lihtsalt renderdame mĂ€ngu viimast vĂ€rskendust, siis FPS ei saa kunagi ĂŒletada 30, sellepĂ€rast et me ei saa serverilt kunagi rohkem kui 30 vĂ€rskendust sekundis. Isegi kui me kutsume render() 60 korda sekundis, siis pool neist kutsumistest lihtsalt uuendab sama asja, tegelikult mitte midagi tegemata. Veel ĂŒks naiivse rakenduse probleem on see, et see on viivitustele avatud. Ideaalsete internetikiirusete korral saab klient mĂ€ngu vĂ€rskenduse tĂ€pselt iga 33 ms jĂ€rel (30 korda sekundis):

Mitme kasutaja veebimĂ€ngu loomine .io ĆŸanris
Kahjuks ei ole miski ideaalne. Rohkem reaalne pilt oleks jÀrgmine:
Mitme kasutaja veebimĂ€ngu loomine .io ĆŸanris
Naivne rakendus on praktiliselt halvima juhtumi nÀide viivituste osas. Kui mÀngu vÀrskendus saabub 50 ms viivitusega, siis klient viibib tÀiendavad 50 ms, kuna ta renderdab ikkagi mÀngu seisundit eelmisest vÀrskendusest. Kas saate ette kujutada, kui ebamugav see mÀngijale on: ettekirjutatud viivituste tÔttu tundub mÀng katkendlik ja ebastabiilne.

7.2 Parandatud kliendi seisund

Teeme naivse rakenduse suhtes mÔned parendused. Esiteks, me kasutame renderdamise viivitust 100 ms. See tÀhendab, et kliendi "praegune" seisund jÀÀb alati 100 ms vÔrra serveri mÀngu seisundist maha. NÀiteks kui serveri ajal on 150, siis renderdatakse kliendis seisund, milles server viibis ajal 50:

Mitme kasutaja veebimĂ€ngu loomine .io ĆŸanris
See annab meile 100 ms puhvriga, mis vĂ”imaldab ĂŒle elada ettearvamatuid mĂ€ngu vĂ€rskenduste saamise aegu:

Mitme kasutaja veebimĂ€ngu loomine .io ĆŸanris
Selle tĂ”ttu on pidev sisendi viivitus (input lag) 100 ms. See on vĂ€ike ohver sujuva mĂ€ngukogemuse nimel — enamik mĂ€ngijaid (eriti juhuslikud) ei mĂ€rkagi seda viivitust. Inimestel on palju lihtsam harjuda pĂŒsiva 100 ms viivitusega, kui mĂ€ngida ettearvamatute viivitustega.

Me saame kasutada ka teist tehnikat, mida nimetatakse "klientpoolne prognoosimine", mis loobus hÀsti tajutud viivituste vÀhendamisega, kuid seda postitust ei kÀsitleta.

Veel ĂŒks parendamine, mida me kasutame, on lineaarne interpolatsioon. Renderdamise viivituse tĂ”ttu oleme tavaliselt vĂ€hemalt ĂŒhe vĂ€rskenduse vĂ”rra praegu ajast ees kliendis. Kui kutsutakse getCurrentState(), saame teha lineaarset interpolatsiooni. mĂ€ngu vĂ€rskenduste vahel otse enne ja pĂ€rast praegust aega kliendis:

Mitme kasutaja veebimĂ€ngu loomine .io ĆŸanris
See lahendab kaadrifrekventsiga seotud probleemi: nĂŒĂŒd saame renderdada ainulaadseid kaadreid mistahes soovitud sagedusel!

7.3 Parandatud kliendi oleku rakendamine

Rakendamise nÀide src/client/state.js kasutab nii renderdamise viivitust kui ka lineaarselt interpoleerimist, kuid mitte kauaks. Jagame koodi kaheks osaks. Siin on esimene:

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

  // Hoia ainult ĂŒhte mĂ€ngu vĂ€rskendust enne praegust serveri aega
  const base = getBaseUpdate();
  if (base > 0) {
    gameUpdates.splice(0, base);
  }
}

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

// Tagastab baasi vÀrskenduse indeksi, esimese mÀngu vÀrskenduse enne
// praegust serveri aega, vÔi -1, kui pole saadaval.
function getBaseUpdate() {
  const serverTime = currentServerTime();
  for (let i = gameUpdates.length - 1; i >= 0; i--) {
    if (gameUpdates[i].t <= serverTime) {
      return i;
    }
  }
  return -1;
}

Esiteks peame aru saama, mida teeb currentServerTime(). Nagu me varem nÀgime, sisaldab iga mÀngu vÀrskendus serveri ajatemplit. Soovime kasutada renderdamise viivitust, et renderdada pilti, mis on serverist 100 ms kaugusel, kuid me ei saa kunagi teada, mis on serveri praegune aeg, sest me ei tea, kui kaua on kulunud, et mis tahes vÀrskendus meieni jÔuaks. Internet on ettearvamatu ja selle kiirus vÔib vÀga suuresti varieeruda!

Selle probleemi vÀltimiseks vÔime kasutada mÔistlikku lÀhendust: me teeskleme, et esimene vÀrskendus saabus koheselt. Kui see oleks tÔsi, siis me teaksime serveri aega just sel hetkel! Salvestame serveri ajatempli firstServerTimestamp ja salvestame meie kohaliku (kliendi) ajatempli samal hetkel gameStart.

Oi, oota. Kas serveri aeg ei peaks olema vĂ”rreldav kliendi ajaga? Miks me eristame "serveri ajatemplit" ja "kliendi ajatemplit"? See on suurepĂ€rane kĂŒsimus! Selgub, et need ei ole samad. Date.now() tagastab erinevaid ajatempleid kliendis ja serveris, ning see sĂ”ltub nende masinate lokaalsetest teguridest. Ärge kunagi eeldage, et ajatempli vÀÀrtused on kĂ”ikidel masinatel samad.

NĂŒĂŒd mĂ”istame, mida teeb currentServerTime(): see tagastab serveri ajatempli praegusel renderdamise ajal. TeisisĂ”nu, see on serveri praegune aeg (firstServerTimestamp <+ (Date.now() - gameStart)) miinus renderdamise viivitus (RENDER_DELAY).

NĂŒĂŒd vaatame, kuidas me mĂ€ngu uuendusi töötleme. Serverilt uuenduse saamisel kutsume esile processGameUpdate(), ja salvestame uue uuenduse massiivi gameUpdates. SeejĂ€rel, et jĂ€lgida mĂ€lu kasutamist, kustutame kĂ”ik vanad uuendused kuni alusuuenduseni, kuna need pole meile enam vajalikud.

Mis on «alusuuendus»? See on esimene uuendus, mille leiame, liikudes tagasi serveri praegusest ajast. Kas mÀletate seda skeemi?

Mitme kasutaja veebimĂ€ngu loomine .io ĆŸanris
MĂ€ngu uuendus, mis asub «Client Render Time» vasakul kĂŒljel, on alusuuendus.

Kuhu kasutatakse alusuuendust? Miks saame aluseks olevad uuendused kÔrvale jÀtta? Selle arusaamiseks vaatame lÔpuks rakendust getCurrentState():

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

Töötame kolme juhtumiga:

  1. base < 0 tÀhendab, et enne praegust renderdamise aega ei ole uuendusi (vt eespool rakendust getBaseUpdate()). See vÔib juhtuda kohe mÀngu alguses renderdamise viivituse tÔttu. Sellisel juhul kasutame viimast saadud uuendust.
  2. base — see on viimane uuendus, mis meil on. See vĂ”ib juhtuda ka vĂ”rgu viivituse vĂ”i halva internetiĂŒhenduse tĂ”ttu. Sellisel juhul kasutame samuti viimast saadud uuendust.
  3. Meil on uuendus nii enne kui ka pÀrast praegust renderdamise aega, seega on vÔimalik interpoleerida!

KĂ”ik, mis jÀÀb ĂŒle, on state.js – see on lineaarse interpoleerimise rakendus, mis on lihtne (aga igav) matemaatika. Kui soovite seda iseseisvalt uurida, avage state.js . Tundub, et Github.

Osa 2. Tagaplaani server

Selles osas vaatleme b2b-Node.js tagaplaani, mis haldab meie mÀngu .io nÀidet..

1. Serveri sisenemispunkt

Veebiserveri juhtimiseks kasutame populaarset Node.js veebiraamistiku nimega Express. Selle seadistuse teeb meie serveri sisenemispunkti fail src/server/server.js:

server.js, osa 1

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

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

if (process.env.NODE_ENV === 'development') {
  // Seadista Webpack arenduseks
  const compiler = webpack(webpackConfig);
  app.use(webpackDevMiddleware(compiler));
} else {
  // Statiliselt serveeri dist\/ kaust tootmises
  app.use(express.static('dist'));
}

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

Kas mÀletate, et esimeses osas rÀÀkisime Webpackist? Just siin hakkame me kasutama meie Webpacki konfiguratsioone. Me rakendame neid kahel viisil:

  • Kasutage webpack-dev-middleware arendustegevuse pakettide automaatseks taastootmiseks, vĂ”i
  • Statiliselt edastada kaust dist\/, kuhu Webpack kirjutab meie failid pĂ€rast tootmist.

Veel ĂŒks oluline ĂŒlesanne server.js on serveri seadistamine socket.io, mis lihtsalt ĂŒhendub Express serveriga:

server.js, osa 2

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

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

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

// Kuula socket.io ĂŒhendusi
io.on('connection', socket => {
  console.log('MĂ€ngija on ĂŒhendatud!', socket.id);

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

PĂ€rast socket.io eduka ĂŒhenduse seadistamist serveriga seadistame me uue soketi jaoks sĂŒndmuste kĂ€sitlejad. SĂŒndmuste kĂ€sitlejad töötlevad klientidelt saadud sĂ”numeid, delegeerides need objekti-sĂŒgismĂ”jule game:

server.js, osa 3

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

// ...

// Seadista MĂ€ng
const game = new Game();

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

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

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

Me loome mĂ€ngu ĆŸanris .io, seetĂ”ttu vajame me ainult ĂŒhte eksemplari MĂ€ng („MĂ€ng“) – kĂ”ik mĂ€ngijad mĂ€ngivad ĂŒhel arenel! JĂ€rgmises osas vaatame, kuidas see klass töötab MĂ€ng.

2. MĂ€ngu server

Klass MĂ€ng sisaldab kĂ”ige olulisemat loogikat serveripoolselt. Tal on kaks peamist ĂŒlesannet: mĂ€ngijate haldamine ja mĂ€ngu simuleerimine.

Alustame esimesest ĂŒlesandest – mĂ€ngijate haldamisest.

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

    // Đ“Đ”ĐœĐ”Ń€ĐžŃ€ŃƒĐ”ĐŒ ĐżĐŸĐ·ĐžŃ†ĐžŃŽ, с ĐșĐŸŃ‚ĐŸŃ€ĐŸĐč ĐœĐ°Ń‡ĐœĐ”Ń‚ŃŃ ŃŃ‚ĐŸŃ‚ ĐžĐłŃ€ĐŸĐș.
    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);
    }
  }

  // ...
}

Selles mÀngus identifitseerime mÀngijad nende id socket.io vÀljade kaudu (kui oled segaduses, vaata tagasi server.js). Socket.io mÀÀrab igale soketile ainulaadse id, seega ei pea me selle pÀrast muretsema. Ma nimetatakse seda mÀngija ID-ks.

Pidades seda meeles, uurime klassi MĂ€ng:

  • sockets — see on objekt, mis seob mĂ€ngija ID soketiga, mis on seotud mĂ€ngijaga. See vĂ”imaldab meil pidevalt juurde pÀÀseda soketitele nende mĂ€ngija ID-de kaudu.
  • players — see on objekt, mis seob mĂ€ngija ID mĂ€ngija objekti

bullets — see on objekti massiiv Bullet, millel ei ole kindlat jĂ€rjekorda.
lastUpdateTime — see on mĂ€ngu viimase vĂ€rskenduse ajatemple. Kohe nĂ€eme, kuidas seda kasutatakse.
shouldSendUpdate — see on abimuutuja. Selle kasutamine selgub peagi.
Meetodid addPlayer(), removePlayer() ja handleInput() ei vaja selgitamist, neid kasutatakse server.js. Kui sul on vaja mĂ€lu vĂ€rskendada, vaata natuke ĂŒlespoole.

Viimane rida constructor() kĂ€ivitab mĂ€ngu vĂ€rskenduse tsĂŒkli (60 vĂ€rskendust/s): game.js, osa 2

game.js, часть 2

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

class Game {
  \/\/ ...

  update() {
    \/\/ Arvutame möödunud aja
    const now = Date.now();
    const dt = (now - this.lastUpdateTime) \/ 1000;
    this.lastUpdateTime = now;

    \/\/ Uuendame iga kuuli
    const bulletsToRemove = [];
    this.bullets.forEach(bullet => {
      if (bullet.update(dt)) {
        \/\/ HĂ€vitame selle kuuli
        bulletsToRemove.push(bullet);
      }
    });
    this.bullets = this.bullets.filter(
      bullet => !bulletsToRemove.includes(bullet),
    );

    \/\/ Uuendame iga mÀngijat
    Object.keys(this.sockets).forEach(playerID => {
      const player = this.players[playerID];
      const newBullet = player.update(dt);
      if (newBullet) {
        this.bullets.push(newBullet);
      }
    });

    \/\/ Rakendame kokkupÔrkeid, anname mÀngijatele punkte kuuli tabamise eest
    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),
    );

    \/\/ Kontrollime, kas mÔni mÀngija on surnud
    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);
      }
    });

    \/\/ Saadame mÀngu vÀrskenduse igale mÀngijale iga teise korra jÀrel
    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;
    }
  }

  \/\/ ...
}

Meetod update() sisaldab tÔenÀoliselt kÔige olulisemat osa serveri loogikast. Loetleme jÀrjest kÔik, mida ta teeb:

  1. Arvutab, kui palju aega dt on möödunud viimase update().
  2. Uuendab iga kuuli ja hÀvitab neid vajadusel. Selle funktsiooni teostust nÀeme hiljem. Praegu piisab, kui teada, et bullet.update() toob tagasi true, kui kuul peab olema hÀvitatud (ta on vÀljunud areenist).
  3. Uuendab iga mÀngijat ja loob vajadusel kuuli. Seda teostust nÀeme ka hiljem - player.update() vÔib tagastada objekti Bullet.
  4. Kontrollib kokkupÔrkeid kuuli ja mÀngijate vahel kasutades applyCollisions(), mis tagastab massiivi kuule, mis tabasid mÀngijaid. Iga tagastatud kuuli jaoks suurendame mÀngija punkte, kes selle vÀlja lÔi (kasutades player.onDealtDamage()), ja seejÀrel eemaldame kuuli massiivist bullets.
  5. Teavitab ja hÀvitab kÔik surnud mÀngijad.
  6. Saadab kĂ”igile mĂ€ngijatele mĂ€ngu vĂ€rskenduse iga teine kord, kui seda kutsutakse update(). See aitab meil jĂ€lgida ĂŒlalmainitud abimuutujat shouldSendUpdate. Kuna update() kutsutakse 60 korda/s, me saadame mĂ€ngu uuendusi 30 korda/s. SeelĂ€bi, kellafrekvents serveri kellafrekvents on 30 takti/s (olemme rÀÀkinud kellafrekventsist esimeses osas).

Miks saata mÀngu uuendusi ainult iga teise korra ? Kanalite sÀÀstmiseks. 30 mÀngu uuendust sekundis on vÀga palju!

Miks siis lihtsalt mitte kutsuda update() 30 korda sekundis? MĂ€ngu simuleerimise parandamiseks. Mida sagedamini kutset tehakse, update(), seda tĂ€psem on mĂ€ngu simuleerimine. Kuid ei tasu liialdada kutsete arvuga, update()sest see on arvutuslikult kulukas ĂŒlesanne — 60 sekundis on tĂ€iesti piisav.

Klassi ĂŒlejÀÀnud osa MĂ€ng koosneb abimeetoditest, mida kasutatakse update():

game.js, osa 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)  b.distanceTo(player)  p.serializeForUpdate()),
      bullets: nearbyBullets.map(b => b.serializeForUpdate()),
      leaderboard,
    };
  }
}

getLeaderboard() on ĂŒsna lihtne — see sorteerib mĂ€ngijad punktide jĂ€rgi, vĂ”tab viis parimat ja tagastab igaĂŒhe jaoks kasutajanime ja skoori.

createUpdate() kasutatakse update() mĂ€ngu uuenduste loomiseks, mis edastatakse mĂ€ngijatele. Selle peamine ĂŒlesanne on kutsete tegemine meetoditele serializeForUpdate(), mis on rakendatud klassides Player ja Bullet. Pange tĂ€hele, et see edastab igale mĂ€ngijale teavet ainult lĂ€hedalolevatest mĂ€ngijatest ja mĂŒrskudest – pole vajadust edastada teavet mĂ€ngu objektide kohta, mis asuvad mĂ€ngijast kaugel!

3. MĂ€ngu objektid serveris

Meie mĂ€ngus on mĂŒrskud ja mĂ€ngijad tegelikult vĂ€ga sarnased: need on abstraktsed ĂŒmmargused liikuvad mĂ€ngu objektid. Kasutamaks seda sarnasust mĂ€ngijate ja mĂŒrskude vahel, alustame pĂ”hiklassi rakendamisest 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,
    };
  }
}

Siin ei toimu midagi keerulist. See klass on hea lÀhtepunkt laienemiseks. Vaatame, kuidas klass Bullet kasutab 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;
  }

  // Tagastab true, kui kuul tuleks hÀvitada
  update(dt) {
    super.update(dt);
    return this.x  Constants.MAP_SIZE || this.y  Constants.MAP_SIZE;
  }
}

Rakendamine Bullet on vĂ€ga lĂŒhike! Oleme lisanud Object ainult jĂ€rgmised laiendused:

  • Paketi kasutamine shortid juhuslikuks genereerimiseks id kuuli.
  • VĂ€lja lisamine parentID, et saaksime jĂ€lgida mĂ€ngijat, kes selle kuuli lĂ”i.
  • Tagastava vÀÀrtuse lisamine update(), mis on true, kui kuul on areenist vĂ€ljas (mĂ€letate, me rÀÀkisime sellest eelmisel peatĂŒkil?).

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

  // Tagastab Àsja loodud kuuli, vÔi null.
  update(dt) {
    super.update(dt);

    // Uuendab skoori
    this.score += dt * Constants.SCORE_PER_SECOND;

    // Veenduge, et mÀngija jÀÀb piiridesse
    this.x = Math.max(0, Math.min(Constants.MAP_SIZE, this.x));
    this.y = Math.max(0, Math.min(Constants.MAP_SIZE, this.y));

    // Tulista kuul, kui vajalik
    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,
    };
  }
}

MĂ€ngijad on keerulisemad kui kuulid, seega peab see klass talletama veel mĂ”nda vĂ€lja. Tema meetod update() teeb rohkem tööd, sealhulgas tagastab just loodud kuuli, kui enam ei ole fireCooldown mĂ€letate, me rÀÀkisime sellest eelmisel peatĂŒkil?). Samuti laiendab ta meetodit serializeForUpdate(), sest peame mĂ€ngu uuendamiseks lisama mĂ€ngija tĂ€iendavad vĂ€lja.

PĂ”hiklassi olemasolu Object on oluline samm, et vĂ€ltida koodi kordamist. NĂ€iteks ilma klassita Object peaks iga mĂ€nguobjekt omama sama rakendust distanceTo(), ja nende rakenduste kopeerimise sĂŒnkroniseerimine mitmes failis oleks Ă”udusunenĂ€gu. See muutub eriti oluliseks suurtes projektides, kui laienemise Object klasside arv suureneb.

4. KokkuvÔrkimiste tuvastamine

Ainus asi, mis meile jÀÀb, on tuvastada, millal mĂŒrsud tabavad mĂ€ngijaid! Pea meeles seda koodijuppi meetodist update() klassist MĂ€ng:

game.js

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

class Game {
  // ...

  update() {
    // ...

    // Rakenda kokkupĂ”rkeid, anna mĂ€ngijatele skoor mĂŒrsude tabamise eest
    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),
    );

    // ...
  }
}

Me peame rakendama meetodi applyCollisions(), mis tagastab kĂ”ik mĂŒrsud, mis tabavad mĂ€ngijaid. Õnneks ei ole seda keeruline teostada, sest

  • KĂ”ik kokkupĂ”rkavad objektid on ringid, mis on lihtsaimad rakendatavad kokkupĂ”rke tuvastamise kujund.
  • Meil on juba meetod distanceTo(), mille me eelmisel jaotisel rakendasime klassis Object.

Nii nÀeb vÀlja meie kokkupÔrke tuvastamise rakendus:

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

See lihtne kokkupÔrke tuvastamine pÔhineb tÔsiasjal, et kaks ringi pÔrkuvad, kui nende keskust vahemaa on vÀiksem kui nende raadiuste summa. Siin on juhtum, kui kahe ringi keskuste vahemaa on tÀpselt nende raadiuste summa:

Mitme kasutaja veebimĂ€ngu loomine .io ĆŸanris
Siin tuleb arvestada veel paari aspektiga:

  • MĂŒrsu ei tohi langeda see mĂ€ngija, kes selle lĂ”i. Seda saab saavutada, vĂ”rreldes bullet.parentID jot player.id.
  • MĂŒrsu peab tabama ainult ĂŒks kord, juhul kui mitme mĂ€ngijaga toimub samaaegne kokkupĂ”rge. Selle ĂŒlesande lahendame operaatori abil break: kui leidub mĂ€ngija, kes pĂ”rkas mĂŒrsuga kokku, lĂ”petame otsingu ja liigume jĂ€rgmise mĂŒrsu juurde.

LÔpp

Ja kĂ”ik! Oleme kĂ€sitlenud kĂ”ike, mida peab teadma veebimĂ€ngu loomise kohta ĆŸanris .io. Mis edasi? Koguge oma .io mĂ€ng!

Kogu nÀidiskood on avatud lÀhtekoodiga ja saadaval aadressil Github.

Allikas: habr.com

Osta usaldusvÀÀrne hostimine veebilehtede jaoks DDoS-i kaitsega, VPS VDS serverid đŸ”„ Osta usaldusvÀÀrne hostimine veebilehtede jaoks DDoS-i kaitsega, VPS VDS serverid | ProHoster