మల్టీప్లేయర్ .io వెబ్ గేమ్‌ని సృష్టిస్తోంది

మల్టీప్లేయర్ .io వెబ్ గేమ్‌ని సృష్టిస్తోంది
2015లో విడుదలైంది Agar.io కొత్త కళా ప్రక్రియకు మూలపురుషుడు అయ్యాడు ఆటలు .ioఅప్పటి నుండి ప్రజాదరణ పెరిగింది. నేను వ్యక్తిగతంగా .io గేమ్‌ల ప్రజాదరణ పెరుగుదలను అనుభవించాను: గత మూడు సంవత్సరాలలో, నేను కలిగి ఉన్నాను ఈ కళా ప్రక్రియ యొక్క రెండు గేమ్‌లను సృష్టించి విక్రయించింది..

ఒకవేళ మీరు ఈ గేమ్‌ల గురించి ఇంతకు ముందెన్నడూ విననట్లయితే, ఇవి ఆడటానికి సులభమైన ఉచిత మల్టీప్లేయర్ వెబ్ గేమ్‌లు (ఖాతా అవసరం లేదు). వారు సాధారణంగా ఒకే ఎరీనాలో చాలా మంది ప్రత్యర్థి ఆటగాళ్లను ఎదుర్కొంటారు. ఇతర ప్రసిద్ధ .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/ సర్వర్ ద్వారా స్థిరంగా సమర్పించబడుతుంది. 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) క్లయింట్ యొక్క ఎంట్రీ పాయింట్. వెబ్‌ప్యాక్ ఇక్కడ నుండి ప్రారంభమవుతుంది మరియు ఇతర దిగుమతి చేయబడిన ఫైల్‌ల కోసం పునరావృతంగా శోధిస్తుంది.
  • మా వెబ్‌ప్యాక్ బిల్డ్ యొక్క అవుట్‌పుట్ 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

మరియు వెబ్ బ్రౌజర్‌కి వెళ్లండి localhost: 3000. కోడ్ మారినప్పుడు డెవలప్‌మెంట్ సర్వర్ స్వయంచాలకంగా JS మరియు CSS ప్యాకేజీలను పునర్నిర్మిస్తుంది - అన్ని మార్పులను చూడటానికి పేజీని రిఫ్రెష్ చేయండి!

3. క్లయింట్ ఎంట్రీ పాయింట్లు

గేమ్ కోడ్ విషయానికి వెళ్దాం. మొదట మనకు ఒక పేజీ అవసరం index.html, సైట్‌ను సందర్శించినప్పుడు, బ్రౌజర్ దీన్ని ముందుగా లోడ్ చేస్తుంది. మా పేజీ చాలా సరళంగా ఉంటుంది:

index.html

ఒక ఉదాహరణ .io గేమ్  ఆడండి

ఈ కోడ్ ఉదాహరణ స్పష్టత కోసం కొద్దిగా సరళీకృతం చేయబడింది మరియు నేను అనేక ఇతర పోస్ట్ ఉదాహరణలతో కూడా అదే చేస్తాను. పూర్తి కోడ్‌ని ఎల్లప్పుడూ వీక్షించవచ్చు Github.

మాకు ఉన్నాయి:

  • HTML5 కాన్వాస్ మూలకం (<canvas>) మేము గేమ్‌ను రెండర్ చేయడానికి ఉపయోగిస్తాము.
  • <link> మా CSS ప్యాకేజీని జోడించడానికి.
  • <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. "PLAY" బటన్‌ను నొక్కడం కోసం హ్యాండ్లర్‌ని సెట్ చేస్తోంది. బటన్‌ను నొక్కినప్పుడు, కోడ్ గేమ్‌ను ప్రారంభిస్తుంది మరియు మేము ఆడటానికి సిద్ధంగా ఉన్నామని సర్వర్‌కు తెలియజేస్తుంది.

