MĂ€ngu loomine mitme mĂ€ngijaga veebimĂ€ng ĆŸanris .io

MĂ€ngu loomine mitme mĂ€ngijaga veebimĂ€ng ĆŸanris .io
KĂ€ivitati 2015. aastal Agar.io sai uue ĆŸanri mĂ€ngude .io eellaseks, mille populaarsus on sellest ajast palju kasvanud. Olen kogenud .io mĂ€ngude populaarsuse kasvu ise: viimase kolme aasta jooksul olen loomine ja mĂŒĂŒk kahest selle ĆŸanri mĂ€ngust..

Kui te pole kunagi varem sellistest mĂ€ngudest kuulnud: need on tasuta mitmikmĂ€ngu veebimĂ€ngud, kuhu on lihtne liituda (kasutajakonto pole vajalik). TĂŒĂŒpiliselt toovad nad kokku palju vastaseid mĂ€ngijaid ĂŒhel areenil. Teised tuntud .io ĆŸanri mĂ€ngud: Slither.io ja Diep.io.

Selles postituses uurime, kuidas nullist luua .io mĂ€ngu. Selleks on vaja vaid Javascripti teadmist: peate mĂ”istma selliseid asju nagu sĂŒntaks ES6, mĂ€rksĂ”na this ja Promises. Isegi kui te ei valda Javascripti tĂ€ielikult, saate siiski enamikus postitusest hakkama.

NÀide .io mÀngust

Koolituse toetamiseks viitame nÀidisele .io mÀngust. Proovige seda mÀngida!

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

1. Projekte ĂŒlevaade / struktuur

Soovitan laadida alla lÀhtekood nÀidismÀngust, et saaksite minuga jÀrgida.

NÀites kasutatakse jÀrgmist:

  • Express on kĂ”ige populaarsem veebiraamistik Node.js jaoks, mis haldab mĂ€ngu veebiserverit.
  • socket.io on websocketteek, mis vĂ”imaldab andmete vahetamist brauseri ja serveri vahel.
  • Webpack on moodulihaldur. Webpacki kasutamise pĂ”hjustest saate lugeda siit.

Nii nÀeb projekti katalooge struktuur vÀlja:

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

public/

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

src/

Kogu lÀhtekood asub kaustas src/. Nimetused client/ ja server/ rÀÀgivad enda eest, ning shared/ sisaldab konstantsfaili, mida impordivad nii klient kui ka server.

2. Kogumid / projekti seaded

Nagu ĂŒlal 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 JavaScripti (JS) kliendi sisenemispunkt. Webpack alustab siit ja otsib rekursiivselt teisi imporditud faile.
  • Meie Webpacki vĂ€ljund-JS asub kaustas dist/. Ma nimetan seda faili meie JS paketiks.
  • Me kasutame Babel, ja konkreetselt seadistust @babel/preset-env , et transpileerida meie JS kood vanematesse brauseritesse.
  • Kasutame pluginat, mis ekstraktib kĂ”ik CSS-id, mida JS failid viitavad, ja ĂŒhendab need ĂŒhte kohta. Ma nimetan seda meie CSS paketiks.

