Ƙirƙirar Wasan Yanar Gizon Multiplayer .io

Ƙirƙirar Wasan Yanar Gizon Multiplayer .io
An sake shi a cikin 2015 Agar.io ya zama magabata na sabon salo games .iowanda ya karu cikin farin jini tun lokacin. Ni da kaina na fuskanci haɓakar shaharar wasannin .io: a cikin shekaru uku da suka gabata, ina da halitta da sayar da wasanni biyu na wannan nau'in..

Idan baku taɓa jin labarin waɗannan wasannin ba, waɗannan wasannin gidan yanar gizo ne masu yawa masu sauƙin kunnawa (babu asusu da ake buƙata). Yawancin lokaci suna fuskantar 'yan wasa da yawa masu adawa da juna a fage daya. Wasu shahararrun wasannin .io: Slither.io и Diep.io.

A cikin wannan post, za mu bincika yadda ƙirƙirar wasan .io daga karce. Don wannan, ilimin Javascript kawai zai isa: kuna buƙatar fahimtar abubuwa kamar syntax ES6, keyword this и alkawura. Ko da ilimin ku na Javascript bai cika ba, har yanzu kuna iya fahimtar yawancin post ɗin.

Misalin wasan .io

Don taimakon ilmantarwa, za mu koma zuwa misali game .io. Yi ƙoƙarin kunna shi!

Ƙirƙirar Wasan Yanar Gizon Multiplayer .io
Wasan yana da sauƙi: kuna sarrafa jirgi a cikin fage tare da wasu 'yan wasa. Jirgin naku yana harba manyan injina ta atomatik kuma kuna ƙoƙarin buga wasu 'yan wasa yayin da kuke guje wa kayan aikin su.

1. Takaitaccen bayani / tsarin aikin

Ina bada shawara zazzage lambar tushe wasan misali don ku biyo ni.

Misali yana amfani da wadannan:

  • Express shine mafi mashahuri tsarin gidan yanar gizo na Node.js wanda ke sarrafa sabar gidan yanar gizon wasan.
  • socket.io - ɗakin karatu na gidan yanar gizo don musayar bayanai tsakanin mai bincike da sabar.
  • Shafin gidan yanar gizo - mai sarrafa module. Kuna iya karanta game da dalilin da yasa ake amfani da Webpack. a nan.

Ga yadda tsarin kundin tsarin aiki yayi kama da:

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

jama'a/

Komai yana cikin babban fayil public/ uwar garken za ta ƙaddamar da shi a tsaye. IN public/assets/ ya ƙunshi hotuna da aikinmu ke amfani da shi.

src /

Duk lambar tushe tana cikin babban fayil ɗin src/. Lakabi client/ и server/ magana da kansu kuma shared/ ya ƙunshi fayil akai-akai wanda duka abokin ciniki da uwar garken ke shigo da su.

2. Tattaunawa / sigogin aikin

Kamar yadda aka ambata a sama, muna amfani da mai sarrafa module don gina aikin. Shafin gidan yanar gizo. Bari mu kalli tsarin mu na 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',
    }),
  ],
};

Layukan da suka fi muhimmanci a nan su ne kamar haka:

  • src/client/index.js shine wurin shigarwa na abokin ciniki na Javascript (JS). Fakitin gidan yanar gizo zai fara daga nan kuma yana neman sauran fayilolin da aka shigo da su akai-akai.
  • Fitowar JS na ginin fakitin gidan yanar gizon mu zai kasance a cikin kundin adireshi dist/. Zan kira wannan fayil ɗin mu kunshin js.
  • Muna amfani Babel, da kuma musamman tsarin @babel/preset-env don fassara lambar JS ɗin mu don tsofaffin masu bincike.
  • Muna amfani da plugin don fitar da duk CSS da fayilolin JS suka yi nuni kuma mu haɗa su wuri ɗaya. Zan kira shi namu css kunshin.

