ባለብዙ ተጫዋች .io የድር ጨዋታ መፍጠር

ባለብዙ ተጫዋች .io የድር ጨዋታ መፍጠር
በ2015 ተለቋል አግአርዮ.ዮ. የአዲሱ ዘውግ ቅድመ አያት ሆነ ጨዋታዎች .ioከዚያን ጊዜ ጀምሮ በታዋቂነት ያደገው. በግሌ የ.io ጨዋታዎች ተወዳጅነት መጨመር አጋጥሞኛል፡ ባለፉት ሶስት አመታት ውስጥ፣ አለኝ የዚህ ዘውግ ሁለት ጨዋታዎችን ፈጠረ እና ተሽጧል።.

ስለእነዚህ ጨዋታዎች ከዚህ በፊት ሰምተህ የማታውቀው ከሆነ፣ ለመጫወት ቀላል የሆኑ ነጻ ባለብዙ ተጫዋች የድር ጨዋታዎች ናቸው (ምንም መለያ አያስፈልግም)። ብዙውን ጊዜ በተመሳሳይ መድረክ ብዙ ተቃራኒ ተጫዋቾችን ይጋፈጣሉ። ሌሎች ታዋቂ የ.io ጨዋታዎች፡- Slither.io и ዲፒፒዮ.

በዚህ ጽሑፍ ውስጥ, እንዴት እንደሆነ እንመረምራለን ከባዶ የ.io ጨዋታ ይፍጠሩ. ለዚህ, የጃቫስክሪፕት እውቀት ብቻ በቂ ይሆናል: እንደ አገባብ ያሉ ነገሮችን መረዳት ያስፈልግዎታል ES6፣ ቁልፍ ቃል this и ተስፋዎች. የጃቫስክሪፕት እውቀትዎ ፍፁም ባይሆንም አብዛኛውን ልጥፉን መረዳት ይችላሉ።

.io ጨዋታ ምሳሌ

ለትምህርት እርዳታ፣ እንጠቅሳለን። .io ጨዋታ ምሳሌ. እሱን ለመጫወት ይሞክሩ!

ባለብዙ ተጫዋች .io የድር ጨዋታ መፍጠር
ጨዋታው በጣም ቀላል ነው፡ ሌሎች ተጫዋቾች ባሉበት መድረክ ላይ መርከብን ትቆጣጠራላችሁ። የእርስዎ መርከብ በራስ-ሰር ፕሮጄክተሮችን ያቃጥላል እና ሌሎች ተጫዋቾችን ፕሮጄክተሮችን በማስወገድ ላይ እያሉ ለመምታት ይሞክራሉ።

1. የፕሮጀክቱ አጭር መግለጫ / መዋቅር

እኔ አመሰግናለሁ ምንጭ ኮድ አውርድ እኔን መከተል እንዲችሉ የምሳሌ ጨዋታ።

ምሳሌው የሚከተሉትን ይጠቀማል:

  • ይግለጹ የጨዋታውን የድር አገልጋይ የሚያስተዳድር በጣም ታዋቂው የ Node.js የድር ማዕቀፍ ነው።
  • ሶኬት.io - በአሳሽ እና በአገልጋይ መካከል ውሂብ ለመለዋወጥ የዌብሶኬት ቤተ-መጽሐፍት።
  • Webpack። - ሞጁል አስተዳዳሪ. ለምን 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) ደንበኛ መግቢያ ነጥብ ነው። ዌብፓክ ከዚህ ይጀምራል እና ሌሎች ወደ ሀገር ውስጥ የሚገቡ ፋይሎችን በየጊዜው ይፈልጋል።
  • የእኛ የዌብፓክ ግንባታ ውጤት JS በማውጫው ውስጥ ይቀመጣል dist/. ይህንን ፋይል የኛ እደውላለሁ። js ጥቅል.
  • እኛ እንጠቀማለን ባቤል, እና በተለይም አወቃቀሩ @babel/preset-env ለአሮጌ አሳሾች የኛን JS ኮድ ለመፃፍ።
  • በJS ፋይሎች የተጠቀሱ ሁሉንም CSS ለማውጣት እና በአንድ ቦታ ለማጣመር ፕለጊን እየተጠቀምን ነው። የኛ እለዋለሁ። css ጥቅል.