VĂ”isite mĂ€rgata kummalisi failinimesid pakettide kohta '[name].[contenthash].ext'. Need to include file name placeholders Webpack: [name] will be replaced by the entry point name (in our case it's game), and [contenthash] will be replaced by the content hash of the file. We do this to optimize the project for hashing — allowing browsers to cache our JS bundles indefinitely because if the bundle changes, its file name changes as well (it changes contenthash). The resulting file name will look like game.dbeee76e91a97d0c7207.js.

File webpack.common.js — this is the basic configuration file that we import into the development and production configurations. Here, for example, is the development configuration:

webpack.dev.js

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

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

For efficiency, we use during development webpack.dev.js, and switches to webpack.prod.js, to optimize bundle sizes for production deployment.

Local setup

I recommend setting up the project on your local machine so you can follow the steps outlined in this post. The setup is simple: first, you need to have SÔlm ja NPMinstalled on your system. Next, run

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

ja olete valmis tööle! Arendusteenuse kÀivitamiseks piisab, kui tÀita

$ npm run develop

ja avada veebibrauseris localhost:3000. Arendusteenus koostab JS ja CSS paketid automaatselt uuesti, kui koodimuudatused on tehtud — lihtsalt uuendage lehte, et nĂ€ha kĂ”iki muudatusi!

3. Klientide sisenemispunktid

Alustame mĂ€ngu koodist. Esiteks vajame lehte index.html, mille brauser laadib saidi kĂŒlastamisel esimesena. Meie leht saab olema ĂŒ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 pisut lihtsustatud arusaadavuse huvides; sama teen ka paljude teiste postituse nÀidete puhul. TÀielikku koodi saab alati vaadata Github.

Meil on:

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

PÀrast esilehe laadimist hakkab brauseris kÀima Javascript-kood, alustades jÀrgmise sissepÀÀsufailiga: 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 toimub siin mitte nii palju toiminguid:

  1. Mitme muu JS-faili importimine.
  2. CSS-i importimine (et Webpack teaks, et tuleb need meie CSS-paketti lisada).
  3. KĂ€ivita connect() serveriga ĂŒhenduse loomiseks ja mĂ€ngu kĂ€ivitamiseks. downloadAssets() mĂ€ngu renderdamiseks vajalike piltide allalaadimiseks.
  4. Muidugi, pĂ€rast 3. etapi lĂ”petamist kuvatakse peamenĂŒĂŒ (playMenu).
  5. Nuppu 'PLAY' vajutamise kÀitleja seadistamine. Nupule vajutamisel algatab kood mÀngu ja teavitab serverit, et oleme mÀngimiseks valmis.

Meie kliendi-serveri loogika peamine osa asub nendes failides, mis imporditi failist. index.js. NĂŒĂŒd vaatame neid kĂ”iki jĂ€rjekorras.

4. Kliendi andmevahetus

Selles mĂ€ngus serveriga suhtlemiseks kasutame hĂ€sti tuntud teeki socket.io. Socket.io-l on sisseehitatud tugi WebSockets, mis sobivad suurepĂ€raselt kahepoolsesse suhtlemisse: saame serverile sĂ”numeid saata ja server saab meile sama ĂŒhenduse kaudu sĂ”numeid saata.

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(() => {
    // Registreeri tagasiside sisestused
    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 huvides samuti veidi lĂŒhendatud.

Selles failis toimub kolm peamist tegevust:

  • Proovime serveriga ĂŒhendada. connectedPromise lahendatakse ainult siis, kui oleme ĂŒhenduse loonud.
  • Kui ĂŒhendus on edukalt loodud, registreerime callback-funktsioonid (processGameUpdate() ja onGameOver()) serverilt saadud sĂ”numite jaoks.
  • Ekspordime play() ja updateDirection(), et neid saaksid kasutada teised failid.

5. Kliendi renderdamine

On aeg kuvada ekraanile pilt!


kuid enne seda peame alla laadima kÔik pildid (ressursid), mis selleks vajalikud. 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 idee on sÀilitada objekt assets, mis seob faili nime vÔtme objekti vÀÀrtusega Image. Kui ressurss on alla laaditud, salvestame selle objekti assets kiireks hankimiseks tulevikus. Kui iga eraldi ressursi allalaadimine lubatakse (st ressursid on laetud), lubame kÔik ressursid), me lubame downloadPromise.

Allala saadud ressursid, saab alustada renderdamist. Nagu varem öeldud, kasutame veebilehe joonistamiseks HTML5 Canvas (<canvas>). Meie mĂ€ng on ĂŒsna lihtne, seega piisab, kui joonistame vaid jĂ€rgmised elemendid:

  1. Taust
  2. MĂ€ngija laev
  3. Teised mÀngijad, kes on mÀngus
  4. KĂŒtted

Siin on olulised fragmendid src/client/render.js, mis joonistavad just nimelt ĂŒlaltoodud neli 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;

// Hangi kanva graafikakontekst
const canvas = document.getElementById('game-canvas');
const context = canvas.getContext('2d');

// Tee kanvas 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 kĂŒtted
  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 ka lĂŒhendatud selguse huvides.

render() — selle faili peamine funktsioon. startRendering() ja stopRendering() hallitsevad renderdamise tsĂŒkli aktiveerimist sagedusel 60 FPS.

Teatud renderdamise abifunktsioonide konkreetsed teostused (nt 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 oleme varem nÀinud asset.js!

Kui teid huvitab teiste renderdamise abifunktsioonide uurimine, lugege ĂŒlejÀÀnud osa src/client/render.js.

6. Klientide sisend

On aeg muuta mĂ€ng mĂ€ngitavaks! KontrollisĂŒsteem on vĂ€ga lihtne: liikumissuuna muutmiseks saab kasutada hiirt (arvutis) vĂ”i ekraani puudutamist (mobiilseadmest). Selle teostamiseks registreerime Event Listeners hiire ja puutetundlike sĂŒndmuste jaoks.
KÔigega 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, ĐČызыĐČающОД updateDirection() (allikas networking.js) про ŃĐŸĐČĐ”Ń€ŃˆĐ”ĐœĐžĐž ŃĐŸĐ±Ń‹Ń‚ĐžŃ ĐČĐČĐŸĐŽĐ° (ĐœĐ°ĐżŃ€ĐžĐŒĐ”Ń€, про ĐżĐ”Ń€Đ”ĐŒĐ”Ń‰Đ”ĐœĐžĐž ĐŒŃ‹ŃˆĐž). updateDirection() ĐŸŃŃƒŃ‰Đ”ŃŃ‚ĐČĐ»ŃĐ”Ń‚ сĐČŃĐ·ŃŒ с сДрĐČĐ”Ń€ĐŸĐŒ, ĐșĐŸŃ‚ĐŸŃ€Ń‹Đč ĐŸĐ±Ń€Đ°Đ±Đ°Ń‚Ń‹ĐČаДт ŃĐŸĐ±Ń‹Ń‚ĐžĐ” ĐČĐČĐŸĐŽĐ° Đž ŃĐŸĐŸŃ‚ĐČДтстĐČŃƒŃŽŃ‰ĐžĐŒ ĐŸĐ±Ń€Đ°Đ·ĐŸĐŒ ĐŸĐ±ĐœĐŸĐČĐ»ŃĐ”Ń‚ ŃĐŸŃŃ‚ĐŸŃĐœĐžĐ” огры.

7. ĐšĐ»ĐžĐ”ĐœŃ‚ŃĐșĐŸĐ” ŃĐŸŃŃ‚ĐŸŃĐœĐžĐ”

Đ­Ń‚ĐŸŃ‚ разЎДл — ŃĐ°ĐŒŃ‹Đč ŃĐ»ĐŸĐ¶ĐœŃ‹Đč ĐČ ĐżĐ”Ń€ĐČĐŸĐč часто ĐżĐŸŃŃ‚Đ°. ĐĐ” расстраоĐČаĐčŃ‚Đ”ŃŃŒ, ДслО ĐœĐ” ĐżĐŸĐčĐŒŃ‘Ń‚Đ” Đ”ĐłĐŸ с пДрĐČĐŸĐłĐŸ ĐżŃ€ĐŸŃ‡Ń‚Đ”ĐœĐžŃ! ĐœĐŸĐ¶Đ”Ń‚Đ” ЎажД ĐżŃ€ĐŸĐżŃƒŃŃ‚ĐžŃ‚ŃŒ Đ”ĐłĐŸ Đž ĐČĐ”Ń€ĐœŃƒŃ‚ŃŒŃŃ Đș ĐœĐ”ĐŒŃƒ ĐżĐŸĐ·Đ¶Đ”.

ĐŸĐŸŃĐ»Đ”ĐŽĐœĐžĐč ĐșŃƒŃĐŸĐș пазла, ĐșĐŸŃ‚ĐŸŃ€Ń‹Đč ĐœŃƒĐ¶Đ”Đœ ĐŽĐ»Ń заĐČĐ”Ń€ŃˆĐ”ĐœĐžŃ ĐșĐ»ĐžĐ”ĐœŃ‚-сДрĐČĐ”Ń€ĐœĐŸĐłĐŸ ĐșĐŸĐŽĐ° — ŃŃ‚ĐŸ state. ĐŸĐŸĐŒĐœĐžŃ‚Đ” Ń„Ń€Đ°ĐłĐŒĐ”ĐœŃ‚ ĐșĐŸĐŽĐ° Оз разЎДла Â«Đ Đ”ĐœĐŽĐ”Ń€ĐžĐœĐł ĐșĐ»ĐžĐ”ĐœŃ‚Đ°Â»?

render.js

import { getCurrentState } from './state';

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

  // Đ’Ń‹ĐżĐŸĐ»ĐœĐžŃ‚Đ” Ń€Đ”ĐœĐŽĐ”Ń€ĐžĐœĐł
  // ...
}

getCurrentState() peab olema suuteline andma meile mÀngu hetkeseisu kliendis igast ajast serverilt saadud vÀrskenduste pÔhjal. Siin on nÀide mÀngu vÀrskendusest, 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": "Player",
      "score": 3
    }
  ]
}

