Олон тоглогч .io вэб тоглоом үүсгэх

Олон тоглогч .io вэб тоглоом үүсгэх
2015 онд гарсан Agar.io шинэ жанрын өвөг дээдэс болсон тоглоомууд .ioтэр цагаас хойш нэр хүнд нь өссөн. Би хувьдаа .io тоглоомуудын нэр хүнд өсөхийг мэдэрсэн: сүүлийн гурван жилийн хугацаанд би үүнийг мэдэрсэн энэ төрлийн хоёр тоглоом бүтээж зарсан..

Хэрэв та эдгээр тоглоомуудын талаар хэзээ ч сонсож байгаагүй бол эдгээр нь тоглоход хялбар олон тоглогчтой үнэгүй вэб тоглоомууд юм (бүртгэл шаардлагагүй). Тэд ихэвчлэн нэг талбарт олон тооны өрсөлдөгчидтэй тулгардаг. Бусад алдартай .io тоглоомууд: Slither.io и Diep.io.

Энэ нийтлэлд бид хэрхэн хийхийг судлах болно .io тоглоомыг эхнээс нь үүсгэх. Үүний тулд зөвхөн Javascript-ийн мэдлэг хангалттай байх болно: та синтакс гэх мэт зүйлсийг ойлгох хэрэгтэй ES6, түлхүүр үг this и амлалтууд. Таны Javascript-ийн мэдлэг төгс биш байсан ч та нийтлэлийн ихэнхийг ойлгож чадна.

.io тоглоомын жишээ

Сурах тусламжийн хувьд бид лавлана .io тоглоомын жишээ. Тоглож үзээрэй!

Олон тоглогч .io вэб тоглоом үүсгэх
Тоглоом нь маш энгийн: та бусад тоглогчид байдаг талбайд хөлөг онгоцыг удирддаг. Таны хөлөг онгоц автоматаар сум харвадаг бөгөөд та бусад тоглогчдыг сумнаас нь зайлсхийж цохихыг оролддог.

1. Төслийн товч тойм / бүтэц

Би санал болгож байна эх кодыг татаж авах Та намайг дагах боломжтой тоглоомын жишээ.

Жишээ нь дараахь зүйлийг ашигладаг.

  • илэрхийл тоглоомын вэб серверийг удирддаг хамгийн алдартай Node.js вэб фреймворк юм.
  • socket.io - хөтөч болон сервер хооронд өгөгдөл солилцох websocket номын сан.
  • Вэб хуудас - модулийн менежер. Та яагаад Webpack ашиглах талаар уншиж болно. энд.

Төслийн лавлах бүтэц нь дараах байдалтай байна.

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

үзэгчид/

Бүх зүйл хавтасанд байна public/ серверээс статик байдлаар илгээгдэх болно. IN public/assets/ Манай төслийн ашигласан зургуудыг агуулна.

SRC /

Бүх эх код хавтас дотор байна src/. Гарчиг client/ и server/ өөрсдийнхөө төлөө ярих ба shared/ Үйлчлүүлэгч болон серверийн аль алиных нь импортолсон тогтмол файлыг агуулдаг.

2. Ассемблей/төслийн тохиргоо

Дээр дурдсанчлан бид төслийг бүтээхдээ модулийн менежерийг ашигладаг. Вэб хуудас. Манай 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',
    }),
  ],
};

Энд байгаа хамгийн чухал мөрүүд нь:

  • src/client/index.js Javascript (JS) үйлчлүүлэгчийн нэвтрэх цэг юм. Webpack эндээс эхэлж, бусад импортын файлуудыг рекурсив байдлаар хайх болно.
  • Манай Webpack-ийн гаралтын JS нь лавлахад байрлана dist/. Би энэ файлыг манайх гэж нэрлэх болно js багц.
  • Бидний хэрэглэдэг Babel, ялангуяа тохиргоо @babel/preset-env Манай JS кодыг хуучин хөтчүүдэд шилжүүлэх.
  • Бид JS файлуудын эш татсан бүх CSS-г задалж, нэг газар нэгтгэхийн тулд залгаасыг ашиглаж байна. Би түүнийг манайх гэж дуудна css багц.

Та хачирхалтай багц файлын нэрийг анзаарсан байх '[name].[contenthash].ext'. Тэд агуулдаг файлын нэр солих Вэбпак: [name] оролтын цэгийн нэрээр солигдох болно (бидний тохиолдолд энэ game), ба [contenthash] файлын агуулгын хэшээр солигдох болно. Бид үүнийг хийдэг хэш хийх төслийг оновчтой болгох - Та хөтөчүүдэд манай JS багцуудыг тодорхойгүй хугацаагаар кэш хийхийг хэлж болно, учир нь хэрэв багц өөрчлөгдвөл түүний файлын нэр мөн өөрчлөгдөнө (өөрчлөлт contenthash). Эцсийн үр дүн нь харах файлын нэр байх болно game.dbeee76e91a97d0c7207.js.

