Izlaists 2015. gadÄ Agar.io kļuva par jauna žanra priekÅ”teci .io spÄleskura popularitÄte kopÅ” tÄ laika ir kļuvusi arvien populÄrÄka. Es personÄ«gi esmu pieredzÄjis .io spÄļu popularitÄtes pieaugumu: pÄdÄjo trÄ«s gadu laikÄ esmu piedzÄ«vojis izveidoja un pÄrdeva divas Ŕī žanra spÄles..
Ja jÅ«s nekad iepriekÅ” neesat dzirdÄjis par Ŕīm spÄlÄm, Ŕīs ir bezmaksas vairÄku spÄlÄtÄju tÄ«mekļa spÄles, kuras ir viegli spÄlÄt (nav nepiecieÅ”ams konts). ViÅi parasti saskaras ar daudziem pretinieku spÄlÄtÄjiem vienÄ arÄnÄ. Citas slavenas .io spÄles: Slither.io Šø Diep.io.
Å ajÄ rakstÄ mÄs izpÄtÄ«sim, kÄ izveidot .io spÄli no nulles. Å im nolÅ«kam pietiks tikai ar Javascript zinÄÅ”anÄm: jums ir jÄsaprot, piemÄram, sintakse ES6, atslÄgvÄrds this Šø solÄ«jumi. Pat ja jÅ«su zinÄÅ”anas par Javascript nav ideÄlas, jÅ«s joprojÄm varat saprast lielÄko daļu ziÅojuma.
.io spÄles piemÄrs
Lai palÄ«dzÄtu mÄcÄ«ties, mÄs atsauksimies uz .io spÄles piemÄrs. MÄÄ£iniet to spÄlÄt!
SpÄle ir pavisam vienkÄrÅ”a: tu kontrolÄ kuÄ£i arÄnÄ, kur ir citi spÄlÄtÄji. JÅ«su kuÄ£is automÄtiski izÅ”auj Å”ÄviÅus, un jÅ«s mÄÄ£inÄt trÄpÄ«t citiem spÄlÄtÄjiem, izvairoties no viÅu Å”ÄviÅiem.
Viss mapÄ public/ serveris iesniegs statiski. IN public/assets/ satur attÄlus, ko izmanto mÅ«su projekts.
src /
Viss avota kods atrodas mapÄ src/. Nosaukumi client/ Šø server/ runÄ paÅ”i par sevi un shared/ satur konstantu failu, ko importÄ gan klients, gan serveris.
src/client/index.js ir Javascript (JS) klienta ieejas punkts. Webpack sÄksies no Å”ejienes un rekursÄ«vi meklÄs citus importÄtos failus.
MÅ«su Webpack versijas izvades JS atradÄ«sies direktorijÄ dist/. Es nosaukÅ”u Å”o failu par mÅ«su js pakotni.
MÄs izmantojam KÅadaun jo Ä«paÅ”i konfigurÄciju @babel/preset-env lai pÄrsÅ«tÄ«tu mÅ«su JS kodu vecÄkÄm pÄrlÅ«kprogrammÄm.
MÄs izmantojam spraudni, lai izvilktu visus CSS, uz kuriem atsaucas JS faili, un apvienotu tos vienuviet. Es viÅu saukÅ”u par mÅ«su css pakotne.
IespÄjams, esat pamanÄ«jis dÄ«vainus pakotÅu failu nosaukumus '[name].[contenthash].ext'. Tie satur failu nosaukumu aizstÄÅ”ana TÄ«mekļa pakotne: [name] tiks aizstÄts ar ievades punkta nosaukumu (mÅ«su gadÄ«jumÄ Å”is game) un [contenthash] tiks aizstÄts ar faila satura jauktu. MÄs to darÄm, lai optimizÄt projektu jaukÅ”anai - JÅ«s varat likt pÄrlÅ«kprogrammÄm saglabÄt mÅ«su JS pakotnes keÅ”atmiÅÄ uz nenoteiktu laiku, jo ja mainÄs pakotne, mainÄs arÄ« tÄs faila nosaukums (izmaiÅas contenthash). Gala rezultÄts bÅ«s skata faila nosaukums game.dbeee76e91a97d0c7207.js.
fails webpack.common.js ir bÄzes konfigurÄcijas fails, ko mÄs importÄjam izstrÄdes un pabeigtÄ projekta konfigurÄcijÄs. Å eit ir izstrÄdes konfigurÄcijas piemÄrs:
EfektivitÄtes labad mÄs izmantojam izstrÄdes procesÄ webpack.dev.js, un pÄrslÄdzas uz webpack.prod.jslai optimizÄtu iepakojuma izmÄrus, izvietojot to ražoÅ”anÄ.
VietÄjais iestatÄ«jums
Es iesaku instalÄt projektu vietÄjÄ datorÄ, lai jÅ«s varÄtu veikt Å”ajÄ ziÅojumÄ norÄdÄ«tÄs darbÄ«bas. IestatÄ«Å”ana ir vienkÄrÅ”a: pirmkÄrt, sistÄmai jÄbÅ«t instalÄtai mezgls Šø NPM. TÄlÄk jums jÄdara
$ git clone https://github.com/vzhou842/example-.io-game.git
$ cd example-.io-game
$ npm install
un tu esi gatavs doties! Lai palaistu izstrÄdes serveri, vienkÄrÅ”i palaidiet
$ npm run develop
un dodieties uz tÄ«mekļa pÄrlÅ«kprogrammu localhost: 3000. IzstrÄdes serveris automÄtiski pÄrbÅ«vÄs JS un CSS pakotnes, mainoties kodam ā vienkÄrÅ”i atsvaidziniet lapu, lai redzÄtu visas izmaiÅas!
3. Klientu ieejas punkti
SÄksim pie paÅ”a spÄles koda. Vispirms mums ir vajadzÄ«ga lapa index.html, apmeklÄjot vietni, pÄrlÅ«kprogramma to vispirms ielÄdÄs. MÅ«su lapa bÅ«s diezgan vienkÄrÅ”a:
index.html
io spÄles piemÄrs SPÄLÄT
Å is koda piemÄrs skaidrÄ«bas labad ir nedaudz vienkÄrÅ”ots, un es darÄ«Å”u to paÅ”u ar daudziem citiem ziÅu piemÄriem. Pilnu kodu vienmÄr var apskatÄ«t vietnÄ GitHub.
Tas var izklausÄ«ties sarežģīti, taÄu Å”eit nekas daudz nenotiek:
VairÄku citu JS failu importÄÅ”ana.
CSS importÄÅ”ana (tÄ Webpack zina, ka tÄs ir jÄiekļauj mÅ«su CSS pakotnÄ).
ŠŠ°ŠæŃŃŠŗ connect() lai izveidotu savienojumu ar serveri un palaistu downloadAssets() lai lejupielÄdÄtu attÄlus, kas nepiecieÅ”ami spÄles renderÄÅ”anai.
PÄc 3. posma pabeigÅ”anas tiek parÄdÄ«ta galvenÄ izvÄlne (playMenu).
ApdarinÄtÄja iestatÄ«Å”ana pogas "PLAY" nospieÅ”anai. Kad poga tiek nospiesta, kods inicializÄ spÄli un paziÅo serverim, ka esam gatavi spÄlÄt.
MÅ«su klienta-servera loÄ£ikas galvenÄ "gaļa" ir failos, kurus importÄja fails index.js. Tagad mÄs tos visus izskatÄ«sim kÄrtÄ«bÄ.
4. Klientu datu apmaiÅa
Å ajÄ spÄlÄ mÄs izmantojam labi zinÄmu bibliotÄku, lai sazinÄtos ar serveri ligzda.io. Socket.io ir vietÄjais atbalsts WebSockets, kas ir labi piemÄroti divvirzienu saziÅai: mÄs varam nosÅ«tÄ«t ziÅojumus uz serveri Šø serveris var nosÅ«tÄ«t mums ziÅojumus, izmantojot to paÅ”u savienojumu.
Mums bÅ«s viens fails src/client/networking.jskurÅ” parÅ«pÄsies visi saziÅa ar serveri:
Å is kods arÄ« ir nedaudz saÄ«sinÄts skaidrÄ«bas labad.
Å ajÄ failÄ ir trÄ«s galvenÄs darbÄ«bas:
MÄs cenÅ”amies izveidot savienojumu ar serveri. connectedPromise atļauts tikai tad, kad esam izveidojuÅ”i savienojumu.
Ja savienojums ir veiksmÄ«gs, mÄs reÄ£istrÄjam atzvanÄ«Å”anas funkcijas (processGameUpdate() Šø onGameOver()) ziÅojumiem, ko varam saÅemt no servera.
MÄs eksportÄjam play() Šø updateDirection()lai citi faili tos varÄtu izmantot.
5. Klientu renderÄÅ”ana
Ir pienÄcis laiks parÄdÄ«t attÄlu ekrÄnÄ!
ā¦bet pirms mÄs to varam izdarÄ«t, mums ir jÄlejupielÄdÄ visi Å”im nolÅ«kam nepiecieÅ”amie attÄli (resursi). UzrakstÄ«sim resursu pÄrvaldnieku:
Resursu pÄrvaldÄ«ba nav tik grÅ«ti Ä«stenojama! GalvenÄ ideja ir objekta uzglabÄÅ”ana assets, kas saistÄ«s faila nosaukuma atslÄgu ar objekta vÄrtÄ«bu Image. Kad resurss ir ielÄdÄts, mÄs to uzglabÄjam objektÄ assets Ätrai piekļuvei nÄkotnÄ. Kad tiks atļauts lejupielÄdÄt katru atseviŔķu resursu (tas ir, viss resursi), mÄs pieļaujam downloadPromise.
PÄc resursu lejupielÄdes varat sÄkt renderÄÅ”anu. KÄ minÄts iepriekÅ”, lai zÄ«mÄtu tÄ«mekļa lapÄ, mÄs izmantojam HTML5 kanvas (<canvas>). MÅ«su spÄle ir diezgan vienkÄrÅ”a, tÄpÄc mums ir jÄuzzÄ«mÄ tikai sekojoÅ”ais:
fons
SpÄlÄtÄju kuÄ£is
Citi spÄlÄtÄji spÄlÄ
Äaumalas
Å eit ir svarÄ«gi fragmenti src/client/render.js, kas atveido tieÅ”i Äetrus iepriekÅ” uzskaitÄ«tos vienumus:
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);
}
SkaidrÄ«bas labad Å”is kods ir arÄ« saÄ«sinÄts.
render() ir Ŕī faila galvenÄ funkcija. startRendering() Šø stopRendering() kontrolÄt renderÄÅ”anas cilpas aktivizÄÅ”anu ar Ätrumu 60 kadri/s.
AtseviŔķu renderÄÅ”anas palÄ«gu funkciju Ä«paÅ”as ievieÅ”anas (piemÄram renderBullet()) nav tik svarÄ«gi, taÄu Å”eit ir viens vienkÄrÅ”s piemÄrs:
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,
);
}
Å emiet vÄrÄ, ka mÄs izmantojam metodi getAsset(), kas iepriekÅ” bija redzams asset.js!
Ja vÄlaties uzzinÄt par citiem renderÄÅ”anas palÄ«giem, izlasiet pÄrÄjo. src/client/render.js.
6. Klienta ievade
Ir pienÄcis laiks izveidot spÄli spÄlÄjama! VadÄ«bas shÄma bÅ«s ļoti vienkÄrÅ”a: lai mainÄ«tu kustÄ«bas virzienu, var izmantot peli (datorÄ) vai pieskarties ekrÄnam (mobilajÄ ierÄ«cÄ). Lai to Ä«stenotu, mÄs reÄ£istrÄsimies PasÄkumu klausÄ«tÄji Mouse and Touch notikumiem.
Par to visu parÅ«pÄsies src/client/input.js:
ievade.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() ir notikumu klausÄ«tÄji, kas zvana updateDirection() (no networking.js), kad notiek ievades notikums (piemÄram, kad tiek pÄrvietota pele). updateDirection() apstrÄdÄ ziÅojumapmaiÅu ar serveri, kas apstrÄdÄ ievades notikumu un attiecÄ«gi atjaunina spÄles stÄvokli.
7. Klienta statuss
Å Ä« sadaļa ir visgrÅ«tÄkÄ ziÅojuma pirmajÄ daļÄ. NezaudÄjiet drosmi, ja to nesaprotat pirmajÄ lasÄ«Å”anas reizÄ! Varat pat to izlaist un atgriezties pie tÄ vÄlÄk.
PÄdÄjais mÄ«klas gabals, kas nepiecieÅ”ams, lai aizpildÄ«tu klienta/servera kodu, ir bija. Atcerieties koda fragmentu no sadaļas Klientu renderÄÅ”ana?
render.js
import { getCurrentState } from './state';
function render() {
const { me, others, bullets } = getCurrentState();
// Do the rendering
// ...
}
getCurrentState() jÄspÄj sniegt mums informÄciju par paÅ”reizÄjo klienta spÄles stÄvokli jebkurÄ brÄ«dÄ« pamatojoties uz atjauninÄjumiem, kas saÅemti no servera. Å eit ir piemÄrs spÄles atjauninÄjumam, ko serveris var nosÅ«tÄ«t:
Katrs spÄles atjauninÄjums satur piecus identiskus laukus:
t: servera laikspiedols, kas norÄda, kad Å”is atjauninÄjums tika izveidots.
me: informÄcija par atskaÅotÄju, kas saÅem Å”o atjauninÄjumu.
pÄrÄjie: informÄcijas klÄsts par citiem spÄlÄtÄjiem, kas piedalÄs tajÄ paÅ”Ä spÄlÄ.
lodes: informÄcijas masÄ«vs par spÄles Å”ÄviÅiem.
megabanneris: paÅ”reizÄjie uzvarÄtÄju saraksta dati. Å ajÄ amatÄ mÄs tos neapskatÄ«sim.
7.1. Naivs klienta stÄvoklis
Naiva Ä«stenoÅ”ana getCurrentState() var tieÅ”i atgriezt tikai pÄdÄjÄ saÅemtÄ spÄles atjauninÄjuma datus.
naivvalsts.js
let lastGameUpdate = null;
// Handle a newly received game update.
export function processGameUpdate(update) {
lastGameUpdate = update;
}
export function getCurrentState() {
return lastGameUpdate;
}
Skaisti un skaidri! Bet ja vien tas bÅ«tu tik vienkÄrÅ”i. Viens no iemesliem, kÄpÄc Ŕī ievieÅ”ana ir problemÄtiska: tas ierobežo renderÄÅ”anas kadru Ätrumu lÄ«dz servera pulksteÅa Ätrumam.
Kadru Ätrums: kadru skaits (t.i., zvanu render()) sekundÄ vai FPS. SpÄles parasti cenÅ”as sasniegt vismaz 60 FPS.
AtzÄ«mes likme: biežums, kÄdÄ serveris klientiem nosÅ«ta spÄļu atjauninÄjumus. Bieži vien tas ir mazÄks par kadru Ätrumu. MÅ«su spÄlÄ serveris darbojas ar frekvenci 30 cikli sekundÄ.
Ja mÄs vienkÄrÅ”i atveidosim jaunÄko spÄles atjauninÄjumu, FPS bÅ«tÄ«bÄ nekad nepÄrsniegs 30, jo mÄs nekad nesaÅemam vairÄk par 30 atjauninÄjumiem sekundÄ no servera. Pat ja mÄs piezvanÄ«sim render() 60 reizes sekundÄ, tad puse no Å”iem zvaniem vienkÄrÅ”i pÄrzÄ«mÄs vienu un to paÅ”u, bÅ«tÄ«bÄ neko nedarot. VÄl viena naivÄs ievieÅ”anas problÄma ir tÄ nosliece uz kavÄÅ”anos. Ar ideÄlu interneta Ätrumu klients saÅems spÄles atjauninÄjumu tieÅ”i ik pÄc 33 ms (30 sekundÄ):
DiemžÄl nekas nav ideÄls. ReÄlistiskÄks attÄls bÅ«tu:
NaivÄ ievieÅ”ana ir praktiski sliktÄkais gadÄ«jums, kad runa ir par latentumu. Ja spÄles atjauninÄjums tiek saÅemts ar 50 ms aizkavi, tad klientu stendi papildu 50 ms, jo tas joprojÄm atveido spÄles stÄvokli no iepriekÅ”ÄjÄ atjauninÄjuma. Varat iedomÄties, cik tas ir neÄrti spÄlÄtÄjam: patvaļīga bremzÄÅ”ana liks spÄlei justies saraustÄ«tai un nestabilai.
7.2 Uzlabots klienta stÄvoklis
MÄs veiksim dažus uzlabojumus naivÄ ievieÅ”anÄ. PirmkÄrt, mÄs izmantojam renderÄÅ”anas kavÄÅ”anÄs uz 100 ms. Tas nozÄ«mÄ, ka klienta "paÅ”reizÄjais" stÄvoklis vienmÄr atpaliks no spÄles stÄvokļa serverÄ« par 100 ms. PiemÄram, ja laiks serverÄ« ir 150, tad klients atveidos stÄvokli, kÄdÄ serveris tajÄ laikÄ bija 50:
Tas dod mums 100 ms buferi, lai izdzÄ«votu neparedzamos spÄļu atjauninÄÅ”anas laikos:
Izmaksa par to bÅ«s pastÄvÄ«ga ievades aizkave uz 100 ms. Tas ir neliels upuris vienmÄrÄ«gai spÄlei - lielÄkÄ daļa spÄlÄtÄju (Ä«paÅ”i gadÄ«juma spÄlÄtÄji) pat nepamanÄ«s Å”o aizkavi. CilvÄkiem ir daudz vieglÄk pielÄgoties pastÄvÄ«gam 100 ms latentumam, nekÄ spÄlÄt ar neparedzamu latentumu.
MÄs varam izmantot arÄ« citu tehniku, ko sauc klienta puses prognozÄÅ”ana, kas labi samazina uztverto latentumu, taÄu tas netiks apskatÄ«ts Å”ajÄ ziÅojumÄ.
VÄl viens uzlabojums, ko mÄs izmantojam, ir lineÄrÄ interpolÄcija. RenderÄÅ”anas aizkavÄÅ”anÄs dÄļ mÄs parasti esam vismaz vienu atjauninÄjumu priekÅ”Ä klienta paÅ”reizÄjam laikam. Kad sauc getCurrentState(), mÄs varam izpildÄ«t lineÄrÄ interpolÄcija starp spÄles atjauninÄjumiem tieÅ”i pirms un pÄc paÅ”reizÄjÄ laika klientÄ:
Tas atrisina kadru Ätruma problÄmu: tagad mÄs varam renderÄt unikÄlus kadrus ar jebkuru vÄlamo kadru nomaiÅas Ätrumu!
7.3. Uzlabota klienta stÄvokļa ievieÅ”ana
ÄŖstenoÅ”anas piemÄrs iekÅ”Ä src/client/state.js izmanto gan renderÄÅ”anas nobÄ«di, gan lineÄro interpolÄciju, bet ne ilgi. SadalÄ«sim kodu divÄs daļÄs. Å eit ir pirmais:
state.js 1. daļa
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;
}
Pirmais solis ir izdomÄt, kas currentServerTime(). KÄ redzÄjÄm iepriekÅ”, katrÄ spÄles atjauninÄjumÄ ir iekļauts servera laikspiedols. MÄs vÄlamies izmantot renderÄÅ”anas latentumu, lai attÄlu renderÄtu 100 ms aiz servera, taÄu mÄs nekad neuzzinÄsim paÅ”reizÄjo laiku serverÄ«, jo mÄs nevaram zinÄt, cik ilgs laiks pagÄja, lÄ«dz kÄds no atjauninÄjumiem nonÄca pie mums. Internets ir neprognozÄjams, un tÄ Ätrums var ievÄrojami atŔķirties!
Lai apietu Å”o problÄmu, mÄs varam izmantot saprÄtÄ«gu tuvinÄjumu: mÄs izlikties, ka pirmais atjauninÄjums ieradÄs uzreiz. Ja tÄ bÅ«tu taisnÄ«ba, tad mÄs zinÄtu servera laiku Å”ajÄ konkrÄtajÄ brÄ«dÄ«! MÄs saglabÄjam servera laikspiedolu firstServerTimestamp un paturiet mÅ«su vietÄjÄ (klienta) laikspiedols tajÄ paÅ”Ä brÄ«dÄ« gameStart.
Pagaidi. Vai tam nevajadzÄtu bÅ«t servera laikam = klienta laikam? KÄpÄc mÄs atŔķiram "servera laikspiedolu" un "klienta laikspiedolu"? Tas ir lielisks jautÄjums! IzrÄdÄs, ka tie nav viens un tas pats. Date.now() klientÄ un serverÄ« atgriezÄ«s dažÄdus laikspiedolus, un tas ir atkarÄ«gs no faktoriem, kas ir lokÄli Å”ajÄs iekÄrtÄs. Nekad neuzÅemieties, ka laikspiedoli visÄs iekÄrtÄs bÅ«s vienÄdi.
Tagad mÄs saprotam, ko dara currentServerTime(): tas atgriežas paÅ”reizÄjÄ renderÄÅ”anas laika servera laikspiedols. Citiem vÄrdiem sakot, Å”is ir servera paÅ”reizÄjais laiks (firstServerTimestamp <+ (Date.now() - gameStart)) mÄ«nus renderÄÅ”anas aizkave (RENDER_DELAY).
Tagad apskatÄ«sim, kÄ mÄs apstrÄdÄjam spÄļu atjauninÄjumus. SaÅemot no atjauninÄÅ”anas servera, tas tiek izsaukts processGameUpdate()un mÄs saglabÄjam jauno atjauninÄjumu masÄ«vÄ gameUpdates. PÄc tam, lai pÄrbaudÄ«tu atmiÅas lietojumu, mÄs noÅemam visus vecos atjauninÄjumus bÄzes atjauninÄjumsjo mums tÄs vairs nav vajadzÄ«gas.
Kas ir "pamata atjauninÄjums"? Å is pirmais atjauninÄjums, ko atrodam, pÄrejot atpakaļ no servera paÅ”reizÄjÄ laika. Atcerieties Å”o diagrammu?
SpÄles atjauninÄjums tieÅ”i pa kreisi no "Client Render Time" ir pamata atjauninÄjums.
Kam tiek izmantots bÄzes atjauninÄjums? KÄpÄc mÄs varam atlaist atjauninÄjumus uz sÄkotnÄjo lÄ«meni? Lai to noskaidrotu, pieÅemsim beidzot apsveriet ievieÅ”anu getCurrentState():
state.js 2. daļa
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),
};
}
}
MÄs izskatÄm trÄ«s lietas:
base < 0 nozÄ«mÄ, ka lÄ«dz paÅ”reizÄjam renderÄÅ”anas laikam nav atjauninÄjumu (skatiet iepriekÅ” ievieÅ”anu getBaseUpdate()). Tas var notikt tieÅ”i spÄles sÄkumÄ renderÄÅ”anas aizkavÄÅ”anÄs dÄļ. Å ajÄ gadÄ«jumÄ mÄs izmantojam jaunÄko saÅemto atjauninÄjumu.
base ir jaunÄkais mÅ«su atjauninÄjums. Tas var bÅ«t tÄ«kla aizkaves vai slikta interneta savienojuma dÄļ. Å ajÄ gadÄ«jumÄ mÄs izmantojam arÄ« jaunÄko atjauninÄjumu.
Mums ir atjauninÄjums gan pirms, gan pÄc paÅ”reizÄjÄ renderÄÅ”anas laika, tÄpÄc mÄs varam interpolÄt!
Viss, kas palicis iekÅ”Ä state.js ir lineÄrÄs interpolÄcijas ievieÅ”ana, kas ir vienkÄrÅ”a (bet garlaicÄ«ga) matemÄtika. Ja vÄlaties to izpÄtÄ«t pats, atveriet state.js par GitHub.
2. daļa. AizmugursistÄmas serveris
Å ajÄ daÄ¼Ä mÄs apskatÄ«sim Node.js aizmugursistÄmu, kas kontrolÄ mÅ«su .io spÄles piemÄrs.
1. Server Entry Point
Lai pÄrvaldÄ«tu tÄ«mekļa serveri, mÄs izmantosim populÄru tÄ«mekļa ietvaru Node.js ar nosaukumu Kurjers. To konfigurÄs mÅ«su servera ieejas punkta fails src/server/server.js:
server.js 1. daļa
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}`);
Atcerieties, ka pirmajÄ daÄ¼Ä mÄs apspriedÄm Webpack? Å eit mÄs izmantosim mÅ«su Webpack konfigurÄcijas. MÄs tos izmantosim divos veidos:
PÄc veiksmÄ«gas socket.io savienojuma izveides ar serveri mÄs iestatÄ«jÄm notikumu apdarinÄtÄjus jaunajai ligzdai. Notikumu apstrÄdÄtÄji apstrÄdÄ ziÅojumus, kas saÅemti no klientiem, deleÄ£Äjot to vienam objektam game:
server.js 3. daļa
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);
}
MÄs veidojam .io spÄli, tÄpÄc mums ir nepiecieÅ”ams tikai viens eksemplÄrs Game ("SpÄle") - visi spÄlÄtÄji spÄlÄ vienÄ arÄnÄ! NÄkamajÄ sadaÄ¼Ä mÄs redzÄsim, kÄ Å”Ä« klase darbojas. Game.
2. SpÄļu serveri
Klase Game satur vissvarÄ«gÄko loÄ£iku servera pusÄ. Tam ir divi galvenie uzdevumi: spÄlÄtÄju vadÄ«ba Šø spÄles simulÄcija.
SÄksim ar pirmo uzdevumu, spÄlÄtÄju menedžmentu.
game.js 1. daļa
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);
}
}
// ...
}
Å ajÄ spÄlÄ mÄs noteiksim spÄlÄtÄjus pÄc laukuma id viÅu socket.io ligzda (ja apmulsÄ«sit, atgriezieties pie server.js). Socket.io pati katrai ligzdai pieŔķir unikÄlu idtÄpÄc mums par to nav jÄuztraucas. Es viÅam piezvanÄ«Å”u SpÄlÄtÄja ID.
Paturot to prÄtÄ, izpÄtÄ«sim klases mainÄ«gos Game:
sockets ir objekts, kas saista spÄlÄtÄja ID ar ligzdu, kas ir saistÄ«ta ar atskaÅotÄju. Tas ļauj mums pastÄvÄ«gi piekļūt ligzdÄm pÄc to atskaÅotÄja ID.
players ir objekts, kas saista spÄlÄtÄja ID ar kodu>SpÄlÄtÄja objekts
bullets ir objektu masÄ«vs Bullet, kam nav noteiktas kÄrtÄ«bas. lastUpdateTime ir pÄdÄjÄs spÄles atjauninÄÅ”anas laika zÄ«mogs. DrÄ«zumÄ redzÄsim, kÄ tas tiks izmantots. shouldSendUpdate ir papildu mainÄ«gais. DrÄ«zumÄ redzÄsim arÄ« tÄ izmantoÅ”anu.
Metodes addPlayer(), removePlayer() Šø handleInput() nav jÄskaidro, tie tiek izmantoti server.js. Ja jums ir jÄatsvaidzina atmiÅa, atgriezieties nedaudz augstÄk.
PÄdÄjÄ rinda constructor() sÄk darboties atjauninÄÅ”anas cikls spÄles (ar biežumu 60 atjauninÄjumi / s):
game.js 2. daļa
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;
}
}
// ...
}
Metode update() satur, iespÄjams, vissvarÄ«gÄko servera puses loÄ£ikas daļu. LÅ«k, ko tas dara secÄ«bÄ:
AprÄÄ·ina, cik ilgi dt pagÄjis kopÅ” pÄdÄjÄs update().
Atsvaidzina katru Å”ÄviÅu un iznÄ«cina, ja nepiecieÅ”ams. Å Ä«s funkcionalitÄtes ievieÅ”anu redzÄsim vÄlÄk. PagaidÄm mums pietiek ar to zinÄt bullet.update()atgriežas trueja Å”ÄviÅÅ” bÅ«tu jÄiznÄ«cina (viÅÅ” izkÄpa no arÄnas).
Atjaunina katru spÄlÄtÄju un, ja nepiecieÅ”ams, rada Å”ÄviÅu. Å o ievieÅ”anu redzÄsim arÄ« vÄlÄk - player.update()var atgriezt objektu Bullet.
PÄrbauda, āāvai nav sadursmes starp Å”ÄviÅiem un spÄlÄtÄjiem ar applyCollisions(), kas atgriež virkni lÄdiÅu, kas trÄpÄ«ja spÄlÄtÄjiem. Par katru atgriezto Å”ÄviÅu mÄs palielinÄm tÄ spÄlÄtÄja punktu skaitu, kurÅ” to izÅ”Äva (izmantojot player.onDealtDamage()) un pÄc tam noÅemiet Å”ÄviÅu no bloka bullets.
PaziÅo un iznÄ«cina visus nogalinÄtos spÄlÄtÄjus.
NosÅ«ta spÄles atjauninÄjumu visiem spÄlÄtÄjiem katru sekundi reizes, kad zvanÄ«ja update(). Tas palÄ«dz mums sekot lÄ«dzi iepriekÅ” minÄtajam papildu mainÄ«gajam. shouldSendUpdate. KÄ update() zvana 60 reizes/s, spÄļu atjauninÄjumus nosÅ«tÄm 30 reizes/s. TÄdÄjÄdi pulksteÅa frekvence servera pulkstenis ir 30 pulksteÅi/s (par takts frekvencÄm runÄjÄm pirmajÄ daļÄ).
KÄpÄc sÅ«tÄ«t tikai spÄļu atjauninÄjumus cauri laikam ? Lai saglabÄtu kanÄlu. 30 spÄļu atjauninÄjumi sekundÄ ir daudz!
KÄpÄc ne tikai piezvanÄ«t update() 30 reizes sekundÄ? Lai uzlabotu spÄles simulÄciju. Jo biežÄk sauc update(), jo precÄ«zÄka bÅ«s spÄles simulÄcija. TaÄu pÄrÄk neaizraujieties ar daudzajiem izaicinÄjumiem. update(), jo tas ir skaitļoÅ”anas ziÅÄ dÄrgs uzdevums - pietiek ar 60 sekundÄ.
PÄrÄjÄ klase Game sastÄv no palÄ«gmetodÄm, kas izmantotas update():
getLeaderboard() diezgan vienkÄrÅ”i ā tas saŔķiro spÄlÄtÄjus pÄc rezultÄta, ieÅem piecus labÄkos un katram atgriež lietotÄjvÄrdu un rezultÄtu.
createUpdate() izmantots gadÄ update() lai izveidotu spÄļu atjauninÄjumus, kas tiek izplatÄ«ti spÄlÄtÄjiem. TÄs galvenais uzdevums ir izsaukt metodes serializeForUpdate()ieviesta klasÄm Player Šø Bullet. Å emiet vÄrÄ, ka tas katram spÄlÄtÄjam nodod datus tikai par tuvÄkais spÄlÄtÄji un Å”ÄviÅi - nav nepiecieÅ”ams pÄrraidÄ«t informÄciju par spÄles objektiem, kas atrodas tÄlu no spÄlÄtÄja!
3. SpÄļu objekti uz servera
MÅ«su spÄlÄ Å”ÄviÅi un spÄlÄtÄji patiesÄ«bÄ ir ļoti lÄ«dzÄ«gi: tie ir abstrakti, apaļi, kustÄ«gi spÄles objekti. Lai izmantotu Å”o spÄlÄtÄju un Å”ÄviÅu lÄ«dzÄ«bu, sÄksim ar bÄzes klases ievieÅ”anu Object:
Izmantojot iepakojumu shortid nejauÅ”ai Ä£enerÄÅ”anai id Å”ÄviÅÅ”.
Lauka pievienoÅ”ana parentIDlai jÅ«s varÄtu izsekot spÄlÄtÄjam, kurÅ” izveidoja Å”o Å”ÄviÅu.
AtgrieÅ”anas vÄrtÄ«bas pievienoÅ”ana update(), kas ir vienÄds ar trueja Å”ÄviÅÅ” atrodas Ärpus arÄnas (atcerieties, ka mÄs par to runÄjÄm pÄdÄjÄ sadaļÄ?).
PÄriesim pie 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,
};
}
}
SpÄlÄtÄji ir sarežģītÄki par Å”ÄviÅiem, tÄpÄc Å”ajÄ klasÄ ir jÄuzglabÄ vÄl daži lauki. ViÅa metode update() paveic daudz darba, jo Ä«paÅ”i atdod jaunizveidoto Å”ÄviÅu, ja tÄda vairs nav fireCooldown (atcerieties, ka mÄs par to runÄjÄm iepriekÅ”ÄjÄ sadaļÄ?). Tas arÄ« paplaÅ”ina metodi serializeForUpdate(), jo mums ir jÄiekļauj papildu lauki spÄlÄtÄjam spÄles atjauninÄjumÄ.
Ir pamatklase Object - svarÄ«gs solis, lai izvairÄ«tos no koda atkÄrtoÅ”anas. PiemÄram, nav klases Object katram spÄles objektam ir jÄbÅ«t vienÄdai Ä«stenoÅ”anai distanceTo(), un visu Å”o implementÄciju kopÄÅ”ana un ielÄ«mÄÅ”ana vairÄkos failos bÅ«tu murgs. Tas kļūst Ä«paÅ”i svarÄ«gi lieliem projektiem.kad skaits paplaÅ”inÄs Object klases pieaug.
4. Sadursmes noteikŔana
Mums atliek tikai atpazÄ«t, kad Å”ÄviÅi trÄpÄ«ja spÄlÄtÄjiem! Atcerieties Å”o koda daļu no metodes update() klasÄ Game:
spÄle.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),
);
// ...
}
}
Mums ir jÄÄ«steno metode applyCollisions(), kas atgriež visus Å”ÄviÅus, kas trÄpÄ«ja spÄlÄtÄjiem. Par laimi to nav tik grÅ«ti izdarÄ«t, jo
Visi sadursmes objekti ir apļi, kas ir visvienkÄrÅ”ÄkÄ forma sadursmes noteikÅ”anai.
Mums jau ir metode distanceTo(), ko ieviesÄm klasÄ iepriekÅ”ÄjÄ sadaÄ¼Ä Object.
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;
}
Å Ä« vienkÄrÅ”Ä sadursmes noteikÅ”ana ir balstÄ«ta uz to, ka divi apļi saduras, ja attÄlums starp to centriem ir mazÄks par to rÄdiusu summu. Å eit ir gadÄ«jums, kad attÄlums starp divu apļu centriem ir tieÅ”i vienÄds ar to rÄdiusu summu:
Å eit ir jÄÅem vÄrÄ vÄl daži aspekti:
Å ÄviÅÅ” nedrÄ«kst trÄpÄ«t spÄlÄtÄjam, kurÅ” to radÄ«jis. To var panÄkt, salÄ«dzinot bullet.parentID Ń player.id.
Å ÄviÅam ir jÄtrÄpa tikai vienu reizi, ja vienlaikus saduras vairÄki spÄlÄtÄji. MÄs atrisinÄsim Å”o problÄmu, izmantojot operatoru break: tiklÄ«dz tiek atrasts spÄlÄtÄjs, kurÅ” saduras ar Å”ÄviÅu, mÄs pÄrtraucam meklÄÅ”anu un pÄrejam pie nÄkamÄ Å”ÄviÅa.
beigas
Tas ir viss! MÄs esam apskatÄ«juÅ”i visu, kas jums jÄzina, lai izveidotu .io tÄ«mekļa spÄli. Ko tÄlÄk? Izveidojiet savu .io spÄli!
Viss parauga kods ir atvÄrtÄ pirmkoda un publicÄts GitHub.