Creu Gêm We Aml-chwaraewr .io

Creu Gêm We Aml-chwaraewr .io
Rhyddhawyd yn 2015 Agar.io daeth yn epil genre newydd gemau .iosydd wedi tyfu mewn poblogrwydd ers hynny. Yn bersonol, rwyf wedi profi'r cynnydd ym mhoblogrwydd gemau .io: dros y tair blynedd diwethaf, rwyf wedi creu a gwerthu dwy gêm o'r genre hwn..

Rhag ofn nad ydych erioed wedi clywed am y gemau hyn o'r blaen, mae'r rhain yn gemau aml-chwaraewr rhad ac am ddim ar y we sy'n hawdd eu chwarae (nid oes angen cyfrif). Maent fel arfer yn wynebu llawer o chwaraewyr gwrthwynebol yn yr un arena. Gemau .io enwog eraill: Slither.io и Diep.io.

Yn y swydd hon, byddwn yn archwilio sut creu gêm .io o'r dechrau. Ar gyfer hyn, dim ond gwybodaeth o Javascript fydd yn ddigon: mae angen i chi ddeall pethau fel cystrawen ES6, allweddair this и Addewidion. Hyd yn oed os nad yw eich gwybodaeth am Javascript yn berffaith, gallwch ddal i ddeall y rhan fwyaf o'r post.

.io enghraifft gêm

Am gymorth dysgu, byddwn yn cyfeirio at .io enghraifft gêm. Ceisiwch ei chwarae!

Creu Gêm We Aml-chwaraewr .io
Mae'r gêm yn eithaf syml: rydych chi'n rheoli llong mewn arena lle mae chwaraewyr eraill. Mae eich llong yn tanio taflegrau yn awtomatig ac rydych chi'n ceisio taro chwaraewyr eraill wrth osgoi eu tafluniau.

1. Trosolwg / strwythur cryno o'r prosiect

Rwy'n argymell lawrlwytho cod ffynhonnell gêm enghreifftiol fel y gallwch chi fy nilyn.

Mae'r enghraifft yn defnyddio'r canlynol:

  • Express yw'r fframwaith gwe mwyaf poblogaidd Node.js sy'n rheoli gweinydd gwe'r gêm.
  • soced.io - llyfrgell websoced ar gyfer cyfnewid data rhwng porwr a gweinydd.
  • Webpack - rheolwr modiwl. Gallwch ddarllen pam i ddefnyddio Webpack. yma.

Dyma sut olwg sydd ar strwythur cyfeiriadur y prosiect:

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

cyhoeddus /

Popeth mewn ffolder public/ yn cael ei gyflwyno'n statig gan y gweinydd. YN public/assets/ yn cynnwys delweddau a ddefnyddir gan ein prosiect.

src /

Mae'r holl god ffynhonnell yn y ffolder src/. Teitlau client/ и server/ siarad drostynt eu hunain a shared/ yn cynnwys ffeil cysonion sy'n cael ei fewnforio gan y cleient a'r gweinydd.

2. Gwasanaethau/gosodiadau prosiect

Fel y soniwyd uchod, rydym yn defnyddio'r rheolwr modiwl i adeiladu'r prosiect. Webpack. Gadewch i ni edrych ar ein cyfluniad Webpack:

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

Y llinellau pwysicaf yma yw:

  • src/client/index.js yw pwynt mynediad y cleient Javascript (JS). Bydd Webpack yn cychwyn o'r fan hon ac yn chwilio'n rheolaidd am ffeiliau eraill a fewnforiwyd.
  • Bydd allbwn JS o'n fersiwn Webpack yn cael ei leoli yn y cyfeiriadur dist/. Byddaf yn galw'r ffeil hon yn ein js pecyn.
  • Rydym yn defnyddio Babel, ac yn enwedig y cyfluniad @babel/preset-env i drawsffurfio ein cod JS ar gyfer porwyr hŷn.
  • Rydym yn defnyddio ategyn i echdynnu'r holl CSS y mae'r ffeiliau JS yn cyfeirio atynt a'u cyfuno mewn un lle. Byddaf yn ei alw yn ein pecyn css.

Efallai eich bod wedi sylwi ar enwau ffeiliau pecyn rhyfedd '[name].[contenthash].ext'. Maent yn cynnwys amnewidion enw ffeil Pecyn gwe: [name] yn cael ei ddisodli gan enw'r pwynt mewnbwn (yn ein hachos ni, hwn game), a [contenthash] yn cael ei ddisodli gan stwnsh o gynnwys y ffeil. Rydym yn ei wneud i optimeiddio'r prosiect ar gyfer stwnsio - gallwch ddweud wrth borwyr i storio ein pecynnau JS am gyfnod amhenodol, oherwydd os bydd pecyn yn newid, yna mae enw ei ffeil hefyd yn newid (newidiadau contenthash). Y canlyniad terfynol fydd enw'r ffeil gweld game.dbeee76e91a97d0c7207.js.

file webpack.common.js yw'r ffeil ffurfweddu sylfaen yr ydym yn ei fewnforio i'r datblygiad a ffurfweddau prosiect gorffenedig. Dyma enghraifft o ffurfweddiad datblygu:

webpack.dev.js

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

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

Ar gyfer effeithlonrwydd, rydym yn defnyddio yn y broses ddatblygu webpack.dev.js, ac yn newid i webpack.prod.jsi optimeiddio meintiau pecynnau wrth eu defnyddio i gynhyrchu.

Lleoliad lleol

Rwy'n argymell gosod y prosiect ar beiriant lleol fel y gallwch ddilyn y camau a restrir yn y swydd hon. Mae'r gosodiad yn syml: yn gyntaf, mae'n rhaid bod y system wedi gosod Nôd и NPM. Nesaf mae angen i chi ei wneud

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

ac rydych chi'n barod i fynd! I gychwyn y gweinydd datblygu, dim ond rhedeg

$ npm run develop

a mynd i borwr gwe localhost: 3000. Bydd y gweinydd datblygu yn ailadeiladu'r pecynnau JS a CSS yn awtomatig wrth i'r cod newid - dim ond adnewyddu'r dudalen i weld yr holl newidiadau!

3. Pwyntiau Mynediad Cleient

Gadewch i ni fynd i lawr at y cod gêm ei hun. Yn gyntaf mae angen tudalen arnom index.html, wrth ymweld â'r wefan, bydd y porwr yn ei lwytho gyntaf. Bydd ein tudalen yn eithaf syml:

index.html

Gêm .io enghraifft  CHWARAE

Mae'r enghraifft cod hon wedi'i symleiddio ychydig er eglurder, a byddaf yn gwneud yr un peth â llawer o'r enghreifftiau post eraill. Gellir gweld y cod llawn bob amser yn Github.

Mae gennym ni:

  • Elfen cynfas HTML5 (<canvas>) y byddwn yn ei ddefnyddio i wneud y gêm.
  • <link> i ychwanegu ein pecyn CSS.
  • <script> i ychwanegu ein pecyn Javascript.
  • Prif ddewislen gydag enw defnyddiwr <input> a'r botwm CHWARAE (<button>).

Ar ôl llwytho'r dudalen gartref, bydd y porwr yn dechrau gweithredu cod Javascript, gan ddechrau o'r ffeil JS pwynt mynediad: src/client/index.js.

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

Gall hyn swnio'n gymhleth, ond nid oes llawer yn digwydd yma:

  1. Mewnforio sawl ffeil JS arall.
  2. Mewnforio CSS (felly mae Webpack yn gwybod eu cynnwys yn ein pecyn CSS).
  3. Запуск connect() i sefydlu cysylltiad â'r gweinydd a rhedeg downloadAssets() i lawrlwytho delweddau sydd eu hangen i rendr y gêm.
  4. Ar ôl cwblhau cam 3 dangosir y brif ddewislen (playMenu).
  5. Gosod y triniwr ar gyfer pwyso'r botwm "CHWARAE". Pan fydd y botwm yn cael ei wasgu, mae'r cod yn cychwyn y gêm ac yn dweud wrth y gweinydd ein bod yn barod i chwarae.

Mae prif "cig" ein rhesymeg cleient-gweinydd yn y ffeiliau hynny a fewnforiwyd gan y ffeil index.js. Nawr byddwn yn eu hystyried i gyd mewn trefn.

4. Cyfnewid data cwsmeriaid

Yn y gêm hon, rydym yn defnyddio llyfrgell adnabyddus i gyfathrebu â'r gweinydd soced.io. Mae gan Socket.io gefnogaeth frodorol WebSocedi, sy'n addas iawn ar gyfer cyfathrebu dwy ffordd: gallwn anfon negeseuon at y gweinydd и gall y gweinydd anfon negeseuon atom ar yr un cysylltiad.

Bydd gennym un ffeil src/client/networking.jspwy fydd yn gofalu pawb cyfathrebu â'r gweinydd:

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

Mae'r cod hwn hefyd wedi'i fyrhau ychydig er eglurder.

Mae tri phrif weithred yn y ffeil hon:

  • Rydym yn ceisio cysylltu â'r gweinydd. connectedPromise dim ond pan fyddwn wedi sefydlu cysylltiad y caniateir hynny.
  • Os yw'r cysylltiad yn llwyddiannus, rydym yn cofrestru swyddogaethau galw'n ôl (processGameUpdate() и onGameOver()) ar gyfer negeseuon y gallwn eu derbyn gan y gweinydd.
  • Rydym yn allforio play() и updateDirection()fel y gall ffeiliau eraill eu defnyddio.

5. Rendro Cleient

Mae'n amser i arddangos y llun ar y sgrin!

…ond cyn y gallwn wneud hynny, mae angen i ni lawrlwytho'r holl ddelweddau (adnoddau) sydd eu hangen ar gyfer hyn. Gadewch i ni ysgrifennu rheolwr adnoddau:

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

