Kupanga masewera apaintaneti a anthu ambiri mumtundu wa .io

Kupanga masewera apaintaneti a anthu ambiri mumtundu wa .io
Inatulutsidwa mu 2015 Agar.io anakhala kholo la mtundu watsopano masewera.io, amene kutchuka kwawo kwakula kwambiri kuyambira pamenepo. Ndaona kukwera kwa kutchuka kwa masewera a .io ndekha: pazaka zitatu zapitazi, I adapanga ndikugulitsa masewera awiri mumtundu uwu..

Ngati simunamvepo zamasewerawa, ndi masewera aulere, osewera ambiri omwe ndi osavuta kusewera (palibe akaunti yofunikira). Nthawi zambiri amaika osewera ambiri otsutsana m'bwalo limodzi. Masewera ena otchuka a .io: Slither.io ΠΈ Diep.io.

Mu positi iyi tiwona momwe pangani masewera a .io kuyambira poyambira. Kuti muchite izi, chidziwitso chokha cha Javascript chidzakhala chokwanira: muyenera kumvetsetsa zinthu monga syntax ES6, mawu ofunika this ΠΈ malonjezo. Ngakhale simukudziwa Javascript bwino, mutha kumvetsetsa zambiri zomwe mwalemba.

Chitsanzo cha masewera a .io

Kuti tithandizire maphunziro, tidzakambirana chitsanzo masewera .io. Yesani kusewera!

Kupanga masewera apaintaneti a anthu ambiri mumtundu wa .io
Masewerawa ndi osavuta: mumawongolera sitima pabwalo ndi osewera ena. Sitima yanu imawotcha ma projectiles ndipo mumayesa kugunda osewera ena ndikupewa ma projectiles awo.

1. Chidule chachidule/mapangidwe a projekiti

Ndikupangira download source kodi mwachitsanzo masewera kuti munditsatire.

Chitsanzo chimagwiritsa ntchito zotsatirazi:

  • kufotokoza ndiye tsamba lodziwika bwino la Node.js lomwe limayang'anira seva yamasewera.
  • alireza - laibulale ya websocket yosinthana data pakati pa osatsegula ndi seva.
  • Tsamba - Woyang'anira module. Mutha kuwerenga chifukwa chake kugwiritsa ntchito Webpack apa.

Izi ndi zomwe dongosolo lachikwatu cha polojekiti limawonekera:

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

pagulu/

Zonse zili mufoda public/ idzatumizidwa ndi seva. MU public/assets/ ili ndi zithunzi zomwe zimagwiritsidwa ntchito ndi polojekiti yathu.

src /

Khodi yonse yoyambira ili mufoda src/. Maina audindo client/ ΠΈ server/ azilankhula okha ndi shared/ ili ndi fayilo yokhazikika yomwe idatumizidwa ndi kasitomala ndi seva.

2. Misonkhano / magawo a polojekiti

Monga tafotokozera pamwambapa, timagwiritsa ntchito ma module kuti timange projekiti Tsamba. Tiyeni tiwone masinthidwe athu a 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',
    }),
  ],
};

Mizere yofunika kwambiri apa ndi iyi:

  • src/client/index.js ndiye polowera kasitomala wa Javascript (JS). Webpack iyambira pano ndikuyang'ananso mafayilo ena omwe adatumizidwa kunja.
  • Zotsatira za JS za Webpack yathu yomanga zidzapezeka m'ndandanda dist/. Ndiyitcha fayilo yathu JS phukusi.
  • Timagwiritsa ntchito Babele, ndipo makamaka kasinthidwe @babel/preset-env kuti mutumize kachidindo kathu ka JS kwa asakatuli akale.
  • Timagwiritsa ntchito pulogalamu yowonjezera kuchotsa ma CSS onse otchulidwa ndi mafayilo a JS ndikuwaphatikiza kukhala malo amodzi. Ndidzaitcha yathu CSS phukusi.