Iga mÀngu vÀrskendus sisaldab viit sama vÀlist:

  • t: serveri ajamĂ€rk, mis tĂ€histab selle vĂ€rskenduse loomise aega.
  • me: teave mĂ€ngijast, kes selle vĂ€rskenduse saab.
  • others: teiste mĂ€ngijate teabe massiiv, kes osalevad samas mĂ€ngus.
  • bullets: mĂ€ngus toimuvate projektide teabe massiiv.
  • leaderboard: tabeli hetkeseisu andmed. Selles postituses me neid arvesse ei vĂ”ta.

7.1 Lihtne kliendi seisund

Lihtne rakendus getCurrentState() suudab ainult vahetult tagastada kÔige vÀrskema saadud mÀngu vÀrskenduse andmed.

naive-state.js

let lastGameUpdate = null;

// Uuendatud mÀngu haldamine.
export function processGameUpdate(update) {
  lastGameUpdate = update;
}

export function getCurrentState() {
  return lastGameUpdate;
}

Kaunis ja arusaadav! Kuid kui vaid kĂ”ik oleks nii lihtne. Üks pĂ”hjus, miks selline teostus on probleemne: see piirab renderdamise kaadrisagedust serveri taktsagedusega.

Kaadrisagedus (Frame Rate): kaadrite arv (st kĂ”nede render()) sekundis, vĂ”i FPS. MĂ€ngudes pĂŒĂŒtakse tavaliselt saavutada vĂ€hemalt 60 FPS.

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 renderdame lihtsalt viimase mĂ€ngu uuenduse, ei saa FPS pĂ”himĂ”tteliselt kunagi ĂŒletada 30, sest me ei saa serverilt kunagi rohkem kui 30 uuendust sekundis. Isegi kui kutsume render() 60 korda sekundis, siis pool neist kĂ”nede lihtsalt joonistab sama uuesti, sisuliselt mitte midagi tehes. Veel ĂŒks naiivse teostuse probleem on see, et see on viivitustele tundlik. Kui Interneti kiirus on ideaalne, saab klient mĂ€ngu vĂ€rskenduse iga 33 ms jĂ€rel (30 korda sekundis):

