io صنف ۾ ملٽي پليئر ويب گيم ٺاهڻ

io صنف ۾ ملٽي پليئر ويب گيم ٺاهڻ
2015 ۾ جاري ڪيو ويو Agar.io هڪ نئين صنف جو پرچارڪ بڻجي ويو games.ioجنهن جي مقبوليت ان وقت کان وٺي تمام گهڻي وڌي وئي آهي. مون پاڻ کي .io راندين جي مقبوليت ۾ اضافو محسوس ڪيو آهي: گذريل ٽن سالن کان، I هن صنف ۾ ٻه رانديون ٺاهي ۽ وڪرو ڪيون ويون..

جيڪڏهن توهان انهن راندين بابت اڳ ڪڏهن به نه ٻڌو آهي، اهي مفت آهن، ملٽي پليئر ويب رانديون جيڪي کيڏڻ آسان آهن (ڪنهن به اڪائونٽ جي ضرورت ناهي). اهي عام طور تي ڪيترن ئي مخالف رانديگرن کي هڪ ميدان ۾ وجهي ڇڏيندا آهن. ٻيون مشهور .io رانديون: Slither.io и Diep.io.

هن پوسٽ ۾ اسان اهو معلوم ڪنداسين ته ڪيئن شروع کان هڪ .io راند ٺاهيو. هن کي ڪرڻ لاء، صرف جاوا اسڪرپٽ جي ڄاڻ ڪافي هوندي: توهان کي شين کي سمجهڻ جي ضرورت آهي نحو وانگر ES6، لفظ this и واعدو. جيتوڻيڪ توهان جاوا اسڪرپٽ کي مڪمل طور تي نه ڄاڻندا آهيو، توهان اڃا تائين تمام گهڻو پوسٽ سمجهي سگهو ٿا.

io راند جو مثال

تربيتي مدد لاءِ اسان حوالو ڏينداسين مثال جي راند .io. ان کي راند ڪرڻ جي ڪوشش ڪريو!

io صنف ۾ ملٽي پليئر ويب گيم ٺاهڻ
راند بلڪل سادو آهي: توهان ٻين رانديگرن سان گڏ ميدان ۾ هڪ ٻيڙي کي سنڀاليو. توهان جو جهاز خود بخود پروجيڪٽائلز کي فائر ڪري ٿو ۽ توهان ٻين رانديگرن کي مارڻ جي ڪوشش ڪندا آهيو جڏهن انهن جي پروجيڪٽ کان پاسو ڪندا آهيو.

1. مختصر جائزو/ منصوبي جي جوڙجڪ

مان صلاح ڪريان ٿو ڊائون لوڊ سورس ڪوڊ مثال جي راند، تنهنڪري توهان مون کي پيروي ڪري سگهو ٿا.

مثال هيٺ ڏنل استعمال ڪري ٿو:

  • مظاهرو Node.js لاءِ تمام مشهور ويب فريم ورڪ آهي جيڪو راند جي ويب سرور کي منظم ڪري ٿو.
  • socket.io - برائوزر ۽ سرور جي وچ ۾ ڊيٽا مٽائڻ لاءِ ويب ساکٽ لائبريري.
  • ويبپيڪ - ماڊل مئنيجر. توھان پڙھي سگھوٿا Webpack ڇو استعمال ڪجي هتي.

اھو اھو آھي جيڪو پروجيڪٽ ڊاريڪٽري جي جوڙجڪ جھڙو آھي:

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

عوامي /

هر شي فولڊر ۾ آهي public/ statically سرور ذريعي منتقل ڪيو ويندو. 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 جاوا اسڪرپٽ (JS) ڪلائنٽ جو داخلا پوائنٽ آهي. Webpack ھتان کان شروع ٿيندو ۽ بار بار ٻين درآمد ٿيل فائلن کي ڳوليندا.
  • اسان جي Webpack تعمير جي پيداوار JS ڊاريڪٽري ۾ واقع ٿيندي dist/. مان هن فائل کي سڏيندس اسان جي جي ايس پيڪيج.
  • اسان استعمال ڪريون ٿا ببل، ۽ خاص طور تي ترتيب @babel/preset-env پراڻن برائوزرن لاءِ اسان جي JS ڪوڊ کي منتقل ڪرڻ لاءِ.
  • اسان هڪ پلگ ان استعمال ڪندا آهيون سڀني CSS کي ڪڍڻ لاءِ JS فائلن جي حوالي سان ۽ انهن کي هڪ جاءِ تي گڏ ڪرڻ. مان ان کي سڏيندس اسان جي CSS پيڪيج.