మా క్లయింట్-సర్వర్ లాజిక్ యొక్క ప్రధాన "మాంసం" ఫైల్ ద్వారా దిగుమతి చేయబడిన ఫైల్‌లలో ఉంది index.js. ఇప్పుడు మేము వాటిని అన్నింటినీ క్రమంలో పరిశీలిస్తాము.

4. కస్టమర్ డేటా మార్పిడి

ఈ గేమ్‌లో, సర్వర్‌తో కమ్యూనికేట్ చేయడానికి మేము బాగా తెలిసిన లైబ్రరీని ఉపయోగిస్తాము socket.io. Socket.ioకి స్థానిక మద్దతు ఉంది వెబ్‌సాకెట్లు, రెండు-మార్గం కమ్యూనికేషన్ కోసం బాగా సరిపోతాయి: మేము సర్వర్‌కు సందేశాలను పంపవచ్చు и సర్వర్ అదే కనెక్షన్‌లో మాకు సందేశాలను పంపగలదు.

మాకు ఒక ఫైల్ ఉంటుంది src/client/networking.jsఎవరు చూసుకుంటారు ప్రతి ఒక్కరూ సర్వర్‌తో కమ్యూనికేషన్:

networking.js

import io from 'socket.io-client';
import { processGameUpdate } from './state';

const Constants = require('../shared/constants');

const socket = io(`ws://${window.location.host}`);
const connectedPromise = new Promise(resolve => {
  socket.on('connect', () => {
    console.log('Connected to server!');
    resolve();
  });
});

export const connect = onGameOver => (
  connectedPromise.then(() => {
    // Register callbacks
    socket.on(Constants.MSG_TYPES.GAME_UPDATE, processGameUpdate);
    socket.on(Constants.MSG_TYPES.GAME_OVER, onGameOver);
  })
);

export const play = username => {
  socket.emit(Constants.MSG_TYPES.JOIN_GAME, username);
};

export const updateDirection = dir => {
  socket.emit(Constants.MSG_TYPES.INPUT, dir);
};

స్పష్టత కోసం ఈ కోడ్ కూడా కొద్దిగా కుదించబడింది.

ఈ ఫైల్‌లో మూడు ప్రధాన చర్యలు ఉన్నాయి:

  • మేము సర్వర్‌కి కనెక్ట్ చేయడానికి ప్రయత్నిస్తున్నాము. connectedPromise మేము కనెక్షన్‌ని ఏర్పాటు చేసుకున్నప్పుడు మాత్రమే అనుమతించబడుతుంది.
  • కనెక్షన్ విజయవంతమైతే, మేము కాల్ బ్యాక్ ఫంక్షన్లను నమోదు చేస్తాము (processGameUpdate() и onGameOver()) సందేశాల కోసం మేము సర్వర్ నుండి స్వీకరించగలము.
  • మేము ఎగుమతి చేస్తాము play() и updateDirection()తద్వారా ఇతర ఫైల్‌లు వాటిని ఉపయోగించగలవు.

5. క్లయింట్ రెండరింగ్

చిత్రాన్ని తెరపై ప్రదర్శించడానికి ఇది సమయం!

…కానీ మనం దీన్ని చేయడానికి ముందు, దీనికి అవసరమైన అన్ని చిత్రాలను (వనరులు) డౌన్‌లోడ్ చేసుకోవాలి. రిసోర్స్ మేనేజర్‌ని వ్రాద్దాం:

ఆస్తులు.js

const ASSET_NAMES = ['ship.svg', 'bullet.svg'];

const assets = {};
const downloadPromise = Promise.all(ASSET_NAMES.map(downloadAsset));

function downloadAsset(assetName) {
  return new Promise(resolve => {
    const asset = new Image();
    asset.onload = () => {
      console.log(`Downloaded ${assetName}`);
      assets[assetName] = asset;
      resolve();
    };
    asset.src = `/assets/${assetName}`;
  });
}

export const downloadAssets = () => downloadPromise;
export const getAsset = assetName => assets[assetName];