файл webpack.common.js нь бидний хөгжүүлэлт болон дууссан төслийн тохиргоонд импортлох үндсэн тохиргооны файл юм. Энд жишээ хөгжүүлэлтийн тохиргоо байна:

webpack.dev.js

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

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

Үр ашгийн хувьд бид хөгжлийн явцад ашигладаг webpack.dev.js, мөн рүү шилждэг webpack.prod.jsүйлдвэрлэлд нэвтрүүлэх үед багцын хэмжээг оновчтой болгох.

Орон нутгийн тохиргоо

Би уг төслийг локал машин дээр суулгахыг зөвлөж байна, ингэснээр та энэ нийтлэлд жагсаасан алхмуудыг дагаж болно. Тохируулга нь энгийн: эхлээд систем суулгасан байх ёстой Зангилаа и NPM. Дараа нь та хийх хэрэгтэй

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

мөн та явахад бэлэн байна! Хөгжүүлэгч серверийг эхлүүлэхийн тулд зүгээр л ажиллуулна уу

$ npm run develop

вэб хөтөч рүү очно уу localhost: 3000. Код өөрчлөгдөхөд хөгжүүлэлтийн сервер нь JS болон CSS багцуудыг автоматаар дахин бүтээх болно - бүх өөрчлөлтийг харахын тулд хуудсыг шинэчилнэ үү!

3. Үйлчлүүлэгчийн нэвтрэх цэгүүд

Тоглоомын код руугаа орцгооё. Эхлээд бидэнд хуудас хэрэгтэй index.html, сайтад зочлох үед хөтөч эхлээд ачаалах болно. Манай хуудас маш энгийн байх болно:

index.html

Жишээ нь io тоглоом  ТОГЛОХ

Энэ кодын жишээг ойлгомжтой болгох үүднээс бага зэрэг хялбаршуулсан бөгөөд би бусад олон нийтлэлийн жишээн дээр ижил зүйлийг хийх болно. Бүрэн кодыг үргэлж эндээс үзэх боломжтой Github.

Бидэнд байгаа:

  • HTML5 зураг элемент (<canvas>) үүнийг бид тоглоомыг үзүүлэхэд ашиглах болно.
  • <link> Манай CSS багцыг нэмэх.
  • <script> Манай Javascript багцыг нэмэх.
  • Хэрэглэгчийн нэр бүхий үндсэн цэс <input> болон PLAY товчлуур (<button>).

Нүүр хуудсыг ачаалсны дараа хөтөч JS файлын оролтын цэгээс эхлэн Javascript кодыг ажиллуулж эхэлнэ. 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);
  };
});

Энэ нь төвөгтэй мэт санагдаж болох ч энд тийм ч их зүйл алга:

  1. Бусад хэд хэдэн JS файлыг импортлож байна.
  2. CSS импорт (Тиймээс Webpack тэдгээрийг манай CSS багцад оруулахаа мэддэг).
  3. Ажиллуулах connect() сервертэй холбоо тогтоож ажиллуулна downloadAssets() тоглоомыг үзүүлэхэд шаардлагатай зургийг татаж авах.
  4. 3-р үе шат дууссаны дараа үндсэн цэс гарч ирнэ (playMenu).
  5. "PLAY" товчийг дарахад тохируулагчийг тохируулна уу. Товчлуур дээр дарахад код нь тоглоомыг эхлүүлж, серверт бид тоглоход бэлэн байна гэж хэлдэг.

Манай клиент-серверийн логикийн гол "мах" нь файлаар импортлогдсон файлуудад байдаг index.js. Одоо бид бүгдийг дарааллаар нь авч үзэх болно.

4. Хэрэглэгчийн мэдээлэл солилцох

Энэ тоглоомонд бид сервертэй харилцахдаа алдартай номын санг ашигладаг socket.io. Socket.io нь үндсэн дэмжлэгтэй вэб залгуурууд, энэ нь хоёр талын харилцаанд тохиромжтой: бид сервер рүү мессеж илгээх боломжтой и сервер нь ижил холболтоор бидэнд мессеж илгээх боломжтой.

Бидэнд нэг файл байх болно src/client/networking.jsхэн халамжлах вэ хүн бүр сервертэй харилцах:

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

Энэ кодыг мөн тодорхой болгох үүднээс бага зэрэг богиносгосон.

Энэ файлд гурван үндсэн үйлдэл байна:

  • Бид серверт холбогдохыг оролдож байна. connectedPromise зөвхөн холболт тогтоосон үед л зөвшөөрнө.
  • Хэрэв холболт амжилттай бол бид буцаан залгах функцийг бүртгэдэг (processGameUpdate() и onGameOver()) серверээс хүлээн авах мессежүүдийн хувьд.
  • Бид экспортолдог play() и updateDirection()Ингэснээр бусад файлууд тэдгээрийг ашиглах боломжтой болно.