Wataƙila kun lura da bakon sunaye na fakitin '[name].[contenthash].ext'. Sun ƙunshi maye gurbin sunan fayil Shafin yanar gizo: [name] za a maye gurbinsu da sunan wurin shigarwa (a cikin yanayinmu, wannan game), da [contenthash] za a maye gurbinsu da zanta na abubuwan da ke cikin fayil ɗin. Mun yi shi don inganta aikin don hashing - za ku iya gaya wa masu bincike su cache fakitinmu na JS har abada, saboda idan kunshin ya canza, to sunan fayil ɗin shima yana canzawa (canje-canje contenthash). Sakamakon ƙarshe shine sunan fayil ɗin duba game.dbeee76e91a97d0c7207.js.

fayil webpack.common.js shine fayil ɗin sanyi na tushe wanda muke shigo da shi cikin haɓakawa da gama tsarin saitin aikin. Misali, ga tsarin ci gaba:

webpack.dev.js

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

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

Don dacewa, muna amfani da tsarin ci gaba webpack.dev.js, kuma ya canza zuwa webpack.prod.jsdon inganta girman fakiti lokacin turawa zuwa samarwa.

Saitin gida

Ina ba da shawarar shigar da aikin a kan injin gida don ku iya bin matakan da aka jera a cikin wannan sakon. Saitin yana da sauƙi: na farko, dole ne a shigar da tsarin kumburi и NPM. Na gaba kuna buƙatar yin

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

kuma kuna shirye ku tafi! Don fara uwar garken ci gaba, kawai gudu

$ npm run develop

kuma je zuwa mashigar yanar gizo localhost: 3000. Sabar ci gaba za ta sake gina fakitin JS da CSS ta atomatik yayin da lambar ta canza - kawai sabunta shafin don ganin duk canje-canje!

3. Abubuwan Shiga Abokin Ciniki

Bari mu gangara zuwa lambar wasan kanta. Da farko muna buƙatar shafi index.html, lokacin ziyartar shafin, mai binciken zai fara loda shi. Shafin namu zai kasance mai sauqi qwarai:

index.html

Misali .io game  WASA

An sauƙaƙa wannan misalin lambar don tsabta, kuma zan yi daidai da yawancin sauran misalan post. Ana iya duba cikakken lambar koyaushe a Github.

Muna da:

  • HTML5 canvas element (<canvas>) wanda za mu yi amfani da shi don yin wasan.
  • <link> don ƙara fakitinmu na CSS.
  • <script> don ƙara fakitin Javascript ɗin mu.
  • Babban menu tare da sunan mai amfani <input> da maɓallin PLAY (<button>).

Bayan loda shafin gida, mai binciken zai fara aiwatar da lambar Javascript, yana farawa daga wurin shigarwa JS fayil: 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);
  };
});

Wannan na iya zama kamar rikitarwa, amma babu abin da ke faruwa a nan:

  1. Ana shigo da wasu fayilolin JS da yawa.
  2. Shigowar CSS (don haka Webpack ya san ya haɗa su a cikin kunshin CSS ɗin mu).
  3. Kaddamarwa connect() don kafa haɗi tare da uwar garken da gudu downloadAssets() don zazzage hotunan da ake buƙata don yin wasan.
  4. Bayan kammala mataki na 3 Ana nuna babban menu (playMenu).
  5. Saita mai sarrafa don danna maɓallin "PLAY". Lokacin da aka danna maɓallin, lambar ta fara wasan kuma ta gaya wa uwar garken cewa a shirye muke mu yi wasa.

Babban “nama” na dabarar sabar abokin cinikinmu yana cikin waɗancan fayilolin da fayil ɗin ya shigo da su index.js. Yanzu za mu yi la'akari da su duka a cikin tsari.

4. Musanya bayanan abokin ciniki

A cikin wannan wasan, muna amfani da sanannen ɗakin karatu don sadarwa tare da uwar garken socket.io. Socket.io yana da goyon baya na asali shafukan yanar gizo, waɗanda suka dace da sadarwa ta hanyoyi biyu: za mu iya aika saƙonni zuwa uwar garken и uwar garken na iya aiko mana da saƙon akan haɗin kai ɗaya.

Za mu sami fayil ɗaya src/client/networking.jswanda zai kula kowa da kowa sadarwa tare da uwar garken:

sadarwar.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);
};

An kuma gajarta wannan lambar don haske.

Akwai manyan ayyuka guda uku a cikin wannan fayil:

  • Muna ƙoƙarin haɗi zuwa uwar garken. connectedPromise kawai an yarda lokacin da muka kafa haɗin gwiwa.
  • Idan haɗin ya yi nasara, muna yin rajistar ayyukan sake kira (processGameUpdate() и onGameOver()) don saƙonnin da za mu iya karɓa daga uwar garken.
  • Muna fitarwa play() и updateDirection()domin sauran fayiloli su yi amfani da su.

5. Ma'anar abokin ciniki

Lokaci yayi don nuna hoton akan allon!

Amma kafin mu iya yin hakan, muna buƙatar saukar da duk hotuna (albarkatun) waɗanda ake buƙata don wannan. Bari mu rubuta manajan albarkatun:

dukiya.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];

Gudanar da albarkatun ba shine wahalar aiwatarwa ba! Babban batu shine adana abu assets, wanda zai ɗaure maɓallin sunan fayil ɗin zuwa ƙimar abin Image. Lokacin da aka ɗora kayan aiki, muna adana shi a cikin wani abu assets don saurin shiga nan gaba. Yaushe ne za a ba wa kowane kayan aiki damar saukewa (wato, duk albarkatun), mun yarda downloadPromise.

Bayan zazzage albarkatun, zaku iya fara nunawa. Kamar yadda aka fada a baya, don zana kan shafin yanar gizon, muna amfani HTML5 Canvas (<canvas>). Wasan mu abu ne mai sauƙi, don haka kawai muna buƙatar zana abubuwa masu zuwa:

  1. Bayan Fage
  2. Jirgin ruwa mai wasa
  3. Sauran 'yan wasa a wasan
  4. Harsashi

Anan ga mahimman snippets src/client/render.js, wanda ke mayar da daidai abubuwa huɗu da aka jera a sama:

kawo.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);
}

Hakanan an gajarta wannan lambar don tsabta.

render() shine babban aikin wannan fayil. startRendering() и stopRendering() sarrafa kunna madauki na madauki a 60 FPS.

Ƙaddamar da aiwatar da ayyukan taimakon kai tsaye (misali. renderBullet()) ba su da mahimmanci, amma ga misali ɗaya mai sauƙi:

kawo.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,
  );
}

Lura cewa muna amfani da hanyar getAsset(), wanda a baya aka gani a ciki asset.js!

Idan kuna sha'awar koyo game da wasu mataimaka masu bayarwa, to ku karanta sauran. src/abokin ciniki/render.js.

6. Shigar abokin ciniki

Lokaci yayi da za a yi wasa m! Tsarin sarrafawa zai zama mai sauƙi: don canza yanayin motsi, zaka iya amfani da linzamin kwamfuta (a kan kwamfuta) ko taɓa allon (akan na'urar hannu). Don aiwatar da wannan, za mu yi rajista Masu Sauraron Taro don abubuwan Mouse da Touch.
Zai kula da duk wannan src/client/input.js:

shigar.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() su ne Event Listeners cewa kira updateDirection() (na networking.js) lokacin da abin shigarwa ya faru (misali, lokacin da aka motsa linzamin kwamfuta). updateDirection() yana sarrafa saƙo tare da uwar garken, wanda ke sarrafa taron shigar da kuma sabunta yanayin wasan daidai.

7. Matsayin abokin ciniki

Wannan sashe shine mafi wahala a sashin farko na sakon. Kada ku karaya idan ba ku fahimce shi ba a karon farko da kuka karanta shi! Kuna iya ma tsallake shi kuma ku dawo gare shi daga baya.

Yanki na ƙarshe na wasan wasa da ake buƙata don kammala abokin ciniki/lambar uwar garken shine jihar. Ka tuna snippet code daga sashen Rendering Client?

kawo.js

import { getCurrentState } from './state';

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

  // Do the rendering
  // ...
}