Mwina mwawonapo mayina a mafayilo achilendo '[name].[contenthash].ext'. Iwo ali filename m'malo Msakatuli: [name] idzasinthidwa ndi dzina la malo olowera (kwa ife ndi game), ndipo [contenthash] idzasinthidwa ndi hashi ya zomwe zili mufayilo. Timachita izi konzani projekiti ya hashing - titha kuuza asakatuli kuti asungire mapaketi athu a JS mpaka kalekale chifukwa ngati phukusi lisintha, dzina lake lafayilo limasinthanso (zosintha contenthash). Chotsatira chomalizidwa chidzakhala dzina lafayilo la mawonekedwe game.dbeee76e91a97d0c7207.js.

file webpack.common.js - Ili ndiye fayilo yoyambira yomwe timalowetsa muzotukuka ndikumaliza kukonza projekiti. Mwachitsanzo, nayi kasinthidwe kachitukuko:

webpack.dev.js

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

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

Kuti tichite bwino, timagwiritsa ntchito popanga chitukuko webpack.dev.js, ndi kusintha kwa webpack.prod.js, kukhathamiritsa kukula kwa phukusi potumiza kupanga.

Kukonzekera kwanuko

Ndikupangira kukhazikitsa pulojekitiyi pamakina anu am'deralo kuti muthe kutsatira zomwe zalembedwa patsambali. Kukonzekera ndikosavuta: choyamba, dongosolo liyenera kukhala nalo Node ΠΈ NPM. Kenako muyenera kuchita

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

ndipo mwakonzeka kupita! Kuti muyambe seva yachitukuko, ingothamangani

$ npm run develop

ndi kupita ku msakatuli wanu localhost: 3000. Seva yachitukuko imangopanganso ma phukusi a JS ndi CSS pomwe kusintha kwa ma code kumachitika - ingotsitsimutsani tsamba kuti muwone zosintha zonse!

3. Malo olowera kasitomala

Tiyeni titsike ku code yamasewera yomwe. Choyamba tiyenera tsamba index.html, mukamayendera tsambalo, msakatuli amatsegula kaye. Tsamba lathu likhala losavuta:

index.html

Chitsanzo cha .io masewera  SEWERANI

Chitsanzo cha codechi chasinthidwa pang'ono kuti chimveke bwino, ndipo ndidzachitanso chimodzimodzi ndi zitsanzo zina zambiri zomwe zili mu positi. Mutha kuyang'ana pa code yonse Github.

Tili ndi:

  • HTML5 Canvas element (<canvas>), zomwe tidzagwiritse ntchito popereka masewerawo.
  • <link> kuwonjezera phukusi lathu la CSS.
  • <script> kuwonjezera phukusi lathu la Javascript.
  • Menyu yayikulu yokhala ndi dzina lolowera <input> ndi batani la "PLAY" (<button>).

Tsamba lanyumba likangodzaza, msakatuli ayamba kugwiritsa ntchito Javascript code, kuyambira ndi fayilo ya 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);
  };
});

Izi zitha kumveka zovuta, koma palibe zambiri zomwe zikuchitika apa:

  1. Lowetsani mafayilo ena angapo a JS.
  2. Lowetsani CSS (kotero Webpack ikudziwa kuziphatikiza mu phukusi lathu la CSS).
  3. Yambitsani connect() kukhazikitsa kulumikizana kwa seva ndikuyamba downloadAssets() kutsitsa zithunzi zofunika kupereka masewerawo.
  4. Mukamaliza gawo 3 menyu yayikulu ikuwonetsedwa (playMenu).
  5. Kukhazikitsa batani la "PLAY" dinani chowongolera. Batani likakanikiza, codeyo imayambitsa masewerawo ndikuuza seva kuti takonzeka kusewera.

"Nyama" yayikulu yamalingaliro athu a kasitomala-server ili m'mafayilo omwe adatumizidwa ndi fayilo index.js. Tsopano tiwona onse mwadongosolo.

4. Kusinthana kwa data ya kasitomala

Mu masewerawa timagwiritsa ntchito laibulale yodziwika bwino kuti tilankhule ndi seva alireza. Socket.io ili ndi chithandizo chokhazikika WebSoketi, omwe ali oyenerera kulankhulana kwa njira ziwiri: tikhoza kutumiza mauthenga ku seva ΠΈ seva imatha kutumiza mauthenga kwa ife pa intaneti yomweyo.

Tidzakhala ndi fayilo imodzi src/client/networking.jsamene adzasamalira aliyense kulumikizana ndi seva:

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