እንግዳ የጥቅል ፋይል ስሞችን አስተውለህ ይሆናል። '[name].[contenthash].ext'. ይይዛሉ የፋይል ስም ምትክ የድር ቦርሳ [name] በግቤት ነጥቡ ስም ይተካዋል (በእኛ ሁኔታ ይህ game) ፣ እና [contenthash] በፋይሉ ይዘት ሃሽ ይተካል። እናደርገዋለን ፕሮጀክቱን ለ hashing ያመቻቹ - አሳሾች የእኛን JS ጥቅሎች ላልተወሰነ ጊዜ እንዲሸጎጡ መንገር ይችላሉ፣ ምክንያቱም አንድ ጥቅል ከተለወጠ የፋይሉ ስምም ይለወጣል (ይለውጣል contenthash). የመጨረሻው ውጤት የእይታ ፋይል ስም ይሆናል game.dbeee76e91a97d0c7207.js.

ፋይል webpack.common.js ወደ ግንባታ እና የተጠናቀቁ የፕሮጀክት ውቅሮች የምናስገባው የመሠረት ውቅር ፋይል ነው። የልማት ውቅር ምሳሌ ይኸውና፡

webpack.dev.js

const merge = require('webpack-merge');
const common = require('./webpack.common.js');

module.exports = merge(common, {
  mode: 'development',
});

ለውጤታማነት, በልማት ሂደት ውስጥ እንጠቀማለን webpack.dev.js, እና ወደ ይቀይራል webpack.prod.jsወደ ምርት በሚሰራጭበት ጊዜ የጥቅል መጠኖችን ለማመቻቸት.

የአካባቢ ቅንብር

በዚህ ጽሑፍ ውስጥ የተዘረዘሩትን ደረጃዎች መከተል እንዲችሉ ፕሮጀክቱን በአካባቢው ማሽን ላይ እንዲጭኑ እመክራለሁ. ማዋቀሩ ቀላል ነው: በመጀመሪያ, ስርዓቱ መጫን አለበት አንጓ и 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 ጥቅል ውስጥ እንደሚያካትታቸው ያውቃል)።
  3. Запуск connect() ከአገልጋዩ ጋር ግንኙነት ለመመስረት እና ለማሄድ downloadAssets() ጨዋታውን ለመስራት የሚያስፈልጉ ምስሎችን ለማውረድ።
  4. ደረጃ 3 ከተጠናቀቀ በኋላ ዋናው ምናሌ ይታያል (playMenu).
  5. የ"PLAY" ቁልፍን ለመጫን ተቆጣጣሪውን በማዘጋጀት ላይ። ቁልፉ ሲጫን ኮዱ ጨዋታውን ይጀምራል እና እኛ ለመጫወት ዝግጁ መሆናችንን ለአገልጋዩ ይነግረዋል።

የእኛ የደንበኛ-አገልጋይ አመክንዮ ዋናው "ስጋ" በፋይሉ ወደ ሀገር ውስጥ በገቡት ፋይሎች ውስጥ ነው index.js. አሁን ሁሉንም በቅደም ተከተል እንመለከታለን.

4. የደንበኛ ውሂብ መለዋወጥ

በዚህ ጨዋታ ከአገልጋዩ ጋር ለመገናኘት የታወቀ ቤተ-መጽሐፍትን እንጠቀማለን። ሶኬት.io. Socket.io ቤተኛ ድጋፍ አለው። ዌብሳይቶች, ለሁለት መንገድ ግንኙነት በጣም ተስማሚ የሆኑት: ወደ አገልጋዩ መልእክት መላክ እንችላለን и አገልጋዩ በተመሳሳይ ግንኙነት ላይ መልእክት ሊልክልን ይችላል።

አንድ ፋይል ይኖረናል src/client/networking.jsማን ይንከባከባል ሁሉም ሰው ከአገልጋዩ ጋር መገናኘት;

አውታረ መረብ.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ከላይ የተዘረዘሩትን አራት ነገሮች በትክክል የሚያቀርብ፡-

ማቅረብ.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() የ render loop ን እንቅስቃሴ በ60 FPS ይቆጣጠሩ።

የግለሰባዊ ረዳት ተግባራት ተጨባጭ ትግበራዎች (ለምሳሌ ፣ renderBullet()) ያን ያህል አስፈላጊ አይደሉም፣ ግን አንድ ቀላል ምሳሌ እዚህ አለ፡-