వనరుల నిర్వహణ అమలు చేయడం అంత కష్టం కాదు! ఒక వస్తువును నిల్వ చేయడం ప్రధాన ఆలోచన assets, ఇది ఫైల్ పేరు యొక్క కీని వస్తువు విలువకు బంధిస్తుంది Image. వనరు లోడ్ అయినప్పుడు, మేము దానిని ఒక వస్తువులో నిల్వ చేస్తాము assets భవిష్యత్తులో శీఘ్ర ప్రాప్యత కోసం. ప్రతి వ్యక్తి వనరు ఎప్పుడు డౌన్‌లోడ్ చేసుకోవడానికి అనుమతించబడుతుంది (అంటే, అన్ని వనరులు), మేము అనుమతిస్తాము downloadPromise.

వనరులను డౌన్‌లోడ్ చేసిన తర్వాత, మీరు రెండరింగ్ ప్రారంభించవచ్చు. ముందుగా చెప్పినట్లుగా, వెబ్ పేజీలో గీయడానికి, మేము ఉపయోగిస్తాము HTML5 కాన్వాస్ (<canvas>) మా ఆట చాలా సులభం, కాబట్టి మేము ఈ క్రింది వాటిని మాత్రమే గీయాలి:

  1. నేపథ్య
  2. ప్లేయర్ షిప్
  3. ఆటలో ఇతర ఆటగాళ్ళు
  4. పెంకులు

ఇక్కడ ముఖ్యమైన స్నిప్పెట్‌లు ఉన్నాయి src/client/render.js, ఇది పైన జాబితా చేయబడిన నాలుగు అంశాలను సరిగ్గా రెండర్ చేస్తుంది:

render.js

import { getAsset } from './assets';
import { getCurrentState } from './state';

const Constants = require('../shared/constants');
const { PLAYER_RADIUS, PLAYER_MAX_HP, BULLET_RADIUS, MAP_SIZE } = Constants;

// Get the canvas graphics context
const canvas = document.getElementById('game-canvas');
const context = canvas.getContext('2d');

// Make the canvas fullscreen
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

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

  // Draw background
  renderBackground(me.x, me.y);

  // Draw all bullets
  bullets.forEach(renderBullet.bind(null, me));

  // Draw all players
  renderPlayer(me, me);
  others.forEach(renderPlayer.bind(null, me));
}

// ... Helper functions here excluded

let renderInterval = null;
export function startRendering() {
  renderInterval = setInterval(render, 1000 / 60);
}
export function stopRendering() {
  clearInterval(renderInterval);
}

ఈ కోడ్ స్పష్టత కోసం కూడా కుదించబడింది.

render() అనేది ఈ ఫైల్ యొక్క ప్రధాన విధి. startRendering() и stopRendering() 60 FPS వద్ద రెండర్ లూప్ యొక్క క్రియాశీలతను నియంత్రించండి.

వ్యక్తిగత రెండరింగ్ సహాయక విధుల యొక్క కాంక్రీట్ అమలులు (ఉదా. renderBullet()) అంత ముఖ్యమైనది కాదు, కానీ ఇక్కడ ఒక సాధారణ ఉదాహరణ:

render.js

function renderBullet(me, bullet) {
  const { x, y } = bullet;
  context.drawImage(
    getAsset('bullet.svg'),
    canvas.width / 2 + x - me.x - BULLET_RADIUS,
    canvas.height / 2 + y - me.y - BULLET_RADIUS,
    BULLET_RADIUS * 2,
    BULLET_RADIUS * 2,
  );
}

మేము పద్ధతిని ఉపయోగిస్తున్నామని గమనించండి getAsset(), ఇది గతంలో కనిపించింది asset.js!

ఇతర రెండరింగ్ సహాయకులను అన్వేషించడానికి మీకు ఆసక్తి ఉంటే, మిగిలిన వాటిని చదవండి src/client/render.js.

6. క్లయింట్ ఇన్‌పుట్

