Vairāku spēlētāju .io tīmekļa spēles izveide

Vairāku spēlētāju .io tīmekļa spēles izveide
Izlaists 2015. gadā Agar.io kļuva par jauna žanra priekÅ”teci .io spēleskura popularitāte kopÅ” tā laika ir kļuvusi arvien populārāka. Es personÄ«gi esmu pieredzējis .io spēļu popularitātes pieaugumu: pēdējo trÄ«s gadu laikā esmu piedzÄ«vojis izveidoja un pārdeva divas Ŕī žanra spēles..

Ja jÅ«s nekad iepriekÅ” neesat dzirdējis par Ŕīm spēlēm, Ŕīs ir bezmaksas vairāku spēlētāju tÄ«mekļa spēles, kuras ir viegli spēlēt (nav nepiecieÅ”ams konts). Viņi parasti saskaras ar daudziem pretinieku spēlētājiem vienā arēnā. Citas slavenas .io spēles: Slither.io Šø Diep.io.

Å ajā rakstā mēs izpētÄ«sim, kā izveidot .io spēli no nulles. Å im nolÅ«kam pietiks tikai ar Javascript zināŔanām: jums ir jāsaprot, piemēram, sintakse ES6, atslēgvārds this Šø solÄ«jumi. Pat ja jÅ«su zināŔanas par Javascript nav ideālas, jÅ«s joprojām varat saprast lielāko daļu ziņojuma.

.io spēles piemērs

Lai palīdzētu mācīties, mēs atsauksimies uz .io spēles piemērs. Mēģiniet to spēlēt!

Vairāku spēlētāju .io tīmekļa spēles izveide
Spēle ir pavisam vienkārÅ”a: tu kontrolē kuÄ£i arēnā, kur ir citi spēlētāji. JÅ«su kuÄ£is automātiski izÅ”auj Ŕāviņus, un jÅ«s mēģināt trāpÄ«t citiem spēlētājiem, izvairoties no viņu Ŕāviņiem.

1. ÄŖss pārskats / projekta struktÅ«ra

ieteikt lejupielādēt avota kodu spēles piemērs, lai jūs varētu man sekot.

Piemērā tiek izmantots Ŕāds:

  • Kurjers ir vispopulārākais Node.js tÄ«mekļa ietvars, kas pārvalda spēles tÄ«mekļa serveri.
  • ligzda.io - Websocket bibliotēka datu apmaiņai starp pārlÅ«kprogrammu un serveri.
  • Webpack - moduļa vadÄ«tājs. Varat lasÄ«t par to, kāpēc izmantot Webpack. Å”eit.

Lūk, kā izskatās projekta direktoriju struktūra:

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

publisks/

Viss mapē public/ serveris iesniegs statiski. IN public/assets/ satur attēlus, ko izmanto mūsu projekts.

src /

Viss avota kods atrodas mapē src/. Nosaukumi client/ Šø server/ runā paÅ”i par sevi un shared/ satur konstantu failu, ko importē gan klients, gan serveris.

2. Montāžas/projekta iestatījumi

Kā minēts iepriekÅ”, mēs izmantojam moduļa pārvaldnieku, lai izveidotu projektu. Webpack. ApskatÄ«sim mÅ«su Webpack konfigurāciju:

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

Vissvarīgākās rindas Ŕeit ir:

  • src/client/index.js ir Javascript (JS) klienta ieejas punkts. Webpack sāksies no Å”ejienes un rekursÄ«vi meklēs citus importētos failus.
  • MÅ«su Webpack versijas izvades JS atradÄ«sies direktorijā dist/. Es nosaukÅ”u Å”o failu par mÅ«su js pakotni.
  • Mēs izmantojam Kņadaun jo Ä«paÅ”i konfigurāciju @babel/preset-env lai pārsÅ«tÄ«tu mÅ«su JS kodu vecākām pārlÅ«kprogrammām.
  • Mēs izmantojam spraudni, lai izvilktu visus CSS, uz kuriem atsaucas JS faili, un apvienotu tos vienuviet. Es viņu saukÅ”u par mÅ«su css pakotne.