Khodi iyi imafupikitsidwanso pang'ono kuti imveke bwino.

Pali zinthu zitatu zazikulu zomwe zikuchitika mufayilo iyi:

  • Tikuyesera kulumikiza ku seva. connectedPromise zimaloledwa pokhapokha takhazikitsa kulumikizana.
  • Ngati kulumikizana kwabwino, timalembetsa ntchito zoyimba foni (processGameUpdate() ΠΈ onGameOver()) kwa mauthenga omwe tingalandire kuchokera kwa seva.
  • Timatumiza kunja play() ΠΈ updateDirection()kotero kuti mafayilo ena azigwiritsa ntchito.

5. Kupereka kwa kasitomala

Yakwana nthawi yoti muwonetse chithunzicho pazenera!

...koma tisanachite izi, tiyenera kutsitsa zithunzi zonse (zothandizira) zomwe zimafunikira pa izi. Tiyeni tilembe woyang'anira zothandizira:

assets.js

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

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

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

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

Kasamalidwe ka zinthu sizovuta kuchita! Mfundo yaikulu ndi kusunga chinthu assets, yomwe imangiriza kiyi ya filename ku mtengo wa chinthu Image. Pamene gwero ladzaza, timasunga ku chinthu assets kuti mulandire mwachangu mtsogolo. Kodi kutsitsa kwa chinthu chilichonse kudzaloledwa liti (ndiko kuti, kutsitsa onse zothandizira), timalola downloadPromise.

Pambuyo otsitsira chuma, mukhoza kuyamba yomasulira. Monga tanena kale, kujambula patsamba lomwe timagwiritsa ntchito HTML5 Canvas (<canvas>). Masewera athu ndi osavuta, choncho timangofunika kupereka zotsatirazi:

  1. Mbiri
  2. Sitima yapamadzi
  3. Osewera ena mumasewerawa
  4. Zipolopolo

Nawa mawu ofunikira src/client/render.js, zomwe zikujambula ndendende mfundo zinayi zomwe zatchulidwa pamwambapa:

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

Khodi iyi imafupikitsidwanso kuti imveke bwino.

render() ndiye ntchito yayikulu ya fayiloyi. startRendering() ΠΈ stopRendering() wongolerani kutsegulira kwa kuzungulira kwa 60 FPS.

Kukhazikitsa kwachindunji kwa ntchito zothandizira othandizira (mwachitsanzo renderBullet()) sizofunikira, koma nachi chitsanzo chimodzi chosavuta:

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

Dziwani kuti tikugwiritsa ntchito njira getAsset(), zomwe zidawoneka kale mu asset.js!

Ngati mukufuna kuwona ntchito zina zothandizira othandizira, werengani zina zonse src/client/render.js.

6. Kulowetsa kwa kasitomala

Yakwana nthawi yopanga masewera zoseweredwa! Dongosolo lowongolera lidzakhala losavuta: kusintha njira yoyendetsera, mutha kugwiritsa ntchito mbewa (pakompyuta) kapena kukhudza chinsalu (pa foni yam'manja). Kuti tikwaniritse izi tidzalembetsa Omvera Zochitika pazochitika za Mouse ndi Touch.
Adzasamalira zonsezi 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() ndi Omvera Zochitika omwe amayimba updateDirection() (ya networking.js) pamene cholowetsa chichitika (mwachitsanzo, mbewa ikasunthidwa). updateDirection() imagwira ntchito ndi kusinthana kwa mauthenga ndi seva, yomwe imayendetsa zochitika zolowetsamo ndikusintha dziko lamasewera moyenerera.

7. Mkhalidwe wa kasitomala

Gawo ili ndilovuta kwambiri mu gawo loyamba la positi. Musataye mtima ngati simukumvetsa nthawi yoyamba yomwe mudawerenga! Mutha kulumphira ndikubwereranso pambuyo pake.

Chidutswa chomaliza chazithunzi chomwe chimafunikira kuti mumalize nambala ya kasitomala-seva ndi Boma. Mukukumbukira mawu achinsinsi ochokera kugawo la Client Rendering?

render.js

import { getCurrentState } from './state';

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

  // Do the rendering
  // ...
}