ఇది గేమ్ చేయడానికి సమయం ఆడదగిన! నియంత్రణ పథకం చాలా సరళంగా ఉంటుంది: కదలిక దిశను మార్చడానికి, మీరు మౌస్ (కంప్యూటర్లో) ఉపయోగించవచ్చు లేదా స్క్రీన్ (మొబైల్ పరికరంలో) తాకవచ్చు. దీన్ని అమలు చేయడానికి, మేము నమోదు చేస్తాము ఈవెంట్ శ్రోతలు మౌస్ మరియు టచ్ ఈవెంట్‌ల కోసం.
ఇవన్నీ చూసుకుంటా src/client/input.js:

input.js

import { updateDirection } from './networking';

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

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

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

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

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

onMouseInput() и onTouchInput() కాల్ చేసే ఈవెంట్ లిజనర్స్ updateDirection() (యొక్క networking.js) ఇన్‌పుట్ ఈవెంట్ జరిగినప్పుడు (ఉదాహరణకు, మౌస్ తరలించబడినప్పుడు). updateDirection() సర్వర్‌తో సందేశాన్ని నిర్వహిస్తుంది, ఇది ఇన్‌పుట్ ఈవెంట్‌ను నిర్వహిస్తుంది మరియు తదనుగుణంగా గేమ్ స్థితిని నవీకరిస్తుంది.

7. క్లయింట్ స్థితి

పోస్ట్ యొక్క మొదటి భాగంలో ఈ విభాగం చాలా కష్టం. మీరు దీన్ని మొదటిసారి చదివినప్పుడు మీకు అర్థం కాకపోతే నిరుత్సాహపడకండి! మీరు దానిని దాటవేయవచ్చు మరియు తర్వాత దానికి తిరిగి రావచ్చు.

క్లయింట్/సర్వర్ కోడ్‌ని పూర్తి చేయడానికి అవసరమైన పజిల్ చివరి భాగం రాష్ట్ర. క్లయింట్ రెండరింగ్ విభాగం నుండి కోడ్ స్నిప్పెట్ గుర్తుందా?

render.js

import { getCurrentState } from './state';

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

  // Do the rendering
  // ...
}

getCurrentState() క్లయింట్‌లో ఆట యొక్క ప్రస్తుత స్థితిని మాకు అందించగలగాలి ఏ సమయంలోనైనా సర్వర్ నుండి అందుకున్న నవీకరణల ఆధారంగా. సర్వర్ పంపగల గేమ్ అప్‌డేట్ యొక్క ఉదాహరణ ఇక్కడ ఉంది:

{
  "t": 1555960373725,
  "me": {
    "x": 2213.8050880413657,
    "y": 1469.370893425012,
    "direction": 1.3082443894581433,
    "id": "AhzgAtklgo2FJvwWAADO",
    "hp": 100
  },
  "others": [],
  "bullets": [
    {
      "id": "RUJfJ8Y18n",
      "x": 2354.029197099604,
      "y": 1431.6848318262666
    },
    {
      "id": "ctg5rht5s",
      "x": 2260.546457727445,
      "y": 1456.8088728920968
    }
  ],
  "leaderboard": [
    {
      "username": "Player",
      "score": 3
    }
  ]
}

ప్రతి గేమ్ అప్‌డేట్ ఐదు ఒకే ఫీల్డ్‌లను కలిగి ఉంటుంది:

  • t: ఈ నవీకరణ ఎప్పుడు సృష్టించబడిందో సూచిస్తున్న సర్వర్ టైమ్‌స్టాంప్.
  • me: ఈ నవీకరణను స్వీకరించే ప్లేయర్ గురించిన సమాచారం.
  • ఇతరులు: అదే గేమ్‌లో పాల్గొనే ఇతర ఆటగాళ్ల గురించి సమాచారం యొక్క శ్రేణి.
  • బులెట్లు: గేమ్‌లోని ప్రక్షేపకాల గురించి సమాచారం యొక్క శ్రేణి.
  • లీడర్బోర్డ్: ప్రస్తుత లీడర్‌బోర్డ్ డేటా. ఈ పోస్ట్‌లో, మేము వాటిని పరిగణించము.

7.1 అమాయక క్లయింట్ స్థితి