Iespējams, esat pamanÄ«jis dÄ«vainus pakotņu failu nosaukumus '[name].[contenthash].ext'. Tie satur failu nosaukumu aizstāŔana TÄ«mekļa pakotne: [name] tiks aizstāts ar ievades punkta nosaukumu (mÅ«su gadÄ«jumā Å”is game) un [contenthash] tiks aizstāts ar faila satura jauktu. Mēs to darām, lai optimizēt projektu jaukÅ”anai - JÅ«s varat likt pārlÅ«kprogrammām saglabāt mÅ«su JS pakotnes keÅ”atmiņā uz nenoteiktu laiku, jo ja mainās pakotne, mainās arÄ« tās faila nosaukums (izmaiņas contenthash). Gala rezultāts bÅ«s skata faila nosaukums game.dbeee76e91a97d0c7207.js.

fails webpack.common.js ir bāzes konfigurācijas fails, ko mēs importējam izstrādes un pabeigtā projekta konfigurācijās. Šeit ir izstrādes konfigurācijas piemērs:

webpack.dev.js

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

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

Efektivitātes labad mēs izmantojam izstrādes procesā webpack.dev.js, un pārslēdzas uz webpack.prod.jslai optimizētu iepakojuma izmērus, izvietojot to ražoÅ”anā.

Vietējais iestatījums

Es iesaku instalēt projektu vietējā datorā, lai jÅ«s varētu veikt Å”ajā ziņojumā norādÄ«tās darbÄ«bas. IestatÄ«Å”ana ir vienkārÅ”a: pirmkārt, sistēmai jābÅ«t instalētai mezgls Šø NPM. Tālāk jums jādara

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

un tu esi gatavs doties! Lai palaistu izstrādes serveri, vienkārŔi palaidiet

$ npm run develop

un dodieties uz tÄ«mekļa pārlÅ«kprogrammu localhost: 3000. Izstrādes serveris automātiski pārbÅ«vēs JS un CSS pakotnes, mainoties kodam ā€” vienkārÅ”i atsvaidziniet lapu, lai redzētu visas izmaiņas!

3. Klientu ieejas punkti

Sāksim pie paÅ”a spēles koda. Vispirms mums ir vajadzÄ«ga lapa index.html, apmeklējot vietni, pārlÅ«kprogramma to vispirms ielādēs. MÅ«su lapa bÅ«s diezgan vienkārÅ”a:

index.html

io spēles piemērs  SPĒLĒT

Å is koda piemērs skaidrÄ«bas labad ir nedaudz vienkārÅ”ots, un es darÄ«Å”u to paÅ”u ar daudziem citiem ziņu piemēriem. Pilnu kodu vienmēr var apskatÄ«t vietnē GitHub.

Mums ir:

  • HTML5 kanvas elements (<canvas>), ko izmantosim spēles renderÄ“Å”anai.
  • <link> lai pievienotu mÅ«su CSS pakotni.
  • <script> lai pievienotu mÅ«su Javascript pakotni.
  • Galvenā izvēlne ar lietotājvārdu <input> un pogu PLAY (<button>).

Pēc sākumlapas ielādes pārlūkprogramma sāks izpildīt Javascript kodu, sākot no ieejas punkta JS faila: 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);
  };
});

Tas var izklausīties sarežģīti, taču Ŕeit nekas daudz nenotiek:

  1. Vairāku citu JS failu importÄ“Å”ana.
  2. CSS importÄ“Å”ana (tā Webpack zina, ka tās ir jāiekļauj mÅ«su CSS pakotnē).
  3. Š—Š°ŠæусŠŗ connect() lai izveidotu savienojumu ar serveri un palaistu downloadAssets() lai lejupielādētu attēlus, kas nepiecieÅ”ami spēles renderÄ“Å”anai.
  4. Pēc 3. posma pabeigÅ”anas tiek parādÄ«ta galvenā izvēlne (playMenu).
  5. Apdarinātāja iestatÄ«Å”ana pogas "PLAY" nospieÅ”anai. Kad poga tiek nospiesta, kods inicializē spēli un paziņo serverim, ka esam gatavi spēlēt.

Mūsu klienta-servera loģikas galvenā "gaļa" ir failos, kurus importēja fails index.js. Tagad mēs tos visus izskatīsim kārtībā.

4. Klientu datu apmaiņa

Å ajā spēlē mēs izmantojam labi zināmu bibliotēku, lai sazinātos ar serveri ligzda.io. Socket.io ir vietējais atbalsts WebSockets, kas ir labi piemēroti divvirzienu saziņai: mēs varam nosÅ«tÄ«t ziņojumus uz serveri Šø serveris var nosÅ«tÄ«t mums ziņojumus, izmantojot to paÅ”u savienojumu.

Mums bÅ«s viens fails src/client/networking.jskurÅ” parÅ«pēsies visi saziņa ar serveri:

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