Nid yw rheoli adnoddau mor anodd i'w weithredu! Y prif syniad yw storio gwrthrych assets, a fydd yn rhwymo allwedd enw'r ffeil i werth y gwrthrych Image. Pan fydd yr adnodd wedi'i lwytho, rydyn ni'n ei storio mewn gwrthrych assets ar gyfer mynediad cyflym yn y dyfodol. Pryd fydd pob adnodd unigol yn cael ei lwytho i lawr (hynny yw, holl adnoddau), rydym yn caniatáu downloadPromise.

Ar ôl llwytho i lawr yr adnoddau, gallwch ddechrau rendro. Fel y dywedwyd yn gynharach, i dynnu ar dudalen we, rydym yn defnyddio Cynfas HTML5 (<canvas>). Mae ein gêm yn eithaf syml, felly dim ond y canlynol y mae angen i ni eu tynnu:

  1. Cefndir
  2. Llong chwaraewr
  3. Chwaraewyr eraill yn y gêm
  4. Cregyn

Dyma'r pytiau pwysig src/client/render.js, sy'n gwneud yn union y pedair eitem a restrir uchod:

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

Mae'r cod hwn hefyd yn cael ei fyrhau er eglurder.

render() yw prif swyddogaeth y ffeil hon. startRendering() и stopRendering() rheoli actifadu'r ddolen rendrad ar 60 FPS.

Gweithrediadau concrid o swyddogaethau cynorthwyydd rendro unigol (e.e. renderBullet()) ddim mor bwysig â hynny, ond dyma un enghraifft syml:

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

Sylwch ein bod yn defnyddio'r dull getAsset(), a welwyd o'r blaen yn asset.js!

Os oes gennych chi ddiddordeb mewn archwilio cynorthwywyr rendro eraill, yna darllenwch y gweddill src/client/render.js.

6. Mewnbwn cleient

Mae'n amser gwneud gêm chwaraeadwy! Bydd y cynllun rheoli yn syml iawn: i newid cyfeiriad symud, gallwch ddefnyddio'r llygoden (ar gyfrifiadur) neu gyffwrdd â'r sgrin (ar ddyfais symudol). Er mwyn gweithredu hyn, byddwn yn cofrestru Gwrandawyr Digwyddiad ar gyfer digwyddiadau Llygoden a Chyffwrdd.
Bydd yn gofalu am hyn i gyd src/client/input.js:

mewnbwn.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() yw Gwrandawyr Digwyddiadau sy'n galw updateDirection() (o networking.js) pan fydd digwyddiad mewnbwn yn digwydd (er enghraifft, pan symudir y llygoden). updateDirection() yn trin negeseuon gyda'r gweinydd, sy'n trin y digwyddiad mewnbwn ac yn diweddaru cyflwr y gêm yn unol â hynny.

7. Statws Cleient

Yr adran hon yw'r anoddaf yn rhan gyntaf y post. Peidiwch â digalonni os nad ydych chi'n ei ddeall y tro cyntaf i chi ei ddarllen! Gallwch hyd yn oed ei hepgor a dod yn ôl ato yn nes ymlaen.

Y darn olaf o'r pos sydd ei angen i gwblhau'r cod cleient/gweinydd yw Roedd. Cofiwch y pyt cod o'r adran Rendro Cleient?

rendr.js

import { getCurrentState } from './state';

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

  // Do the rendering
  // ...
}

getCurrentState() Dylai fod yn gallu rhoi i ni gyflwr presennol y gêm yn y cleient ar unrhyw adeg yn seiliedig ar ddiweddariadau a dderbyniwyd gan y gweinydd. Dyma enghraifft o ddiweddariad gêm y gall y gweinydd ei anfon:

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

Mae pob diweddariad gêm yn cynnwys pum maes union yr un fath:

  • t: Stamp amser gweinydd yn nodi pryd y cafodd y diweddariad hwn ei greu.
  • me: Gwybodaeth am y chwaraewr sy'n derbyn y diweddariad hwn.
  • eraill: Amrywiaeth o wybodaeth am chwaraewyr eraill sy'n cymryd rhan yn yr un gêm.
  • bwledi: amrywiaeth o wybodaeth am daflegrau yn y gêm.
  • arweinwyr: Data bwrdd arweinwyr cyfredol. Yn y swydd hon, ni fyddwn yn eu hystyried.

7.1 Cyflwr cleient naïf

Gweithredu naïf getCurrentState() dim ond yn uniongyrchol y gellir dychwelyd data'r diweddariad gêm mwyaf diweddar.

naïf-wladwriaeth.js

let lastGameUpdate = null;

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

export function getCurrentState() {
  return lastGameUpdate;
}

Neis ac yn glir! Ond pe bai mor syml â hynny. Un o'r rhesymau y mae'r gweithrediad hwn yn broblemus: mae'n cyfyngu'r gyfradd ffrâm rendro i gyfradd cloc y gweinydd.

Cyfradd Ffrâm: nifer y fframiau (h.y. galwadau render()) yr eiliad, neu FPS. Mae gemau fel arfer yn ymdrechu i gyflawni o leiaf 60 FPS.

Cyfradd Tic: Pa mor aml y mae'r gweinydd yn anfon diweddariadau gêm i gleientiaid. Mae'n aml yn is na'r gyfradd ffrâm. Yn ein gêm, mae'r gweinydd yn rhedeg ar amlder o 30 cylch yr eiliad.

