.io ප්‍රභේදයේ බහු ක්‍රීඩක වෙබ් ක්‍රීඩාවක් නිර්මාණය කිරීම

.io ප්‍රභේදයේ බහු ක්‍රීඩක වෙබ් ක්‍රීඩාවක් නිර්මාණය කිරීම
2015 දී නිකුත් කරන ලදී Agar.io නව ප්‍රභේදයක ප්‍රධානියා බවට පත් විය games.io, එහි ජනප්‍රියත්වය එතැන් සිට විශාල ලෙස වර්ධනය වී ඇත. මම .io ක්‍රීඩා වල ජනප්‍රියතාවයේ නැගීම අත්විඳිමි: පසුගිය වසර තුන පුරා, අයි මෙම ප්‍රභේදයේ ක්‍රීඩා දෙකක් නිර්මාණය කර විකුණන ලදී..

ඔබ මීට පෙර මෙම ක්‍රීඩා ගැන අසා නොමැති නම්, ඒවා නොමිලේ, ක්‍රීඩා කිරීමට පහසු බහු ක්‍රීඩක වෙබ් ක්‍රීඩා වේ (ගිණුම අවශ්‍ය නොවේ). ඔවුන් සාමාන්‍යයෙන් බොහෝ ප්‍රතිවාදී ක්‍රීඩකයින් එක් පිටියකට දමයි. වෙනත් ප්‍රසිද්ධ .io ක්‍රීඩා: Slither.io и Diep.io.

මෙම ලිපියෙන් අපි එය කරන්නේ කෙසේදැයි සොයා බලමු මුල සිටම .io ක්‍රීඩාවක් සාදන්න. මෙය සිදු කිරීම සඳහා, Javascript පිළිබඳ දැනුම පමණක් ප්රමාණවත් වනු ඇත: ඔබ සින්ටැක්ස් වැනි දේ තේරුම් ගත යුතුය ES6, මූල පදය this и පොරොන්දු. ඔබ ජාවාස්ක්‍රිප්ට් නිවැරදිව නොදන්නවා වුවද, ඔබට තවමත් බොහෝ පෝස්ට් තේරුම් ගත හැකිය.

.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/ සේවාදායකය විසින් ස්ථිතිකව සම්ප්‍රේෂණය කරනු ලැබේ. තුල 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 ජාවාස්ක්‍රිප්ට් (ජේඑස්) සේවාලාභියාගේ ප්‍රවේශ ලක්ෂ්‍යය වේ. Webpack මෙතැන් සිට ආරම්භ වන අතර වෙනත් ආයාත කළ ගොනු සඳහා පුනරාවර්තන ලෙස සොයනු ඇත.
  • අපගේ Webpack ගොඩනැගීමේ ප්‍රතිදාන JS නාමාවලියෙහි පිහිටා ඇත dist/. මම මේ ෆයිල් එක අපේ කියලා කියන්නම් JS පැකේජය.
  • අපි පාවිච්චි කරන්නේ බබෙල්, සහ විශේෂයෙන්ම වින්යාසය @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, නිෂ්පාදනයට යෙදවීමේදී පැකේජ ප්‍රමාණය ප්‍රශස්ත කිරීමට.

දේශීය සැකසුම

ඔබගේ දේශීය යන්ත්‍රය මත ව්‍යාපෘතිය ස්ථාපනය කිරීමට මම නිර්දේශ කරමි, එවිට ඔබට මෙම පෝස්ටුවෙහි ලැයිස්තුගත කර ඇති පියවර අනුගමනය කළ හැක. සැකසුම සරලයි: පළමුව, පද්ධතිය තිබිය යුතුය node එකක් මතම ඊට අදාල и එන්පීඑම්. ඊළඟට ඔබ කළ යුතුයි

$ 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 ගොනුවෙන් ආරම්භ වේ: 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. සේවාලාභී විදැහුම්කරණය

තිරය ​​මත පින්තූරය පෙන්වීමට කාලයයි!

...හැබැයි මේක කරන්න කලින් අපි මේකට අවශ්‍ය සියලුම පින්තූර (සම්පත්) බාගන්න ඕන. අපි සම්පත් කළමනාකරුවෙකු ලියන්නෙමු:

වත්කම්.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>) අපගේ ක්‍රීඩාව ඉතා සරලයි, එබැවින් අපට අවශ්‍ය වන්නේ පහත සඳහන් දේ පමණක් ඉදිරිපත් කිරීමයි:

  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. සේවාදායක තත්ත්වය

මෙම කොටස ලිපියේ පළමු කොටසේ වඩාත්ම දුෂ්කර ය. ඔබ එය පළමු වරට කියවන විට ඔබට එය නොතේරෙන්නේ නම් අධෛර්යමත් නොවන්න! ඔබට එය මඟ හැර පසුව එයට ආපසු පැමිණිය හැකිය.