MĂ€ngu loomine mitme mĂ€ngijaga veebimĂ€ng ĆŸanris .io
Kahjuks ei ole miski ideaalne. Realistlikum pilt on jÀrgmine:
MĂ€ngu loomine mitme mĂ€ngijaga veebimĂ€ng ĆŸanris .io
Naive rakendus on tegelikult halvim juhtum viivituste osas. Kui mÀngu vÀrskendus tuleb 50 ms hilinemisega, siis kliendi viivitus on 50 ms tÀiendavalt, kuna ta renderdab endiselt mÀngu olekut eelmise vÀrskenduse pÔhjal. Saate aru, kui ebamugav see mÀngijale on: juhuslike viivituste tÔttu tundub mÀng katkestusteta ja ebastabiilne.

7.2 Parandatud kliendi olek

Teeme naĂŻvesse rakendusse mĂ”ned parandused. Esiteks kasutame renderdamise viivitust 100 ms. See tĂ€hendab, et kliendi „praegune” olek on alati 100 ms vĂ”rra mĂ€ngu seisust serveris maha jÀÀnud. NĂ€iteks kui serveris aeg on 150, siis renderdatakse kliendis olek, mis oli serveris ajal 50:

MĂ€ngu loomine mitme mĂ€ngijaga veebimĂ€ng ĆŸanris .io
See annab meile 100 ms puhvrina, et taluda ettearvamatut mÀngu vÀrskenduste saamise aega:

MĂ€ngu loomine mitme mĂ€ngijaga veebimĂ€ng ĆŸanris .io
Selle hinna eest on pidev sisendi viivitus (input lag) 100 ms-nĂ€. See on vĂ€ike ohverdus sujuva mĂ€ngukogemuse nimel — enamik mĂ€ngijaid (eriti juhuslikud) ei pane seda viivitust isegi tĂ€hele. Inimestel on palju lihtsam harjuda pideva 100 ms viivitusega kui mĂ€ngida ettearvamatute viivitustega.

Saame kasutada ka teist tehnikat nimega „kliendipoolne prognoosimine“, mis aitab tĂ”husalt vĂ€hendada tajutavaid viivitusi, kuid seda ei kĂ€sitleta selles postituses.

Veel ĂŒks tĂ€iustamine, mida me kasutame, on lineaarne interpolatsioon. Renderdamise viivitusest tulenevalt oleme tavaliselt vĂ€hemalt ĂŒhe vĂ€rskenduse vĂ”rra ajast ette. Kui getCurrentState()kutsutakse vĂ€lja, saame teha lineaarset interpolatsiooni mĂ€ngu vĂ€rskenduste vahel, mis toimuvad otse enne ja pĂ€rast kliendi praegust aega:

MĂ€ngu loomine mitme mĂ€ngijaga veebimĂ€ng ĆŸanris .io
See lahendab kaadrisageduse probleemi: nĂŒĂŒd saame renderdada unikaalseid kaadreid mistahes soovitud sagedusega!

7.3 Parandatud kliendi oleku rakendamine

Rakenduse nÀidis src/client/state.js kasutab nii renderdamise viivitust kui ka lineaarset interpolatsiooni, kuid see ei kesta kaua. 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);

  // Hoidke enne kehtiva serveri aega ainult ĂŒks mĂ€ngu vĂ€rskendus
  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 ei ole.
function getBaseUpdate() {
  const serverTime = currentServerTime();
  for (let i = gameUpdates.length - 1; i >= 0; i--) {
    if (gameUpdates[i].t <= serverTime) {
      return i;
    }
  }
  return -1;
}

Esiteks tuleb mÔista, mida currentServerTime(). Nagu me varem nÀgime, sisaldab iga mÀngu vÀrskendus serveri ajamÀrki. Soovime kasutada renderdamise viivitust, et kuvada pilti, mis jÀÀb serverist 100 ms vÔrra maha, kuid me ei saa kunagi teada, mis on serveri praegune aeg, kuna me ei saa teada, kui kaua on vÔtnud endale iga vÀrskenduse jÔudmine meie juurde. Internet on ettearvamatu ning selle kiirus vÔib vÀga palju varieeruda!

Selle probleemi lahendamiseks saame kasutada mÔistlikku lÀhendust: me teeme nÀo, et esimene vÀrskendus saabus koheselt. Kui see oleks tÔsi, teaksime serveri aega just sel hetkel! Salvestame serveri ajamÀrgi firstServerTimestamp ja salvestame meie kohaliku (kliendi) ajamÀrgi samal hetkel gameStart.

Oi, oota. Kas serveri aeg ei peaks olema sama, mis kliendi aeg? Miks eristame 'serveri ajamĂ€rki' ja 'kliendi ajamĂ€rki'? See on suurepĂ€rane kĂŒsimus! Selgub, et need ei ole samad. Date.now() tagastab erinevad ajamĂ€rgid kliendi ja serveri jaoks ning see sĂ”ltub nende masinate kohalikest teguritest. Ärge kunagi eeldage, et ajamĂ€rgid on kĂ”igil masinatel samad.

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

NĂŒĂŒd uurime, kuidas kĂ€sitleme mĂ€ngu uuendusi. Serverilt uuenduste saamisel kutsutakse processGameUpdate(), ja salvestame uue uuenduse massiivi gameUpdates. Siis, et kontrollida mĂ€lu kasutamist, eemaldame kĂ”ik vanad uuendused kuni baasuudistus, sest nad ei ole meile enam vajalikud.

Mis on siis «pÔhivÀrskendus»? See on esimene vÀrskendus, mille leiame, liikudes tagasi serveri praegusest ajast. Kas mÀletate seda skeemi?

MĂ€ngu loomine mitme mĂ€ngijaga veebimĂ€ng ĆŸanris .io
VÀrskendus, mis asub otse «Client Render Time» vasakul pool, on pÔhivÀrskendus.