Os ydym yn gwneud y diweddariad diweddaraf o'r gêm yn unig, yna yn y bôn ni fydd yr FPS byth yn mynd dros 30, oherwydd nid ydym byth yn cael mwy na 30 o ddiweddariadau yr eiliad gan y gweinydd. Hyd yn oed os ydym yn galw render() 60 gwaith yr eiliad, yna bydd hanner y galwadau hyn yn ail-lunio'r un peth yn unig, gan wneud dim yn y bôn. Problem arall gyda'r gweithredu naïf yw ei fod yn dueddol o oedi. Gyda chyflymder Rhyngrwyd delfrydol, bydd y cleient yn derbyn diweddariad gêm yn union bob 33ms (30 yr eiliad):

Creu Gêm We Aml-chwaraewr .io
Yn anffodus, does dim byd yn berffaith. Darlun mwy realistig fyddai:
Creu Gêm We Aml-chwaraewr .io
Y gweithredu naïf yw'r achos gwaethaf bron pan ddaw'n fater o hwyrni. Os derbynnir diweddariad gêm gydag oedi o 50ms, yna stondinau cleient 50ms ychwanegol oherwydd ei fod yn dal i rendro'r cyflwr gêm o'r diweddariad blaenorol. Gallwch ddychmygu pa mor anghyfforddus yw hyn i'r chwaraewr: bydd brecio mympwyol yn gwneud i'r gêm deimlo'n hercian ac yn ansefydlog.

7.2 Gwell cyflwr cleient

Byddwn yn gwneud rhai gwelliannau i'r gweithredu naïf. Yn gyntaf, rydym yn defnyddio oedi rendro am 100 ms. Mae hyn yn golygu y bydd cyflwr "presennol" y cleient bob amser yn llusgo 100ms y tu ôl i gyflwr y gêm ar y gweinydd. Er enghraifft, os yw'r amser ar y gweinydd 150, yna bydd y cleient yn rhoi'r cyflwr yr oedd y gweinydd ynddo ar y pryd 50:

Creu Gêm We Aml-chwaraewr .io
Mae hyn yn rhoi byffer 100ms inni oroesi amseroedd diweddaru gêm anrhagweladwy:

Creu Gêm We Aml-chwaraewr .io
Bydd yr ad-daliad ar gyfer hyn yn barhaol oedi mewnbwn am 100 ms. Mae hwn yn aberth bach ar gyfer gameplay llyfn - ni fydd y rhan fwyaf o chwaraewyr (yn enwedig chwaraewyr achlysurol) hyd yn oed yn sylwi ar yr oedi hwn. Mae'n llawer haws i bobl addasu i hwyrni cyson o 100ms nag ydyw i chwarae gyda hwyrni anrhagweladwy.

Gallwn hefyd ddefnyddio techneg arall o'r enw rhagfynegiad ochr y cleient, sy'n gwneud gwaith da o leihau hwyrni canfyddedig, ond na fydd yn cael ei gynnwys yn y swydd hon.

Gwelliant arall yr ydym yn ei ddefnyddio yw rhyngosod llinol. Oherwydd oedi rendro, rydym fel arfer o leiaf un diweddariad cyn yr amser presennol yn y cleient. Pan gafodd ei alw getCurrentState(), gallwn weithredu rhyngosod llinol rhwng diweddariadau gêm ychydig cyn ac ar ôl yr amser presennol yn y cleient:

Creu Gêm We Aml-chwaraewr .io
Mae hyn yn datrys y mater cyfradd ffrâm: gallwn nawr rendro fframiau unigryw ar unrhyw gyfradd ffrâm yr ydym ei eisiau!

7.3 Gweithredu gwell cyflwr cleient

Enghraifft o weithredu yn src/client/state.js yn defnyddio oedi rendrad a rhyngosod llinol, ond nid yn hir. Gadewch i ni dorri'r cod yn ddwy ran. Dyma'r un cyntaf:

gwladwriaeth.js rhan 1

const RENDER_DELAY = 100;

const gameUpdates = [];
let gameStart = 0;
let firstServerTimestamp = 0;

export function initState() {
  gameStart = 0;
  firstServerTimestamp = 0;
}

export function processGameUpdate(update) {
  if (!firstServerTimestamp) {
    firstServerTimestamp = update.t;
    gameStart = Date.now();
  }
  gameUpdates.push(update);

  // Keep only one game update before the current server time
  const base = getBaseUpdate();
  if (base > 0) {
    gameUpdates.splice(0, base);
  }
}

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

// Returns the index of the base update, the first game update before
// current server time, or -1 if N/A.
function getBaseUpdate() {
  const serverTime = currentServerTime();
  for (let i = gameUpdates.length - 1; i >= 0; i--) {
    if (gameUpdates[i].t <= serverTime) {
      return i;
    }
  }
  return -1;
}