5. Client Rendering

Дэлгэц дээр зургийг харуулах цаг боллоо!

…гэхдээ үүнийг хийхээс өмнө бид үүнд шаардлагатай бүх зургийг (нөөцийг) татаж авах хэрэгтэй. Нөөцийн менежер бичье:

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

Нөөцийн менежментийг хэрэгжүүлэх нь тийм ч хэцүү биш юм! Гол санаа нь объектыг хадгалах явдал юм assets, энэ нь файлын нэрний түлхүүрийг объектын утгатай холбох болно Image. Нөөцийг ачаалах үед бид үүнийг объектод хадгалдаг assets ирээдүйд хурдан нэвтрэхийн тулд. Хувь хүний ​​нөөц бүрийг хэзээ татаж авахыг зөвшөөрөх вэ (өөрөөр хэлбэл, бүх нөөц), бид зөвшөөрдөг downloadPromise.

Нөөцүүдийг татаж авсны дараа та дүрсэлж эхлэх боломжтой. Өмнө дурьдсанчлан, бид вэб хуудсан дээр зурахдаа ашигладаг HTML5 Canvas (<canvas>). Бидний тоглоом маш энгийн тул бид дараах зүйлийг зурахад л хангалттай.

  1. Оршил
  2. Тоглогчийн хөлөг онгоц
  3. Тоглоомын бусад тоглогчид
  4. хясаа

Энд чухал хэсгүүд байна src/client/render.js, энэ нь дээр дурдсан яг дөрвөн зүйлийг харуулж байна:

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

Энэ кодыг мөн тодорхой болгох үүднээс товчилсон болно.

render() нь энэ файлын үндсэн функц юм. startRendering() и stopRendering() 60 FPS дээр үзүүлэх давталтыг идэвхжүүлэхийг хянах.

Туслах туслах функцүүдийн бие даасан хэрэгжилт (жишээ нь: renderBullet()) тийм ч чухал биш, гэхдээ энд нэг энгийн жишээ байна:

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

Бид энэ аргыг хэрэглэж байгааг анхаарна уу getAsset(), өмнө нь харж байсан asset.js!

Хэрэв та бусад рендерийн туслахуудыг судлах сонирхолтой байгаа бол үлдсэн хэсгийг нь уншина уу src/client/render.js.

6. Үйлчлүүлэгчийн оролт

Тоглоом хийх цаг болжээ тоглох боломжтой! Хяналтын схем нь маш энгийн байх болно: хөдөлгөөний чиглэлийг өөрчлөхийн тулд та хулгана (компьютер дээр) эсвэл дэлгэц дээр (хөдөлгөөнт төхөөрөмж дээр) хүрч болно. Үүнийг хэрэгжүүлэхийн тулд бид бүртгүүлнэ Арга хэмжээний сонсогчид Хулгана болон мэдрэгчтэй үйл явдлуудад зориулагдсан.
Энэ бүхэнд санаа тавих болно src/client/input.js:

input.js

import { updateDirection } from './networking';

function onMouseInput(e) {
  handleInput(e.clientX, e.clientY);
}

function onTouchInput(e) {
  const touch = e.touches[0];
  handleInput(touch.clientX, touch.clientY);
}

function handleInput(x, y) {
  const dir = Math.atan2(x - window.innerWidth / 2, window.innerHeight / 2 - y);
  updateDirection(dir);
}

export function startCapturingInput() {
  window.addEventListener('mousemove', onMouseInput);
  window.addEventListener('touchmove', onTouchInput);
}

export function stopCapturingInput() {
  window.removeEventListener('mousemove', onMouseInput);
  window.removeEventListener('touchmove', onTouchInput);
}

onMouseInput() и onTouchInput() дууддаг Үйл явдлын сонсогчид юм updateDirection() (аас.) networking.js) оролтын үйл явдал тохиолдох үед (жишээлбэл, хулганыг хөдөлгөх үед). updateDirection() оролтын үйл явдлыг зохицуулж, үүний дагуу тоглоомын төлөвийг шинэчилдэг сервертэй мессеж илгээдэг.

7. Үйлчлүүлэгчийн статус

Энэ хэсэг нь нийтлэлийн эхний хэсэгт хамгийн хэцүү байдаг. Хэрэв та үүнийг анх уншихдаа ойлгоогүй бол сэтгэлээр унах хэрэггүй! Та үүнийг алгасаад дараа нь эргэж ирж болно.

Үйлчлүүлэгч/серверийн кодыг бөглөхөд шаардлагатай оньсогоны сүүлчийн хэсэг төлөв. Client Rendering хэсгийн кодын хэсгийг санаж байна уу?

render.js