Milleks kasutatakse pÔhivÀrskendust? Miks me saame kÔrvaldada vÀrskendused kuni pÔhini? Selle mÔistmiseks 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),
    };
  }
}

KĂ€sitleme kolme juhtumit:

  1. base < 0 tĂ€hendab, et praeguse renderdamisaja jaoks ei ole vĂ€rskendusi (vt ĂŒlaltoodud rakendus getBaseUpdate()). See vĂ”ib juhtuda kohe mĂ€ngu alguses renderdamise viivituse tĂ”ttu. Sellisel juhul kasutame kĂ”ige viimast saadud vĂ€rskendust.
  2. base on kĂ”ige viimane vĂ€rskendus, mis meil on. See vĂ”ib juhtuda vĂ”rgu viivituse vĂ”i kehva Interneti-ĂŒhenduse tĂ”ttu. Sellisel juhul kasutame samuti kĂ”ige viimast vĂ€rskendust, mis meil on.
  3. Meil on vÀrskendus nii enne kui ka pÀrast praegust renderdamise aega, seega on vÔimalik interpoleerida!

KĂ”ik, mis on jÀÀnud state.js — on lineaarse interpoleerimise rakendus, mis esindab lihtsat (kuid igavat) matemaatikat. Kui soovite seda ise uurida, avage state.js jĂ€rgnevaga Github.

Osa 2. TagakĂŒljeteenus

Selles osas vaatame Node.js tagakÀyttuuri, mis haldab meie nÀidet mÀngust .io.

1. Serveri sisenemispunkt

Veebiserveri haldamiseks kasutame populaarset Node.js veebiraamistiku, mida nimetatakse Express. Selle seadistamisega tegeleb 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');

// Loome Expressi serveri
const app = express();
app.use(express.static('public'));

if (process.env.NODE_ENV === 'development') {
  // Seadistame Webpacki arendamiseks
  const compiler = webpack(webpackConfig);
  app.use(webpackDevMiddleware(compiler));
} else {
  // Statistiliselt serveerime dist/ kausta tootmises
  app.use(express.static('dist'));
}

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

Pidage meeles, et esimeses osas rÀÀkisime Webpackist? Just siin hakkame kasutama meie Webpacki konfiguratsioone. Rakendame neid kahel viisil:

  • Kasutage webpack-dev-middleware meie arenduspakettide automaatseks uuesti kompileerimiseks, vĂ”i
  • Statistiliselt edastada kausta dist/, kuhu Webpack meie failid tootmisse kogumise jĂ€rel kirjutab.

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 kuulab porti ${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 loomist serveriga seadistame sĂŒndmuste töötlejad uue socketi jaoks. SĂŒndmuste töötlejad kĂ€sitlevad klientidelt saadud sĂ”numeid, delegeerides need singelton-objektile 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);
}

Loome .io ĆŸanri mĂ€ngu, seega vajame ainult ĂŒhte eksemplari MĂ€ng („MĂ€ng“) – kĂ”ik mĂ€ngijad mĂ€ngivad ĂŒhel areenil! JĂ€rgmises osas vaatame, kuidas see klass toimib MĂ€ng.

2. Serveri MĂ€ng

Klass MĂ€ng on sisaldab kĂ”ige olulisemat loogikat serveripoolses osas. Sellel 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;

    // Genereeri positsioon, kus see mÀngija alustab.
    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 soketi kaudu (kui olete segaduses, minge tagasi server.js). Socket.io mÀÀrab igale soketile unikaalse id, seega ei pea me selle pÀrast muretsema. Ma nimetan seda mÀngija ID-ks.

Seda meeles pidades uurime klassi MĂ€ng:

  • sockets — on objekt, mis seob mĂ€ngija ID soketiga, mis on seotud mĂ€ngijaga. See vĂ”imaldab meil pideva aja jooksul ligipÀÀsu soketitele nende mĂ€ngija ID-de jĂ€rgi.
  • mĂ€ngijad — on objekt, mis seob mĂ€ngija ID objekti code>Player.