සේවාදායක-සේවාදායක කේතය සම්පූර්ණ කිරීමට අවශ්‍ය ප්‍රහේලිකාවේ අවසාන කොටස වේ රජයේ. සේවාලාභී විදැහුම්කරණ අංශයෙන් කේත කොටස මතකද?

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

ලස්සන සහ පැහැදිලි! නමුත් එය එතරම් සරල නම්. මෙම ක්‍රියාත්මක කිරීම ගැටළු සහගත වීමට එක් හේතුවක්: එය විදැහුම් රාමු අනුපාතය සේවාදායක ඔරලෝසු වේගයට සීමා කරයි.

රාමු අනුපාතය: රාමු ගණන (එනම් ඇමතුම් render()) තත්පරයකට, හෝ FPS. ක්‍රීඩා සාමාන්‍යයෙන් අවම වශයෙන් FPS 60ක් ලබා ගැනීමට උත්සාහ කරයි.

ටික් අනුපාතය: සේවාදායකයා ක්‍රීඩා යාවත්කාලීනයන් සේවාලාභීන් වෙත යවන වාර ගණන. එය බොහෝ විට රාමු අනුපාතයට වඩා අඩුය. අපගේ ක්‍රීඩාවේදී, සේවාදායකය තත්පරයට ටික් 30 බැගින් ක්‍රියාත්මක වේ.

අපි නවතම ක්‍රීඩා යාවත්කාලීනය ලබා දෙන්නේ නම්, FPS හට අත්‍යවශ්‍යයෙන්ම 30 ඉක්මවීමට නොහැකි වනු ඇත අපට සේවාදායකයෙන් තත්පරයකට යාවත්කාලීන 30කට වඩා නොලැබේ. අපි කතා කළත් render() තත්පරයකට 60 වතාවක්, එවිට මෙම ඇමතුම්වලින් අඩක් එකම දේ නැවත අඳිනු ඇත, මූලික වශයෙන් කිසිවක් නොකරයි. බොළඳ ක්‍රියාත්මක කිරීමේ තවත් ගැටලුවක් වන්නේ එයයි ප්රමාදයන්ට යටත් වේ. නියම අන්තර්ජාල වේගයකදී, සේවාදායකයාට හරියටම සෑම ms 33 කට වරක් (තත්පරයට 30) ක්‍රීඩා යාවත්කාලීනයක් ලැබෙනු ඇත:

.io ප්‍රභේදයේ බහු ක්‍රීඩක වෙබ් ක්‍රීඩාවක් නිර්මාණය කිරීම
අවාසනාවකට, කිසිවක් පරිපූර්ණ නොවේ. වඩාත් යථාර්ථවාදී පින්තූරයක් වනු ඇත:
.io ප්‍රභේදයේ බහු ක්‍රීඩක වෙබ් ක්‍රීඩාවක් නිර්මාණය කිරීම
ප්‍රමාදය සම්බන්ධයෙන් බොළඳ ක්‍රියාත්මක කිරීම නරකම අවස්ථාවයි. 50ms ප්‍රමාදයකින් ක්‍රීඩා යාවත්කාලීනයක් ලැබුනේ නම්, එවිට සේවාදායකයා මන්දගාමී වේ අමතර 50ms කින් එය තවමත් පෙර යාවත්කාලීනයෙන් ක්‍රීඩා තත්ත්වය විදැහුම් කරන බැවිනි. මෙය ක්‍රීඩකයාට කෙතරම් අපහසු දැයි ඔබට සිතාගත හැකිය: හිතුවක්කාර මන්දගාමිත්වය හේතුවෙන්, ක්‍රීඩාව අවුල් සහගත සහ අස්ථායී බවක් පෙනෙනු ඇත.

7.2 වැඩිදියුණු කළ සේවාදායක තත්ත්වය

අපි බොළඳ ක්‍රියාත්මක කිරීම සඳහා යම් වැඩිදියුණු කිරීම් කරන්නෙමු. පළමුව, අපි භාවිතා කරමු විදැහුම් ප්රමාදය 100 ms කින්. මෙයින් අදහස් කරන්නේ සේවාදායකයාගේ "වත්මන්" තත්වය සෑම විටම සේවාදායකයේ ක්‍රීඩා තත්වයට වඩා 100ms පිටුපසින් පවතින බවයි. උදාහරණයක් ලෙස, සේවාදායකයේ වේලාව නම් 150, එවිට සේවාලාභියා එම අවස්ථාවේ සේවාදායකය සිටි තත්ත්වය ලබා දෙනු ඇත 50:

.io ප්‍රභේදයේ බහු ක්‍රීඩක වෙබ් ක්‍රීඩාවක් නිර්මාණය කිරීම
ක්‍රීඩා යාවත්කාලීනවල අනපේක්ෂිත වේලාවෙන් බේරීමට මෙය අපට 100ms බෆරයක් ලබා දෙයි:

.io ප්‍රභේදයේ බහු ක්‍රීඩක වෙබ් ක්‍රීඩාවක් නිර්මාණය කිරීම
මේ සඳහා මිල ස්ථිර වනු ඇත ආදාන ප්‍රමාදය 100 ms කින්. මෙය සුමට ක්‍රීඩාව සඳහා සුළු කැපකිරීමකි - බොහෝ ක්‍රීඩකයින් (විශේෂයෙන් අනියම් අය) මෙම ප්‍රමාදය නොදකිනු ඇත. අනපේක්ෂිත ප්‍රමාදය සමඟ සෙල්ලම් කිරීමට වඩා මිනිසුන්ට නියත 100ms ප්‍රමාදයකට ගැළපීම ඉතා පහසු වේ.

යනුවෙන් හැඳින්වෙන තවත් තාක්ෂණයක් අපට භාවිතා කළ හැකිය "සේවාදායක පාර්ශවීය පුරෝකථනය", එය වටහාගත් ප්‍රමාදය අඩු කිරීමේ හොඳ කාර්යයක් කරයි, නමුත් මෙම පෝස්ටුවෙහි සාකච්ඡා නොකරනු ඇත.

අපි භාවිතා කරන තවත් දියුණුවක් රේඛීය මැදිහත්වීම. විදැහුම්කරණයේ ප්‍රමාදය හේතුවෙන්, අපි සාමාන්‍යයෙන් සේවාලාභියාගේ වත්මන් කාලයට වඩා අවම වශයෙන් එක් යාවත්කාලීනයක් හෝ ඉදිරියෙන් සිටිමු. ඇමතූ විට getCurrentState(), අපට ඉටු කළ හැකිය රේඛීය මැදිහත්වීම සේවාලාභියාගේ වත්මන් වේලාවට පෙර සහ පසුව ක්‍රීඩා යාවත්කාලීන අතර:

.io ප්‍රභේදයේ බහු ක්‍රීඩක වෙබ් ක්‍රීඩාවක් නිර්මාණය කිරීම
මෙය රාමු අනුපාත ගැටළුව විසඳයි: අපට දැන් අපට අවශ්‍ය ඕනෑම රාමු අනුපාතයකින් අද්විතීය රාමු ලබා දිය හැකිය!

7.3 වැඩිදියුණු කළ සේවාදායක තත්වයක් ක්‍රියාත්මක කිරීම

ආදර්ශ ක්රියාත්මක කිරීම src/client/state.js විදැහුම් ප්‍රමාදය සහ රේඛීය මැදිහත්වීම යන දෙකම භාවිතා කරයි, නමුත් මෙය දිගු කල් පවතින්නේ නැත. කෝඩ් එක කොටස් දෙකකට කඩමු. මෙන්න පළමු එක:

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(). අප කලින් දුටු පරිදි, සෑම ක්‍රීඩා යාවත්කාලීනයකටම සේවාදායක කාල මුද්‍රාවක් ඇතුළත් වේ. සේවාදායකය පිටුපසින් 100ms රූපය විදැහුම් කිරීමට අපට render latency භාවිතා කිරීමට අවශ්‍යයි, නමුත් සේවාදායකයේ වත්මන් වේලාව අපි කිසිදා නොදනිමු, මක්නිසාද යත් ඕනෑම යාවත්කාලීනයක් අප වෙත ළඟා වීමට කොපමණ කාලයක් ගත වූවාද යන්න අපට දැනගත නොහැකි බැවිනි. අන්තර්ජාලය අනපේක්ෂිත වන අතර එහි වේගය බෙහෙවින් වෙනස් විය හැකිය!

මෙම ගැටළුව මඟහරවා ගැනීම සඳහා, අපට සාධාරණ ආසන්න කිරීමක් භාවිතා කළ හැකිය: අපි පළමු යාවත්කාලීනය ක්ෂණිකව පැමිණි බව මවාපාමු. මෙය සත්‍ය නම්, අපි එම නිශ්චිත මොහොතේ සේවාදායකයේ වේලාව දැන ගනිමු! අපි සේවාදායක වේලා මුද්‍රාව ගබඩා කරමු firstServerTimestamp සහ අපේ ඉතිරි කරන්න දේශීය (සේවාදායක) එම මොහොතේම කාල මුද්‍රාව gameStart.