అమాయక అమలు getCurrentState() ఇటీవల స్వీకరించిన గేమ్ అప్‌డేట్ డేటాను మాత్రమే నేరుగా తిరిగి ఇవ్వగలదు.

naive-state.js

let lastGameUpdate = null;

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

export function getCurrentState() {
  return lastGameUpdate;
}

బాగుంది మరియు స్పష్టంగా! కానీ అది చాలా సరళంగా ఉంటే. ఈ అమలు సమస్యాత్మకమైన కారణాలలో ఒకటి: ఇది రెండరింగ్ ఫ్రేమ్ రేట్‌ను సర్వర్ క్లాక్ రేట్‌కు పరిమితం చేస్తుంది.

ఫ్రేమ్ రేట్: ఫ్రేమ్‌ల సంఖ్య (అంటే కాల్‌లు render()) సెకనుకు, లేదా FPS. ఆటలు సాధారణంగా కనీసం 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 కోసం. ఇది సున్నితమైన గేమ్‌ప్లే కోసం చిన్న త్యాగం - చాలా మంది ఆటగాళ్ళు (ముఖ్యంగా సాధారణ ఆటగాళ్ళు) ఈ ఆలస్యాన్ని కూడా గమనించలేరు. అనూహ్య జాప్యంతో ఆడడం కంటే స్థిరమైన 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.jsGithub.

పార్ట్ 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-dev-middleware మా అభివృద్ధి ప్యాకేజీలను స్వయంచాలకంగా పునర్నిర్మించడానికి, లేదా
  • స్థిరంగా బదిలీ ఫోల్డర్ dist/, ఉత్పత్తి బిల్డ్ తర్వాత వెబ్‌ప్యాక్ మా ఫైల్‌లను వ్రాస్తుంది.

మరొక ముఖ్యమైన పని server.js సర్వర్‌ను సెటప్ చేయడం socket.ioఇది కేవలం ఎక్స్‌ప్రెస్ సర్వర్‌కు కనెక్ట్ చేస్తుంది:

server.js భాగం 2

const socketio = require('socket.io');
const Constants = require('../shared/constants');

// Setup Express
// ...
const server = app.listen(port);
console.log(`Server listening on port ${port}`);

// Setup socket.io
const io = socketio(server);

// Listen for socket.io connections
io.on('connection', socket => {
  console.log('Player connected!', socket.id);

  socket.on(Constants.MSG_TYPES.JOIN_GAME, joinGame);
  socket.on(Constants.MSG_TYPES.INPUT, handleInput);
  socket.on('disconnect', onDisconnect);
});

సర్వర్‌కు socket.io కనెక్షన్‌ని విజయవంతంగా ఏర్పాటు చేసిన తర్వాత, మేము కొత్త సాకెట్ కోసం ఈవెంట్ హ్యాండ్లర్‌లను సెటప్ చేస్తాము. ఈవెంట్ హ్యాండ్లర్లు క్లయింట్‌ల నుండి స్వీకరించిన సందేశాలను సింగిల్‌టన్ ఆబ్జెక్ట్‌కు అప్పగించడం ద్వారా నిర్వహిస్తారు game:

server.js భాగం 3

const Game = require('./game');

// ...

// Setup the Game
const game = new Game();

function joinGame(username) {
  game.addPlayer(this, username);
}

function handleInput(dir) {
  game.handleInput(this, dir);
}

function onDisconnect() {
  game.removePlayer(this);
}

మేము .io గేమ్‌ని సృష్టిస్తున్నాము, కాబట్టి మాకు ఒక కాపీ మాత్రమే అవసరం Game ("గేమ్") - ఆటగాళ్లందరూ ఒకే ఎరీనాలో ఆడతారు! తదుపరి విభాగంలో, ఈ తరగతి ఎలా పనిచేస్తుందో చూద్దాం. Game.

2. గేమ్ సర్వర్లు