Y cam cyntaf yw darganfod beth currentServerTime(). Fel y gwelsom yn gynharach, mae pob diweddariad gêm yn cynnwys stamp amser gweinydd. Rydym am ddefnyddio latency rendr i wneud y ddelwedd 100ms y tu ôl i'r gweinydd, ond ni fyddwn byth yn gwybod yr amser presennol ar y gweinydd, oherwydd ni allwn wybod faint o amser a gymerodd i unrhyw un o'r diweddariadau ein cyrraedd. Mae'r Rhyngrwyd yn anrhagweladwy a gall ei gyflymder amrywio'n fawr!

I fynd o gwmpas y broblem hon, gallwn ddefnyddio brasamcan rhesymol: ni esgus bod y diweddariad cyntaf wedi cyrraedd ar unwaith. Pe bai hyn yn wir, yna byddem yn gwybod amser y gweinydd ar yr eiliad arbennig hon! Rydym yn storio stamp amser y gweinydd i mewn firstServerTimestamp a chadw ein lleol stamp amser (cleient) ar yr un funud yn gameStart.

O aros. Oni ddylai fod yn amser gweinydd = amser cleient? Pam ydym ni'n gwahaniaethu rhwng "stamp amser gweinydd" a "stamp amser cleient"? Mae hwn yn gwestiwn gwych! Mae'n troi allan nad ydynt yr un peth. Date.now() yn dychwelyd gwahanol stampiau amser yn y cleient a'r gweinydd, ac mae'n dibynnu ar ffactorau sy'n lleol i'r peiriannau hyn. Peidiwch byth â chymryd yn ganiataol y bydd y stampiau amser yr un fath ar bob peiriant.

Nawr rydyn ni'n deall beth mae'n ei wneud currentServerTime(): mae'n dychwelyd stamp amser gweinydd yr amser rendrad cyfredol. Mewn geiriau eraill, dyma amser presennol y gweinydd (firstServerTimestamp <+ (Date.now() - gameStart)) llai oedi rendrad (RENDER_DELAY).

Nawr gadewch i ni edrych ar sut rydyn ni'n trin diweddariadau gêm. Pan gaiff ei dderbyn gan y gweinydd diweddaru, fe'i gelwir processGameUpdate()ac rydym yn arbed y diweddariad newydd i arae gameUpdates. Yna, i wirio'r defnydd o gof, rydym yn dileu'r holl hen ddiweddariadau o'r blaen diweddariad sylfaenoherwydd nid oes arnom eu hangen mwyach.

Beth yw "diweddariad sylfaenol"? hwn y diweddariad cyntaf a ddarganfyddwn trwy symud yn ôl o amser presennol y gweinydd. Cofiwch y diagram hwn?

Creu Gêm We Aml-chwaraewr .io
Y diweddariad gêm yn uniongyrchol i'r chwith o "Client Render Time" yw'r diweddariad sylfaenol.

Ar gyfer beth mae'r diweddariad sylfaenol yn cael ei ddefnyddio? Pam allwn ni ollwng diweddariadau i'r llinell sylfaen? I ddarganfod hyn, gadewch i ni o'r diwedd ystyried y gweithredu getCurrentState():

gwladwriaeth.js rhan 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),
    };
  }
}

Rydym yn ymdrin â thri achos:

  1. base < 0 yn golygu nad oes unrhyw ddiweddariadau tan yr amser rendrad presennol (gweler gweithredu uchod getBaseUpdate()). Gall hyn ddigwydd reit ar ddechrau'r gêm oherwydd oedi rendro. Yn yr achos hwn, rydym yn defnyddio'r diweddariad diweddaraf a dderbyniwyd.
  2. base yw'r diweddariad diweddaraf sydd gennym. Gall hyn fod oherwydd oedi rhwydwaith neu gysylltiad rhyngrwyd gwael. Yn yr achos hwn, rydym hefyd yn defnyddio'r diweddariad diweddaraf sydd gennym.
  3. Mae gennym ddiweddariad cyn ac ar ôl yr amser rendrad presennol, felly gallwn ni rhyngosod!

Y cyfan sydd ar ôl i mewn state.js yn gweithredu interpolation llinol sy'n syml (ond diflas) mathemateg. Os ydych chi am ei archwilio eich hun, yna agorwch state.js ar Github.

Rhan 2. Backend Gweinydd

Yn y rhan hon, byddwn yn edrych ar y backend Node.js sy'n rheoli ein .io enghraifft gêm.

1. Man Mynediad Gweinydd

I reoli'r gweinydd gwe, byddwn yn defnyddio fframwaith gwe poblogaidd ar gyfer Node.js o'r enw Express. Bydd yn cael ei ffurfweddu gan ein ffeil pwynt mynediad gweinydd src/server/server.js:

gweinydd.js rhan 1

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

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

if (process.env.NODE_ENV === 'development') {
  // Setup Webpack for development
  const compiler = webpack(webpackConfig);
  app.use(webpackDevMiddleware(compiler));
} else {
  // Static serve the dist/ folder in production
  app.use(express.static('dist'));
}

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