ඔහ්, පොඩ්ඩක් ඉන්න. සේවාදායකයේ වේලාව = සේවාදායකයා මත කාලය තිබිය යුතු නොවේද? අපි "සේවාදායක කාලමුද්‍රාව" සහ "සේවාදායක කාලමුද්‍රාව" අතර වෙනස හඳුනා ගන්නේ ඇයි? මේක නියම ප්‍රශ්නයක්! මේවා එකම දේ නොවන බව පෙනී යයි. Date.now() සේවාලාභියාගේ සහ සේවාදායකයේ විවිධ වේලා මුද්දර ආපසු ලබා දෙනු ඇති අතර මෙය මෙම යන්ත්‍රවලට දේශීය සාධක මත රඳා පවතී. සියලුම යන්ත්‍රවල වේලා මුද්‍රා සමාන වේ යැයි කිසිවිටක නොසිතන්න.

දැන් අපිට තේරෙනවා මොකද කරන්නේ කියලා currentServerTime(): එය නැවත පැමිණේ වත්මන් විදැහුම් කාලයෙහි සේවාදායක වේලා මුද්‍රාව. වෙනත් වචන වලින් කිවහොත්, මෙය වත්මන් සේවාදායක කාලයයි (firstServerTimestamp <+ (Date.now() - gameStart)විදැහුම් ප්‍රමාදය අඩු කරන්න (RENDER_DELAY).

දැන් අපි ක්‍රීඩා යාවත්කාලීන කරන්නේ කෙසේදැයි බලමු. සේවාදායකයෙන් යාවත්කාලීනයක් ලැබුණු විට, එය හැඳින්වේ processGameUpdate(), සහ අපි නව යාවත්කාලීනය අරාවකට සුරකිමු gameUpdates. පසුව, මතක භාවිතය පරීක්ෂා කිරීම සඳහා, අපි පැරණි යාවත්කාලීන සියල්ල ඉවත් කරමු මූලික යාවත්කාලීන කිරීමමන්ද අපට ඒවා තවදුරටත් අවශ්‍ය නොවන බැවිනි.

"core update" යනු කුමක්ද? මෙය වත්මන් සේවාදායක වේලාවෙන් පසුපසට ගමන් කිරීමෙන් අපි සොයා ගන්නා පළමු යාවත්කාලීනය. මෙම රූප සටහන මතකද?

.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 කොටස. පසුබිම් සේවාදායකය

මෙම කොටසේදී අපි අපගේ පාලනය කරන Node.js පසුබිම දෙස බලමු .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/, නිෂ්පාදන ගොඩනැගීමෙන් පසු Webpack අපගේ ගොනු ලියන බවට.

තවත් වැදගත් කාර්යයක් 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 ("ක්‍රීඩාව") - සියලුම ක්‍රීඩකයින් එකම පිටියේ ක්‍රීඩා කරයි! ඊලග කොටසින් අපි බලමු කොහොමද මේ class එක වැඩ කරන්නේ කියලා Game.

2. Game servers

පන්තිය 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 socket.io (ඔබ ව්‍යාකූල නම්, ආපසු යන්න server.js) Socket.io විසින්ම සෑම සොකට් එකකටම අනන්‍ය එකක් පවරයි id, ඒ නිසා අපි ඒ ගැන කලබල විය යුතු නැහැ. මම ඔහුට කතා කරන්නම් ක්‍රීඩක හැඳුනුම්පත.

එය මනසේ තබාගෙන, අපි පන්තියේ ඇති අවස්ථා විචල්‍යයන් පරීක්ෂා කරමු Game:

  • sockets ක්‍රීඩකයා සමඟ සම්බන්ධ වී ඇති සොකට් එකට ක්‍රීඩක හැඳුනුම්පත බැඳ තබන වස්තුවකි. කාලයත් සමඟ ඔවුන්ගේ ක්‍රීඩක හැඳුනුම්පත් මගින් සොකට් වෙත ප්‍රවේශ වීමට එය අපට ඉඩ සලසයි.
  • players ක්‍රීඩක හැඳුනුම්පත කේතය> ක්‍රීඩක වස්තුවට බැඳ තබන වස්තුවකි

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 වතාවක්/s, අපි ක්‍රීඩා යාවත්කාලීන 30 වතාවක්/s යවයි. මේ අනුව, ඔරලෝසු සංඛ්යාතය server එක 30 clock cycles/s (අපි පළමු කොටසේ ඔරලෝසු සංඛ්‍යාතය ගැන කතා කළා).

ක්‍රීඩා යාවත්කාලීන පමණක් යවන්නේ ඇයි කාලය හරහා ? නාලිකාව සුරැකීමට. තත්පරයකට ක්‍රීඩා යාවත්කාලීන කිරීම් 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

අදහස් එක් කරන්න