Jolas hauen berri inoiz entzun ez baduzu, doako jokalari anitzeko web-jokoak dira, jolasteko errazak (ez da konturik behar). Jokalari aurkari askori aurre egin ohi diote eremu berean. Beste .io joko ospetsu batzuk: Slither.io ΠΈ Diep.io.
Post honetan, nola aztertuko dugu sortu .io joko bat hutsetik. Horretarako, Javascript jakitea bakarrik nahikoa izango da: sintaxia bezalako gauzak ulertu behar dituzu ES6, gako-hitza this ΠΈ Promesas. Nahiz eta Javascript-en ezagutza perfektua ez izan, mezuaren zatirik handiena uler dezakezu.
Jokoa nahiko sinplea da: ontzi bat kontrolatzen duzu beste jokalari batzuk dauden eremu batean. Zure ontziak automatikoki jaurtitzen ditu jaurtigaiak eta beste jokalari batzuk jotzen saiatzen zara, haien jaurtigaiak saihestuz.
Karpeta batean dena public/ zerbitzariak estatikoki bidaliko du. IN public/assets/ gure proiektuak erabilitako irudiak ditu.
src /
Iturburu-kode guztia karpetan dago src/. Izenburuak client/ ΠΈ server/ beren kabuz hitz egin eta shared/ bezeroak eta zerbitzariak inportatutako konstante fitxategi bat dauka.
2. Muntaiak/proiektuaren ezarpenak
Goian esan bezala, modulu-kudeatzailea erabiltzen dugu proiektua eraikitzeko. webpack. Ikus dezagun gure Webpack konfigurazioa:
src/client/index.js Javascript (JS) bezeroaren sarrera-puntua da. Webpack hemendik abiatuko da eta inportatutako beste fitxategiak modu errekurtsiboan bilatuko ditu.
Gure Webpack eraikuntzaren irteera JS direktorioan kokatuko da dist/. Fitxategi honi gure izena emango diot js paketea.
Erabiltzen dugu Babel, eta bereziki konfigurazioa @babel/preset-env nabigatzaile zaharrentzako gure JS kodea transpilatzeko.
Plugin bat erabiltzen ari gara JS fitxategiek erreferentziatutako CSS guztiak ateratzeko eta leku bakarrean konbinatzeko. Gure deituko diot css paketea.
Baliteke paketeen fitxategi-izen arraroak nabaritzea '[name].[contenthash].ext'. Edukitzen dute fitxategi-izenen ordezkapenak webpack: [name] sarrera puntuaren izenarekin ordezkatuko da (gure kasuan, hau game), eta [contenthash] fitxategiaren edukiaren hash batekin ordezkatuko da. Guk egiten dugu optimizatu proiektua hashing egiteko - Arakatzaileei esan diezaiekezu gure JS paketeak cachean gordetzeko mugarik gabe, zeren pakete bat aldatzen bada, bere fitxategi-izena ere aldatzen da (aldaketak contenthash). Azken emaitza ikusteko fitxategiaren izena izango da game.dbeee76e91a97d0c7207.js.
fitxategia webpack.common.js garapenerako eta amaitutako proiektuen konfigurazioetara inportatzen dugun oinarrizko konfigurazio fitxategia da. Hona hemen garapenaren konfigurazio adibide bat:
Eraginkortasuna lortzeko, garapen prozesuan erabiltzen dugu webpack.dev.js, eta aldatzen da webpack.prod.jspaketeen tamainak optimizatzeko ekoizpenera zabaltzean.
Tokiko ezarpena
Proiektua tokiko makina batean instalatzea gomendatzen dut, argitalpen honetan agertzen diren urratsak jarraitu ahal izateko. Konfigurazioa erraza da: lehenik, sistema instalatuta egon behar da Nodoa ΠΈ NPM. Hurrengoa egin behar duzu
$ git clone https://github.com/vzhou842/example-.io-game.git
$ cd example-.io-game
$ npm install
eta prest zaude joateko! Garapen zerbitzaria abiarazteko, exekutatu besterik ez duzu
$ npm run develop
eta joan web arakatzailera localhost: 3000. Garapen zerbitzariak JS eta CSS paketeak automatikoki berreraikiko ditu kodea aldatzen den heinean - freskatu orrialdea aldaketa guztiak ikusteko!
3. Bezeroen Sarrera Puntuak
Goazen jokoaren kodea bera. Lehenik orri bat behar dugu index.html, gunea bisitatzean, nabigatzaileak lehenik kargatuko du. Gure orria nahiko sinplea izango da:
index.html
Adibide bat .io joko bat JOLASTU
Kode-adibide hau apur bat sinplifikatu da argitasunerako, eta gauza bera egingo dut beste post-adibide askorekin. Kode osoa beti ikus daiteke hemen Github.
Konplikatua dirudi, baina hemen ez da gauza handirik gertatzen:
Beste hainbat JS fitxategi inportatzen.
CSS inportazioa (beraz, Webpack-ek badaki gure CSS paketean sartzen dituela).
aireratzea connect() zerbitzariarekin konexioa ezartzeko eta exekutatzeko downloadAssets() jokoa errendatzeko behar diren irudiak deskargatzeko.
3. etapa amaitu ondoren menu nagusia bistaratzen da (playMenu).
"PLAY" botoia sakatzeko kudeatzailea ezartzea. Botoia sakatzean, kodeak jokoa hasieratzen du eta zerbitzariari jokatzeko prest gaudela esaten dio.
Gure bezero-zerbitzari logikaren "haragia" nagusia fitxategiak inportatu zituen fitxategi horietan dago index.js. Orain ordenan aztertuko ditugu guztiak.
4. Bezeroen datuen trukea
Joko honetan, liburutegi ezagun bat erabiltzen dugu zerbitzariarekin komunikatzeko socket.io. Socket.io-k jatorrizko laguntza du WebSocket-ak, bi norabideko komunikaziorako ondo egokitzen direnak: zerbitzariari mezuak bidal ditzakegu ΠΈ zerbitzariak mezuak bidal diezazkiguke konexio berean.
Fitxategi bat izango dugu src/client/networking.jsnork zainduko duen denek zerbitzariarekin komunikazioa:
Baliabideen kudeaketa ez da hain zaila ezartzea! Ideia nagusia objektu bat gordetzea da assets, fitxategi-izenaren gakoa objektuaren balioarekin lotuko duena Image. Baliabidea kargatzen denean, objektu batean gordetzen dugu assets etorkizunean sarbide azkarra izateko. Noiz baimenduko da baliabide bakoitza deskargatzeko (hau da, guztiak baliabideak), onartzen dugu downloadPromise.
Baliabideak deskargatu ondoren, errendatzen has zaitezke. Lehen esan bezala, web orri batean marrazteko, erabiltzen dugu HTML5 mihisea (<canvas>). Gure jokoa nahiko erraza da, beraz, honako hauek marraztu besterik ez dugu egin behar:
hondo
Jokalari ontzia
Jokoan dauden beste jokalari batzuk
Maskorrak
Hona hemen zati garrantzitsuak src/client/render.js, goian zerrendatutako lau elementuak zehatz-mehatz ematen dituztenak:
errendatu.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);
}
Kode hau laburtu egiten da argitasunerako.
render() fitxategi honen funtzio nagusia da. startRendering() ΠΈ stopRendering() kontrolatu errendatze-begizta aktibatzea 60 FPS-tan.
Banakako errendatze-funtzio laguntzaileen inplementazio zehatzak (adibidez. renderBullet()) ez dira horren garrantzitsuak, baina hona hemen adibide sinple bat:
errendatu.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,
);
}
Kontuan izan metodoa erabiltzen ari garela getAsset(), lehenago urtean ikusi zena asset.js!
Errendatzeko beste laguntzaile batzuk ezagutzeko interesa baduzu, irakurri gainerakoa. src/client/render.js.
6. Bezeroaren sarrera
Jolas bat egiteko garaia da erreproduzigarria! Kontrol-eskema oso erraza izango da: mugimenduaren norabidea aldatzeko, sagua erabil dezakezu (ordenagailu batean) edo pantaila ukitu (gailu mugikor batean). Hau gauzatzeko, izena emango dugu Ekitaldi Entzuleak Mouse eta Touch ekitaldietarako.
Hori guztiaz arduratuko da src/client/input.js:
sarrera.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() deitzen duten Gertaeren Entzuleak dira updateDirection() (of networking.js) sarrerako gertaera bat gertatzen denean (adibidez, sagua mugitzen denean). updateDirection() zerbitzariarekin mezularitza kudeatzen du, eta horrek sarrerako gertaera kudeatzen du eta horren arabera jokoaren egoera eguneratzen du.
7. Bezeroaren egoera
Atal hau postaren lehen zatian zailena da. Ez zaitez desanimatu irakurtzen duzun lehen aldian ulertzen ez baduzu! Saltatu eta beranduago itzuli ere egin dezakezu.
Bezero/zerbitzariaren kodea osatzeko behar den puzzlearen azken pieza da egoera. Gogoratzen al duzu Bezeroaren Errendaketa ataleko kode zatia?
errendatu.js
import { getCurrentState } from './state';
function render() {
const { me, others, bullets } = getCurrentState();
// Do the rendering
// ...
}
getCurrentState() Bezeroan jokoaren egungo egoera emateko gai izan beharko luke edozein momentutan zerbitzaritik jasotako eguneraketetan oinarrituta. Hona hemen zerbitzariak bidal dezakeen joko eguneratzearen adibide bat:
Jokoaren eguneratze bakoitzak bost eremu berdin ditu:
t: Eguneratze hau noiz sortu den adierazten duen zerbitzariaren ordu-zigilua.
me: eguneraketa hau jasotzen duen jokalariari buruzko informazioa.
beste batzuk: Joko berean parte hartzen duten beste jokalariei buruzko informazio sorta bat.
balak: jokoko proiektilei buruzko informazio sorta bat.
leaderboard: Uneko sailkapeneko datuak. Post honetan, ez ditugu kontuan hartuko.
7.1 Bezeroaren egoera inozoa
Ezarpen inozoa getCurrentState() Azken jokoaren eguneratzearen datuak zuzenean itzuli ditzake.
naive-state.js
let lastGameUpdate = null;
// Handle a newly received game update.
export function processGameUpdate(update) {
lastGameUpdate = update;
}
export function getCurrentState() {
return lastGameUpdate;
}
Polita eta argia! Baina hain sinplea balitz. Inplementazio honen arrazoietako bat problematikoa da: errendatzeko fotograma-abiadura zerbitzariaren erloju-abiadurara mugatzen du.
Fotograma-tasa: fotograma kopurua (hau da, deiak render()) segundoko edo FPS. Jolasak gutxienez 60 FPS lortzen saiatzen dira.
Tick-tasa: Zerbitzariak jokoaren eguneraketak bezeroei bidaltzen dituen maiztasuna. Askotan fotograma-tasa baino txikiagoa da. Gure jokoan, zerbitzaria segundoko 30 zikloko maiztasunarekin exekutatzen da.
Jokoaren azken eguneratzea erreproduzitzen badugu, FPSak funtsean ez du inoiz 30etik gora igaroko, zeren ez dugu inoiz 30 eguneratze baino gehiago jasotzen segundoko zerbitzaritik. Deitzen badugu ere render() 60 aldiz segundoko, orduan dei horien erdiak gauza bera berriro marraztuko du, funtsean ezer egin gabe. Ezarpen inozoaren beste arazo bat hori da atzerapenak izateko joera. Interneteko abiadura ezin hobea izanik, bezeroak jokoaren eguneraketa bat jasoko du zehazki 33 ms behin (30 segundoko):
Zoritxarrez, ezer ez da perfektua. Irudi errealistagoa izango litzateke:
Inplementazio inozoa da ia kasurik txarrena latentziari dagokionez. Jokoaren eguneratze bat 50 ms-ko atzerapenarekin jasotzen bada, orduan bezeroen postuak 50 ms gehiago aurreko eguneratzetik jokoaren egoera errendatzen ari delako. Imajina dezakezu jokalariarentzat zein deserosoa den hori: balazta arbitrarioak jokoa zalaparta eta ezegonkorra sentiaraziko du.
7.2 Bezeroaren egoera hobetua
Inplementazio inozoan hobekuntza batzuk egingo ditugu. Lehenik eta behin, erabiltzen dugu errendatzeko atzerapena 100 ms-rako. Horrek esan nahi du bezeroaren "uneko" egoera beti zerbitzariko jokoaren egoeraren atzean geratuko dela 100 ms. Adibidez, zerbitzarian ordua bada 150, orduan bezeroak zerbitzariak momentuan zegoen egoera errendatuko du 50:
Honek 100 ms-ko buffer bat ematen digu jokoaren eguneratze-aldi ezustekoak bizirauteko:
Honen ordaina iraunkorra izango da sarrerako atzerapena 100 ms-rako. Jokatzeko sakrifizio txikia da hau: jokalari gehienek (batez ere jokalari casualek) ez dute atzerapen hori nabarituko. Jendearentzat askoz errazagoa da 100 ms-ko latentzia konstante batera egokitzea ezusteko latentziarekin jolastea baino.
izeneko beste teknika bat ere erabil dezakegu bezeroaren aldetik aurreikuspena, hautemandako latentzia murrizteko lan ona egiten duena, baina ez da argitalpen honetan landuko.
Erabiltzen ari garen beste hobekuntza bat da interpolazio lineala. Errendatzeko atzerapena dela eta, bezeroaren uneko orduarekiko gutxienez eguneratze bat aurreratu ohi dugu. Deitzen denean getCurrentState(), exekutatu dezakegu interpolazio lineala jokoaren eguneratzeen artean, bezeroaren uneko orduaren aurretik eta ondoren:
Horrek fotograma-abiaduraren arazoa konpontzen du: orain fotograma bereziak errenda ditzakegu nahi dugun fotograma-abiaduran!
7.3 Bezeroaren egoera hobetua ezartzea
Inplementazio adibidea src/client/state.js Errendatzeko atzerapena eta interpolazio lineala erabiltzen ditu, baina ez luzaroan. Hautsi dezagun kodea bi zatitan. Hona hemen lehenengoa:
egoera.js 1. zatia
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;
}
Lehen urratsa zer den jakitea da currentServerTime(). Lehen ikusi dugun bezala, jokoaren eguneratze bakoitzak zerbitzariaren denbora-zigilua dauka. Errendatzeko latentzia erabili nahi dugu irudia zerbitzariaren atzean 100ms errendatzeko, baina ez dugu inoiz zerbitzarian dagoen uneko ordua jakingo, ezin dugulako jakin zenbat denbora behar izan den eguneratzeren bat guregana iristeko. Internet ezustekoa da eta bere abiadura asko alda daiteke!
Arazo honi aurre egiteko, zentzuzko hurbilketa bat erabil dezakegu: gu lehen eguneratzea berehala iritsi dela iruditu. Hau egia balitz, une zehatz honetan zerbitzariaren ordua jakingo genuke! Zerbitzariaren denbora-zigilua gordetzen dugu firstServerTimestamp eta mantendu gure tokikoa (bezeroa) denbora-zigilua une berean gameStart.
Ai itxaron. Ez al luke zerbitzariaren ordua = bezeroaren ordua izan behar? Zergatik bereizten ditugu "zerbitzariaren denbora-zigilua" eta "bezeroaren denbora-zigilua"? Hau galdera bikaina da! Ematen du ez direla gauza bera. Date.now() denbora-zigilu desberdinak itzuliko ditu bezeroan eta zerbitzarian, eta makina horien tokiko faktoreen araberakoa da. Inoiz ez suposatu denbora-zigiluak makina guztietan berdinak izango direla.
Orain ulertzen dugu zer egiten duen currentServerTime(): itzultzen da uneko errendatze-denboraren zerbitzariaren denbora-zigilua. Beste era batera esanda, zerbitzariaren uneko ordua da (firstServerTimestamp <+ (Date.now() - gameStart)) ken errendatzeko atzerapena (RENDER_DELAY).
Ikus dezagun nola kudeatzen ditugun jokoen eguneraketak. Eguneratze zerbitzaritik jasotzen denean, deitzen zaio processGameUpdate()eta eguneratze berria array batean gordetzen dugu gameUpdates. Ondoren, memoriaren erabilera egiaztatzeko, aurreko eguneratze zahar guztiak kentzen ditugu oinarrizko eguneratzeaez ditugulako gehiago behar.
Zer da "oinarrizko eguneraketa" bat? Hau zerbitzariaren uneko ordutik atzera eginez aurkitzen dugun lehen eguneraketa. Gogoratzen al duzu diagrama hau?
"Client Render Time"-ren ezkerrean dagoen jokoaren eguneratzea oinarrizko eguneratzea da.
Zertarako erabiltzen da oinarrizko eguneratzea? Zergatik jar ditzakegu eguneratzeak oinarrizko lerrora? Hau asmatzeko, goazen azkenik inplementazioa kontuan hartu getCurrentState():
egoera.js 2. zatia
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),
};
}
}
Hiru kasu kudeatzen ditugu:
base < 0 esan nahi du ez dagoela eguneratzerik uneko errendatze-denbora arte (ikus goiko inplementazioa getBaseUpdate()). Hau jokoaren hasieran bertan gerta daiteke errendatzearen desfasearen ondorioz. Kasu honetan, jasotako azken eguneratzea erabiltzen dugu.
base daukagun azken eguneratzea da. Sareko atzerapenagatik edo Interneteko konexio eskasagatik izan daiteke hori. Kasu honetan, daukagun azken eguneratzea ere erabiltzen ari gara.
Eguneratze bat dugu uneko errendatze-denbora baino lehen eta ondoren, beraz interpolatu!
Barruan geratzen dena state.js Matematika sinple (baina aspergarria) den interpolazio linealaren inplementazioa da. Zuk zeuk arakatu nahi baduzu, ireki state.js on Github.
2. zatia. Backend zerbitzaria
Zati honetan, gure kontrolatzen duen Node.js backend-ari begiratuko diogu .io jokoaren adibidea.
1. Zerbitzariaren Sarrera Puntua
Web zerbitzaria kudeatzeko, Node.js izeneko web esparru ezagun bat erabiliko dugu Express. Gure zerbitzariaren sarrera-puntuaren fitxategiak konfiguratuko du src/server/server.js:
server.js 1. zatia
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}`);
Gogoratzen al duzu lehen zatian Webpack eztabaidatu genuela? Hemen gure Webpack konfigurazioak erabiliko ditugu. Bi modutara erabiliko ditugu:
erabiltzea webpack-dev-middleware gure garapen paketeak automatikoki berreraikitzeko, edo
estatikoki transferitzeko karpeta dist/, eta bertan Webpack-ek gure fitxategiak idatziko ditu produkzioa eraiki ondoren.
Beste zeregin garrantzitsu bat server.js zerbitzaria konfiguratzea da socket.ioExpress zerbitzariarekin konektatzen dena:
Socket.io zerbitzarirako konexioa behar bezala ezarri ondoren, socket berrirako gertaeren kudeatzaileak konfiguratu ditugu. Gertaeren kudeatzaileek bezeroengandik jasotako mezuak kudeatzen dituzte singleton objektu bati delegatuz game:
server.js 3. zatia
const Game = require('./game');
// ...
// Setup the Game
const game = new Game();
function joinGame(username) {
game.addPlayer(this, username);
}
function handleInput(dir) {
game.handleInput(this, dir);
}
function onDisconnect() {
game.removePlayer(this);
}
.io joko bat sortzen ari gara, beraz, kopia bakarra behar dugu Game ("Jokoa") - jokalari guztiek eremu berean jokatzen dute! Hurrengo atalean, klase honek nola funtzionatzen duen ikusiko dugu. Game.
2. Joko zerbitzariak
Class Game zerbitzariaren aldean logika garrantzitsuena dauka. Bi zeregin nagusi ditu: jokalarien kudeaketa ΠΈ jokoaren simulazioa.
Has gaitezen lehen zereginarekin, jokalarien kudeaketarekin.
game.js 1. zatia
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);
}
}
// ...
}
Joko honetan, jokalariak eremuaren arabera identifikatuko ditugu id beren socket.io socket (nahasten bazara, itzuli server.js). Socket.io-k berak socket bakoitzari bakarra esleitzen dio idberaz, ez dugu horregatik kezkatu behar. deituko diot Jokalariaren IDa.
Hori kontuan izanda, azter ditzagun klase bateko instantzia-aldagaiak Game:
sockets jokalariaren IDa jokalariarekin lotutako socketarekin lotzen duen objektu bat da. Beren jokalari IDen bidez socketetara sartzeko aukera ematen digu denbora etengabean.
players Jokalari IDa kodea>Jokalari objektua lotzen duen objektua da
bullets objektu sorta bat da Bullet, ordena zehatzik ez duena. lastUpdateTime Jokoa eguneratu zen azkeneko denbora-zigilua da. Laster ikusiko dugu nola erabiltzen den. shouldSendUpdate aldagai laguntzailea da. Bere erabilera ere laster ikusiko dugu.
metodoak addPlayer(), removePlayer() ΠΈ handleInput() ez dago azaldu beharrik, erabiltzen dira server.js. Memoria freskatu behar baduzu, atzera pixka bat gorago.
Azken lerroa constructor() martxan jartzen da eguneratzeko zikloa jokoak (60 eguneratze/s-ko maiztasunarekin):
game.js 2. zatia
const Constants = require('../shared/constants');
const applyCollisions = require('./collisions');
class Game {
// ...
update() {
// Calculate time elapsed
const now = Date.now();
const dt = (now - this.lastUpdateTime) / 1000;
this.lastUpdateTime = now;
// Update each bullet
const bulletsToRemove = [];
this.bullets.forEach(bullet => {
if (bullet.update(dt)) {
// Destroy this bullet
bulletsToRemove.push(bullet);
}
});
this.bullets = this.bullets.filter(
bullet => !bulletsToRemove.includes(bullet),
);
// Update each player
Object.keys(this.sockets).forEach(playerID => {
const player = this.players[playerID];
const newBullet = player.update(dt);
if (newBullet) {
this.bullets.push(newBullet);
}
});
// Apply collisions, give players score for hitting bullets
const destroyedBullets = applyCollisions(
Object.values(this.players),
this.bullets,
);
destroyedBullets.forEach(b => {
if (this.players[b.parentID]) {
this.players[b.parentID].onDealtDamage();
}
});
this.bullets = this.bullets.filter(
bullet => !destroyedBullets.includes(bullet),
);
// Check if any players are dead
Object.keys(this.sockets).forEach(playerID => {
const socket = this.sockets[playerID];
const player = this.players[playerID];
if (player.hp <= 0) {
socket.emit(Constants.MSG_TYPES.GAME_OVER);
this.removePlayer(socket);
}
});
// Send a game update to each player every other time
if (this.shouldSendUpdate) {
const leaderboard = this.getLeaderboard();
Object.keys(this.sockets).forEach(playerID => {
const socket = this.sockets[playerID];
const player = this.players[playerID];
socket.emit(
Constants.MSG_TYPES.GAME_UPDATE,
this.createUpdate(player, leaderboard),
);
});
this.shouldSendUpdate = false;
} else {
this.shouldSendUpdate = true;
}
}
// ...
}
ΠΠ΅ΡΠΎΠ΄ update() du agian zerbitzariaren logikaren zatirik garrantzitsuena. Hona hemen zer egiten duen, ordenan:
Zenbat denbora kalkulatzen du dt azkenetik pasatu zen update().
Proyectil bakoitza freskatzen du eta beharrezkoa izanez gero suntsitzen ditu. Funtzio honen ezarpena aurrerago ikusiko dugu. Oraingoz, nahikoa da hori jakitea bullet.update()itzultzen truejaurtigaia suntsitu behar bada (Arenatik atera zen).
Jokalari bakoitza eguneratzen du eta behar izanez gero, jaurtigai bat sortzen du. Inplementazio hau geroago ikusiko dugu - player.update()objektu bat itzul dezake Bullet.
Proyectilen eta jokalarien arteko talkak egiaztatzen ditu applyCollisions(), jokalariak jotzen dituen proiektil sorta bat itzultzen duena. Itzulitako jaurtigai bakoitzeko, jaurti duen jokalariaren puntuak handitzen ditugu (erabiliz player.onDealtDamage()) eta, ondoren, kendu proiekta arraytik bullets.
Hildako jokalari guztiak jakinarazi eta suntsitzen ditu.
Jokalari guztiei jokoaren eguneraketa bidaltzen die segundoro aldiz deituta update(). Horrek goian aipatutako aldagai laguntzailearen jarraipena egiten laguntzen digu. shouldSendUpdate. As update() 60 aldiz/s deituta, jokoaren eguneraketak 30 aldiz/s bidaltzen ditugu. Horrela, erlojuaren maiztasuna zerbitzariaren erlojua 30 erloju/s-koa da (erlojuaren maizetaz hitz egin genuen lehen zatian).
Zergatik bidali jokoen eguneraketak soilik denboran zehar ? Kanala gordetzeko. 30 jokoen eguneraketa segundoko asko da!
Zergatik ez deitu besterik ez update() 30 aldiz segundoko? Jokoaren simulazioa hobetzeko. Zenbat eta maizago deitu update(), orduan eta zehatzagoa izango da jokoaren simulazioa. Baina ez zaitez gehiegi eraman erronka kopuruarekin. update(), hau konputazionalki lan garestia delako - 60 segundoko nahikoa da.
Gainerako klaseak Game urtean erabiltzen diren metodo laguntzaileek osatzen dute update():
getLeaderboard() nahiko sinplea: jokalariak puntuazioaren arabera ordenatzen ditu, lehen bostenak hartzen ditu eta bakoitzaren erabiltzaile-izena eta puntuazioa itzultzen ditu.
createUpdate() urtean erabiltzen da update() jokalariei banatzen zaizkien jokoen eguneraketak sortzeko. Bere zeregin nagusia metodoak deitzea da serializeForUpdate()klaseetarako ezarrita Player ΠΈ Bullet. Kontuan izan jokalari bakoitzari buruzko datuak soilik pasatzen dizkiola hurbilena jokalariak eta proiektilak - ez dago jokalariengandik urrun dauden joko-objektuei buruzko informazioa transmititu beharrik!
3. Joko-objektuak zerbitzarian
Gure jokoan, proiektilak eta jokalariak oso antzekoak dira benetan: joko-objektu abstraktuak, biribilak eta higigarriak dira. Jokalari eta jaurtigaien arteko antzekotasun hori aprobetxatzeko, has gaitezen oinarrizko klasea inplementatzen Object:
Hemen ez dago ezer konplikaturik gertatzen. Klase hau luzapenerako aingura-puntu ona izango da. Ea nola klasea Bullet erabilerak 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;
}
}
Inplementazioa Bullet oso laburra! Gehitu dugu Object luzapen hauek bakarrik:
Paketea erabiliz laburra ausazko belaunaldirako id jaurtigaia.
Eremu bat gehitzea parentIDhorrela, jaurtigai hau sortu duen jokalariaren jarraipena egin dezakezu.
Honi itzulera-balioa gehitzea update(), hau da, berdina truejaurtiketa arenatik kanpo badago (gogoratzen al duzu azken atalean honetaz hitz egin genuela?).
Goazen Player:
jokalari.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,
};
}
}
Jokalariak proiektilak baino konplexuagoak dira, beraz, eremu batzuk gehiago gorde behar dira klase honetan. Bere metodoa update() lan asko egiten du, bereziki, sortu berria den jaurtigaia itzultzen du ezer geratzen ez bada fireCooldown (Gogoratzen duzu honetaz hitz egin genuela aurreko atalean?). Metodoa ere zabaltzen du serializeForUpdate(), jokoaren eguneratzean jokalariarentzat eremu osagarriak sartu behar ditugulako.
Oinarrizko klasea izatea Object - urrats garrantzitsua kodea errepika ez dadin. Adibidez, klaserik ez Object joko-objektu guztiek inplementazio bera izan behar dute distanceTo(), eta inplementazio horiek guztiak fitxategi anitzetan kopiatu-itsatsi amesgaizto bat izango litzateke. Hau bereziki garrantzitsua da proiektu handietarako.zabaltzeko kopurua denean Object klaseak hazten ari dira.
4. Talkak hautematea
Guri geratzen zaigun gauza bakarra da errekonozitzea jaurtigaiek jokalariak noiz jotzen dituzten! Gogoratu metodoko kode zati hau update() klasean Game:
jokoa.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),
);
// ...
}
}
Metodoa ezarri behar dugu applyCollisions(), jokalariak jotzen dituzten jaurtigai guztiak itzultzen dituena. Zorionez, ez da hain zaila egiten
Talka egiten duten objektu guztiak zirkuluak dira, talkak detektatzeko forma errazena da.
Dagoeneko badugu metodo bat distanceTo(), klasean aurreko atalean ezarri genuena Object.
Hona hemen gure talkak hautematearen ezarpena nolakoa den:
talkak.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;
}
Talkak hautemate sinple hau horretan oinarritzen da bi zirkuluk talka egiten dute haien zentroen arteko distantzia erradioen batura baino txikiagoa bada. Hona hemen bi zirkuluren zentroen arteko distantzia beren erradioen baturaren berdina den kasua:
Hemen kontuan hartu beharreko pare bat alderdi gehiago daude:
Proyectilak ez du jo behar sortu duen jokalaria. Konparatuz lor daiteke bullet.parentID Ρ player.id.
Proyectilak behin bakarrik jo behar du hainbat jokalarik aldi berean talka egiten duten kasu mugatuan. Arazo hau operadorea erabiliz konponduko dugu break: proiektilarekin talka egiten duen jokalaria aurkitu bezain laster, bilaketa gelditu eta hurrengo proiektilara igaroko gara.
end
Hori da dena! .io web-joko bat sortzeko jakin behar duzun guztia azaldu dugu. Zer da hurrengoa? Eraiki zure .io jokoa!
Lagin-kode guztia kode irekia da eta bertan argitaratuta dago Github.