Šis kods arī ir nedaudz saīsināts skaidrības labad.

Šajā failā ir trīs galvenās darbības:

  • Mēs cenÅ”amies izveidot savienojumu ar serveri. connectedPromise atļauts tikai tad, kad esam izveidojuÅ”i savienojumu.
  • Ja savienojums ir veiksmÄ«gs, mēs reÄ£istrējam atzvanÄ«Å”anas funkcijas (processGameUpdate() Šø onGameOver()) ziņojumiem, ko varam saņemt no servera.
  • Mēs eksportējam play() Šø updateDirection()lai citi faili tos varētu izmantot.

5. Klientu renderēŔana

Ir pienācis laiks parādīt attēlu ekrānā!

ā€¦bet pirms mēs to varam izdarÄ«t, mums ir jālejupielādē visi Å”im nolÅ«kam nepiecieÅ”amie attēli (resursi). UzrakstÄ«sim resursu pārvaldnieku:

aktīvi.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];

Resursu pārvaldÄ«ba nav tik grÅ«ti Ä«stenojama! Galvenā ideja ir objekta uzglabāŔana assets, kas saistÄ«s faila nosaukuma atslēgu ar objekta vērtÄ«bu Image. Kad resurss ir ielādēts, mēs to uzglabājam objektā assets ātrai piekļuvei nākotnē. Kad tiks atļauts lejupielādēt katru atseviŔķu resursu (tas ir, viss resursi), mēs pieļaujam downloadPromise.

Pēc resursu lejupielādes varat sākt renderÄ“Å”anu. Kā minēts iepriekÅ”, lai zÄ«mētu tÄ«mekļa lapā, mēs izmantojam HTML5 kanvas (<canvas>). MÅ«su spēle ir diezgan vienkārÅ”a, tāpēc mums ir jāuzzÄ«mē tikai sekojoÅ”ais:

  1. fons
  2. Spēlētāju kuģis
  3. Citi spēlētāji spēlē
  4. čaumalas

Šeit ir svarīgi fragmenti src/client/render.js, kas atveido tieŔi četrus iepriekŔ uzskaitītos vienumus:

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

Skaidrības labad Ŕis kods ir arī saīsināts.

render() ir Ŕī faila galvenā funkcija. startRendering() Šø stopRendering() kontrolēt renderÄ“Å”anas cilpas aktivizÄ“Å”anu ar ātrumu 60 kadri/s.

AtseviŔķu renderÄ“Å”anas palÄ«gu funkciju Ä«paÅ”as ievieÅ”anas (piemēram renderBullet()) nav tik svarÄ«gi, taču Å”eit ir viens vienkārÅ”s piemērs:

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

Ņemiet vērā, ka mēs izmantojam metodi getAsset(), kas iepriekÅ” bija redzams asset.js!

Ja vēlaties uzzināt par citiem renderÄ“Å”anas palÄ«giem, izlasiet pārējo. src/client/render.js.

6. Klienta ievade

Ir pienācis laiks izveidot spēli spēlējama! VadÄ«bas shēma bÅ«s ļoti vienkārÅ”a: lai mainÄ«tu kustÄ«bas virzienu, var izmantot peli (datorā) vai pieskarties ekrānam (mobilajā ierÄ«cē). Lai to Ä«stenotu, mēs reÄ£istrēsimies Pasākumu klausÄ«tāji Mouse and Touch notikumiem.
Par to visu parūpēsies src/client/input.js:

ievade.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() ir notikumu klausÄ«tāji, kas zvana updateDirection() (no networking.js), kad notiek ievades notikums (piemēram, kad tiek pārvietota pele). updateDirection() apstrādā ziņojumapmaiņu ar serveri, kas apstrādā ievades notikumu un attiecÄ«gi atjaunina spēles stāvokli.

7. Klienta statuss

Å Ä« sadaļa ir visgrÅ«tākā ziņojuma pirmajā daļā. Nezaudējiet drosmi, ja to nesaprotat pirmajā lasÄ«Å”anas reizē! Varat pat to izlaist un atgriezties pie tā vēlāk.

Pēdējais mÄ«klas gabals, kas nepiecieÅ”ams, lai aizpildÄ«tu klienta/servera kodu, ir bija. Atcerieties koda fragmentu no sadaļas Klientu renderÄ“Å”ana?

render.js

import { getCurrentState } from './state';

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

  // Do the rendering
  // ...
}