import { getCurrentState } from './state';

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

  // Do the rendering
  // ...
}

getCurrentState() Үйлчлүүлэгчийн тоглоомын өнөөгийн байдлыг бидэнд өгөх боломжтой байх ёстой цаг хугацааны аль ч үед серверээс хүлээн авсан шинэчлэлтүүд дээр үндэслэсэн. Серверийн илгээж болох тоглоомын шинэчлэлтийн жишээ энд байна:

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

Тоглоомын шинэчлэлт бүр таван ижил талбарыг агуулна:

  • t: Энэ шинэчлэлтийг хэзээ үүсгэснийг харуулсан серверийн цагийн тэмдэг.
  • me: Энэ шинэчлэлтийг хүлээн авч буй тоглогчийн талаарх мэдээлэл.
  • бусад: Нэг тоглоомонд оролцож буй бусад тоглогчдын талаарх мэдээллийн багц.
  • сумнууд: тоглоомын сумны тухай мэдээллийн массив.
  • тэргүүлэгчдийн самбар: Одоогийн тэргүүлэгчдийн самбарын өгөгдөл. Энэ нийтлэлд бид тэдгээрийг авч үзэхгүй.

7.1 Гэнэн үйлчлүүлэгчийн төлөв

Гэнэн хэрэгжилт getCurrentState() зөвхөн хамгийн сүүлд хүлээн авсан тоглоомын шинэчлэлийн өгөгдлийг шууд буцааж өгөх боломжтой.

naive-state.js

let lastGameUpdate = null;

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

export function getCurrentState() {
  return lastGameUpdate;
}

Сайхан бөгөөд ойлгомжтой! Гэхдээ ийм энгийн байсан бол. Энэхүү хэрэгжилт нь асуудалтай байгаа шалтгаануудын нэг нь: Энэ нь үзүүлэх фрэймийн хурдыг серверийн цагийн хурдаар хязгаарладаг.

Frame Rate: фрэймийн тоо (жишээ нь дуудлага render()секундэд ) эсвэл FPS. Тоглоомууд ихэвчлэн дор хаяж 60 FPS хүрэхийг хичээдэг.

Шалгалтын ханш: Серверээс үйлчлүүлэгчид тоглоомын шинэчлэлтийг илгээх давтамж. Энэ нь ихэвчлэн фрэймийн хурдаас бага байдаг. Манай тоглоомонд сервер секундэд 30 циклийн давтамжтайгаар ажилладаг.

Хэрэв бид тоглоомын хамгийн сүүлийн үеийн шинэчлэлтийг хийвэл FPS нь үндсэндээ 30-аас дээш гарахгүй Бид серверээс секундэд 30-аас илүү шинэчлэлт авдаггүй. Бид залгасан ч гэсэн render() Секундэд 60 удаа, дараа нь эдгээр дуудлагын тал нь ижил зүйлийг дахин зурж, үндсэндээ юу ч хийхгүй. Гэнэн хэрэгжүүлэлтийн өөр нэг асуудал бол энэ юм саатал гарах хандлагатай. Интернэтийн хамгийн тохиромжтой хурдтай бол үйлчлүүлэгч яг 33 мс тутамд (секундэд 30) тоглоомын шинэчлэлтийг хүлээн авах болно:

Олон тоглогч .io вэб тоглоом үүсгэх
Харамсалтай нь төгс зүйл гэж байдаггүй. Илүү бодитой зураг байх болно:
Олон тоглогч .io вэб тоглоом үүсгэх
Гэнэн хэрэгжүүлэлт нь хоцролттой холбоотой бараг хамгийн муу тохиолдол юм. Хэрэв тоглоомын шинэчлэлтийг 50 мс-ийн сааталтайгаар хүлээн авбал үйлчлүүлэгчийн лангуу Энэ нь өмнөх шинэчлэлтээс тоглоомын төлөвийг харуулсан хэвээр байгаа тул нэмэлт 50 мс. Тоглогчийн хувьд энэ нь ямар эвгүй байгааг та төсөөлж болно: дур мэдэн тоормослох нь тоглоомыг хурцадмал, тогтворгүй болгоно.

7.2 Үйлчлүүлэгчийн төлөв байдал сайжирсан

Бид гэнэн хэрэглэгдэхүүнд зарим нэг сайжруулалт хийх болно. Нэгдүгээрт, бид ашигладаг үзүүлэх саатал 100 мс. Энэ нь үйлчлүүлэгчийн "одоогийн" төлөв нь сервер дээрх тоглоомын төлөвөөс үргэлж 100 мс хоцрох болно гэсэн үг юм. Жишээлбэл, сервер дээрх цаг бол 150, дараа нь үйлчлүүлэгч тухайн үед сервер байсан төлөвийг үзүүлэх болно 50:

Олон тоглогч .io вэб тоглоом үүсгэх
Энэ нь тоглоомын шинэчлэлтийн урьдчилан таамаглах боломжгүй цагийг даван туулахын тулд 100 мс буфер өгдөг:

Олон тоглогч .io вэб тоглоом үүсгэх
Үүний үр өгөөж нь байнгын байх болно оролтын хоцрогдол 100 мс. Энэ бол тоглоомын жигд тоглолтын төлөөх өчүүхэн золиослол юм - ихэнх тоглогчид (ялангуяа энгийн тоглогчид) энэ саатлыг анзаардаггүй. Хүмүүс урьдчилан таамаглах боломжгүй хоцролттой тоглохоос хамаагүй 100 мс-ийн хоцрогдолд дасан зохицоход илүү хялбар байдаг.

Бид бас нэртэй өөр техникийг ашиглаж болно үйлчлүүлэгч талын таамаглал, энэ нь хүлээгдэж буй хоцролтыг багасгах сайн ажил хийдэг боловч энэ нийтлэлд тусгагдаагүй болно.

Бидний ашиглаж байгаа өөр нэг сайжруулалт шугаман интерполяци. Үзүүлэлтийн хоцрогдлын улмаас бид ихэвчлэн үйлчлүүлэгчийн одоогийн цаг хугацаанаас дор хаяж нэг удаа шинэчлэгдсэн байдаг. Дуудсан үед getCurrentState(), бид гүйцэтгэж чадна шугаман интерполяци Үйлчлүүлэгчийн одоогийн цагийн өмнөх болон дараа нь тоглоомын шинэчлэлтүүдийн хооронд:

Олон тоглогч .io вэб тоглоом үүсгэх
Энэ нь фрэймийн хурдтай холбоотой асуудлыг шийддэг: бид одоо хүссэн фрэймийн хурдаараа өвөрмөц фрэймүүдийг үзүүлэх боломжтой боллоо!

7.3 Сайжруулсан үйлчлүүлэгчийн төлөвийг хэрэгжүүлэх

Хэрэгжүүлэх жишээ нь src/client/state.js render lag болон шугаман интерполяцыг хоёуланг нь ашигладаг боловч удаан биш. Кодоо хоёр хэсэгт хувааж үзье. Энд эхнийх нь:

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

Эхний алхам бол юу болохыг олж мэдэх явдал юм currentServerTime(). Бидний өмнө нь харсанчлан тоглоомын шинэчлэлт бүр серверийн цагийн тэмдэгтэй байдаг. Бид серверийн ард 100 мс-ийн зайд зургийг гаргахын тулд үзүүлэх хоцролтыг ашиглахыг хүсч байна, гэхдээ Бид сервер дээрх одоогийн цагийг хэзээ ч мэдэхгүй, учир нь бид ямар ч шинэчлэлтийг авах хүртэл хэр удсаныг мэдэхгүй. Интернет нь урьдчилан таамаглах аргагүй бөгөөд хурд нь маш өөр байж болно!

Энэ асуудлыг тойрч гарахын тулд бид боломжийн ойролцооллыг ашиглаж болно: бид Эхний шинэчлэлт шууд ирсэн мэт дүр эсгэх. Хэрэв энэ үнэн байсан бол бид яг энэ мөчид серверийн цагийг мэдэх байсан! Бид серверийн цагийн тэмдгийг хадгалдаг firstServerTimestamp мөн бидний орон нутгийн (үйлчлүүлэгч) тухайн агшинд цагийн тэмдэг gameStart.

Хүлээгээрэй. Энэ нь серверийн цаг = үйлчлүүлэгчийн цаг байх ёстой биш үү? Бид яагаад "серверийн цагийн тэмдэг" болон "үйлчлүүлэгчийн цагийн тэмдэг"-ийг ялгадаг вэ? Энэ бол гайхалтай асуулт! Энэ нь тэд ижил зүйл биш юм. Date.now() үйлчлүүлэгч болон серверт өөр өөр цагийн тэмдэг буцаах бөгөөд энэ нь эдгээр машинуудын локал хүчин зүйлээс хамаарна. Цагийн тэмдэг нь бүх машин дээр ижил байна гэж хэзээ ч бүү бод.

Одоо бид юу болохыг ойлгож байна currentServerTime(): буцаж ирнэ одоогийн үзүүлэх цагийн серверийн цагийн тэмдэг. Өөрөөр хэлбэл, энэ нь серверийн одоогийн цаг (firstServerTimestamp <+ (Date.now() - gameStart)) үзүүлэх саатлыг хасах (RENDER_DELAY).

Одоо бид тоглоомын шинэчлэлтийг хэрхэн зохицуулж байгааг харцгаая. Шинэчлэлтийн серверээс хүлээн авах үед үүнийг дууддаг processGameUpdate()мөн бид шинэ шинэчлэлтийг массив болгон хадгалдаг gameUpdates. Дараа нь санах ойн ашиглалтыг шалгахын тулд бид өмнөх бүх хуучин шинэчлэлтүүдийг устгадаг суурь шинэчлэлУчир нь тэд бидэнд хэрэггүй болсон.