getCurrentState() akuyenera kutipatsa momwe masewerawa alili panopa mu kasitomala nthawi iliyonse kutengera zosintha zomwe zalandilidwa kuchokera ku seva. Nachi chitsanzo cha zosintha zamasewera zomwe seva ingatumize:

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

Kusintha kulikonse kwamasewera kumakhala ndi magawo asanu ofanana:

  • t: Chidindo chanthawi ya seva chosonyeza nthawi yomwe izi zidapangidwa.
  • me: Zambiri za wosewerayo akulandila izi.
  • ena: Zambiri za osewera ena omwe akuchita nawo masewera amodzi.
  • zipolopolo: Zambiri zokhudzana ndi projectiles pamasewerawa.
  • gulu lotsogolera: Zomwe zilipo panopa. Sitidzawaganizira mu positi iyi.

7.1 Kusazindikira kwa kasitomala

Kukhazikitsa kwa Naive getCurrentState() akhoza kungobweza mwachindunji deta kuchokera ku zosinthidwa zamasewera zomwe zalandiridwa posachedwa.

naive-state.js

let lastGameUpdate = null;

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

export function getCurrentState() {
  return lastGameUpdate;
}

Zokongola komanso zomveka! Koma zikadakhala zosavuta. Chimodzi mwa zifukwa zomwe kukhazikitsa uku kumakhala kovuta: imachepetsa kuchuluka kwa chimango choperekera ku liwiro la wotchi ya seva.

Mtengo wa chimango: chiwerengero cha mafelemu (ie ma call render()) pa sekondi iliyonse, kapena FPS. Masewera nthawi zambiri amayesetsa kukwaniritsa 60 FPS.

Mtengo wa Tick: Mafupipafupi omwe seva imatumiza zosintha zamasewera kwa makasitomala. Nthawi zambiri imakhala yotsika kuposa mtengo wa chimango. M'masewera athu, seva imayendetsa nkhupakupa 30 pamphindikati.

Ngati tingopereka zosintha zaposachedwa, ndiye kuti FPS sichitha kupitilira 30 chifukwa sitilandila zosintha zopitilira 30 pamphindikati kuchokera pa seva. Ngakhale tiyitana render() 60 pa sekondi iliyonse, ndiye theka la mafoni awa amangojambulanso zomwezo, osachita kalikonse. Vuto lina ndi kukhazikitsa mosadziwa ndilokuti kutengera kuchedwa. Pa liwiro loyenera la intaneti, kasitomala alandila zosintha zamasewera ndendende ma 33 ms aliwonse (30 pamphindikati):

Kupanga masewera apaintaneti a anthu ambiri mumtundu wa .io
Mwatsoka, palibe changwiro. Chithunzi chowoneka bwino chingakhale:
Kupanga masewera apaintaneti a anthu ambiri mumtundu wa .io
Kukhazikitsa kopanda nzeru ndizovuta kwambiri zikafika pa latency. Ngati zosintha zamasewera zikulandiridwa ndikuchedwa kwa 50ms, ndiye kasitomala amachedwa ndi 50ms owonjezera chifukwa ikuperekabe masewerawa kuchokera pazomwe zasintha. Mutha kulingalira momwe izi zimavutira kwa wosewera mpira: chifukwa cha kuchepa kwapang'onopang'ono, masewerawa amawoneka ngati osakhazikika komanso osakhazikika.

7.2 Chitukuko cha kasitomala

Tidzakonza zina pakukhazikitsa kopanda nzeru. Choyamba, timagwiritsa ntchito kupereka kuchedwa pa 100ms. Izi zikutanthauza kuti "panopa" kasitomala nthawi zonse amakhala 100ms kumbuyo kwamasewera pa seva. Mwachitsanzo, ngati nthawi ya seva ili 150, ndiye kasitomala adzapereka dziko lomwe seva inali nthawiyo 50:

Kupanga masewera apaintaneti a anthu ambiri mumtundu wa .io
Izi zimatipatsa chitetezo cha 100ms kuti tipulumuke nthawi yosayembekezereka ya zosintha zamasewera:

Kupanga masewera apaintaneti a anthu ambiri mumtundu wa .io
Mtengo wake udzakhala wamuyaya kulowa mkati pa 100ms. Uku ndi nsembe yaying'ono pamasewera osalala - osewera ambiri (makamaka wamba) sangazindikire kuchedwa uku. Ndikosavuta kuti anthu azolowere kuchedwa kwa 100ms kusiyana ndi kusewera ndi latency yosayembekezereka.