ማቅረብ.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/ደንበኛ/render.js.

6. የደንበኛ ግቤት

ጨዋታ ለመስራት ጊዜው አሁን ነው። ሊጫወት የሚችል! የቁጥጥር መርሃግብሩ በጣም ቀላል ይሆናል-የእንቅስቃሴውን አቅጣጫ ለመለወጥ, አይጤውን (በኮምፒተር ላይ) መጠቀም ወይም ማያ ገጹን (በሞባይል መሳሪያ ላይ) መንካት ይችላሉ. ይህንን ተግባራዊ ለማድረግ እንመዘግባለን። የዝግጅት አድማጮች ለ Mouse እና Touch ክስተቶች.
ይህንን ሁሉ ይንከባከባል src/client/input.js:

ግብዓት.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. የደንበኛ ሁኔታ

ይህ ክፍል በፖስታው የመጀመሪያ ክፍል ውስጥ በጣም አስቸጋሪው ነው. ለመጀመሪያ ጊዜ ስታነቡት ካልተረዳችሁት ተስፋ አትቁረጡ! እንዲያውም መዝለል ትችላለህ እና በኋላ ወደ እሱ መመለስ ትችላለህ።

የደንበኛው/የአገልጋይ ኮድ ለማጠናቀቅ የሚያስፈልገው የመጨረሻው የእንቆቅልሽ ክፍል ነው። ግዛት. ከደንበኛ አተረጓጎም ክፍል የተገኘውን የኮድ ቅንጣቢ አስታውስ?

ማቅረብ.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 Naive ደንበኛ ሁኔታ

የዋህ አተገባበር 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ሚሴ (በሴኮንድ 30 በሰከንድ) የጨዋታ ዝማኔ ይቀበላል።

ባለብዙ ተጫዋች .io የድር ጨዋታ መፍጠር
በሚያሳዝን ሁኔታ, ምንም ነገር ፍጹም አይደለም. የበለጠ ትክክለኛ ምስል የሚከተለው ይሆናል-
ባለብዙ ተጫዋች .io የድር ጨዋታ መፍጠር
የዋህነት አተገባበር ወደ መዘግየት ሲመጣ በተግባር እጅግ የከፋ ነው። የጨዋታ ዝመና በ 50ms መዘግየት ከደረሰ ፣ ከዚያ የደንበኛ መሸጫዎች ተጨማሪ 50 ሚሴ ምክንያቱም አሁንም የጨዋታውን ሁኔታ ከቀደመው ዝማኔ እያቀረበ ነው። ይህ ለተጫዋቹ ምን ያህል የማይመች እንደሆነ መገመት ትችላላችሁ፡ የዘፈቀደ ብሬኪንግ ጨዋታውን ያሸበረቀ እና ያልተረጋጋ ያደርገዋል።

7.2 የተሻሻለ የደንበኛ ሁኔታ

በአፈፃፀሙ ላይ አንዳንድ ማሻሻያዎችን እናደርጋለን። በመጀመሪያ, እንጠቀማለን መዘግየት ማሳየት ለ 100 ms. ይህ ማለት የደንበኛው "የአሁኑ" ሁኔታ ሁል ጊዜ በአገልጋዩ ላይ ካለው የጨዋታ ሁኔታ በ100 ሚ.ሴ. ለምሳሌ, በአገልጋዩ ላይ ያለው ጊዜ ከሆነ 150, ከዚያም ደንበኛው አገልጋዩ በወቅቱ የነበረውን ሁኔታ ያቀርባል 50:

ባለብዙ ተጫዋች .io የድር ጨዋታ መፍጠር
ይህ የማይገመት የጨዋታ ዝመና ጊዜዎችን ለመትረፍ የ100ms ቋት ይሰጠናል፡

ባለብዙ ተጫዋች .io የድር ጨዋታ መፍጠር
ለዚህ ያለው ክፍያ ዘላቂ ይሆናል የግቤት መዘግየት ለ 100 ms. ይህ ለስላሳ አጨዋወት ቀላል መስዋእትነት ነው - አብዛኞቹ ተጫዋቾች (በተለይ ተራ ተጫዋቾች) ይህን መዘግየት እንኳን አያስተውሉም። ባልተጠበቀ መዘግየት ከመጫወት ይልቅ ሰዎች ቋሚ የሆነ የ100 ሚ.ሜ መዘግየትን ማስተካከል ቀላል ነው።

