ایک ملٹی پلیئر .io ویب گیم بنانا

ایک ملٹی پلیئر .io ویب گیم بنانا
2015 میں ریلیز ہوا۔ اگرو.یو ایک نئی صنف کا پیشوا بن گیا۔ گیمز .ioجس کے بعد سے مقبولیت میں اضافہ ہوا ہے۔ میں نے ذاتی طور پر .io گیمز کی مقبولیت میں اضافے کا تجربہ کیا ہے: پچھلے تین سالوں میں، میں نے اس صنف کے دو گیمز بنائے اور بیچے۔.

اگر آپ نے ان گیمز کے بارے میں پہلے کبھی نہیں سنا ہو، تو یہ مفت ملٹی پلیئر ویب گیمز ہیں جو کھیلنے میں آسان ہیں (کوئی اکاؤنٹ درکار نہیں)۔ وہ عموماً ایک ہی میدان میں کئی مخالف کھلاڑیوں کا سامنا کرتے ہیں۔ دیگر مشہور .io گیمز: Slither.io и Diep.io.

اس پوسٹ میں، ہم دریافت کریں گے کہ کیسے شروع سے ایک .io گیم بنائیں. اس کے لیے صرف جاوا اسکرپٹ کا علم ہی کافی ہوگا: آپ کو نحو جیسی چیزوں کو سمجھنے کی ضرورت ہے۔ ES6، کلیدی لفظ this и وعدوں. یہاں تک کہ اگر آپ کا Javascript کا علم کامل نہیں ہے، تب بھی آپ زیادہ تر پوسٹ کو سمجھ سکتے ہیں۔

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/ سرور کے ذریعہ جامد طور پر جمع کرایا جائے گا۔ میں 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 کی تعمیر کا آؤٹ پٹ JS ڈائریکٹری میں واقع ہوگا۔ dist/. میں اس فائل کو ہماری کہوں گا۔ js پیکیج.
  • ہم استعمال کرتے ہیں بابل، اور خاص طور پر ترتیب @babel/preset-env پرانے براؤزرز کے لیے ہمارے جے ایس کوڈ کو منتقل کرنے کے لیے۔
  • ہم JS فائلوں کے ذریعہ حوالہ کردہ تمام 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پیداوار میں تعینات کرتے وقت پیکیج کے سائز کو بہتر بنانے کے لیے۔

مقامی ترتیب

میں تجویز کرتا ہوں کہ پروجیکٹ کو مقامی مشین پر انسٹال کریں تاکہ آپ اس پوسٹ میں درج مراحل پر عمل کر سکیں۔ سیٹ اپ آسان ہے: سب سے پہلے، سسٹم انسٹال ہونا ضروری ہے۔ نوڈ и این پی ایم. اگلا آپ کو کرنے کی ضرورت ہے۔

$ 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> ہمارے سی ایس ایس پیکج کو شامل کرنے کے لیے۔
  • <script> ہمارے جاوا اسکرپٹ پیکج کو شامل کرنے کے لیے۔
  • صارف نام کے ساتھ مین مینو <input> اور "پلے" بٹن (<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. "پلے" بٹن دبانے کے لیے ہینڈلر سیٹ کرنا۔ جب بٹن دبایا جاتا ہے، کوڈ گیم کو شروع کرتا ہے اور سرور کو بتاتا ہے کہ ہم کھیلنے کے لیے تیار ہیں۔

ہمارے کلائنٹ سرور کی منطق کا بنیادی "گوشت" ان فائلوں میں ہے جو فائل کے ذریعہ درآمد کی گئی تھیں۔ 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. کلائنٹ رینڈرنگ

اسکرین پر تصویر ظاہر کرنے کا وقت آگیا ہے!

لیکن اس سے پہلے کہ ہم ایسا کر سکیں، ہمیں ان تمام تصاویر (وسائل) کو ڈاؤن لوڈ کرنے کی ضرورت ہے جو اس کے لیے درکار ہیں۔ آئیے ایک ریسورس مینیجر لکھتے ہیں:

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 لیٹنسی کو ایڈجسٹ کرنا بہت آسان ہے۔

ہم ایک اور تکنیک بھی استعمال کر سکتے ہیں جسے کہتے ہیں۔ کلائنٹ کی طرف کی پیشن گوئی، جو سمجھی جانے والی تاخیر کو کم کرنے کا ایک اچھا کام کرتا ہے، لیکن اس پوسٹ میں اس کا احاطہ نہیں کیا جائے گا۔

ایک اور بہتری جو ہم استعمال کر رہے ہیں۔ لکیری انٹرپولیشن. رینڈرنگ لیگ کی وجہ سے، ہم عام طور پر کلائنٹ میں موجودہ وقت سے کم از کم ایک اپ ڈیٹ ہوتے ہیں۔ جب بلایا 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 رینڈر کرنے کے لیے رینڈر لیٹنسی استعمال کرنا چاہتے ہیں، لیکن ہم سرور پر موجودہ وقت کو کبھی نہیں جان پائیں گے۔کیونکہ ہم نہیں جان سکتے کہ کسی بھی اپ ڈیٹ کو ہم تک پہنچنے میں کتنا وقت لگا۔ انٹرنیٹ غیر متوقع ہے اور اس کی رفتار بہت مختلف ہو سکتی ہے!

اس مسئلے کو حل کرنے کے لیے، ہم ایک معقول تخمینہ استعمال کر سکتے ہیں: ہم دکھاوا کریں کہ پہلی اپ ڈیٹ فوری طور پر پہنچ گئی۔. اگر یہ سچ ہوتا تو ہمیں اس خاص لمحے میں سرور کا وقت معلوم ہوتا! ہم سرور کا ٹائم اسٹیمپ اس میں محفوظ کرتے ہیں۔ 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 their 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 بار فی سیکنڈ کہا جاتا ہے، ہم گیم اپڈیٹس 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

نیا تبصرہ شامل کریں