Titha kugwiritsa ntchito njira ina yotchedwa "Zolosera za mbali ya kasitomala", zomwe zimagwira ntchito yabwino yochepetsera kuchedwa kwachidziwitso, koma sizidzakambidwa m'nkhaniyi.

Kuwongolera kwina komwe timagwiritsa ntchito ndi kumasulira kwa mzere. Chifukwa cha kuchedwa kwapang'onopang'ono, nthawi zambiri timakhala ndikusintha kamodzi patsogolo pa nthawi yomwe kasitomala amalandila. Akaitanidwa getCurrentState(), tingakwaniritse kumasulira kwa mzere pakati pa zosintha zamasewera atangotsala pang'ono komanso pambuyo pa nthawi yapano mwa kasitomala:

Kupanga masewera apaintaneti a anthu ambiri mumtundu wa .io
Izi zimathetsa vuto la mafelemu: tsopano titha kupereka mafelemu apadera pamlingo uliwonse womwe tikufuna!

7.3 Kukhazikitsa boma labwino lamakasitomala

Chitsanzo chokonzekera mu src/client/state.js amagwiritsa ntchito kuchedwa kwa kumasulira ndi kumasulira kwa mzere, koma izi sizitenga nthawi yayitali. Tiyeni tigawe kachidindo m'magawo awiri. Nayi yoyamba:

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

Choyambirira chomwe muyenera kuchita ndikuzindikira zomwe akuchita currentServerTime(). Monga tawonera kale, zosintha zilizonse zamasewera zimakhala ndi sitampu yanthawi ya seva. Tikufuna kugwiritsa ntchito render latency kuti tipereke chithunzicho 100ms kuseri kwa seva, koma sitidzadziwa nthawi yomwe ilipo pa seva, chifukwa sitingadziwe kuti zinatenga nthawi yayitali bwanji kuti zosintha zina zifike kwa ife. Intaneti ndi yosadziΕ΅ika bwino ndipo liwiro lake limasiyana kwambiri!

Kuti tithane ndi vutoli, titha kugwiritsa ntchito kuyerekezera koyenera: ife tiyerekeze kuti zosintha zoyamba zafika nthawi yomweyo. Izi zikadakhala zoona, tikadadziwa nthawi ya seva panthawiyo! Timasunga sitampu yanthawi ya seva mkati firstServerTimestamp ndi kupulumutsa athu kwanuko (client) timestamp nthawi yomweyo mu gameStart.

O, dikirani miniti. Kodi sikuyenera kukhala nthawi pa seva = nthawi pa kasitomala? Chifukwa chiyani timasiyanitsa "chidindo chanthawi ya seva" ndi "chidindo chamakasitomala"? Ili ndi funso lalikulu! Zikuoneka kuti izi si chinthu chomwecho. Date.now() idzabwezeranso masitampu osiyanasiyana mu kasitomala ndi seva ndipo izi zimatengera zinthu zapafupi ndi makinawa. Musaganize kuti ma timestamp adzakhala ofanana pamakina onse.

Tsopano ife tikumvetsa chimene icho chimachita currentServerTime(): ikubwerera chidindo chanthawi ya seva chanthawi yoperekera. Mwanjira ina, iyi ndi nthawi ya seva yamakono (firstServerTimestamp <+ (Date.now() - gameStart)) kuchotsera kuchedwa (RENDER_DELAY).

Tsopano tiyeni tiwone momwe timachitira zosintha zamasewera. Zosintha zikalandiridwa kuchokera ku seva, zimatchedwa processGameUpdate(), ndipo timasunga zosintha zatsopano pamndandanda gameUpdates. Kenako, kuti tiwone kugwiritsa ntchito kukumbukira, timachotsa zosintha zonse zakale zosintha zoyambirachifukwa sitikuzifunanso.

Kodi "core update" ndi chiyani? Izi kusinthidwa koyamba komwe timapeza pobwerera m'mbuyo kuchokera pa nthawi ya seva yamakono. Mukukumbukira chithunzichi?