getCurrentState() ya kamata ya iya ba mu halin yanzu na wasan a cikin abokin ciniki a kowane lokaci dangane da sabuntawa da aka samu daga uwar garken. Ga misalin sabunta wasan da uwar garken zata iya aikawa:

{
  "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
    }
  ]
}

Kowane sabunta wasan ya ƙunshi fage guda biyar iri ɗaya:

  • t: Tambarin lokaci na uwar garken yana nuna lokacin da aka ƙirƙiri wannan ɗaukakawar.
  • me: Bayani game da mai kunnawa da ke karɓar wannan sabuntawa.
  • wasu: Tsari na bayanai game da sauran 'yan wasan da ke shiga wasa ɗaya.
  • harsasai: tsararrun bayanai game da majigi a cikin wasan.
  • leaderboard: Bayanan jagora na yanzu. A cikin wannan sakon, ba za mu yi la'akari da su ba.

7.1 Halin butulci na abokin ciniki

aiwatar da butulci getCurrentState() kawai zai iya dawo da bayanan sabunta wasan da aka karɓa kwanan nan.

butulci-jihar.js

let lastGameUpdate = null;

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

export function getCurrentState() {
  return lastGameUpdate;
}

Da kyau kuma bayyananne! Amma idan kawai ya kasance mai sauƙi. Ɗaya daga cikin dalilan wannan aiwatarwa yana da matsala: yana iyakance ƙimar firam ɗin bayarwa zuwa ƙimar agogon uwar garken.

Matsakaicin Tsari: adadin firam (watau kira render()) a sakan daya, ko FPS. Wasanni yawanci suna ƙoƙarin cimma aƙalla 60 FPS.

Tick ​​Rate: Mitar da uwar garken ke aika sabuntawar wasanni ga abokan ciniki. Yawancin lokaci yana ƙasa da ƙimar firam. A cikin wasanmu, uwar garken yana gudana a ticks 30 a sakan daya.

Idan kawai muka yi sabon sabuntawar wasan, to FPS da gaske ba za su iya wuce 30 ba saboda ba mu taɓa karɓar sabuntawa sama da 30 a sakan daya daga uwar garken ba. Ko da mun kira render() Sau 60 a cikin dakika guda, to rabin waɗannan kiran za su sake zana abu ɗaya kawai, da gaske ba za su yi komai ba. Wata matsala tare da aiwatar da butulci ita ce ta batun jinkiri. Tare da ingantaccen saurin Intanet, abokin ciniki zai karɓi sabuntawar wasa daidai kowane 33ms (30 a sakan daya):

Ƙirƙirar Wasan Yanar Gizon Multiplayer .io
Abin takaici, babu abin da yake cikakke. Hoton da ya fi dacewa zai kasance:
Ƙirƙirar Wasan Yanar Gizon Multiplayer .io
Aiwatar da butulci a zahiri shine mafi munin lamarin idan ya zo ga jinkiri. Idan an karɓi sabuntawar wasa tare da jinkiri na 50ms, to rumbun abokin ciniki ta ƙarin 50ms saboda har yanzu yana sanya yanayin wasan daga sabuntawar da ya gabata. Kuna iya tunanin yadda wannan bai dace ba ga mai kunnawa: saboda raguwar sabani, wasan zai yi kama da mara kyau da rashin kwanciyar hankali.

7.2 Inganta yanayin abokin ciniki

Za mu yi wasu gyare-gyare ga aiwatar da butulci. Da farko, muna amfani yin jinkiri ku 100 ms. Wannan yana nufin cewa yanayin "na yanzu" na abokin ciniki koyaushe zai kasance baya bayan yanayin wasan akan sabar ta 100ms. Misali, idan lokacin akan uwar garken ne 150, to abokin ciniki zai sanya yanayin da uwar garken yake a lokacin 50:

Ƙirƙirar Wasan Yanar Gizon Multiplayer .io
Wannan yana ba mu madaidaicin 100ms don tsira lokacin sabunta wasan da ba a iya faɗi ba:

Ƙirƙirar Wasan Yanar Gizon Multiplayer .io
Sakamakon wannan zai kasance na dindindin rashin jin daɗi ku 100 ms. Wannan ƙaramar sadaukarwa ce don wasa mai santsi - yawancin 'yan wasa (musamman 'yan wasa na yau da kullun) ba za su ma lura da wannan jinkiri ba. Yana da sauƙi ga mutane su daidaita zuwa tsayin daka na 100ms fiye da yin wasa tare da jinkirin da ba a iya faɗi ba.

Za mu iya amfani da wata dabara mai suna tsinkayar gefen abokin ciniki, wanda ke yin aiki mai kyau na rage jinkirin da aka sani, amma ba za a tattauna a cikin wannan sakon ba.

Wani cigaban da muke amfani dashi shine layika interpolation. Saboda jinkirin bayarwa, yawanci muna aƙalla sabuntawa guda ɗaya kafin lokacin yanzu a cikin abokin ciniki. Lokacin da aka kira getCurrentState(), za mu iya cika layika interpolation tsakanin sabunta wasan kafin da kuma bayan lokaci na yanzu a cikin abokin ciniki:

Ƙirƙirar Wasan Yanar Gizon Multiplayer .io
Wannan yana magance matsalar ƙimar firam: yanzu za mu iya yin firam na musamman a kowane ƙimar firam ɗin da muke so!

7.3 Aiwatar da ingantaccen yanayin abokin ciniki

Misalin aiwatarwa a src/client/state.js yana amfani da jinkirin ba da jinkiri da tsaka-tsakin layi, amma wannan ba ya daɗe. Mu karya lambar zuwa kashi biyu. Ga na farko:

state.js part 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;
}

Mataki na farko shine gano menene currentServerTime(). Kamar yadda muka gani a baya, kowane sabuntawar wasa ya haɗa da tambarin lokutan sabar sabar. Muna son yin amfani da latency don sanya hoton 100ms a bayan sabar, amma ba za mu taɓa sanin lokacin yanzu akan sabar ba, saboda ba za mu iya sanin tsawon lokacin da aka ɗauka don kowane sabuntawa ya isa gare mu ba. Intanet ba shi da tabbas kuma saurin sa na iya bambanta sosai!

Don magance wannan matsala, za mu iya amfani da ma'auni mai ma'ana: mu yi kamar sabuntawa na farko ya zo nan take. Idan wannan gaskiya ne, to da mun san lokacin uwar garken a wannan lokacin! Muna adana tambarin sabar a ciki firstServerTimestamp kuma kiyaye mu na gida (abokin ciniki) timestamp a lokaci guda a ciki gameStart.

Oh jira. Shin bai kamata ya zama lokacin uwar garken = lokacin abokin ciniki ba? Me yasa muke bambanta tsakanin "tambarin sabar" da "tambarin abokin ciniki"? Wannan babbar tambaya ce! Sai ya zama ba haka ba ne. Date.now() zai dawo daban-daban tambura a cikin abokin ciniki da uwar garken, kuma ya dogara da abubuwan gida zuwa waɗannan injina. Kar a taɓa ɗauka cewa tambarin lokaci zai kasance iri ɗaya akan duk injina.

Yanzu mun fahimci abin da ya aikata currentServerTime(): yana dawowa sabar timestamp na lokacin bayarwa na yanzu. A takaice dai, wannan shine lokacin uwar garken na yanzu (firstServerTimestamp <+ (Date.now() - gameStart)) rage jinkiri (RENDER_DELAY).

Yanzu bari mu kalli yadda muke tafiyar da sabunta wasanni. Lokacin da aka karɓi sabuntawa daga uwar garken, ana kiran shi processGameUpdate()kuma muna ajiye sabon sabuntawa zuwa tsararru gameUpdates. Sa'an nan, don bincika amfanin ƙwaƙwalwar ajiya, muna cire duk tsofaffin abubuwan da suka gabata tushe updatedomin bama bukatarsu kuma.

Menene "sabuntawa na asali"? Wannan sabuntawa na farko da muke samu ta hanyar komawa baya daga lokacin uwar garken na yanzu. Ka tuna wannan zane?

Ƙirƙirar Wasan Yanar Gizon Multiplayer .io
Sabunta wasan kai tsaye zuwa hagu na "Lokacin Sadar da Abokin Ciniki" shine sabunta tushe.