Cofiwch ein bod yn trafod Webpack yn y rhan gyntaf? Dyma lle byddwn yn defnyddio ein ffurfweddiadau Webpack. Byddwn yn eu defnyddio mewn dwy ffordd:

  • Defnyddio webpack-dev-midleware i ailadeiladu ein pecynnau datblygu yn awtomatig, neu
  • ffolder trosglwyddo statig dist/, y bydd Webpack yn ysgrifennu ein ffeiliau iddo ar ôl adeiladu'r cynhyrchiad.

Tasg bwysig arall server.js yw sefydlu'r gweinydd soced.iosydd ond yn cysylltu â'r gweinydd Express:

gweinydd.js rhan 2

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

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

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

// Listen for socket.io connections
io.on('connection', socket => {
  console.log('Player connected!', socket.id);

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

Ar ôl sefydlu cysylltiad socket.io â'r gweinydd yn llwyddiannus, fe wnaethom sefydlu trinwyr digwyddiadau ar gyfer y soced newydd. Mae trinwyr digwyddiadau yn trin negeseuon a dderbynnir gan gleientiaid trwy ddirprwyo i wrthrych singleton game:

gweinydd.js rhan 3

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

// ...

// Setup the Game
const game = new Game();

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

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

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

Rydym yn creu gêm .io, felly dim ond un copi sydd ei angen arnom Game ("Gêm") - mae pob chwaraewr yn chwarae yn yr un arena! Yn yr adran nesaf, byddwn yn gweld sut mae'r dosbarth hwn yn gweithio. Game.

2. gweinyddwyr gêm

Dosbarth Game yn cynnwys y rhesymeg bwysicaf ar ochr y gweinydd. Mae ganddo ddwy brif dasg: rheoli chwaraewyr и efelychiad gêm.

Gadewch i ni ddechrau gyda'r dasg gyntaf, rheoli chwaraewyr.

gêm.js rhan 1

const Constants = require('../shared/constants');
const Player = require('./player');

class Game {
  constructor() {
    this.sockets = {};
    this.players = {};
    this.bullets = [];
    this.lastUpdateTime = Date.now();
    this.shouldSendUpdate = false;
    setInterval(this.update.bind(this), 1000 / 60);
  }

  addPlayer(socket, username) {
    this.sockets[socket.id] = socket;

    // Generate a position to start this player at.
    const x = Constants.MAP_SIZE * (0.25 + Math.random() * 0.5);
    const y = Constants.MAP_SIZE * (0.25 + Math.random() * 0.5);
    this.players[socket.id] = new Player(socket.id, username, x, y);
  }

  removePlayer(socket) {
    delete this.sockets[socket.id];
    delete this.players[socket.id];
  }

  handleInput(socket, dir) {
    if (this.players[socket.id]) {
      this.players[socket.id].setDirection(dir);
    }
  }

  // ...
}

Yn y gêm hon, byddwn yn adnabod y chwaraewyr wrth y cae id eu soced socket.io (os ydych chi'n drysu, yna ewch yn ôl i server.js). Mae Socket.io ei hun yn aseinio unigryw i bob soced idfelly nid oes angen inni boeni am hynny. Byddaf yn ei alw ID Chwaraewr.

Gyda hynny mewn golwg, gadewch i ni archwilio newidynnau enghreifftiol mewn dosbarth Game:

  • sockets yn wrthrych sy'n clymu ID y chwaraewr i'r soced sy'n gysylltiedig â'r chwaraewr. Mae'n caniatáu i ni gael mynediad i socedi gan eu rhifau adnabod chwaraewyr mewn amser cyson.
  • players yn wrthrych sy'n clymu ID y chwaraewr i'r cod> gwrthrych Chwaraewr

bullets yn amrywiaeth o wrthrychau Bullet, sydd heb unrhyw drefn bendant.
lastUpdateTime yw stamp amser y tro diwethaf i'r gêm gael ei diweddaru. Cawn weld sut y caiff ei ddefnyddio yn fuan.
shouldSendUpdate yn newidyn ategol. Byddwn hefyd yn gweld ei ddefnydd yn fuan.
Dulliau addPlayer(), removePlayer() и handleInput() nid oes angen esbonio, maent yn cael eu defnyddio yn server.js. Os oes angen i chi adnewyddu'ch cof, ewch yn ôl ychydig yn uwch.

Llinell olaf constructor() yn cychwyn cylch diweddaru gemau (gydag amledd o 60 diweddariad yr eiliad):

gêm.js rhan 2

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

class Game {
  // ...

  update() {
    // Calculate time elapsed
    const now = Date.now();
    const dt = (now - this.lastUpdateTime) / 1000;
    this.lastUpdateTime = now;

    // Update each bullet
    const bulletsToRemove = [];
    this.bullets.forEach(bullet => {
      if (bullet.update(dt)) {
        // Destroy this bullet
        bulletsToRemove.push(bullet);
      }
    });
    this.bullets = this.bullets.filter(
      bullet => !bulletsToRemove.includes(bullet),
    );

    // Update each player
    Object.keys(this.sockets).forEach(playerID => {
      const player = this.players[playerID];
      const newBullet = player.update(dt);
      if (newBullet) {
        this.bullets.push(newBullet);
      }
    });

    // Apply collisions, give players score for hitting bullets
    const destroyedBullets = applyCollisions(
      Object.values(this.players),
      this.bullets,
    );
    destroyedBullets.forEach(b => {
      if (this.players[b.parentID]) {
        this.players[b.parentID].onDealtDamage();
      }
    });
    this.bullets = this.bullets.filter(
      bullet => !destroyedBullets.includes(bullet),
    );

    // Check if any players are dead
    Object.keys(this.sockets).forEach(playerID => {
      const socket = this.sockets[playerID];
      const player = this.players[playerID];
      if (player.hp <= 0) {
        socket.emit(Constants.MSG_TYPES.GAME_OVER);
        this.removePlayer(socket);
      }
    });

    // Send a game update to each player every other time
    if (this.shouldSendUpdate) {
      const leaderboard = this.getLeaderboard();
      Object.keys(this.sockets).forEach(playerID => {
        const socket = this.sockets[playerID];
        const player = this.players[playerID];
        socket.emit(
          Constants.MSG_TYPES.GAME_UPDATE,
          this.createUpdate(player, leaderboard),
        );
      });
      this.shouldSendUpdate = false;
    } else {
      this.shouldSendUpdate = true;
    }
  }

  // ...
}

Dull update() yn cynnwys efallai'r darn pwysicaf o resymeg ochr y gweinydd. Dyma beth mae'n ei wneud, mewn trefn:

  1. Yn cyfrifo pa mor hir dt pasio ers y diweddaf update().
  2. Yn adnewyddu pob taflunydd ac yn eu dinistrio os oes angen. Byddwn yn gweld gweithrediad y swyddogaeth hon yn ddiweddarach. Am y tro, mae'n ddigon i ni wybod hynny bullet.update() yn dychwelyd trueos dylid dinistrio'r taflunydd (camodd o'r arena).
  3. Yn diweddaru pob chwaraewr ac yn silio taflunydd os oes angen. Byddwn hefyd yn gweld y gweithrediad hwn yn ddiweddarach - player.update() yn gallu dychwelyd gwrthrych Bullet.
  4. Gwiriadau am wrthdrawiadau rhwng taflegrau a chwaraewyr gyda applyCollisions(), sy'n dychwelyd amrywiaeth o daflegrau sy'n taro chwaraewyr. Ar gyfer pob taflunydd a ddychwelir, rydym yn cynyddu pwyntiau'r chwaraewr a'i taniodd (gan ddefnyddio player.onDealtDamage()) ac yna tynnwch y taflunydd o'r arae bullets.
  5. Hysbysu a dinistrio'r holl chwaraewyr a laddwyd.
  6. Yn anfon diweddariad gêm i bob chwaraewr bob eiliad adegau pan y'i gelwir update(). Mae hyn yn ein helpu i gadw golwg ar y newidyn ategol a grybwyllir uchod. shouldSendUpdate. Fel update() o'r enw 60 gwaith / s, rydym yn anfon diweddariadau gêm 30 gwaith / s. Felly, amlder cloc cloc gweinydd yw 30 cloc/s (fe wnaethom siarad am gyfraddau cloc yn y rhan gyntaf).

Pam anfon diweddariadau gêm yn unig trwy amser ? I arbed sianel. Mae 30 diweddariad gêm yr eiliad yn llawer!

Beth am ffonio update() 30 gwaith yr eiliad? Er mwyn gwella'r efelychiad gêm. Po amlaf a elwir update(), y mwyaf cywir fydd yr efelychiad gêm. Ond peidiwch â mynd yn rhy bell gyda nifer yr heriau. update(), oherwydd mae hon yn dasg gyfrifiadol ddrud - mae 60 yr eiliad yn ddigon.

Gweddill y dosbarth Game yn cynnwys dulliau cynorthwyydd a ddefnyddir yn update():

gêm.js rhan 3

class Game {
  // ...

  getLeaderboard() {
    return Object.values(this.players)
      .sort((p1, p2) => p2.score - p1.score)
      .slice(0, 5)
      .map(p => ({ username: p.username, score: Math.round(p.score) }));
  }

  createUpdate(player, leaderboard) {
    const nearbyPlayers = Object.values(this.players).filter(
      p => p !== player && p.distanceTo(player) <= Constants.MAP_SIZE / 2,
    );
    const nearbyBullets = this.bullets.filter(
      b => b.distanceTo(player) <= Constants.MAP_SIZE / 2,
    );

    return {
      t: Date.now(),
      me: player.serializeForUpdate(),
      others: nearbyPlayers.map(p => p.serializeForUpdate()),
      bullets: nearbyBullets.map(b => b.serializeForUpdate()),
      leaderboard,
    };
  }
}

getLeaderboard() eithaf syml - mae'n didoli'r chwaraewyr yn ôl sgôr, yn cymryd y pump uchaf, ac yn dychwelyd yr enw defnyddiwr a sgôr ar gyfer pob un.

createUpdate() a ddefnyddir yn update() i greu diweddariadau gêm sy'n cael eu dosbarthu i chwaraewyr. Ei brif dasg yw galw dulliau serializeForUpdate()gweithredu ar gyfer dosbarthiadau Player и Bullet. Sylwch ei fod ond yn trosglwyddo data i bob chwaraewr o gwmpas agosaf chwaraewyr a thaflegrau - nid oes angen trosglwyddo gwybodaeth am wrthrychau gêm sy'n bell oddi wrth y chwaraewr!

3. Gwrthrychau gêm ar y gweinydd

Yn ein gêm, mae taflegrau a chwaraewyr yn debyg iawn mewn gwirionedd: maen nhw'n wrthrychau gêm haniaethol, crwn, symudol. Er mwyn manteisio ar y tebygrwydd hwn rhwng chwaraewyr a thaflegrau, gadewch i ni ddechrau trwy weithredu'r dosbarth sylfaen Object:

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

Nid oes dim byd cymhleth yn digwydd yma. Bydd y dosbarth hwn yn bwynt angori da ar gyfer yr estyniad. Gawn ni weld sut mae'r dosbarth Bullet defnyddiau Object:

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

Gweithredu Bullet byr iawn! Rydym wedi ychwanegu at Object dim ond yr estyniadau canlynol:

  • Gan ddefnyddio'r pecyn shortid ar gyfer cynhyrchu ar hap id tafell.
  • Ychwanegu maes parentIDfel y gallwch olrhain y chwaraewr a greodd y taflun hwn.
  • Ychwanegu gwerth dychwelyd i update(), sy'n hafal i trueos yw'r taflunydd y tu allan i'r arena (cofiwch i ni siarad am hyn yn yr adran olaf?).

Gadewch i ni symud ymlaen i Player:

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

Mae chwaraewyr yn fwy cymhleth na thaflegrau, felly dylid storio ychydig mwy o feysydd yn y dosbarth hwn. Ei ddull update() yn gwneud llawer o waith, yn arbennig, yn dychwelyd y taflunydd newydd ei greu os nad oes un ar ôl fireCooldown (cofiwch i ni siarad am hyn yn yr adran flaenorol?). Hefyd mae'n ymestyn y dull serializeForUpdate(), oherwydd mae angen i ni gynnwys meysydd ychwanegol ar gyfer y chwaraewr yn y diweddariad gêm.

Cael dosbarth sylfaen Object - cam pwysig i osgoi ailadrodd cod. Er enghraifft, dim dosbarth Object rhaid i bob gwrthrych gêm gael yr un gweithrediad distanceTo(), a byddai copïo'r holl weithrediadau hyn ar draws sawl ffeil yn hunllef. Daw hyn yn arbennig o bwysig ar gyfer prosiectau mawr.pan fydd nifer y ehangu Object dosbarthiadau yn tyfu.

4. Canfod gwrthdrawiadau

Yr unig beth sydd ar ôl i ni yw adnabod pan fydd y tafluniau yn taro'r chwaraewyr! Cofiwch y darn hwn o god o'r dull update() yn y dosbarth Game:

gêm.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),
    );

    // ...
  }
}