Kupanga masewera apaintaneti a anthu ambiri mumtundu wa .io
Zosintha zamasewera kumanzere kwa "Client Render Time" ndiye zosintha zoyambira.

Kodi zoyambira zomwe zimagwiritsidwa ntchito ndi chiyani? Chifukwa chiyani titha kusiya zosintha mpaka pano? Kuti timvetse izi, tiyeni Pomaliza tiyeni tione kukhazikitsa getCurrentState():

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

Timapereka milandu itatu:

  1. base < 0 zikutanthauza kuti palibe zosintha mpaka nthawi yoperekera (onani kukhazikitsa pamwambapa getBaseUpdate()). Izi zitha kuchitika kumayambiriro kwamasewera chifukwa cha kuchedwa. Pankhaniyi, timagwiritsa ntchito zosinthidwa zaposachedwa kwambiri.
  2. base ndiye zosintha zaposachedwa kwambiri zomwe tili nazo. Izi zitha kuchitika chifukwa chakuchedwa kwa netiweki kapena kusalumikizana bwino kwa intaneti. M'nkhani inonso timagwiritsa ntchito zosintha zaposachedwa zomwe tili nazo.
  3. Tili ndi zosintha nthawi isanakwane komanso itatha, kuti tithe phatikiza!

Zonse zomwe zatsalamo state.js ndikukhazikitsa masamu omasulira omwe ndi osavuta (koma otopetsa). Ngati mukufuna kufufuza nokha, ndiye tsegulani state.js pa Github.

Gawo 2. Backend seva

Mu gawo ili tiwona Node.js backend yomwe imayang'anira zathu chitsanzo cha masewera a .io.

1. Malo olowera seva

Kuwongolera seva yapaintaneti tidzagwiritsa ntchito tsamba lodziwika bwino la Node.js lotchedwa kufotokoza. Idzakonzedwa ndi fayilo yathu yolowera pa seva src/server/server.js:

seva.js, gawo 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}`);

Kumbukirani kuti mu gawo loyamba tidakambirana Webpack? Apa ndipamene tidzagwiritsa ntchito masanjidwe athu a Webpack. Tidzawagwiritsa ntchito m'njira ziwiri:

  • Gwiritsani ntchito webpack-dev-middleware kumanganso phukusi lathu lachitukuko, kapena
  • Tumizani chikwatu dist/, momwe Webpack idzalembera mafayilo athu pambuyo popanga kupanga.

Ntchito ina yofunika server.js imakhala ndi kukhazikitsa seva alirezazomwe zimangolumikizana ndi seva ya Express:

seva.js, gawo 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);
});

Titakhazikitsa bwino kulumikizana kwa socket.io ndi seva, timakonza zosamalira zochitika pa socket yatsopano. Othandizira zochitika amakonza mauthenga omwe alandiridwa kuchokera kwa makasitomala powagawira ku chinthu cha singleton game:

seva.js, gawo 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);
}

Tikupanga masewera a .io, kotero tingofunika kope limodzi lokha Game ("Masewera") - osewera onse amasewera m'bwalo limodzi! Mu gawo lotsatira tiwona momwe kalasiyi imagwirira ntchito Game.

2. Ma seva amasewera

Kalasi Game ili ndi malingaliro ofunikira kwambiri pa seva. Lili ndi ntchito ziwiri zazikulu: kasamalidwe ka osewera ΠΈ masewera kayeseleledwe.

Tiyeni tiyambe ndi ntchito yoyamba - kuyang'anira osewera.

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

  // ...
}

Mu masewerowa tidziwa osewera ndi mabwalo id socket socket.io (ngati mwasokonezeka, bwererani ku server.js). Socket.io palokha imagawira soketi iliyonse kukhala yapadera id, kotero sitiyenera kuda nkhawa nazo. ndidzamuyitana Player ID.

Poganizira izi, tiyeni tiwone mitundu yosiyanasiyana m'kalasi Game:

  • sockets ndi chinthu chomwe chimamanga ID ya wosewera mpira ku socket yomwe imagwirizana ndi wosewera mpira. Zimatipatsa mwayi wopeza masiketi ndi ma ID awo osewera pakapita nthawi.
  • players ndi chinthu chomwe chimamanga ID ya player ku code> Player chinthu

bullets ndi mndandanda wa zinthu Bullet, opanda dongosolo lenileni.
lastUpdateTime - Ichi ndiye chizindikiro chanthawi yamasewera omaliza. Tiwona momwe zidzagwiritsidwe ntchito posachedwa.
shouldSendUpdate ndi chosinthira chothandizira. Tidzawonanso kugwiritsidwa ntchito kwake posachedwa.
Njira addPlayer(), removePlayer() ΠΈ handleInput() palibe chifukwa chofotokozera, amagwiritsidwa ntchito mu server.js. Ngati mukufuna chotsitsimutsa, bwererani pamwamba pang'ono.

Mzere womaliza constructor() akuyamba kusintha kuzungulira masewera (ndi pafupipafupi 60 zosintha/s):

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

  // ...
}

Njira update() mwina ili ndi gawo lofunikira kwambiri pamalingaliro ambali ya seva. Tiyeni titchule zonse zomwe imachita motsatira:

  1. Imawerengera nthawi yomwe ili dt zakhala ziri kuyambira kotsiriza update().
  2. Imatsitsimutsa projectile iliyonse ndikuwononga ngati kuli kofunikira. Tiwona kukhazikitsidwa kwa ntchitoyi pambuyo pake. Pakali pano ndi zokwanira kuti tidziwe zimenezo bullet.update() zobwerera true, ngati projectile iyenera kuwonongedwa (anatuluka panja pabwalo).
  3. Sinthani wosewera aliyense ndikupanga projectile ngati kuli kofunikira. Tiwonanso kukhazikitsidwa uku pambuyo pake - player.update() akhoza kubweza chinthu Bullet.
  4. Imayang'ana kugundana pakati pa projectiles ndi osewera omwe amagwiritsa ntchito applyCollisions(), yomwe imabweretsanso ma projectile angapo omwe amagunda osewera. Pa projectile iliyonse yomwe yabwezedwa, timawonjezera kuchuluka kwa wosewera yemwe adawombera (pogwiritsa ntchito player.onDealtDamage()), ndiyeno chotsani projectile kuchokera pamndandanda bullets.
  5. Imadziwitsa ndikuwononga osewera onse omwe adaphedwa.
  6. Amatumiza zosintha zamasewera kwa osewera onse mphindi iliyonse nthawi zoyitanidwa update(). Kusintha kothandizira komwe tatchula pamwambapa kumatithandiza kutsatira izi shouldSendUpdate. Monga update() otchedwa 60 times/s, timatumiza zosintha zamasewera 30 times/s. Choncho, pafupipafupi wotchi seva ndi 30 clock cycles / s (tinalankhula za mafupipafupi a wotchi mu gawo loyamba).

Bwanji kutumiza zosintha zamasewera okha kupyolera mu nthawi ? Kusunga tchanelo. Zosintha zamasewera 30 pamphindikati ndizambiri!

Bwanji osangoyimba ndiye? update() 30 nthawi pa sekondi iliyonse? Kupititsa patsogolo kayeseleledwe kamasewera. Nthawi zambiri amatchedwa update(), kuyerekezera kolondola kwamasewera kudzakhala. Koma musatengeke kwambiri ndi kuchuluka kwa zovuta update(), chifukwa ichi ndi ntchito computationally mtengo - 60 pa sekondi zokwanira ndithu.

Ena onse a kalasi Game imakhala ndi njira zothandizira zomwe zimagwiritsidwa ntchito mu update():

game.js, gawo 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() Ndiwosavuta - imasankha osewera ndi zigoli, imatenga asanu apamwamba, ndikubweza dzina lolowera ndikugoletsa aliyense.

createUpdate() ntchito mu update() kupanga zosintha zamasewera zomwe zimagawidwa kwa osewera. Ntchito yake yayikulu ndikuyitana njira serializeForUpdate(), zokhazikitsidwa pamakalasi Player ΠΈ Bullet. Dziwani kuti amangotumiza deta aliyense wosewera mpira za pafupi osewera ndi projectiles - palibe chifukwa chotumizira zambiri zamasewera omwe ali kutali ndi osewera!

3. Zinthu zamasewera pa seva

M'masewera athu, ma projectiles ndi osewera amafanana kwambiri: ndi zinthu zamasewera zozungulira. Kuti titengere mwayi pakufanana kumeneku pakati pa osewera ndi projectiles, tiyeni tiyambe ndi kukhazikitsa gulu loyambira 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,
    };
  }
}

Palibe chovuta chomwe chikuchitika pano. Kalasi iyi idzakhala poyambira bwino pakukulitsa. Tiyeni tione mmene kalasi Bullet amagwiritsa 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 zazifupi kwambiri! Tawonjezera Object zowonjezera zotsatirazi:

  • Pogwiritsa ntchito package mwachidule kwa kubadwa mwachisawawa id projectile.
  • Kuwonjezera munda parentID, kotero kuti mutha kutsata wosewera yemwe adapanga projectile iyi.
  • Kuonjezera mtengo wobwezera ku update(), zomwe ndi zofanana true, ngati projectile ili kunja kwa bwalo (kumbukirani tidakambirana izi mu gawo lomaliza?).

Tiyeni tipitirire 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,
    };
  }
}

Osewera ndi ovuta kwambiri kuposa ma projectiles, kotero kalasi iyi iyenera kusunga magawo ena angapo. Njira yake update() imagwira ntchito zambiri, makamaka kubweza projectile yomwe yangopangidwa kumene ngati palibe yotsala fireCooldown (kumbukirani tidakambirana izi m'gawo lapitalo?). Imawonjezeranso njira serializeForUpdate(), chifukwa tifunika kuphatikiza magawo owonjezera a wosewera pakusintha kwamasewera.

Kupezeka kwa kalasi yoyambira Object - gawo lofunikira popewa kubwereza kachidindo. Mwachitsanzo, popanda kalasi Object chinthu chilichonse chamasewera chiyenera kukhala ndi kukhazikitsa komweko distanceTo(), ndikuyika-kuyika zonse izi pamafayilo angapo kungakhale kovuta. Izi zimakhala zofunikira makamaka pamapulojekiti akuluakulu, pamene chiwerengero cha kukula Object makalasi akukula.

4. Kuzindikira kugunda

Zomwe zatsala kuti tichite ndikuzindikira ma projectiles akagunda osewera! Kumbukirani kaphatikizidwe ka code iyi kuchokera munjira update() mu class Game:

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

    // ...
  }
}

Tiyenera kukhazikitsa njira applyCollisions(), yomwe imabweretsa ma projectiles onse omwe amagunda osewera. Mwamwayi, izi sizili zovuta kuchita chifukwa

  • Zinthu zonse zowombana ndi zozungulira, ndipo ichi ndi mawonekedwe osavuta kugwiritsa ntchito kuzindikira kugunda.
  • Tili nayo kale njira distanceTo(), zomwe tidazitsatira m'kalasi mu gawo lapitalo Object.

Umu ndi momwe kukhazikitsa kwathu kuzindikira kugunda kumawonekera:

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

Kuzindikira kugunda kosavuta kumeneku kumatengera mfundo yakuti mabwalo awiri amawombana ngati mtunda pakati pa malo awo uli wocheperapo kuchuluka kwa ma radii awo. Nayi nkhani yomwe mtunda wapakati pakati pa mabwalo awiri ndi wofanana ndendende ndi kuchuluka kwa ma radii awo:

Kupanga masewera apaintaneti a anthu ambiri mumtundu wa .io
Apa muyenera kuyang'anitsitsa mbali zina zingapo:

  • Chojambulacho sichiyenera kugunda wosewera yemwe adachipanga. Izi zingatheke poyerekezera bullet.parentID с player.id.
  • The projectile iyenera kugunda kamodzi kokha ngati kugunda osewera angapo nthawi imodzi. Tidzathetsa vutoli pogwiritsa ntchito woyendetsa break: Wosewerera yemwe akuwombana ndi projectile atapezeka, timasiya kufufuza ndikupita ku projectile yotsatira.

ΠšΠΎΠ½Π΅Ρ†

Ndizomwezo! Takuphunzitsani zonse zomwe muyenera kudziwa kuti mupange masewera a pa intaneti a .io. Chotsatira ndi chiyani? Pangani masewera anu a .io!

Zitsanzo zonse zachitsanzo ndizotseguka ndipo zimayikidwa Github.

Source: www.habr.com

Kuwonjezera ndemanga