getCurrentState() jāspēj sniegt mums informāciju par paÅ”reizējo klienta spēles stāvokli jebkurā brÄ«dÄ« pamatojoties uz atjauninājumiem, kas saņemti no servera. Å eit ir piemērs spēles atjauninājumam, ko serveris var nosÅ«tÄ«t:

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

Katrs spēles atjauninājums satur piecus identiskus laukus:

  • t: servera laikspiedols, kas norāda, kad Å”is atjauninājums tika izveidots.
  • me: informācija par atskaņotāju, kas saņem Å”o atjauninājumu.
  • pārējie: informācijas klāsts par citiem spēlētājiem, kas piedalās tajā paŔā spēlē.
  • lodes: informācijas masÄ«vs par spēles Ŕāviņiem.
  • megabanneris: paÅ”reizējie uzvarētāju saraksta dati. Å ajā amatā mēs tos neapskatÄ«sim.

7.1. Naivs klienta stāvoklis

Naiva Ä«stenoÅ”ana getCurrentState() var tieÅ”i atgriezt tikai pēdējā saņemtā spēles atjauninājuma datus.

naivvalsts.js

let lastGameUpdate = null;

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

export function getCurrentState() {
  return lastGameUpdate;
}

Skaisti un skaidri! Bet ja vien tas bÅ«tu tik vienkārÅ”i. Viens no iemesliem, kāpēc Ŕī ievieÅ”ana ir problemātiska: tas ierobežo renderÄ“Å”anas kadru ātrumu lÄ«dz servera pulksteņa ātrumam.

Kadru ātrums: kadru skaits (t.i., zvanu render()) sekundē vai FPS. Spēles parasti cenÅ”as sasniegt vismaz 60 FPS.

Atzīmes likme: biežums, kādā serveris klientiem nosūta spēļu atjauninājumus. Bieži vien tas ir mazāks par kadru ātrumu. Mūsu spēlē serveris darbojas ar frekvenci 30 cikli sekundē.

Ja mēs vienkārÅ”i atveidosim jaunāko spēles atjauninājumu, FPS bÅ«tÄ«bā nekad nepārsniegs 30, jo mēs nekad nesaņemam vairāk par 30 atjauninājumiem sekundē no servera. Pat ja mēs piezvanÄ«sim render() 60 reizes sekundē, tad puse no Å”iem zvaniem vienkārÅ”i pārzÄ«mēs vienu un to paÅ”u, bÅ«tÄ«bā neko nedarot. Vēl viena naivās ievieÅ”anas problēma ir tā nosliece uz kavÄ“Å”anos. Ar ideālu interneta ātrumu klients saņems spēles atjauninājumu tieÅ”i ik pēc 33 ms (30 sekundē):

Vairāku spēlētāju .io tīmekļa spēles izveide
Diemžēl nekas nav ideāls. Reālistiskāks attēls būtu:
Vairāku spēlētāju .io tīmekļa spēles izveide
Naivā ievieÅ”ana ir praktiski sliktākais gadÄ«jums, kad runa ir par latentumu. Ja spēles atjauninājums tiek saņemts ar 50 ms aizkavi, tad klientu stendi papildu 50 ms, jo tas joprojām atveido spēles stāvokli no iepriekŔējā atjauninājuma. Varat iedomāties, cik tas ir neērti spēlētājam: patvaļīga bremzÄ“Å”ana liks spēlei justies saraustÄ«tai un nestabilai.

7.2 Uzlabots klienta stāvoklis

Mēs veiksim dažus uzlabojumus naivā ievieÅ”anā. Pirmkārt, mēs izmantojam renderÄ“Å”anas kavÄ“Å”anās uz 100 ms. Tas nozÄ«mē, ka klienta "paÅ”reizējais" stāvoklis vienmēr atpaliks no spēles stāvokļa serverÄ« par 100 ms. Piemēram, ja laiks serverÄ« ir 150, tad klients atveidos stāvokli, kādā serveris tajā laikā bija 50:

Vairāku spēlētāju .io tīmekļa spēles izveide
Tas dod mums 100 ms buferi, lai izdzÄ«votu neparedzamos spēļu atjaunināŔanas laikos:

Vairāku spēlētāju .io tīmekļa spēles izveide
Izmaksa par to bÅ«s pastāvÄ«ga ievades aizkave uz 100 ms. Tas ir neliels upuris vienmērÄ«gai spēlei - lielākā daļa spēlētāju (Ä«paÅ”i gadÄ«juma spēlētāji) pat nepamanÄ«s Å”o aizkavi. Cilvēkiem ir daudz vieglāk pielāgoties pastāvÄ«gam 100 ms latentumam, nekā spēlēt ar neparedzamu latentumu.