توهان شايد محسوس ڪيو هوندو عجيب پيڪيج فائل جا نالا '[name].[contenthash].ext'. انهن تي مشتمل آهي فائل جو نالو متبادل ويب پيڪ [name] ان پٽ پوائنٽ جي نالي سان تبديل ڪيو ويندو (اسان جي صورت ۾ اهو آهي game)، ۽ [contenthash] فائل جي مواد جي هڪ hash سان تبديل ڪيو ويندو. اسان اهو ڪندا آهيون hashing لاء منصوبي کي بهتر - اسان برائوزرن کي ٻڌائي سگھون ٿا ته اسان جي 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, پيداوار کي ترتيب ڏيڻ دوران پيڪيج جي سائيز کي بهتر ڪرڻ لاء.

مقامي سيٽ اپ

مان توهان جي مقامي مشين تي پروجيڪٽ کي انسٽال ڪرڻ جي صلاح ڏيان ٿو تنهنڪري توهان هن پوسٽ ۾ درج ڪيل قدمن تي عمل ڪري سگهو ٿا. سيٽ اپ سادو آهي: پهريون، سسٽم هجڻ گهرجي نوڊ и اين پي ايم. اڳيون توهان کي ڪرڻو پوندو

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

۽ توهان وڃڻ لاء تيار آهيو! ڊولپمينٽ سرور شروع ڪرڻ لاءِ، بس هلايو

$ npm run develop

۽ پنھنجي ويب برائوزر ڏانھن وڃو مقامي هنڌ: 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 بلٽ ان سپورٽ ڪئي آهي WebSockets، جيڪي ٻه طرفي رابطي لاءِ موزون آهن: اسان سرور ڏانهن پيغام موڪلي سگهون ٿا и سرور اسان کي پيغام موڪلي سگهي ٿو ساڳئي ڪنيڪشن تي.

اسان وٽ هڪ فائل هوندي 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. ڪلائنٽ رينڊرنگ

اهو وقت آهي اسڪرين تي تصوير ڏيکارڻ جو!

... پر ان کان اڳ جو اسان اهو ڪري سگهون، اسان کي اهي سڀئي تصويرون (وسيلا) ڊائون لوڊ ڪرڻ گهرجن جيڪي هن لاءِ گهربل آهن. اچو ته هڪ ريسورس مئنيجر لکون:

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>). اسان جي راند بلڪل سادو آهي، تنهنڪري اسان کي صرف هيٺ ڏنل پيش ڪرڻ جي ضرورت آهي:

  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. رانديون عام طور تي گهٽ ۾ گهٽ 60 FPS حاصل ڪرڻ جي ڪوشش ڪندا آهن.

چيڪ جي شرح: فریکوئنسي جنهن سان سرور موڪلي ٿو گيم اپڊيٽ کلائنٽ ڏانهن. اهو اڪثر ڪري فريم جي شرح کان گهٽ آهي. اسان جي راند ۾، سرور 30 ​​ٽڪر في سيڪنڊ تي هلندو آهي.

جيڪڏهن اسان صرف جديد راند جي تازه ڪاري پيش ڪندا آهيون، پوء FPS لازمي طور تي ڪڏهن به 30 کان وڌيڪ نه ٿي سگهندو ڇو ته اسان ڪڏهن به حاصل نه ڪندا آهيون 30 اپڊيٽ في سيڪنڊ کان وڌيڪ سرور کان. جيتوڻيڪ اسان کي سڏين ٿا render() 60 ڀيرا في سيڪنڊ، پوءِ انهن ڪالن مان اڌ صرف ساڳي شيءِ کي ٻيهر ٺاهيندي، بنيادي طور تي ڪجهه به نه ڪندي. هڪ سادي عمل سان هڪ ٻيو مسئلو اهو آهي ته دير جي تابع. مثالي انٽرنيٽ جي رفتار تي، ڪلائنٽ هر 33 ms (30 في سيڪنڊ) تي هڪ گيم اپڊيٽ حاصل ڪندو:

io صنف ۾ ملٽي پليئر ويب گيم ٺاهڻ
بدقسمتي سان، ڪجھ به مڪمل ناهي. هڪ وڌيڪ حقيقي تصوير هوندي:
io صنف ۾ ملٽي پليئر ويب گيم ٺاهڻ
هڪ غير جانبدار عمل تمام گهڻو بدترين صورت آهي جڏهن اهو دير سان اچي ٿو. جيڪڏهن هڪ راند جي تازه ڪاري 50ms دير سان ملي ٿي، پوءِ ڪلائنٽ کي سست ڪيو ويو آهي اضافي 50ms جي ذريعي ڇو ته اهو اڃا تائين راند جي حالت کي اڳئين تازه ڪاري کان رينجر ڪري رهيو آهي. توھان تصور ڪري سگھو ٿا ته ھي پليئر لاءِ ڪيترو ناگوار آھي: صوابديدي سست رفتاري جي ڪري، راند بيڪار ۽ غير مستحڪم نظر ايندي.

7.2 بهتر ڪيل ڪلائنٽ اسٽيٽ

اسان غير معمولي عمل درآمد لاء ڪجهه سڌارا ڪنداسين. پهرين، اسان استعمال ڪريون ٿا دير ڪرڻ ۾ دير 100 ايم ايس پاران. هن جو مطلب آهي ته ڪلائنٽ جي "موجوده" حالت هميشه 100ms هوندي سرور تي راند رياست جي پويان. مثال طور، جيڪڏهن سرور وقت آهي 150، پوءِ ڪلائنٽ ان رياست کي پيش ڪندو جنهن ۾ سرور ان وقت هو 50:

io صنف ۾ ملٽي پليئر ويب گيم ٺاهڻ
هي اسان کي 100ms بفر ڏئي ٿو راندين جي تازه ڪارين جي غير متوقع وقت کان بچڻ لاءِ:

io صنف ۾ ملٽي پليئر ويب گيم ٺاهڻ
هن جي قيمت مستقل هوندي داخل ٿيڻ ۾ دير 100 ايم ايس پاران. هي هموار راند جي لاءِ هڪ معمولي قرباني آهي - اڪثر رانديگر (خاص طور تي آرام سان) هن دير کي به نوٽيس نه ڪندا. اهو تمام آسان آهي ماڻهن لاءِ مسلسل 100ms جي دير سان ترتيب ڏيڻ بجاءِ غير متوقع دير سان کيڏڻ جي.

اسان هڪ ٻيو ٽيڪنڪ استعمال ڪري سگهون ٿا جنهن کي سڏيو ويندو آهي "ڪلائنٽ پاسي جي اڳڪٿي"، جيڪو سمجھي ويڪرائي کي گهٽائڻ جو سٺو ڪم ڪري ٿو، پر هن پوسٽ ۾ بحث نه ڪيو ويندو.

ٻيو سڌارو جيڪو اسان استعمال ڪندا آهيون اهو آهي لڪير interpolation. رينجرنگ ليگ جي ڪري، اسان عام طور تي ڪلائنٽ ۾ موجوده وقت کان گهٽ ۾ گهٽ هڪ تازه ڪاري آهي. جڏهن سڏيو ويو getCurrentState()، اسان پورو ڪري سگهون ٿا لڪير interpolation ڪلائنٽ ۾ موجوده وقت کان پهريان ۽ بعد ۾ راندين جي تازه ڪارين جي وچ ۾:

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 رينڊر ڪرڻ لاءِ رينڊر ليٽيسي استعمال ڪرڻ چاهيون ٿا، پر اسان کي سرور تي موجوده وقت ڪڏهن به معلوم نه ٿيندو، ڇاڪاڻ ته اسان نٿا ڄاڻون ته ڪنهن به تازه ڪاري کي اسان تائين پهچڻ ۾ ڪيترو وقت لڳو. انٽرنيٽ غير متوقع آهي ۽ ان جي رفتار مختلف ٿي سگهي ٿي!