"Үндсэн шинэчлэл" гэж юу вэ? Энэ серверийн одоогийн цагийг буцаах замаар бидний олсон анхны шинэчлэлт. Энэ диаграмыг санаж байна уу?

Олон тоглогч .io вэб тоглоом үүсгэх
"Client Render Time"-ын зүүн талд байгаа тоглоомын шинэчлэлт нь үндсэн шинэчлэлт юм.

Үндсэн шинэчлэлтийг юунд ашигладаг вэ? Бид яагаад шинэчлэлтүүдийг үндсэн түвшинд буулгаж болох вэ? Үүнийг ойлгохын тулд үзье эцэст нь хэрэгжилтийг авч үзэх getCurrentState():

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

Бид гурван хэргийг хянана:

  1. base < 0 Одоогийн үзүүлэх хугацаа хүртэл шинэчлэлт байхгүй гэсэн үг (дээрх хэрэгжүүлэлтийг үзнэ үү getBaseUpdate()). Энэ нь тоглолтын эхэн үед рендерийн хоцрогдолтойгоор тохиолдож болно. Энэ тохиолдолд бид хамгийн сүүлийн үеийн шинэчлэлтийг ашигладаг.
  2. base нь бидний хамгийн сүүлийн үеийн шинэчлэлт юм. Энэ нь сүлжээний саатал эсвэл интернетийн холболт муутай холбоотой байж болох юм. Энэ тохиолдолд бид хамгийн сүүлийн үеийн шинэчлэлтийг ашиглаж байна.
  3. Бидэнд одоогийн үзүүлэх хугацаанаас өмнө болон дараа шинэчлэлт байгаа тул боломжтой интерполяци хийх!

Үлдсэн бүх зүйл state.js нь энгийн (гэхдээ уйтгартай) математикийн шугаман интерполяцийн хэрэгжилт юм. Хэрэв та өөрөө үүнийг судлахыг хүсч байвал нээнэ үү state.js тухай Github.

2-р хэсэг. Backend сервер

Энэ хэсэгт бид бидний удирддаг Node.js backend-ийг авч үзэх болно .io тоглоомын жишээ.

1. Серверийн нэвтрэх цэг

Вэб серверийг удирдахын тулд бид Node.js нэртэй алдартай вэб хүрээг ашиглах болно илэрхийл. Үүнийг манай серверийн нэвтрэх цэгийн файлаар тохируулах болно src/server/server.js:

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

Эхний хэсэгт бид Webpack-ийн талаар ярилцсаныг санаж байна уу? Энд бид өөрсдийн Webpack тохиргоог ашиглах болно. Бид тэдгээрийг хоёр аргаар ашиглах болно:

  • Хэрэглэх webpack-dev-middleware манай хөгжлийн багцуудыг автоматаар дахин бүтээх, эсвэл
  • фолдерыг статик байдлаар шилжүүлэх dist/, вэбпак нь үйлдвэрлэлийн бүтээцийн дараа бидний файлуудыг бичих болно.

Өөр нэг чухал ажил server.js серверийг тохируулах явдал юм socket.ioЭкспресс сервертэй шууд холбогддог:

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

Сервертэй socket.io холболтыг амжилттай байгуулсны дараа бид шинэ залгуурт үйл явдал зохицуулагчийг тохируулсан. Үйл явдал зохицуулагчид үйлчлүүлэгчдээс хүлээн авсан мессежийг синглтон объект руу шилжүүлэх замаар зохицуулдаг game:

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

Бид .io тоглоом бүтээж байгаа тул бидэнд зөвхөн нэг хуулбар хэрэгтэй Game ("Тоглоом") - бүх тоглогчид нэг талбарт тоглодог! Дараагийн хэсэгт бид энэ анги хэрхэн ажилладагийг харах болно. Game.

2. Тоглоомын серверүүд

Класс Game сервер талын хамгийн чухал логикийг агуулсан. Энэ нь хоёр үндсэн ажилтай: тоглогчийн удирдлага и тоглоомын симуляци.

Эхний даалгавар болох тоглогчийн менежментээс эхэлье.

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

  // ...
}

Энэ тоглоомонд бид тоглогчдыг талбайгаар нь тодорхойлох болно id Тэдний socket.io залгуур (хэрэв та андуурвал буцаж очно уу server.js). Socket.io өөрөө сокет бүрт өвөрмөц оноодог idтиймээс бид үүнд санаа зовох хэрэггүй. Би түүнийг дуудна Тоглогчийн ID.