Mēs varam izmantot arÄ« citu tehniku, ko sauc klienta puses prognozÄ“Å”ana, kas labi samazina uztverto latentumu, taču tas netiks apskatÄ«ts Å”ajā ziņojumā.

Vēl viens uzlabojums, ko mēs izmantojam, ir lineārā interpolācija. RenderÄ“Å”anas aizkavÄ“Å”anās dēļ mēs parasti esam vismaz vienu atjauninājumu priekŔā klienta paÅ”reizējam laikam. Kad sauc getCurrentState(), mēs varam izpildÄ«t lineārā interpolācija starp spēles atjauninājumiem tieÅ”i pirms un pēc paÅ”reizējā laika klientā:

Vairāku spēlētāju .io tīmekļa spēles izveide
Tas atrisina kadru ātruma problēmu: tagad mēs varam renderēt unikālus kadrus ar jebkuru vēlamo kadru nomaiņas ātrumu!

7.3. Uzlabota klienta stāvokļa ievieŔana

ÄŖstenoÅ”anas piemērs iekŔā src/client/state.js izmanto gan renderÄ“Å”anas nobÄ«di, gan lineāro interpolāciju, bet ne ilgi. SadalÄ«sim kodu divās daļās. Å eit ir pirmais:

state.js 1. daļa

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

Pirmais solis ir izdomāt, kas currentServerTime(). Kā redzējām iepriekÅ”, katrā spēles atjauninājumā ir iekļauts servera laikspiedols. Mēs vēlamies izmantot renderÄ“Å”anas latentumu, lai attēlu renderētu 100 ms aiz servera, taču mēs nekad neuzzināsim paÅ”reizējo laiku serverÄ«, jo mēs nevaram zināt, cik ilgs laiks pagāja, lÄ«dz kāds no atjauninājumiem nonāca pie mums. Internets ir neprognozējams, un tā ātrums var ievērojami atŔķirties!

Lai apietu Å”o problēmu, mēs varam izmantot saprātÄ«gu tuvinājumu: mēs izlikties, ka pirmais atjauninājums ieradās uzreiz. Ja tā bÅ«tu taisnÄ«ba, tad mēs zinātu servera laiku Å”ajā konkrētajā brÄ«dÄ«! Mēs saglabājam servera laikspiedolu firstServerTimestamp un paturiet mÅ«su vietējā (klienta) laikspiedols tajā paŔā brÄ«dÄ« gameStart.

Pagaidi. Vai tam nevajadzētu bÅ«t servera laikam = klienta laikam? Kāpēc mēs atŔķiram "servera laikspiedolu" un "klienta laikspiedolu"? Tas ir lielisks jautājums! Izrādās, ka tie nav viens un tas pats. Date.now() klientā un serverÄ« atgriezÄ«s dažādus laikspiedolus, un tas ir atkarÄ«gs no faktoriem, kas ir lokāli Å”ajās iekārtās. Nekad neuzņemieties, ka laikspiedoli visās iekārtās bÅ«s vienādi.

Tagad mēs saprotam, ko dara currentServerTime(): tas atgriežas paÅ”reizējā renderÄ“Å”anas laika servera laikspiedols. Citiem vārdiem sakot, Å”is ir servera paÅ”reizējais laiks (firstServerTimestamp <+ (Date.now() - gameStart)) mÄ«nus renderÄ“Å”anas aizkave (RENDER_DELAY).

Tagad apskatÄ«sim, kā mēs apstrādājam spēļu atjauninājumus. Saņemot no atjaunināŔanas servera, tas tiek izsaukts processGameUpdate()un mēs saglabājam jauno atjauninājumu masÄ«vā gameUpdates. Pēc tam, lai pārbaudÄ«tu atmiņas lietojumu, mēs noņemam visus vecos atjauninājumus bāzes atjauninājumsjo mums tās vairs nav vajadzÄ«gas.

Kas ir "pamata atjauninājums"? Å is pirmais atjauninājums, ko atrodam, pārejot atpakaļ no servera paÅ”reizējā laika. Atcerieties Å”o diagrammu?

Vairāku spēlētāju .io tīmekļa spēles izveide
Spēles atjauninājums tieÅ”i pa kreisi no "Client Render Time" ir pamata atjauninājums.

Kam tiek izmantots bāzes atjauninājums? Kāpēc mēs varam atlaist atjauninājumus uz sākotnējo lÄ«meni? Lai to noskaidrotu, pieņemsim beidzot apsveriet ievieÅ”anu getCurrentState():