هن مسئلي جي چوڌاري حاصل ڪرڻ لاء، اسان هڪ مناسب اندازي مطابق استعمال ڪري سگهون ٿا: اسين اچو ته فرض ڪريون ته پهرين تازه ڪاري فوري طور تي پهچي وئي. جيڪڏهن اهو سچ هجي ها ته پوءِ اسان کي ان وقت سرور جو وقت معلوم ٿئي ها! اسان سرور ٽائم اسٽيمپ ۾ ذخيرو ڪندا آهيون firstServerTimestamp ۽ اسان کي بچايو مقامي (ڪلائنٽ) ٽائم اسٽيمپ ساڳئي وقت ۾ gameStart.

اوه، هڪ منٽ ترسو. ڇا سرور تي وقت نه هجڻ گهرجي = ڪلائنٽ تي وقت؟ اسان "سرور ٽائم اسٽيمپ" ۽ "ڪلائنٽ ٽائم اسٽيمپ" جي وچ ۾ فرق ڇو ٿا ڪريون؟ هي هڪ وڏو سوال آهي! اهو ظاهر ٿئي ٿو ته اهي ساڳيون شيون نه آهن. Date.now() ڪلائنٽ ۽ سرور ۾ مختلف ٽائم اسٽيمپ واپس آڻيندو ۽ اھو انھن مشينن تي مقامي عنصرن تي منحصر آھي. ڪڏهن به فرض نه ڪريو ته ٽائم اسٽيمپ سڀني مشينن تي ساڳيا هوندا.

هاڻي اسان سمجهون ٿا ته اهو ڇا ڪندو آهي currentServerTime(): واپس اچي ٿو موجوده رينڊرنگ وقت جو سرور ٽائم اسٽيمپ. ٻين لفظن ۾، هي موجوده سرور وقت آهي (firstServerTimestamp <+ (Date.now() - gameStart)) منٽ رينڊرنگ دير (RENDER_DELAY).

هاڻي اچو ته ڏسون ته ڪيئن اسان راند جي تازه ڪاري کي سنڀاليندا آهيون. جڏهن سرور مان هڪ تازه ڪاري ملي ٿي، ان کي سڏيو ويندو آهي processGameUpdate()، ۽ اسان نئين تازه ڪاري کي صف ۾ محفوظ ڪريون ٿا gameUpdates. پوء، ياداشت جي استعمال کي جانچڻ لاء، اسان سڀني پراڻي تازه ڪاري کي هٽايو بنيادي اپڊيٽڇاڪاڻ ته اسان کي انهن جي وڌيڪ ضرورت ناهي.

هڪ "بنيادي تازه ڪاري" ڇا آهي؟ هي پهرين تازه ڪاري جيڪا اسان ڳوليندا آهيون موجوده سرور وقت کان پوئتي موٽڻ سان. هي ڊراگرام ياد آهي؟

io صنف ۾ ملٽي پليئر ويب گيم ٺاهڻ
راند جي تازه ڪاري سڌو سنئون کاٻي پاسي "ڪلائنٽ رينڈر ٽائيم" بنيادي تازه ڪاري آهي.

بنيادي اپڊيٽ ڇا لاء استعمال ڪيو ويو آهي؟ اسان بنيادي طور تي اپڊيٽ ڇو ڇڏي سگهون ٿا؟ انهي کي سمجهڻ لاء، اچو آخر ۾ اچو ته عملدرآمد تي نظر 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-midleware اسان جي ڊولپمينٽ پيڪيجز کي خودڪار طور تي ٻيهر تعمير ڪرڻ لاء، يا
  • مستحڪم طور تي فولڊر منتقل ڪريو 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 ("راند") - سڀئي رانديگر هڪ ئي ميدان ۾ راند ڪندا آهن! ايندڙ حصي ۾ اسين ڏسنداسين ته هي ڪلاس ڪيئن ڪم ڪري ٿو 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 کي ساکٽ سان ڳنڍيندو آھي جيڪو پليئر سان لاڳاپيل آھي. اهو اسان کي اجازت ڏئي ٿو ته ساکٽ تائين رسائي انهن جي پليئر آئي ڊي جي وقت سان.
  • 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 ڀيرا/s، اسان موڪليندا آهيون راند تازه ڪاريون 30 ڀيرا/s. اهڙيءَ طرح، ڪلاڪ جي تعدد سرور آهي 30 ڪلاڪ چڪر/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:

اعتراض.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

تبصرو شامل ڪريو