Класс Game సర్వర్ వైపు అత్యంత ముఖ్యమైన లాజిక్‌ను కలిగి ఉంది. దీనికి రెండు ప్రధాన పనులు ఉన్నాయి: ఆటగాడి నిర్వహణ и గేమ్ అనుకరణ.

మొదటి పని, ప్లేయర్ మేనేజ్‌మెంట్‌తో ప్రారంభిద్దాం.

game.js భాగం 1

const Constants = require('../shared/constants');
const Player = require('./player');

class Game {
  constructor() {
    this.sockets = {};
    this.players = {};
    this.bullets = [];
    this.lastUpdateTime = Date.now();
    this.shouldSendUpdate = false;
    setInterval(this.update.bind(this), 1000 / 60);
  }

  addPlayer(socket, username) {
    this.sockets[socket.id] = socket;

    // Generate a position to start this player at.
    const x = Constants.MAP_SIZE * (0.25 + Math.random() * 0.5);
    const y = Constants.MAP_SIZE * (0.25 + Math.random() * 0.5);
    this.players[socket.id] = new Player(socket.id, username, x, y);
  }

  removePlayer(socket) {
    delete this.sockets[socket.id];
    delete this.players[socket.id];
  }

  handleInput(socket, dir) {
    if (this.players[socket.id]) {
      this.players[socket.id].setDirection(dir);
    }
  }

  // ...
}

ఈ గేమ్‌లో, మేము మైదానం ద్వారా ఆటగాళ్లను గుర్తిస్తాము id వారి socket.io సాకెట్ (మీరు గందరగోళంగా ఉంటే, తిరిగి వెళ్లండి server.js) Socket.io ప్రతి సాకెట్‌కు ఒక ప్రత్యేకతను కేటాయించింది idకాబట్టి మనం దాని గురించి ఆందోళన చెందాల్సిన అవసరం లేదు. నేను అతనిని పిలుస్తాను ప్లేయర్ ID.

దీన్ని దృష్టిలో ఉంచుకుని, తరగతిలోని ఉదాహరణ వేరియబుల్‌లను అన్వేషిద్దాం Game:

  • sockets ప్లేయర్‌తో అనుబంధించబడిన సాకెట్‌కు ప్లేయర్ IDని బంధించే వస్తువు. ఇది స్థిరమైన సమయంలో వారి ప్లేయర్ IDల ద్వారా సాకెట్‌లను యాక్సెస్ చేయడానికి మమ్మల్ని అనుమతిస్తుంది.
  • players ప్లేయర్ IDని కోడ్>ప్లేయర్ ఆబ్జెక్ట్‌కి బంధించే వస్తువు

bullets వస్తువుల శ్రేణి Bullet, ఇది ఖచ్చితమైన క్రమం లేదు.
lastUpdateTime గేమ్ చివరిసారిగా అప్‌డేట్ చేయబడిన టైమ్‌స్టాంప్. ఇది ఎలా ఉపయోగించబడుతుందో త్వరలో చూద్దాం.
shouldSendUpdate ఒక సహాయక వేరియబుల్. త్వరలో దాని ఉపయోగం కూడా చూస్తాము.
పద్ధతులు addPlayer(), removePlayer() и handleInput() వివరించాల్సిన అవసరం లేదు, అవి ఉపయోగించబడతాయి server.js. మీరు మీ మెమరీని రిఫ్రెష్ చేయవలసి వస్తే, కొంచెం పైకి వెళ్లండి.

చివరి పంక్తి constructor() మొదలవుతుంది నవీకరణ చక్రం ఆటలు (60 నవీకరణలు / సె ఫ్రీక్వెన్సీతో):

game.js భాగం 2

const Constants = require('../shared/constants');
const applyCollisions = require('./collisions');

class Game {
  // ...