state.js 2. daļa

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

Mēs izskatām trīs lietas:

  1. base < 0 nozÄ«mē, ka lÄ«dz paÅ”reizējam renderÄ“Å”anas laikam nav atjauninājumu (skatiet iepriekÅ” ievieÅ”anu getBaseUpdate()). Tas var notikt tieÅ”i spēles sākumā renderÄ“Å”anas aizkavÄ“Å”anās dēļ. Å ajā gadÄ«jumā mēs izmantojam jaunāko saņemto atjauninājumu.
  2. base ir jaunākais mūsu atjauninājums. Tas var būt tīkla aizkaves vai slikta interneta savienojuma dēļ. Šajā gadījumā mēs izmantojam arī jaunāko atjauninājumu.
  3. Mums ir atjauninājums gan pirms, gan pēc paÅ”reizējā renderÄ“Å”anas laika, tāpēc mēs varam interpolēt!

Viss, kas palicis iekŔā state.js ir lineārās interpolācijas ievieÅ”ana, kas ir vienkārÅ”a (bet garlaicÄ«ga) matemātika. Ja vēlaties to izpētÄ«t pats, atveriet state.js par GitHub.

2. daļa. Aizmugursistēmas serveris

Šajā daļā mēs apskatīsim Node.js aizmugursistēmu, kas kontrolē mūsu .io spēles piemērs.

1. Server Entry Point

Lai pārvaldītu tīmekļa serveri, mēs izmantosim populāru tīmekļa ietvaru Node.js ar nosaukumu Kurjers. To konfigurēs mūsu servera ieejas punkta fails src/server/server.js:

server.js 1. daļa

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

Atcerieties, ka pirmajā daļā mēs apspriedām Webpack? Šeit mēs izmantosim mūsu Webpack konfigurācijas. Mēs tos izmantosim divos veidos:

  • Lietot webpack-dev-middleware lai automātiski atjaunotu mÅ«su izstrādes pakotnes, vai
  • statiski pārsÅ«tÄ«t mapi dist/, kurā Webpack ierakstÄ«s mÅ«su failus pēc ražoÅ”anas versijas.

Vēl viens svarÄ«gs uzdevums server.js ir iestatÄ«t serveri ligzda.iokas vienkārÅ”i izveido savienojumu ar Express serveri:

server.js 2. daļa

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

Pēc veiksmīgas socket.io savienojuma izveides ar serveri mēs iestatījām notikumu apdarinātājus jaunajai ligzdai. Notikumu apstrādātāji apstrādā ziņojumus, kas saņemti no klientiem, deleģējot to vienam objektam game:

server.js 3. daļa

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

Mēs veidojam .io spēli, tāpēc mums ir nepiecieÅ”ams tikai viens eksemplārs Game ("Spēle") - visi spēlētāji spēlē vienā arēnā! Nākamajā sadaļā mēs redzēsim, kā Ŕī klase darbojas. Game.

2. Spēļu serveri

Klase Game satur vissvarÄ«gāko loÄ£iku servera pusē. Tam ir divi galvenie uzdevumi: spēlētāju vadÄ«ba Šø spēles simulācija.

Sāksim ar pirmo uzdevumu, spēlētāju menedžmentu.

game.js 1. daļa

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

  // ...
}

Å ajā spēlē mēs noteiksim spēlētājus pēc laukuma id viņu socket.io ligzda (ja apmulsÄ«sit, atgriezieties pie server.js). Socket.io pati katrai ligzdai pieŔķir unikālu idtāpēc mums par to nav jāuztraucas. Es viņam piezvanÄ«Å”u Spēlētāja ID.

Paturot to prātā, izpētīsim klases mainīgos Game:

  • sockets ir objekts, kas saista spēlētāja ID ar ligzdu, kas ir saistÄ«ta ar atskaņotāju. Tas ļauj mums pastāvÄ«gi piekļūt ligzdām pēc to atskaņotāja ID.
  • players ir objekts, kas saista spēlētāja ID ar kodu>Spēlētāja objekts

bullets ir objektu masīvs Bullet, kam nav noteiktas kārtības.
lastUpdateTime ir pēdējās spēles atjaunināŔanas laika zÄ«mogs. DrÄ«zumā redzēsim, kā tas tiks izmantots.
shouldSendUpdate ir papildu mainÄ«gais. DrÄ«zumā redzēsim arÄ« tā izmantoÅ”anu.
Metodes addPlayer(), removePlayer() Šø handleInput() nav jāskaidro, tie tiek izmantoti server.js. Ja jums ir jāatsvaidzina atmiņa, atgriezieties nedaudz augstāk.

