மல்டிபிளேயர் .io வெப் கேமை உருவாக்குதல்

மல்டிபிளேயர் .io வெப் கேமை உருவாக்குதல்
2015 இல் வெளியிடப்பட்டது அகர்.யோ ஒரு புதிய வகையின் முன்னோடி ஆனார் விளையாட்டுகள் .ioஅதன் பின்னர் பிரபலமாக வளர்ந்துள்ளது. .io கேம்களின் பிரபல்யத்தை நான் தனிப்பட்ட முறையில் அனுபவித்திருக்கிறேன்: கடந்த மூன்று வருடங்களில், என்னிடம் உள்ளது இந்த வகையின் இரண்டு கேம்களை உருவாக்கி விற்றது..

இந்த கேம்களை இதற்கு முன் நீங்கள் கேள்விப்பட்டிருக்கவில்லை என்றால், இவை விளையாடுவதற்கு எளிதான இலவச மல்டிபிளேயர் வெப் கேம்கள் (கணக்கு தேவையில்லை). அவர்கள் பொதுவாக பல எதிரணி வீரர்களை ஒரே அரங்கில் எதிர்கொள்கின்றனர். பிற பிரபலமான .io கேம்கள்: Slither.io и Diep.io.

இந்த இடுகையில் நாம் எப்படி கண்டுபிடிப்போம் புதிதாக ஒரு .io கேமை உருவாக்கவும். இதற்கு, ஜாவாஸ்கிரிப்ட் பற்றிய அறிவு மட்டுமே போதுமானது: நீங்கள் தொடரியல் போன்ற விஷயங்களைப் புரிந்து கொள்ள வேண்டும் ES6, முக்கிய வார்த்தை this и வாக்குறுதிகள். ஜாவாஸ்கிரிப்ட் பற்றிய உங்கள் அறிவு சரியாக இல்லாவிட்டாலும், பெரும்பாலான இடுகைகளை நீங்கள் இன்னும் புரிந்து கொள்ள முடியும்.

.io கேம் உதாரணம்

கற்றல் உதவிக்கு, நாங்கள் குறிப்பிடுவோம் .io கேம் உதாரணம். விளையாட முயற்சிக்கவும்!

மல்டிபிளேயர் .io வெப் கேமை உருவாக்குதல்
விளையாட்டு மிகவும் எளிமையானது: மற்ற வீரர்கள் இருக்கும் அரங்கில் நீங்கள் ஒரு கப்பலைக் கட்டுப்படுத்துகிறீர்கள். உங்கள் கப்பல் தானாகவே எறிகணைகளை சுடுகிறது மற்றும் நீங்கள் மற்ற வீரர்களின் எறிகணைகளைத் தவிர்க்கும் போது அவர்களை தாக்க முயற்சிக்கிறீர்கள்.

1. சுருக்கமான கண்ணோட்டம்/திட்ட அமைப்பு

நான் பரிந்துரைக்கிறேன் மூலக் குறியீட்டைப் பதிவிறக்கவும் உதாரண விளையாட்டு, எனவே நீங்கள் என்னைப் பின்தொடரலாம்.

உதாரணம் பின்வருவனவற்றைப் பயன்படுத்துகிறது:

  • Express விளையாட்டின் இணைய சேவையகத்தை நிர்வகிக்கும் மிகவும் பிரபலமான Node.js வலை கட்டமைப்பாகும்.
  • socket.io - உலாவி மற்றும் சேவையகத்திற்கு இடையே தரவு பரிமாற்றத்திற்கான வெப்சாக்கெட் நூலகம்.
  • webpack - தொகுதி மேலாளர். Webpack ஐ ஏன் பயன்படுத்த வேண்டும் என்பதைப் பற்றி நீங்கள் படிக்கலாம். இங்கே.

திட்ட அடைவு அமைப்பு எப்படி இருக்கிறது என்பது இங்கே:

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

பொது/

எல்லாம் ஒரு கோப்புறையில் public/ சேவையகத்தால் நிலையான முறையில் சமர்ப்பிக்கப்படும். IN public/assets/ எங்கள் திட்டத்தால் பயன்படுத்தப்படும் படங்கள் உள்ளன.

src /