ሌላ የሚባል ቴክኒክ መጠቀም እንችላለን የደንበኛ-ጎን ትንበያ, ይህም የታሰበውን መዘግየትን በመቀነስ ጥሩ ስራ ይሰራል, ነገር ግን በዚህ ጽሑፍ ውስጥ አይካተትም.

ሌላው እየተጠቀምንበት ያለው ማሻሻያ ነው። መስመራዊ ጣልቃገብነት. በማሳየት መዘግየት ምክንያት፣ እኛ ብዙውን ጊዜ በደንበኛው ውስጥ ካለው የአሁኑ ጊዜ በፊት ቢያንስ አንድ ዝመና እንሆናለን። ሲጠራ getCurrentState(), መፈጸም እንችላለን መስመራዊ ጣልቃገብነት በደንበኛው ውስጥ ካለው የአሁኑ ጊዜ በፊት እና በኋላ ባለው የጨዋታ ዝመናዎች መካከል፡-

ባለብዙ ተጫዋች .io የድር ጨዋታ መፍጠር
ይሄ የፍሬም ተመን ችግርን ይፈታል፡ አሁን ልዩ ፍሬሞችን በምንፈልገው የፍሬም ፍጥነት መስራት እንችላለን!

7.3 የተሻሻለ የደንበኛ ሁኔታን በመተግበር ላይ

የትግበራ ምሳሌ በ src/client/state.js ሁለቱንም render lag እና linear interpolation ይጠቀማል፣ ግን ለረጅም ጊዜ አይደለም። ኮዱን በሁለት ክፍሎች እንከፋፍል። የመጀመሪያው ይኸውና፡-

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 ለማቅረብ የምስል ስራ መዘግየትን መጠቀም እንፈልጋለን ነገር ግን በአገልጋዩ ላይ ያለውን ጊዜ ፈጽሞ አናውቅም፣ ምክንያቱም ማሻሻያዎቹ ወደ እኛ ለመድረስ ምን ያህል ጊዜ እንደወሰደ ማወቅ አንችልም። በይነመረቡ የማይታወቅ እና ፍጥነቱ በጣም ሊለያይ ይችላል!

በዚህ ችግር ዙሪያ ለማግኘት, እኛ ምክንያታዊ approximation መጠቀም ይችላሉ: እኛ የመጀመሪያው ዝማኔ ወዲያውኑ እንደደረሰ አስመስሎ. ይህ እውነት ከሆነ፣ በዚህ ጊዜ የአገልጋዩን ጊዜ እናውቅ ነበር! የአገልጋዩን የጊዜ ማህተም እናከማቻለን። 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 የሚታወቅ ታዋቂ የድር ማዕቀፍ እንጠቀማለን። ይግለጹ. በእኛ አገልጋይ መግቢያ ነጥብ ፋይል ይዋቀራል። src/server/server.js:

አገልጋይ.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 እንደተነጋገርን አስታውስ? የኛን የዌብፓክ አወቃቀሮችን የምንጠቀምበት ይህ ነው። በሁለት መንገዶች እንጠቀማቸዋለን፡-

  • ተጠቀም የዌብፓክ-ዴቭ-ሚድልዌር የእኛን የእድገት ፓኬጆችን በራስ ሰር እንደገና ለመገንባት ወይም
  • በስታቲስቲክስ አቃፊ ያስተላልፉ dist/ከምርቱ ግንባታ በኋላ ፋይሎቻችንን በየትኛው የዌብፓክ እንጽፋለን።

ሌላው አስፈላጊ ተግባር server.js አገልጋዩን ማዋቀር ነው። ሶኬት.ioከኤክስፕረስ አገልጋይ ጋር የሚገናኘው፡-

አገልጋይ.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:

አገልጋይ.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 socket (ግራ ከተጋቡ፣ ከዚያ ይመለሱ 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:

ነገር.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:

ጥይት.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 የሚከተሉት ቅጥያዎች ብቻ:

  • ጥቅሉን በመጠቀም shortid ለአጋጣሚ ትውልድ 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 ጨዋታ ይገንቡ!

ሁሉም የናሙና ኮድ ክፍት ምንጭ እና የተለጠፈ ነው። የፊልሙ.

ምንጭ: hab.com

አስተያየት ያክሉ