bullets — on objektide massiiv. Bullet, millel ei ole kindlat jĂ€rjekorda.
viimaneUuenduseAeg — on ajatemperatuur mĂ€ngu viimase uuenduse hetkest. Peagi nĂ€eme, kuidas seda kasutatakse.
peabSaatmaUuenduse — on abimuutuja. Selle kasutust nĂ€eme samuti peagi.
Meetodid lisaMÀngija(), eemaldaMÀngija() ja kÀsitleSisend() ei vaja seletamist, neid kasutatakse. server.js. Kui vajate juhtimiseks mÀlu vÀrskendamist, minge natuke kÔrgemale.

Viimane rida konstruktor() kĂ€ivitab uuenduse tsĂŒkli mĂ€ngu (kiirusel 60 uuendust/s):

game.js, osa 2

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

class Game {
  // ...

  update() {
    // Arvuta möödunud aeg
    const now = Date.now();
    const dt = (now - this.lastUpdateTime) / 1000;
    this.lastUpdateTime = now;

    // Uuenda iga kuul
    const bulletsToRemove = [];
    this.bullets.forEach(bullet => {
      if (bullet.update(dt)) {
        // HĂ€vita see kuul
        bulletsToRemove.push(bullet);
      }
    });
    this.bullets = this.bullets.filter(
      bullet => !bulletsToRemove.includes(bullet),
    );

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

    // Rakenda kokkupĂ”rked, tĂŒhista mĂ€ngijad, kui nad tabavad kuule
    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),
    );

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

    // Saada iga mÀngijale mÀngu uuendus 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() liikluslogi kĂ”ige olulisema osa serveri kĂŒljest. JĂ€rgnevalt loetleme kĂ”ik, mida see teeb:

  1. Arvutab, kui kaua aega dt on möödunud viimasest update().
  2. Uuendab igat mĂŒrsku ja hĂ€vitab need vajadusel. Selle funktsionaalsuse teostuseni jĂ”uame hiljem. Praegu piisab, kui teame, et bullet.update() tagastab true, kui mĂŒrsk tuleb hĂ€vitada (see on vĂ€ljunud areenist).
  3. Uuendab iga mĂ€ngijat ja loob vajadusel mĂŒrsu. Selle teostuse nĂ€eme samuti hiljem — player.update() vĂ”ib tagastada objekti Bullet.
  4. Kontrollib kokkupĂ”rkeid mĂŒrskude ja mĂ€ngijate vahel, kasutades applyCollisions(), mis tagastab mĂŒrskude massiivi, mis on mĂ€ngijatesse tabanud. Iga tagastatud mĂŒrsu jaoks suurendame mĂ€ngija punkte, kes selle lasi (kasutades player.onDealtDamage()), ja seejĂ€rel kustutame mĂŒrsu massiivist bullets.
  5. Teavitab ja hÀvitab kÔik surnud mÀngijad.
  6. Saadab kÔigile mÀngijatele mÀngu uuenduse iga teise korra, kui kutsutakse update(). See aitab meil jÀlgida eelpool mainitud abimuutujat peabSaatmaUuenduse. Kuna update() kutsutakse 60 korda/s, saadame mÀngu uuendusi 30 korda/s. Seega, taktsagedus serveri sagedus on 30 takti/s (oleme taktimugavausest rÀÀkinud esimeses osas).

Miks saata mÀngu uuendusi ainult iga teise korra ? kanali kokkuhoiu nimel. 30 mÀngu uuendust sekundis on tÔeliselt palju!

Miks mitte lihtsalt kutsuda update() 30 korda sekundis? MĂ€ngu simuleerimise parandamiseks. Mida sagedamini kutsutakse update(), seda tĂ€psem on mĂ€ngu simuleerimine. Kuid ei tasu liialdada kutsumiseks update(), kuna 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 jĂ€rjestab mĂ€ngijad punktide arvu jĂ€rgi, vĂ”tab viis parimat ja tagastab igaĂŒhe kasutajanime ja skoori.

createUpdate() kasutatakse update() mĂ€ngu vĂ€rskenduste loomiseks, mis edastatakse mĂ€ngijatele. Selle peamine ĂŒlesanne on kutsuda esile meetodeid serializeForUpdate(), mis on rakendatud klassidele Player ja Bullet. Pange tĂ€hele, et ta edastab igale mĂ€ngijale andmed ainult lĂ€hedastes mĂ€ngijatest ja projektidest – pole vajalik edastada teavet mĂ€nguobjektide kohta, mis on mĂ€ngijast kaugel!

3. MĂ€nguobjektid serveris