Pēdējā rinda constructor() sāk darboties atjaunināŔanas cikls spēles (ar biežumu 60 atjauninājumi / s):

game.js 2. daļa

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

  // ...
}

Metode update() satur, iespējams, vissvarīgāko servera puses loģikas daļu. Lūk, ko tas dara secībā:

  1. Aprēķina, cik ilgi dt pagājis kopÅ” pēdējās update().
  2. Atsvaidzina katru Ŕāviņu un iznÄ«cina, ja nepiecieÅ”ams. Å Ä«s funkcionalitātes ievieÅ”anu redzēsim vēlāk. Pagaidām mums pietiek ar to zināt bullet.update() atgriežas trueja ŔāviņŔ bÅ«tu jāiznÄ«cina (viņŔ izkāpa no arēnas).
  3. Atjaunina katru spēlētāju un, ja nepiecieÅ”ams, rada Ŕāviņu. Å o ievieÅ”anu redzēsim arÄ« vēlāk - player.update() var atgriezt objektu Bullet.
  4. Pārbauda, ā€‹ā€‹vai nav sadursmes starp Ŕāviņiem un spēlētājiem ar applyCollisions(), kas atgriež virkni lādiņu, kas trāpÄ«ja spēlētājiem. Par katru atgriezto Ŕāviņu mēs palielinām tā spēlētāja punktu skaitu, kurÅ” to izŔāva (izmantojot player.onDealtDamage()) un pēc tam noņemiet Ŕāviņu no bloka bullets.
  5. Paziņo un iznīcina visus nogalinātos spēlētājus.
  6. NosÅ«ta spēles atjauninājumu visiem spēlētājiem katru sekundi reizes, kad zvanÄ«ja update(). Tas palÄ«dz mums sekot lÄ«dzi iepriekÅ” minētajam papildu mainÄ«gajam. shouldSendUpdate. Kā update() zvana 60 reizes/s, spēļu atjauninājumus nosÅ«tām 30 reizes/s. Tādējādi pulksteņa frekvence servera pulkstenis ir 30 pulksteņi/s (par takts frekvencēm runājām pirmajā daļā).

Kāpēc sūtīt tikai spēļu atjauninājumus cauri laikam ? Lai saglabātu kanālu. 30 spēļu atjauninājumi sekundē ir daudz!

Kāpēc ne tikai piezvanÄ«t update() 30 reizes sekundē? Lai uzlabotu spēles simulāciju. Jo biežāk sauc update(), jo precÄ«zāka bÅ«s spēles simulācija. Taču pārāk neaizraujieties ar daudzajiem izaicinājumiem. update(), jo tas ir skaitļoÅ”anas ziņā dārgs uzdevums - pietiek ar 60 sekundē.

Pārējā klase Game sastāv no palīgmetodēm, kas izmantotas update():

game.js 3. daļa

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() diezgan vienkārÅ”i ā€” tas saŔķiro spēlētājus pēc rezultāta, ieņem piecus labākos un katram atgriež lietotājvārdu un rezultātu.

createUpdate() izmantots gadā update() lai izveidotu spēļu atjauninājumus, kas tiek izplatÄ«ti spēlētājiem. Tās galvenais uzdevums ir izsaukt metodes serializeForUpdate()ieviesta klasēm Player Šø Bullet. Ņemiet vērā, ka tas katram spēlētājam nodod datus tikai par tuvākais spēlētāji un Ŕāviņi - nav nepiecieÅ”ams pārraidÄ«t informāciju par spēles objektiem, kas atrodas tālu no spēlētāja!

3. Spēļu objekti uz servera

MÅ«su spēlē Ŕāviņi un spēlētāji patiesÄ«bā ir ļoti lÄ«dzÄ«gi: tie ir abstrakti, apaļi, kustÄ«gi spēles objekti. Lai izmantotu Å”o spēlētāju un Ŕāviņu lÄ«dzÄ«bu, sāksim ar bāzes klases ievieÅ”anu Object:

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

Å eit nekas sarežģīts nenotiek. Å Ä« klase bÅ«s labs enkura punkts paplaÅ”inājumam. Redzēsim, kā klasē Bullet izmanto Object:

bullet.js

const shortid = require('shortid');
const ObjectClass = require('./object');
const Constants = require('../shared/constants');