Үүнийг харгалзан анги дахь хувьсагч хувьсагчдыг судалцгаая Game:

  • sockets нь тоглогчийн ID-г тоглогчтой холбоотой залгуурт холбодог объект юм. Энэ нь бидэнд тоглуулагчийн ID-аар нь залгуурт нэвтрэх боломжийг олгодог.
  • players нь тоглогчийн ID-г код>Тоглогчийн объекттой холбодог объект юм

bullets объектуудын массив юм Bullet, ямар ч тодорхой дараалал байхгүй.
lastUpdateTime Энэ нь тоглоомыг хамгийн сүүлд шинэчилсэн цагийн тэмдэг юм. Үүнийг хэрхэн ашиглахыг бид удахгүй харах болно.
shouldSendUpdate нь туслах хувьсагч юм. Бид удахгүй түүний хэрэглээг харах болно.
Арга зүй addPlayer(), removePlayer() и handleInput() тайлбарлах шаардлагагүй, тэдгээрийг ашигладаг server.js. Хэрэв та ой санамжаа сэргээх шаардлагатай бол арай өндөрт буцаж оч.

Сүүлийн мөр constructor() эхэлнэ шинэчлэх мөчлөг тоглоомууд (60 шинэчлэлт / с давтамжтай):

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

  // ...
}

арга update() сервер талын логикийн хамгийн чухал хэсгийг агуулсан байж магадгүй. Энэ нь юу хийдэг вэ, дарааллаар нь:

  1. Хэр удаан болохыг тооцоолно dt сүүлчийнхээс хойш өнгөрсөн update().
  2. Сум тус бүрийг сэргээж, шаардлагатай бол устгана. Энэ функцийн хэрэгжилтийг бид дараа нь харах болно. Одоохондоо үүнийг мэдэхэд хангалттай bullet.update() буцаж ирдэг trueхэрэв сумыг устгах шаардлагатай бол (тэр талбайгаас гарав).
  3. Тоглогч бүрийг шинэчилж, шаардлагатай бол сум гаргадаг. Бид энэ хэрэгжилтийг дараа нь харах болно - player.update() объектыг буцааж болно Bullet.
  4. Пуужингууд болон тоглогчдын хооронд мөргөлдөх эсэхийг шалгана applyCollisions(), энэ нь тоглогчдыг оносон олон тооны сумыг буцаана. Буцсан сум бүрийн хувьд бид түүнийг харвасан тоглогчийн оноог нэмэгдүүлдэг ( player.onDealtDamage()) дараа нь сумыг массиваас салга bullets.
  5. Бүх нас барсан тоглогчдыг мэдэгдэж, устгадаг.
  6. Тоглоомын шинэчлэлтийг бүх тоглогчдод илгээдэг секунд тутамд дуудсан удаа update(). Энэ нь дээр дурдсан туслах хувьсагчийг хянахад бидэнд тусалдаг. shouldSendUpdate... Байдлаар update() 60 удаа/с дуудсан, бид тоглоомын шинэчлэлтийг 30 удаа/с илгээдэг. Тиймээс, цагийн давтамж серверийн цаг нь 30 цаг / с (бид эхний хэсэгт цагийн хурдны талаар ярьсан).

Яагаад зөвхөн тоглоомын шинэчлэлтийг илгээдэг вэ? цаг хугацаагаар ? Суваг хадгалахын тулд. Секундэд 30 тоглоомын шинэчлэлт нь маш их юм!

Яагаад зүгээр залгаж болохгүй гэж update() Секундэд 30 удаа? Тоглоомын симуляцийг сайжруулахын тулд. Илүү олон удаа дууддаг update(), тоглоомын загварчлал илүү нарийвчлалтай байх болно. Гэхдээ олон тооны сорилтод бүү хэт авт. update(), учир нь энэ нь тооцоолоход үнэтэй ажил юм - секундэд 60 хангалттай.

Ангийн үлдсэн хэсэг Game ашигласан туслах аргуудаас бүрдэнэ update():

game.js 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() маш энгийн - энэ нь тоглогчдыг оноогоор нь ангилж, эхний тавыг авч, хэрэглэгчийн нэр, оноог буцаана.

createUpdate() -д ашигласан update() тоглогчдод түгээдэг тоглоомын шинэчлэлийг бий болгох. Үүний гол үүрэг бол аргуудыг дуудах явдал юм serializeForUpdate()ангиудад хэрэгжсэн Player и Bullet. Энэ нь зөвхөн тоглогч бүрт мэдээлэл дамжуулдаг гэдгийг анхаарна уу хамгийн ойр тоглогчид ба сумнууд - тоглогчоос хол байгаа тоглоомын объектын талаар мэдээлэл дамжуулах шаардлагагүй!

3. Сервер дээрх тоглоомын объектууд