Meie mĂ€ngus on projektid ja mĂ€ngijad tegelikult vĂ€ga sarnased: need on abstraktsed ĂŒmmargused liikuvad mĂ€nguobjektid. Selle sarnasuse Ă€ra kasutamiseks mĂ€ngijate ja projektide vahel, alustame baasklassi rakendamisega 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 saab olema hea tugipunkt edasise arendamise jaoks. 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;
  }

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

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

  • Paketi kasutamine shortid juhuslikuks genereerimiseks id kuul.
  • Lisa vĂ€li parentID, et saaks jĂ€lgida mĂ€ngijat, kes selle kuuli lĂ”i.
  • Tagastatava vÀÀrtuse lisamine update(), mis on vĂ”rdne true, kui kuuli asukoht on areenist vĂ€ljas (mĂ€letate, me rÀÀkisime sellest eelmisel korral?).

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

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

MÀngijad on keerulisemad kui kuulid, seetÔttu peab see klass sisaldama veel mitmeid vÀlju. Selle meetod update() teeb suuremat tööd, andes tagasi just loodud kuul, kui neid ei ole jÀÀnud fireCooldown (mÀletage, et me rÀÀkisime sellest eelmises osas?). Samuti laiendab ta meetodit serializeForUpdate(), kuna meil on vaja mÀngu vÀrskendusse lisada tÀiendavad vÀljad mÀngija jaoks.

Alusklassi olemasolu Object — oluline samm, mis aitab vĂ€ltida koodi korduvust. NĂ€iteks, ilma klassita Object peab iga mĂ€nguobjekt omama sama rakendust distanceTo(), ja kĂ”igi nende rakenduste kopeerimise ja kleepimise sĂŒnkroniseerimine mitmes failis oleks Ă”udusunenĂ€gu. See muutub eriti oluliseks suurtes projektides, kui laiendavate Object klasside arv kasvab.

4. KokkupÔrgete tuvastamine

Ainus, mis meil ĂŒle jÀÀb - on tuvastada, kui projektid tabavad mĂ€ngijaid! Meenutage seda koodilĂ”iku meetodist update() klassist MĂ€ng:

game.js

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

class Game {
  // ...

  update() {
    // ...

    // Rakenda kokkupÔrked, anna mÀngijatele punkt, kui nad tabavad kuulidega
    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),
    );

    // ...
  }
}

Peame rakendama meetodi applyCollisions(), mis tagastab kĂ”ik kuulid, mis on mĂ€ngijatesse tabanud. Õnneks ei ole see nii keeruline, sest

  • KĂ”ik kokkupĂ”rked on ringid ja see on lihtsaim kuju, mida rakendamise jaoks tuvastada.
  • Meil on juba meetod distanceTo(), mille me eelmisel jaotisel klassis rakendasime Object.

Siin on meie kokkupÔrgete 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Ôrgete tuvastamine pÔhineb tÔsiasjal, et kaks ringi pÔrkuvad, kui nende keskpunktide vahemaa on vÀiksem kui nende raadiuste summa.Siin on juhtum, kus kahe ringi keskpunktide vahemaa on tÀpselt nende raadiuste summa:

MĂ€ngu loomine mitme mĂ€ngijaga veebimĂ€ng ĆŸanris .io
Siin tuleb tÀhelepanu pöörata ka mÔnele aspektile:

  • KĂŒsimus ei tohi tabada mĂ€ngijat, kes selle lootis. Seda saab saavutada, vĂ”rreldes bullet.parentID koos player.id.
  • KĂŒsimus peab tabama ainult kord, kui mitme mĂ€ngijaga kokkupĂ”rked esinevad. Selle ĂŒlesande lahendame break: kui mĂ€ngija, kes tabas projektiili, on leitud, lĂ”petame otsimise ja liigume jĂ€rgmise projektiili juurde.

LÔpp

Ja kĂ”ik! Oleme vaadanud kĂ”ike, mida peate teadma .io ĆŸanri veebimĂ€ngu loomiseks. Mis edasi? Koge oma .io mĂ€ngu!

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

Allikas: habr.com

Osta usaldusvÀÀrne veebihosting DDoS kaitsega, VPS VDS serverid đŸ”„ Osta usaldusvÀÀrne veebihosting DDoS kaitsega, VPS VDS serverid | ProHoster