  update() {
    // Calculate time elapsed
    const now = Date.now();
    const dt = (now - this.lastUpdateTime) / 1000;
    this.lastUpdateTime = now;

    // Update each bullet
    const bulletsToRemove = [];
    this.bullets.forEach(bullet => {
      if (bullet.update(dt)) {
        // Destroy this bullet
        bulletsToRemove.push(bullet);
      }
    });
    this.bullets = this.bullets.filter(
      bullet => !bulletsToRemove.includes(bullet),
    );

    // Update each player
    Object.keys(this.sockets).forEach(playerID => {
      const player = this.players[playerID];
      const newBullet = player.update(dt);
      if (newBullet) {
        this.bullets.push(newBullet);
      }
    });

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

    // Check if any players are dead
    Object.keys(this.sockets).forEach(playerID => {
      const socket = this.sockets[playerID];
      const player = this.players[playerID];
      if (player.hp <= 0) {
        socket.emit(Constants.MSG_TYPES.GAME_OVER);
        this.removePlayer(socket);
      }
    });

    // Send a game update to each player every other time
    if (this.shouldSendUpdate) {
      const leaderboard = this.getLeaderboard();
      Object.keys(this.sockets).forEach(playerID => {
        const socket = this.sockets[playerID];
        const player = this.players[playerID];
        socket.emit(
          Constants.MSG_TYPES.GAME_UPDATE,
          this.createUpdate(player, leaderboard),
        );
      });
      this.shouldSendUpdate = false;
    } else {
      this.shouldSendUpdate = true;
    }
  }

  // ...
}

పద్ధతి update() సర్వర్ సైడ్ లాజిక్ యొక్క అతి ముఖ్యమైన భాగాన్ని కలిగి ఉంటుంది. ఇది క్రమంలో ఏమి చేస్తుందో ఇక్కడ ఉంది:

  1. ఎంత సేపు ఉంటుందో లెక్కిస్తుంది dt చివరి నుండి ఆమోదించింది update().
  2. ప్రతి ప్రక్షేపకాన్ని రిఫ్రెష్ చేస్తుంది మరియు అవసరమైతే వాటిని నాశనం చేస్తుంది. మేము ఈ ఫంక్షనాలిటీ యొక్క అమలును తరువాత చూస్తాము. ప్రస్తుతానికి అది మనకు తెలిస్తే చాలు bullet.update() తిరిగి trueఒకవేళ ప్రక్షేపకం నాశనం చేయాలి (అతను అరేనా నుండి బయటికి వచ్చాడు).
  3. ప్రతి ప్లేయర్‌ని అప్‌డేట్ చేస్తుంది మరియు అవసరమైతే ఒక ప్రక్షేపకం చేస్తుంది. మేము ఈ అమలును కూడా తర్వాత చూస్తాము - player.update() ఒక వస్తువును తిరిగి ఇవ్వవచ్చు Bullet.
  4. ప్రక్షేపకాలు మరియు ప్లేయర్‌ల మధ్య ఘర్షణల కోసం తనిఖీలు applyCollisions(), ఇది ప్లేయర్‌లను కొట్టే ప్రక్షేపకాల శ్రేణిని అందిస్తుంది. తిరిగి వచ్చిన ప్రతి ప్రక్షేపకం కోసం, మేము దానిని కాల్చిన ఆటగాడి పాయింట్లను పెంచుతాము (ఉపయోగించి player.onDealtDamage()) ఆపై శ్రేణి నుండి ప్రక్షేపకాన్ని తీసివేయండి bullets.
  5. చంపబడిన ఆటగాళ్లందరినీ తెలియజేస్తుంది మరియు నాశనం చేస్తుంది.
  6. ఆటగాళ్లందరికీ గేమ్ అప్‌డేట్‌ను పంపుతుంది ప్రతి క్షణం పిలిచిన సార్లు update(). ఇది పైన పేర్కొన్న సహాయక వేరియబుల్‌ను ట్రాక్ చేయడంలో మాకు సహాయపడుతుంది. shouldSendUpdate. ఎందుకంటే update() 60 సార్లు/s అని పిలుస్తారు, మేము గేమ్ అప్‌డేట్‌లను 30 సార్లు/s పంపుతాము. ఈ విధంగా, గడియారం ఫ్రీక్వెన్సీ సర్వర్ గడియారం 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

ఒక వ్యాఖ్యను జోడించండి