Menene sabuntawar tushe da ake amfani dashi? Me yasa za mu iya sauke sabuntawa zuwa tushe? Don fahimtar wannan, bari mu a ƙarshe la'akari da aiwatarwa getCurrentState():

state.js part 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),
    };
  }
}

Muna gudanar da shari'o'i uku:

  1. base < 0 yana nufin cewa babu sabuntawa har sai lokacin bayarwa na yanzu (duba aiwatarwa na sama getBaseUpdate()). Wannan na iya faruwa daidai a farkon wasan saboda jinkirin yin wasa. A wannan yanayin, muna amfani da sabon sabuntawa da aka karɓa.
  2. base shine sabon sabuntawa da muke dashi. Wannan na iya faruwa saboda jinkirin hanyar sadarwa ko rashin haɗin intanet. A wannan yanayin kuma muna amfani da sabon sabuntawa da muke da shi.
  3. Muna da sabuntawa duka kafin da bayan lokacin bayarwa na yanzu, don haka zamu iya interpolate!

Duk abin da ya rage a ciki state.js shine aiwatar da tsaka-tsakin layi mai sauƙi (amma m) lissafi. Idan kana son gano shi da kanka, to bude state.js a kan Github.

Part 2. Backend Server

A wannan bangare, za mu duba baya Node.js wanda ke sarrafa namu .io wasan misali.

1. Wurin Shiga Sabar

Don sarrafa sabar gidan yanar gizo za mu yi amfani da sanannen tsarin gidan yanar gizo don Node.js da ake kira Express. Za a saita shi ta fayil ɗin shigarwar uwar garken mu src/server/server.js:

server.js part 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}`);

Ka tuna cewa a kashi na farko mun tattauna Webpack? Anan ne za mu yi amfani da saitunan Webpack ɗin mu. Za mu yi amfani da su ta hanyoyi biyu:

  • Amfani webpack-dev-middleware don sake gina fakitin ci gaban mu ta atomatik, ko
  • canja wurin babban fayil a tsaye dist/, wanda Webpack zai rubuta fayilolin mu bayan ginin samarwa.

Wani muhimmin aiki server.js shine saita uwar garken socket.iowanda kawai ya haɗa zuwa uwar garken Express:

server.js part 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);
});

Bayan samun nasarar kafa haɗin socket.io zuwa uwar garken, mun kafa masu gudanar da taron don sabon soket. Masu gudanar da taron suna sarrafa saƙonnin da aka karɓa daga abokan ciniki ta hanyar ba da izini zuwa abu guda ɗaya game:

server.js part 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);
}

Muna ƙirƙirar wasan .io, don haka kwafi ɗaya kawai muke buƙata Game ("Wasan") - duk 'yan wasa suna wasa a fage ɗaya! A sashe na gaba, za mu ga yadda wannan ajin ke aiki. Game.

2. Wasan sabobin

Класс Game ya ƙunshi mafi mahimmanci dabaru a gefen uwar garken. Yana da manyan ayyuka guda biyu: sarrafa player и wasan kwaikwayo na wasan kwaikwayo.

Bari mu fara da aikin farko, sarrafa ɗan wasa.

game.js, part 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);
    }
  }

  // ...
}

A cikin wannan wasa, za mu tantance ’yan wasa ta fili id socket.io socket (idan kun rikice, to koma zuwa server.js). Socket.io da kansa yana ba kowane soket na musamman iddon haka bai kamata mu damu da hakan ba. Zan kira shi ID mai kunnawa.

Tare da wannan a zuciya, bari mu bincika misalai masu canji a cikin aji Game:

  • sockets wani abu ne da ke ɗaure ID ɗin mai kunnawa zuwa soket ɗin da ke da alaƙa da mai kunnawa. Yana ba mu damar samun dama ga soket ta ID ɗin ɗan wasan su a cikin lokaci akai-akai.
  • players abu ne da ke ɗaure ID ɗin mai kunnawa zuwa lambar> Abun ɗan wasa

bullets tsararrun abubuwa ne Bullet, wanda ba shi da takamaiman tsari.
lastUpdateTime shine tambarin lokaci na ƙarshe lokacin da aka sabunta wasan. Za mu ga yadda ake amfani da shi ba da jimawa ba.
shouldSendUpdate madaidaicin taimako ne. Za mu kuma ga amfani da shi nan ba da jimawa ba.
Hanyoyi addPlayer(), removePlayer() и handleInput() babu buƙatar bayyana, ana amfani da su a ciki server.js. Idan kana buƙatar sabunta ƙwaƙwalwar ajiyarka, koma baya kadan sama.

Layin ƙarshe constructor() farawa sabunta sake zagayowar wasanni (tare da mitar sabuntawa / s 60):

game.js, part 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;
    }
  }

  // ...
}

Hanyar update() ya ƙunshi watakila mafi mahimmancin ɓangaren dabaru na gefen uwar garken. Ga abin da yake yi, domin:

  1. Yana ƙididdige tsawon lokacin dt wuce tun daga karshe update().
  2. Yana wartsakar da kowane majigi kuma yana lalata su idan ya cancanta. Za mu ga aiwatar da wannan aikin daga baya. A yanzu, ya ishe mu sanin haka bullet.update() ya dawo true, idan majigin dole ne a lalata (ya fita wajen fage).
  3. Yana sabunta kowane ɗan wasa kuma yana haifar da majigi idan ya cancanta. Za mu kuma ga wannan aiwatarwa daga baya - player.update() iya mayar da abu Bullet.
  4. Bincika don samun karo tsakanin majigi da 'yan wasa tare da applyCollisions(), wanda ke dawo da jerin gwanon da ke buga ƴan wasa. Ga kowane majigi da aka dawo, muna ƙara maki na ɗan wasan da ya kori shi (amfani da player.onDealtDamage()), sa'an nan kuma cire projectile daga tsararru bullets.
  5. Yana sanar da lalata duk 'yan wasan da aka kashe.
  6. Yana aika sabuntawar wasa ga duk 'yan wasa kowane daƙiƙa lokuta idan aka kira update(). Maɓallin ƙarin da aka ambata a sama yana taimaka mana bin wannan shouldSendUpdate. Domin update() da ake kira sau 60/s, muna aika sabuntawar wasanni sau 30/s. Don haka, mitar agogo Agogon uwar garken shine 30 clocks/s (mun yi magana game da ƙimar agogo a ɓangaren farko).

Me yasa aika sabuntawar wasa kawai ta lokaci ? Don ajiye tashar. Sabunta wasanni 30 a sakan daya yana da yawa!

Me yasa ba kawai kira ba update() Sau 30 a sakan daya? Don inganta wasan kwaikwayo. Mafi sau da yawa ake kira update(), mafi daidaitaccen simintin wasan zai kasance. Amma kar a shaku da yawan ƙalubalen. update(), saboda wannan aiki ne mai tsadar lissafi - 60 a kowace daƙiƙa ya isa.

Sauran ajin Game ya ƙunshi hanyoyin taimako da ake amfani da su a ciki update():

game.js, part 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() Yana da sauƙi mai sauƙi - yana rarraba ƴan wasa da maki, yana ɗaukar manyan biyar, kuma yana mayar da sunan mai amfani da maki ga kowane.

createUpdate() amfani dashi update() don ƙirƙirar sabuntawar wasan da aka rarraba wa 'yan wasa. Babban aikinsa shine kiran hanyoyin serializeForUpdate()aiwatar da azuzuwan Player и Bullet. Lura cewa kawai yana canja wurin bayanai zuwa kowane ɗan wasa game da mafi kusa 'yan wasa da majigi - babu buƙatar watsa bayanai game da abubuwan wasan da ke da nisa daga mai kunnawa!

3. Abubuwan wasa akan uwar garken

A cikin wasanmu, majigi da ƴan wasa a haƙiƙa suna kama da juna: abubuwa ne na zahiri, zagaye, abubuwan wasan motsa jiki. Don amfanuwa da wannan kamanceceniya tsakanin ƴan wasa da majigi, bari mu fara da aiwatar da ajin tushe Object:

abu.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,
    };
  }
}

Babu wani abu mai rikitarwa da ke faruwa a nan. Wannan ajin zai zama kyakkyawan ma'ana don tsawaitawa. Bari mu ga yadda ajin Bullet amfani Object:

harsashi.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;
  }
}

Aiwatarwa Bullet gajere sosai! Mun kara zuwa Object kawai kari masu zuwa:

  • Amfani da kunshin shortid domin bazuwar tsara id tsinkaya.
  • Ƙara filin parentIDdomin ku iya bin diddigin dan wasan da ya kirkiro wannan aikin.
  • Ƙara ƙimar dawowa zuwa update(), wanda yake daidai da trueidan majigi yana waje da fage (tuna mun yi magana game da wannan a cikin sashe na ƙarshe?).

Mu ci gaba zuwa Player:

mai wasa.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,
    };
  }
}

'Yan wasa sun fi na'urori masu sarkakiya, don haka ya kamata a adana wasu filaye a cikin wannan ajin. Hanyarsa update() yana yin ƙarin aiki, musamman mayar da sabon aikin da aka ƙirƙira idan babu sauran fireCooldown (ka tuna mun yi magana game da wannan a sashin da ya gabata?). Hakanan yana fadada hanyar serializeForUpdate(), saboda muna buƙatar haɗa ƙarin filayen don mai kunnawa a cikin sabunta wasan.

Samun tushe aji Object - muhimmin mataki don guje wa maimaita lambar. Misali, ba tare da aji ba Object kowane abu na wasa dole ne ya kasance yana aiwatar da iri ɗaya distanceTo(), kuma kwafin duk waɗannan aiwatarwa a cikin fayiloli da yawa zai zama abin tsoro. Wannan ya zama mahimmanci ga manyan ayyuka.lokacin da yawan fadadawa Object azuzuwan suna girma.

4. Gano karo

Abinda kawai ya rage mana shine mu gane lokacin da majigi ya bugi 'yan wasan! Tuna wannan yanki na lambar daga hanyar update() a cikin aji 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),
    );

    // ...
  }
}

Muna buƙatar aiwatar da hanyar applyCollisions(), wanda ke mayar da duk abubuwan da suka faru da 'yan wasa. Sa'ar al'amarin shine, ba shi da wuya a yi saboda

  • Duk abubuwan da suke karo da'ira ne, kuma wannan shine mafi sauƙin siffa don aiwatar da gano karo.
  • Mun riga muna da hanya distanceTo(), wanda muka aiwatar a cikin sashin da ya gabata a cikin aji Object.

Ga yadda aiwatar da aikin mu na gano karo yayi kama:

karo.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;
}

Wannan saurin gano karon ya dogara ne akan gaskiyar cewa dawafi biyu suna karo idan tazarar da ke tsakanin cibiyoyinsu bai kai adadin radiyoyinsu ba. Ga lamarin inda tazarar da ke tsakanin cibiyoyin da’irori biyu daidai yake da jimlar radiyoyinsu:

Ƙirƙirar Wasan Yanar Gizon Multiplayer .io
Anan kuna buƙatar kula sosai ga wasu ƙarin bangarorin biyu:

  • Dole ne majigi ya bugi ɗan wasan da ya ƙirƙira shi. Ana iya samun wannan ta hanyar kwatanta bullet.parentID с player.id.
  • Dole ne majigi ya buga sau ɗaya kawai a cikin iyakance yanayin 'yan wasa da yawa da suka yi karo a lokaci guda. Za mu magance wannan matsala ta amfani da mai aiki break: da zaran an sami mai kunnawa da ya yi karo da majigi, sai mu dakatar da binciken kuma mu matsa zuwa na gaba.

Ƙarshen

Shi ke nan! Mun rufe duk abin da kuke buƙatar sani don ƙirƙirar wasan yanar gizon .io. Menene na gaba? Gina wasan .io naku!

Duk samfurin samfurin buɗaɗɗen tushe ne kuma an buga shi Github.

source: www.habr.com

Add a comment