Манай тоглоомонд сумнууд болон тоглогчид хоорондоо маш төстэй байдаг: тэдгээр нь хийсвэр, дугуй, хөдөлгөөнт тоглоомын объектууд юм. Тоглогчид болон сумны хоорондох ижил төстэй байдлыг ашиглахын тулд үндсэн ангиллыг хэрэгжүүлж эхэлцгээе 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,
    };
  }
}

Энд ямар ч төвөгтэй зүйл байхгүй. Энэ анги нь өргөтгөлийн сайн зангуу цэг байх болно. Хичээл яаж байгааг харцгаая Bullet ашигладаг 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;
  }
}

Реализация Bullet Маш намхан! Бид нэмсэн Object зөвхөн дараах өргөтгөлүүд:

  • Багц ашиглах богинохон санамсаргүй үеийн хувьд id сум.
  • Талбар нэмж байна parentIDИнгэснээр та энэ сумыг бүтээсэн тоглогчийг хянах боломжтой.
  • -д буцах утгыг нэмж байна update(), энэ нь тэнцүү байна trueхэрэв сум талбайн гадна байгаа бол (бид энэ талаар сүүлийн хэсэгт ярьж байсныг санаж байна уу?).

Дараа нь үргэлжлүүлье 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,
    };
  }
}

Тоглогчид сумнаас илүү төвөгтэй тул энэ ангид хэд хэдэн талбайг хадгалах хэрэгтэй. Түүний арга update() маш их ажил хийдэг, ялангуяа шинээр бий болсон сум байхгүй бол буцааж өгдөг fireCooldown (Бид өмнөх хэсэгт энэ тухай ярьж байсныг санаж байна уу?). Мөн энэ нь аргыг өргөжүүлдэг serializeForUpdate(), учир нь бид тоглоомын шинэчлэлд тоглогчийн нэмэлт талбаруудыг оруулах хэрэгтэй.

Суурь ангитай байх Object - кодыг давтахаас зайлсхийх чухал алхам. Жишээлбэл, анги байхгүй Object Тоглоомын объект бүр ижил хэрэгжилттэй байх ёстой distanceTo(), мөн эдгээр бүх хэрэгжилтийг олон файл дээр хуулж буулгах нь хар дарсан зүүд байх болно. Энэ нь ялангуяа томоохон төслүүдэд чухал ач холбогдолтой юм.тоо өргөжих үед Object ангиуд нэмэгдэж байна.

4. Мөргөлдөөнийг илрүүлэх

Бидэнд үлдсэн цорын ганц зүйл бол пуужин тоглогчдыг хэзээ онохыг мэдэх явдал юм! Аргын энэ кодын хэсгийг санаарай update() ангид Game:

game.js

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

class Game {
  // ...

  update() {
    // ...

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

    // ...
  }
}

Бид арга барилаа хэрэгжүүлэх хэрэгтэй applyCollisions(), энэ нь тоглогчдыг оносон бүх сумыг буцаана. Аз болоход үүнийг хийхэд тийм ч хэцүү биш

  • Бүх мөргөлдөж буй объектууд нь тойрог бөгөөд мөргөлдөөнийг илрүүлэх хамгийн энгийн хэлбэр юм.
  • Бидэнд аль хэдийн арга бий distanceTo(), бид өмнөх хэсэгт ангидаа хэрэгжүүлсэн Object.

Мөргөлдөөнийг илрүүлэх бидний хэрэгжилт дараах байдалтай байна.

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

Энэхүү энгийн мөргөлдөөнийг илрүүлэх нь үүнд тулгуурладаг Хэрэв төвүүдийн хоорондох зай нь радиусуудын нийлбэрээс бага бол хоёр тойрог мөргөлдөнө. Хоёр тойргийн төвүүдийн хоорондох зай нь тэдгээрийн радиусуудын нийлбэртэй яг тэнцүү байх тохиолдол энд байна.

Олон тоглогч .io вэб тоглоом үүсгэх
Энд хэд хэдэн зүйлийг анхаарч үзэх хэрэгтэй:

  • Сум нь түүнийг бүтээсэн тоглогчийг онох ёсгүй. Үүнийг харьцуулах замаар хийж болно bullet.parentID с player.id.
  • Олон тоглогч нэгэн зэрэг мөргөлдсөн тохиолдолд сум зөвхөн нэг удаа онох ёстой. Бид энэ асуудлыг оператор ашиглан шийдэх болно break: сумтай мөргөлдсөн тоглогч олдмогц бид хайлтаа зогсоож, дараагийн сум руу шилжинэ.

Төгсгөл

Тэгээд л болоо! Бид таны .io вэб тоглоом бүтээхэд хэрэгтэй бүх зүйлийг авч үзсэн. Дараа нь юу юм? Өөрийн гэсэн .io тоглоом бүтээгээрэй!

Бүх загвар код нь нээлттэй эх сурвалж бөгөөд дээр байрлуулсан Github.

Эх сурвалж: www.habr.com

сэтгэгдэл нэмэх