class Bullet extends ObjectClass {
  constructor(parentID, x, y, dir) {
    super(shortid(), x, y, dir, Constants.BULLET_SPEED);
    this.parentID = parentID;
  }

  // Returns true if the bullet should be destroyed
  update(dt) {
    super.update(dt);
    return this.x < 0 || this.x > Constants.MAP_SIZE || this.y < 0 || this.y > Constants.MAP_SIZE;
  }
}

IevieÅ”ana Bullet ļoti Ä«ss! Mēs esam pievienojuÅ”i Object tikai Ŕādi paplaÅ”inājumi:

  • Izmantojot iepakojumu shortid nejauÅ”ai Ä£enerÄ“Å”anai id ŔāviņŔ.
  • Lauka pievienoÅ”ana parentIDlai jÅ«s varētu izsekot spēlētājam, kurÅ” izveidoja Å”o Ŕāviņu.
  • AtgrieÅ”anas vērtÄ«bas pievienoÅ”ana update(), kas ir vienāds ar trueja ŔāviņŔ atrodas ārpus arēnas (atcerieties, ka mēs par to runājām pēdējā sadaļā?).

Pāriesim pie 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,
    };
  }
}

Spēlētāji ir sarežģītāki par Ŕāviņiem, tāpēc Å”ajā klasē ir jāuzglabā vēl daži lauki. Viņa metode update() paveic daudz darba, jo Ä«paÅ”i atdod jaunizveidoto Ŕāviņu, ja tāda vairs nav fireCooldown (atcerieties, ka mēs par to runājām iepriekŔējā sadaļā?). Tas arÄ« paplaÅ”ina metodi serializeForUpdate(), jo mums ir jāiekļauj papildu lauki spēlētājam spēles atjauninājumā.

Ir pamatklase Object - svarÄ«gs solis, lai izvairÄ«tos no koda atkārtoÅ”anas. Piemēram, nav klases Object katram spēles objektam ir jābÅ«t vienādai Ä«stenoÅ”anai distanceTo(), un visu Å”o implementāciju kopÄ“Å”ana un ielÄ«mÄ“Å”ana vairākos failos bÅ«tu murgs. Tas kļūst Ä«paÅ”i svarÄ«gi lieliem projektiem.kad skaits paplaÅ”inās Object klases pieaug.

4. Sadursmes noteikŔana

Mums atliek tikai atpazÄ«t, kad Ŕāviņi trāpÄ«ja spēlētājiem! Atcerieties Å”o koda daļu no metodes update() klasē Game:

spēle.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),
    );

    // ...
  }
}

Mums ir jāīsteno metode applyCollisions(), kas atgriež visus Ŕāviņus, kas trāpÄ«ja spēlētājiem. Par laimi to nav tik grÅ«ti izdarÄ«t, jo

  • Visi sadursmes objekti ir apļi, kas ir visvienkārŔākā forma sadursmes noteikÅ”anai.
  • Mums jau ir metode distanceTo(), ko ieviesām klasē iepriekŔējā sadaļā Object.

Lūk, kā izskatās mūsu sadursmju noteikŔanas ievieŔana:

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

Šī vienkārŔā sadursmes noteikŔana ir balstīta uz to, ka divi apļi saduras, ja attālums starp to centriem ir mazāks par to rādiusu summu. Šeit ir gadījums, kad attālums starp divu apļu centriem ir tieŔi vienāds ar to rādiusu summu:

Vairāku spēlētāju .io tīmekļa spēles izveide
Šeit ir jāņem vērā vēl daži aspekti:

  • ŠāviņŔ nedrÄ«kst trāpÄ«t spēlētājam, kurÅ” to radÄ«jis. To var panākt, salÄ«dzinot bullet.parentID с player.id.
  • Šāviņam ir jātrāpa tikai vienu reizi, ja vienlaikus saduras vairāki spēlētāji. Mēs atrisināsim Å”o problēmu, izmantojot operatoru break: tiklÄ«dz tiek atrasts spēlētājs, kurÅ” saduras ar Ŕāviņu, mēs pārtraucam meklÄ“Å”anu un pārejam pie nākamā Ŕāviņa.

beigas

Tas ir viss! Mēs esam apskatÄ«juÅ”i visu, kas jums jāzina, lai izveidotu .io tÄ«mekļa spēli. Ko tālāk? Izveidojiet savu .io spēli!

Viss parauga kods ir atvērtā pirmkoda un publicēts GitHub.

Avots: www.habr.com

Pievieno komentāru