அனைத்து மூல குறியீடுகளும் கோப்புறையில் உள்ளன src/. தலைப்புகள் client/ и server/ தங்களை பேச மற்றும் shared/ கிளையன்ட் மற்றும் சர்வர் ஆகிய இரண்டாலும் இறக்குமதி செய்யப்பட்ட மாறிலிகள் கோப்பு உள்ளது.

2. அசெம்பிளிகள்/திட்ட அமைப்புகள்

மேலே குறிப்பிட்டுள்ளபடி, திட்டத்தை உருவாக்க தொகுதி மேலாளரைப் பயன்படுத்துகிறோம். webpack. எங்கள் Webpack உள்ளமைவைப் பார்ப்போம்:

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/. நான் இந்த கோப்பை எங்கள் என்று அழைப்பேன் 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, உற்பத்திக்கு பயன்படுத்தும்போது தொகுப்பு அளவுகளை மேம்படுத்த.

உள்ளூர் அமைப்பு

உள்ளூர் கணினியில் திட்டத்தை நிறுவ பரிந்துரைக்கிறேன், எனவே இந்த இடுகையில் பட்டியலிடப்பட்டுள்ள படிகளைப் பின்பற்றலாம். அமைப்பு எளிதானது: முதலில், கணினி நிறுவப்பட்டிருக்க வேண்டும் கணு и NPM. அடுத்து நீங்கள் செய்ய வேண்டும்

$ 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 விளையாட்டு  விளையாடு

இந்த குறியீட்டு உதாரணம் தெளிவுக்காக சிறிது எளிமைப்படுத்தப்பட்டுள்ளது, மேலும் பல இடுகை எடுத்துக்காட்டுகளிலும் இதைச் செய்வேன். முழு குறியீட்டையும் எப்போதும் பார்க்க முடியும் கிட்ஹப்.