Mae angen inni roi’r dull ar waith applyCollisions(), sy'n dychwelyd yr holl projectiles sy'n taro chwaraewyr. Yn ffodus, nid yw mor anodd ei wneud oherwydd

  • Mae'r holl wrthrychau gwrthdaro yn gylchoedd, sef y siâp symlaf i weithredu canfod gwrthdrawiad.
  • Mae gennym ni ddull yn barod distanceTo(), a weithredwyd gennym yn yr adran flaenorol yn y dosbarth Object.

Dyma sut olwg sydd ar ein gweithrediad o ganfod gwrthdrawiadau:

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

Mae'r canfod gwrthdrawiad syml hwn yn seiliedig ar y ffaith bod mae dau gylch yn gwrthdaro os yw'r pellter rhwng eu canol yn llai na chyfanswm eu radii. Dyma'r achos lle mae'r pellter rhwng canol dau gylch yn union hafal i swm eu radiysau:

Creu Gêm We Aml-chwaraewr .io
Mae cwpl o agweddau eraill i'w hystyried yma:

  • Rhaid i'r taflunydd beidio â tharo'r chwaraewr a'i creodd. Gellir cyflawni hyn trwy gymharu bullet.parentID с player.id.
  • Dim ond unwaith y mae'n rhaid i'r taflunydd daro yn achos cyfyngol chwaraewyr lluosog yn gwrthdaro ar yr un pryd. Byddwn yn datrys y broblem hon gan ddefnyddio'r gweithredwr break: cyn gynted ag y darganfyddir y chwaraewr sy'n gwrthdaro â'r taflunydd, rydyn ni'n atal y chwiliad ac yn symud ymlaen i'r taflunydd nesaf.

Конец

Dyna i gyd! Rydyn ni wedi cwmpasu popeth sydd angen i chi ei wybod i greu gêm we .io. Beth sydd nesaf? Adeiladwch eich gêm .io eich hun!

Mae'r holl god sampl yn ffynhonnell agored ac yn cael ei bostio ymlaen Github.

Ffynhonnell: hab.com

Ychwanegu sylw