எங்களிடம் உள்ளது:

  • HTML5 கேன்வாஸ் உறுப்பு (<canvas>) விளையாட்டை வழங்க நாங்கள் பயன்படுத்துவோம்.
  • <link> எங்கள் CSS தொகுப்பைச் சேர்க்க.
  • <script> எங்கள் ஜாவாஸ்கிரிப்ட் தொகுப்பைச் சேர்க்க.
  • பயனர்பெயருடன் முதன்மை மெனு <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 இறக்குமதி (எனது CSS தொகுப்பில் அவற்றை சேர்க்க Webpack தெரியும்).
  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() (of 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 முறை, இந்த அழைப்புகளில் பாதியானது அதையே மீண்டும் வரையலாம், அடிப்படையில் எதுவும் செய்யாது. அப்பாவியாக செயல்படுத்துவதில் உள்ள மற்றொரு சிக்கல் அது தாமதங்களுக்கு வாய்ப்புள்ளது. சிறந்த இணைய வேகத்துடன், கிளையன்ட் ஒவ்வொரு 33ms (வினாடிக்கு 30) ஒரு கேம் புதுப்பிப்பைப் பெறுவார்:

மல்டிபிளேயர் .io வெப் கேமை உருவாக்குதல்
துரதிர்ஷ்டவசமாக, எதுவும் சரியாக இல்லை. மிகவும் யதார்த்தமான படம் இருக்கும்:
மல்டிபிளேயர் .io வெப் கேமை உருவாக்குதல்
தாமதத்திற்கு வரும்போது அப்பாவியாக செயல்படுத்துவது நடைமுறையில் மிக மோசமானது. 50ms தாமதத்துடன் கேம் புதுப்பிப்பு பெறப்பட்டால் வாடிக்கையாளர் ஸ்டால்கள் கூடுதல் 50ms, ஏனெனில் இது முந்தைய புதுப்பித்தலில் இருந்து விளையாட்டு நிலையை இன்னும் ரெண்டரிங் செய்கிறது. இது வீரருக்கு எவ்வளவு அசௌகரியமாக இருக்கும் என்பதை நீங்கள் கற்பனை செய்யலாம்: தன்னிச்சையான பிரேக்கிங் விளையாட்டை சலிப்பாகவும் நிலையற்றதாகவும் உணர வைக்கும்.

7.2 மேம்படுத்தப்பட்ட வாடிக்கையாளர் நிலை

அப்பாவியாக செயல்படுத்துவதில் சில மேம்பாடுகளைச் செய்வோம். முதலில், நாங்கள் பயன்படுத்துகிறோம் வழங்குதல் தாமதம் 100 msக்கு. இதன் பொருள், கிளையண்டின் "தற்போதைய" நிலை எப்போதும் சேவையகத்தில் உள்ள விளையாட்டின் நிலையை விட 100ms பின்தங்கியே இருக்கும். உதாரணமாக, சர்வரில் நேரம் இருந்தால் 150, பின்னர் கிளையன்ட் அந்த நேரத்தில் சர்வர் இருந்த நிலையை வழங்குவார் 50:

மல்டிபிளேயர் .io வெப் கேமை உருவாக்குதல்
இது கணிக்க முடியாத விளையாட்டு புதுப்பிப்பு நேரங்களைத் தக்கவைக்க 100ms இடையகத்தை வழங்குகிறது:

மல்டிபிளேயர் .io வெப் கேமை உருவாக்குதல்
இதற்கான பலன் நிரந்தரமாக இருக்கும் உள்ளீடு பின்னடைவு 100 msக்கு. மென்மையான விளையாட்டுக்காக இது ஒரு சிறிய தியாகம் - பெரும்பாலான வீரர்கள் (குறிப்பாக சாதாரண வீரர்கள்) இந்த தாமதத்தை கவனிக்க மாட்டார்கள். கணிக்க முடியாத தாமதத்துடன் விளையாடுவதை விட, மக்கள் நிலையான 100எம்எஸ் தாமதத்தை சரிசெய்வது மிகவும் எளிதானது.

என்ற மற்றொரு நுட்பத்தையும் நாம் பயன்படுத்தலாம் வாடிக்கையாளர் பக்க கணிப்பு, இது உணரப்பட்ட தாமதத்தைக் குறைக்கும் ஒரு நல்ல வேலையைச் செய்கிறது, ஆனால் இந்த இடுகையில் விவாதிக்கப்படாது.

நாம் பயன்படுத்தும் மற்றொரு முன்னேற்றம் நேரியல் இடைச்செருகல். ரெண்டரிங் லேக் காரணமாக, கிளையண்டில் உள்ள தற்போதைய நேரத்தை விட குறைந்தபட்சம் ஒரு புதுப்பிப்பையாவது நாங்கள் வழக்கமாக வைத்திருக்கிறோம். அழைத்த போது 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 மீது கிட்ஹப்.

பகுதி 2. பின்தள சேவையகம்

இந்தப் பகுதியில், எங்களுடையதைக் கட்டுப்படுத்தும் Node.js பின்தளத்தைப் பார்ப்போம் .io கேம் உதாரணம்.

1. சர்வர் என்ட்ரி பாயிண்ட்

வலை சேவையகத்தை நிர்வகிக்க, Node.js எனப்படும் பிரபலமான வலை கட்டமைப்பைப் பயன்படுத்துவோம் Express. இது எங்கள் சர்வர் என்ட்ரி பாயின்ட் கோப்பால் கட்டமைக்கப்படும் 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 ("விளையாட்டு") - அனைத்து வீரர்களும் ஒரே அரங்கில் விளையாடுகிறார்கள்! அடுத்த பகுதியில், இந்த வகுப்பு எவ்வாறு செயல்படுகிறது என்பதைப் பார்ப்போம். 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அதனால் நாம் அதை பற்றி கவலைப்பட தேவையில்லை. நான் அவரை அழைக்கிறேன் பிளேயர் ஐடி.

அதை மனதில் கொண்டு, ஒரு வகுப்பில் உள்ள நிகழ்வு மாறிகளை ஆராய்வோம் 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:

வீரர்.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:

விளையாட்டு.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.

மோதல் கண்டறிதலை நாங்கள் செயல்படுத்துவது எப்படி இருக்கிறது:

மோதல்கள்.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 விளையாட்டை உருவாக்குங்கள்!

அனைத்து மாதிரி குறியீடுகளும் ஓப்பன் சோர்ஸ் மற்றும் இடுகையிடப்பட்டவை கிட்ஹப்.

ஆதாரம்: www.habr.com

கருத்தைச் சேர்