Ikhishwe ngo-2015
Esimeni lapho ungakaze uzwe ngale midlalo ngaphambilini, ingamahhala, imidlalo yewebhu yabadlali abaningi okulula ukuyidlala (ayikho i-akhawunti edingekayo). Ngokuvamile bafaka abadlali abaningi abaphikisanayo enkundleni eyodwa. Eminye imidlalo edumile ye-.io:
Kule post sizobona ukuthi kanjani dala igeyimu ye-.io kusukela ekuqaleni. Ukuze wenze lokhu, ulwazi lweJavascript kuphela oluzokwanela: udinga ukuqonda izinto ezifana ne-syntax this
ΠΈ
Isibonelo segeyimu ye-.io
Ukuze uthole usizo lokuqeqeshwa sizobhekisela kukho
Umdlalo ulula: ulawula umkhumbi enkundleni nabanye abadlali. Umkhumbi wakho udubula ngokuzenzakalelayo ama-projectiles bese uzama ukushaya abanye abadlali ngenkathi ugwema ama-projectiles abo.
1. Uhlolojikelele olufushane/isakhiwo sephrojekthi
Ngincoma
landa ikhodi yomthombo isibonelo umdlalo ukuze ukwazi ukungilandela.
Isibonelo sisebenzisa okulandelayo:
Veza wuhlaka lwewebhu oludume kakhulu lwe-Node.js oluphethe iseva yewebhu yomdlalo.isokhethi.io β umtapo wezincwadi we-websocket wokushintshana ngedatha phakathi kwesiphequluli neseva.I-Webpack - Umphathi wemojula. Ungafunda mayelana nokuthi kungani kufanele usebenzise i-Webpacklapha .
Yile ndlela uhlaka lwemibhalo yephrojekthi lubukeka ngayo:
public/
assets/
...
src/
client/
css/
...
html/
index.html
index.js
...
server/
server.js
...
shared/
constants.js
umphakathi/
Yonke into ikufolda public/
izodluliswa ngokwezibalo yiseva. IN public/assets/
iqukethe izithombe ezisetshenziswa iphrojekthi yethu.
src /
Yonke ikhodi yomthombo ikufolda src/
. Izihloko client/
ΠΈ server/
bazikhulumele futhi shared/
iqukethe ifayela elingaguquki elingeniswe yikho kokubili iklayenti neseva.
2. Imingcele yemihlangano/yephrojekthi
Njengoba kushiwo ngenhla, sisebenzisa umphathi wemojuli ukwakha iphrojekthi
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',
}),
],
};
Imigqa ebaluleke kakhulu lapha yile elandelayo:
src/client/index.js
iyindawo yokungena yeklayenti le-Javascript (JS). I-Webpack izoqala lapha futhi ibheke ngokuphindaphindiwe amanye amafayela angenisiwe.- I-JS ephumayo yokwakhiwa kwe-Webpack yethu izotholakala kuhlu lwemibhalo
dist/
. Ngizobiza leli fayela elethu Iphakheji ye-JS. - Sisebenzisa
Babel , futhi ikakhulukazi ukucushwa@babel/preset-env ukudlulisa ikhodi yethu ye-JS yeziphequluli ezindala. - Sisebenzisa i-plugin ukuze sikhiphe yonke i-CSS ebalulwe ngamafayela e-JS futhi siwahlanganise endaweni eyodwa. Ngizoyibiza ngeyethu Iphakheji ye-CSS.
Kungenzeka ukuthi uqaphele amagama efayela lephakheji angajwayelekile '[name].[contenthash].ext'
. Ziqukethe [name]
izothathelwa indawo igama lephoyinti lokufaka (kithina kunjalo game
), futhi [contenthash]
izothathelwa indawo i-hash yokuqukethwe kwefayela. Senza lokhu ukuze contenthash
). Umphumela oqediwe uzoba igama lefayela lokubuka game.dbeee76e91a97d0c7207.js
.
Π€Π°ΠΉΠ» webpack.common.js
- Leli yifayela lokucushwa eliyisisekelo esilingenisa ekuthuthukisweni nasekuqedeni ukulungiselelwa kwephrojekthi. Isibonelo, nakhu ukucushwa kokuthuthukiswa:
webpack.dev.js
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'development',
});
Ngokusebenza kahle, sisebenzisa kunqubo yokuthuthukisa webpack.dev.js
, bese ushintshela ku webpack.prod.js
, ukuthuthukisa osayizi bephakheji lapho kuthunyelwa ekukhiqizeni.
Ukusethwa kwendawo
Ngincoma ukuthi ufake iphrojekthi emshinini wangakini ukuze ukwazi ukulandela izinyathelo ezibalwe kulokhu okuthunyelwe. Ukusetha kulula: okokuqala, isistimu kufanele ibe nayo
$ git clone https://github.com/vzhou842/example-.io-game.git
$ cd example-.io-game
$ npm install
futhi usulungele ukuhamba! Ukuze uqale iseva yokuthuthukisa, vele uqalise
$ npm run develop
bese uye kusiphequluli sakho sewebhu
3. Amaphuzu okungena eklayenti
Ake sehlele kukhodi yegeyimu ngokwayo. Okokuqala sidinga ikhasi index.html
, uma uvakashela isayithi, isiphequluli sizoyilayisha kuqala. Ikhasi lethu lizoba lula kakhulu:
index.html
Isibonelo segeyimu ye-.io DLALA
Lesi sibonelo sekhodi senziwe lula kancane ukuze kucace, futhi ngizokwenza okufanayo ngezinye izibonelo eziningi kokuthunyelwe. Ungahlala ubheka ikhodi egcwele kokuthi
Sine:
Isici sekhanvasi ye-HTML5 (<canvas>
), esizoyisebenzisa ukwenza umdlalo.<link>
ukwengeza iphakheji yethu ye-CSS.<script>
ukwengeza iphakheji yethu ye-Javascript.- Imenyu enkulu enegama lomsebenzisi
<input>
kanye nenkinobho ethi βDLALAβ (<button>
).
Lapho ikhasi lasekhaya selilayishiwe, isiphequluli sizoqala ukusebenzisa ikhodi ye-Javascript, siqale ngendawo yokungena yefayela le-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);
};
});
Lokhu kungase kuzwakale kuyinkimbinkimbi, kodwa empeleni akukho okuningi okuqhubekayo lapha:
- Ngenisa amanye amafayela e-JS ambalwa.
- Ngenisa i-CSS (ukuze iWebpack yazi ukuyifaka kuphakheji yethu ye-CSS).
- Qalisa
connect()
ukusungula uxhumano kuseva bese uqaladownloadAssets()
ukulanda izithombe ezidingekayo ukuze unikeze umdlalo. - Ngemva kokuqeda isigaba sesi-3 imenyu enkulu iyaboniswa (
playMenu
). - Ukusetha isibambi sokuchofoza inkinobho ethi "PLAY". Lapho inkinobho icindezelwa, ikhodi iqalisa umdlalo futhi itshele iseva ukuthi sesilungele ukudlala.
βInyamaβ eyinhloko yomqondo wesiphakeli seklayenti lethu ikulawo mafayela angeniswe ngefayela index.js
. Manje sizozibheka zonke ngokulandelana kwazo.
4. Ukushintshaniswa kwedatha yeklayenti
Kulomdlalo sisebenzisa umtapo wezincwadi owaziwayo ukuxhumana neseva
Sizoba nefayela elilodwa src/client/networking.js
ngubani ozonakekela wonke umuntu ukuxhumana neseva:
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);
};
Le khodi iphinde ifushaniswe kancane ukuze icace.
Kunezinto ezintathu ezibalulekile ezenzeka kuleli fayela:
- Sizama ukuxhuma kuseva.
connectedPromise
kuvunyelwe kuphela uma sesisungule uxhumano. - Uma ukuxhumeka kuphumelele, sibhalisa imisebenzi yokushayela emuva (
processGameUpdate()
ΠΈonGameOver()
) ngemiyalezo esingayithola kuseva. - Siyathekelisa
play()
ΠΈupdateDirection()
ukuze amanye amafayela akwazi ukuwasebenzisa.
5. Ukunikezwa kweklayenti
Sekuyisikhathi sokubonisa isithombe esikrinini!
...kodwa ngaphambi kokuthi senze lokhu, sidinga ukulanda zonke izithombe (izinsiza) ezidingekayo kulokhu. Masibhale umphathi wensiza:
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];
Ukuphathwa kwezinsiza akunzima kangako ukukusebenzisa! Iphuzu eliyinhloko ukugcina into assets
, ezobophezela ukhiye wegama lefayela enani lento Image
. Uma insiza ilayishwa, siyilondoloza entweni assets
ukuze uthole iresidi esheshayo esikhathini esizayo. Kuzovunyelwa nini ukulandwa kwensiza ngayinye (okungukuthi, kuzodawuniloda konke izinsiza), siyakuvumela downloadPromise
.
Ngemva kokulanda izinsiza, ungaqala ukunikeza. Njengoba kushiwo ngaphambili, ukudweba ekhasini lewebhu esilisebenzisayo <canvas>
). Umdlalo wethu ulula, ngakho-ke sidinga kuphela ukunikeza okulandelayo:
- Isendlalelo
- Umkhumbi womdlali
- Abanye abadlali emdlalweni
- Amagobolondo
Nawa amazwibela abalulekile src/client/render.js
, okudweba amaphuzu amane abhalwe ngenhla:
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);
}
Le khodi iphinde ifushaniswe ukuze icace.
render()
umsebenzi oyinhloko waleli fayela. startRendering()
ΠΈ stopRendering()
lawula ukwenziwa kusebenze komjikelezo wokunikeza ku-60 FPS.
Ukuqaliswa okuqondile komsebenzi womsizi wokunikezela ngawodwana (isibonelo renderBullet()
) azibalulekile kangako, kodwa nasi isibonelo esisodwa esilula:
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,
);
}
Qaphela ukuthi sisebenzisa indlela getAsset()
, okwabonwa ngaphambilini ku asset.js
!
Uma ungathanda ukuhlola eminye imisebenzi yomsizi wokunikeza, bese ufunda okusele
src/client/render.js .
6. Okokufaka kweklayenti
Isikhathi sokwenza umdlalo okudlalekayo! Uhlelo lokulawula luzoba lula kakhulu: ukushintsha isiqondiso sokunyakaza, ungasebenzisa igundane (kukhompyutha) noma uthinte isikrini (kumakhalekhukhwini). Ukwenza lokhu sizobhalisa
Uzokunakekela konke lokhu 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()
ngabalaleli bomcimbi abafonayo updateDirection()
(ye networking.js
) uma kwenzeka umcimbi wokufaka (isibonelo, uma igundane lihanjiswa). updateDirection()
ibhekana nokushintshisana kwemilayezo neseva, ecubungula umcimbi wokufakwayo futhi ibuyekeze isimo segeyimu ngokufanele.
7. Isimo seklayenti
Lesi sigaba sinzima kakhulu engxenyeni yokuqala yokuthunyelwe. Ungadangali uma ungakuqondi lapho uyifunda okokuqala! Ungakwazi nokuyeqa bese ubuyela kuyo ngemva kwesikhathi.
Ucezu lokugcina lwendida edingekayo ukuqedela ikhodi yeseva yeklayenti ithi isimo. Khumbula amazwibela ekhodi asuka esigabeni Sokunikezwa Kwekhasimende?
render.js
import { getCurrentState } from './state';
function render() {
const { me, others, bullets } = getCurrentState();
// Do the rendering
// ...
}
getCurrentState()
kufanele ikwazi ukusinikeza isimo samanje segeyimu kuklayenti nganoma yisiphi isikhathi ngokusekelwe kuzibuyekezo ezitholwe kuseva. Nasi isibonelo sesibuyekezo somdlalo esingase sithunyelwe yiseva:
{
"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
}
]
}
Isibuyekezo somdlalo ngamunye siqukethe izinkambu ezinhlanu ezifanayo:
- t: Isitembu sesikhathi seseva esibonisa ukuthi lesi sibuyekezo sadalwa nini.
- me: Ulwazi mayelana nomdlali othola lesi sibuyekezo.
- abanye: Iqoqo lolwazi mayelana nabanye abadlali ababamba iqhaza kugeyimu efanayo.
- amachashazi: ulwazi oluningi mayelana nama-projectiles emdlalweni.
- Ibhodi yabaphambili: Idatha yamanje yebhodi yabaphambili. Ngeke sizicabangele kulokhu okuthunyelwe.
7.1 Isimo sokungazi kwekhasimende
Ukuqaliswa kokungazi getCurrentState()
ingabuyisela ngokuqondile idatha evela kusibuyekezo somdlalo osanda kutholwa.
naive-state.js
let lastGameUpdate = null;
// Handle a newly received game update.
export function processGameUpdate(update) {
lastGameUpdate = update;
}
export function getCurrentState() {
return lastGameUpdate;
}
Kuhle futhi kucace! Kodwa ukube nje bekulula kanjalo. Esinye sezizathu ezenza lokhu kuqaliswa kube yinkinga: ikhawulela ukunikezwa kwezinga lozimele kusivinini sewashi leseva.
Ukukalwa kwe fulemu: inombolo yozimele (okungukuthi amakholi
render()
) ngomzuzwana, noma i-FPS. Imidlalo ivamise ukulwela ukuzuza okungenani ama-FPS angama-60.
Isilinganiso sokumaka: Imvamisa iseva ethumela ngayo izibuyekezo zegeyimu kumakhasimende. Ivamise ukuba ngaphansi kwezinga lozimele. Emdlalweni wethu, iseva isebenza kumakhikhi angama-30 ngomzuzwana.
Uma nje sinikeza isibuyekezo sakamuva somdlalo, khona-ke i-FPS empeleni ngeke ikwazi ukudlula ama-30 ngoba asilokothi sithole izibuyekezo ezingaphezu kuka-30 ngomzuzwana kusuka kuseva. Noma sifona render()
Izikhathi ezingu-60 ngomzuzwana, bese ingxenye yalezi zingcingo izomane idwebe kabusha into efanayo, empeleni ingenzi lutho. Enye inkinga ngokuqaliswa kokungazi ukuthi kunjalo ngaphansi kokubambezeleka. Ngesivinini esikahle se-inthanethi, iklayenti lizothola isibuyekezo segeyimu njalo nje ngo-33 ms (30 ngomzuzwana):
Ngeshwa, akukho lutho oluphelele. Isithombe esingokoqobo kungaba:
Ukuqaliswa kokungenangqondo kuyisimo esibi kakhulu uma kukhulunywa nge-latency. Uma isibuyekezo somdlalo sitholwa ngokulibaziseka okungu-50ms, khona-ke iklayenti lehliswa isivinini ngo-50ms engeziwe ngoba isanikeza isimo somdlalo kusukela kusibuyekezo sangaphambilini. Ungacabanga ukuthi lokhu kuphazamisa kangakanani kumdlali: ngenxa yokwehla kwejubane okungahleliwe, umdlalo uzobonakala ushubile futhi ungazinzile.
7.2 Isimo seklayenti esithuthukisiwe
Sizokwenza ukuthuthukiswa okuthile ekusetshenzisweni okungenangqondo. Okokuqala, sisebenzisa ukulibaziseka kokunikeza ngo 100ms. Lokhu kusho ukuthi isimo "samanje" seklayenti sizohlala singu-100ms ngemuva kwesimo segeyimu kuseva. Isibonelo, uma isikhathi seseva si 150, bese iklayenti lizonikeza isimo lapho iseva yayikuso ngaleso sikhathi 50:
Lokhu kusinika ibhafa engu-100ms ukuze sisinde esikhathini esingalindelekile sezibuyekezo zegeyimu:
Intengo yalokhu izoba ngunaphakade
Singasebenzisa enye indlela ebizwa ngokuthi
"isibikezelo sohlangothi lweklayenti" , okwenza umsebenzi omuhle wokunciphisa ukubambezeleka okubonwayo, kodwa ngeke kuxoxwe ngakho kulokhu okuthunyelwe.
Okunye ukuthuthukiswa esikusebenzisayo ukuhumusha ngomugqa. Ngenxa yokungasebenzi kahle, ngokuvamile siba okungenani isibuyekezo esisodwa ngaphambi kwesikhathi samanje ekhasimendeni. Uma ebizwa getCurrentState()
, singafeza
Lokhu kuxazulula inkinga yesilinganiso sozimele: manje singakwazi ukunikeza ozimele abahlukile nganoma isiphi isilinganiso sozimele esisidingayo!
7.3 Ukusebenzisa isimo sekhasimende esithuthukisiwe
Ukuqaliswa kwesibonelo ku src/client/state.js
isebenzisa kokubili ukubambezeleka kokunikeza kanye nokuhumusha ngomugqa, kodwa lokhu akuhlali isikhathi eside. Ake sihlukanise ikhodi ibe izingxenye ezimbili. Nansi eyokuqala:
state.js, ingxenye 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;
}
Into yokuqala okudingeka ukwenze ukuthola ukuthi yenzani currentServerTime()
. Njengoba sibonile ngaphambili, sonke isibuyekezo somdlalo sifaka isitembu sesikhathi seseva. Sifuna ukusebenzisa i-render latency ukuze sinikeze isithombe ngo-100ms ngemuva kweseva, kodwa ngeke sazi isikhathi samanje kuseva, ngoba asikwazi ukwazi ukuthi kuthathe isikhathi esingakanani ukuthi noma yiziphi izibuyekezo zisifinyelele. I-inthanethi ayibikezeleki futhi ijubane layo lingahluka kakhulu!
Ukuxazulula le nkinga, singasebenzisa isilinganiso esinengqondo: thina ake senze sengathi isibuyekezo sokuqala sifike ngokushesha. Ukube lokhu bekuyiqiniso, besizokwazi isikhathi seseva ngaleso sikhathi! Sigcina isitembu sesikhathi seseva ngaphakathi firstServerTimestamp
futhi ugcine wethu wendawo (iklayenti) isitembu sesikhathi ngesikhathi esifanayo gameStart
.
O, ima kancane. Akufanele yini kube nesikhathi kuseva = isikhathi kuklayenti? Kungani sihlukanisa phakathi "kwesitembu sesikhathi seseva" kanye "nesitembu sesikhathi seklayenti"? Umbuzo omkhulu lo! Kuvele ukuthi lezi azifani. Date.now()
izobuyisela izitembu zesikhathi ezihlukile kuklayenti neseva futhi lokhu kuncike ezintweni zendawo kule mishini. Ungalokothi ucabange ukuthi izitembu zesikhathi zizofana kuyo yonke imishini.
Manje sesiyaqonda ukuthi lenzani currentServerTime()
: iyabuya isitembu sesikhathi seseva sesikhathi samanje sokunikezwa. Ngamanye amazwi, lesi yisikhathi samanje seseva (firstServerTimestamp <+ (Date.now() - gameStart)
) khipha ukubambezeleka kokukhishwa (RENDER_DELAY
).
Manje ake sibheke ukuthi sizisingatha kanjani izibuyekezo zegeyimu. Uma isibuyekezo sitholwa kwiseva, siyabizwa processGameUpdate()
, futhi silondoloza isibuyekezo esisha ohlwini gameUpdates
. Bese, ukuhlola ukusetshenziswa kwememori, sisusa zonke izibuyekezo ezindala kuzo i-base updatengoba asisawadingi.
Kuyini "isibuyekezo esiyinhloko"? Lokhu isibuyekezo sokuqala esisithola ngokuhlehla sisuka esikhathini samanje seseva. Uyawukhumbula lo mdwebo?
Isibuyekezo somdlalo ngokuqondile kwesokunxele "Sesikhathi Sokunikezwa Kweklayenti" siyisibuyekezo esiyisisekelo.
Sisetshenziselwa ini isibuyekezo esiyisisekelo? Kungani singayeka izibuyekezo zisekelwe? Ukuze siqonde lokhu, ake ekugcineni ake sibheke ukuqaliswa getCurrentState()
:
state.js, ingxenye 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),
};
}
}
Siphatha amacala amathathu:
base < 0
kusho ukuthi azikho izibuyekezo kuze kube yisikhathi samanje sokunikezwa (bona ukusetshenziswa ngenhlagetBaseUpdate()
). Lokhu kungenzeka ekuqaleni komdlalo ngenxa ye-rendering lag. Kulokhu, sisebenzisa isibuyekezo sakamuva esitholiwe.base
isibuyekezo sakamuva kakhulu esinaso. Lokhu kungenzeka ngenxa yokubambezeleka kwenethiwekhi noma ukuxhumeka kwe-inthanethi okubi. Nakulokhu sisebenzisa isibuyekezo sakamuva esinaso.- Sinesibuyekezo ngaphambi nangemuva kwesikhathi samanje sokunikezwa, ukuze sikwazi chaza!
Konke okusele phakathi state.js
ukuqaliswa kokuhumusha ngomugqa okulula (kodwa okuyisicefe) izibalo. Uma ufuna ukuzihlolela ngokwakho, vula state.js
on
Ingxenye 2. Iseva engemuva
Kule ngxenye sizobheka i-backend ye-Node.js elawula yethu
1. Indawo yokungena kwiseva
Ukuphatha iseva yewebhu sizosebenzisa uhlaka lwewebhu oludumile lwe-Node.js olubizwa src/server/server.js
:
iseva.js, ingxenye 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}`);
Khumbula ukuthi engxenyeni yokuqala sixoxe ngeWebpack? Lapha yilapho sizosebenzisa khona izilungiselelo zethu ze-Webpack. Sizozisebenzisa ngezindlela ezimbili:
- Sebenzisa
webpack-dev-middleware ukwakha kabusha ngokuzenzakalelayo amaphakheji ethu okuthuthukisa, noma - Dlulisa ifolda ngokuqinile
dist/
, lapho i-Webpack izobhala khona amafayela ethu ngemva kokwakhiwa kokukhiqiza.
Omunye umsebenzi obalulekile server.js
iqukethe ukusetha iseva
iseva.js, ingxenye 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);
});
Ngemva kokusungula ngempumelelo uxhumano lwe-socket.io neseva, silungiselela izibambi zomcimbi zesokhethi entsha. Izibambi zomcimbi zicubungula imilayezo etholwe kumakhasimende ngokuthumela entweni ye-singleton game
:
iseva.js, ingxenye 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);
}
Sakha igeyimu ye-.io, ngakho sizodinga ikhophi eyodwa kuphela Game
(βUmdlaloβ) β bonke abadlali badlala enkundleni efanayo! Esigabeni esilandelayo sizobona ukuthi leli klasi lisebenza kanjani Game
.
2. Amaseva egeyimu
ΠΠ»Π°ΡΡ Game
iqukethe i-logic yohlangothi lweseva ebaluleke kakhulu. Inemisebenzi emibili esemqoka: ukuphathwa komdlali ΠΈ sekulingisa umdlalo.
Ake siqale ngomsebenzi wokuqala - ukuphatha abadlali.
umdlalo.js, ingxenye 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);
}
}
// ...
}
Kulo mdlalo sizohlonza abadlali ngokwenkundla id
isokhethi yabo.io (uma udidekile, bese ubuyela ku server.js
). I-Socket.io ngokwayo yabela isokhethi ngayinye okuhlukile id
, ngakho akudingeki sikhathazeke ngakho. Ngizombiza I-ID yomdlali.
Unalokho engqondweni, ake sihlole izibonelo eziguquguqukayo ekilasini Game
:
sockets
into ebophezela i-ID yomdlali kusokhethi ehlotshaniswa nomdlali. Kusivumela ukuthi sifinyelele amasokhethi ngomazisi bawo abadlali ngokuhamba kwesikhathi.players
into ebophezela i-ID yomdlali kukhodi>Into yomdlali
bullets
iwuxha lwezinto Bullet
, engenawo umyalo othize.
lastUpdateTime
- Lesi isitembu sesikhathi sokubuyekezwa komdlalo wokugcina. Sizobona ukuthi isetshenziswa kanjani maduze.
shouldSendUpdate
iwukuguquguquka okusizayo. Sizobona nokusetshenziswa kwayo maduze.
Izindlela addPlayer()
, removePlayer()
ΠΈ handleInput()
asikho isidingo sokuchaza, zisetshenziswa ku server.js
. Uma udinga isiqalisi, buyela emuva kancane.
Umugqa wokugcina constructor()
iyaqala buyekeza umjikelezo imidlalo (enemvamisa yezibuyekezo ezingama-60/s):
umdlalo.js, ingxenye 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;
}
}
// ...
}
Indlela update()
cishe iqukethe ingxenye ebaluleke kakhulu yomqondo wohlangothi lweseva. Masibhale yonke into eyenzayo ngokulandelana kwayo:
- Ibala ukuthi yisiphi isikhathi
dt
selokhu kwaba okokugcinaupdate()
. - Ivuselela i-projectile ngayinye futhi iwacekele phansi uma kunesidingo. Sizobona ukuqaliswa kwalokhu kusebenza kamuva. Okwamanje sekwanele ukuthi sikwazi lokho
bullet.update()
izimbuyiselotrue
, uma i-projectile kufanele ichithwe (waphumela ngaphandle kwenkundla). - Ibuyekeza umdlali ngamunye futhi idale i-projectile uma kudingeka. Sizobona lokhu kuqaliswa kamuva -
player.update()
ingabuyisela intoBullet
. - Ihlola ukushayisana phakathi kwama-projectiles nabadlali abasebenzisayo
applyCollisions()
, ebuyisela inqwaba yama-projectiles ashaya abadlali. Kuphrojekthi ngayinye ebuyisiwe, sinyusa isikolo somdlali oyidubule (sisebenzisaplayer.onDealtDamage()
), bese ususa i-projectile ohlwinibullets
. - Yazisa futhi ibhubhise bonke abadlali ababulewe.
- Ithumela isibuyekezo somdlalo kubo bonke abadlali njalo ngomzuzwana izikhathi lapho ebizwa
update()
. I-axiliary variable eshiwo ngenhla isisiza ukuthi silandelele lokhushouldSendUpdate
. Ngobaupdate()
ebizwa izikhathi ezingu-60/s, sithumela izibuyekezo zegeyimu izikhathi ezingu-30/s. Ngakho, imvamisa yewashi iseva ingu-30 clock cycles/s (sikhulume ngobuningi bewashi engxenyeni yokuqala).
Kungani uthumele izibuyekezo zegeyimu kuphela ngokuhamba kwesikhathi ? Ukugcina isiteshi. Ukubuyekezwa kwegeyimu engama-30 ngomzuzwana kuningi!
Kungani ungavele ufone ke?
update()
30 izikhathi ngomzuzwana? Ukuthuthukisa sekulingisa game. Kuvame ukubizwa ngokuthiupdate()
, ukulingisa komdlalo kuzoba nembe kakhulu. Kodwa ungathathwa kakhulu inani lezinseleloupdate()
, ngoba lokhu kuwumsebenzi obiza kakhulu - 60 ngomzuzwana kwanele.
Ikilasi lonke Game
siqukethe izindlela ezisizayo ezisetshenziswa ku update()
:
umdlalo.js, ingxenye 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()
Ilula kakhulu - ihlunga abadlali ngamaphuzu, ithatha abahlanu abaphezulu, bese ibuyisela igama lomsebenzisi nemiphumela ngayinye.
createUpdate()
isetshenziswe ku update()
ukudala izibuyekezo zegeyimu ezisatshalaliswa kubadlali. Umsebenzi wawo omkhulu ukubiza izindlela serializeForUpdate()
, yenzelwe amakilasi Player
ΠΈ Bullet
. Qaphela ukuthi idlulisela kuphela idatha kumdlali ngamunye mayelana eliseduze abadlali nama-projectiles - asikho isidingo sokudlulisa ulwazi mayelana nezinto zomdlalo ezitholakala kude nomdlali!
3. Izinto zegeyimu kuseva
Emdlalweni wethu, ama-projectiles nabadlali bafana kakhulu: bayizinto ezingabonakali eziyindilinga ezihambayo. Ukuze usebenzise lokhu kufana phakathi kwabadlali nama-projectiles, ake siqale ngokusebenzisa isigaba sesisekelo 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,
};
}
}
Akukho okuxakayo okwenzekayo lapha. Lesi sigaba sizoba yisiqalo esihle sokwandisa. Ake sibone ukuthi ikilasi kanjani Bullet
isebenzisa Object
:
inhlamvu.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;
}
}
Ukuqaliswa Bullet
kufushane kakhulu! Sengeze ku Object
izandiso ezilandelayo kuphela:
- Usebenzisa iphakheji
shortid okwesizukulwane esingahleliweid
projectile. - Yengeza inkambu
parentID
, ukuze ukwazi ukulandelela umdlali odale le projectile. - Yengeza inani lokubuyisela ku
update()
, okulinganatrue
, uma i-projectile ingaphandle kwenkundla (khumbula sikhulume ngalokhu esigabeni sokugcina?).
Asiqhubekele phambili Player
:
umdlali.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,
};
}
}
Abadlali bayinkimbinkimbi kakhulu kunama-projectile, ngakho-ke leli klasi kufanele ligcine izinkambu ezimbalwa ezengeziwe. Indlela yakhe update()
yenza umsebenzi owengeziwe, ikakhulukazi ukubuyisela i-projectile esanda kwakhiwa uma ingekho esele fireCooldown
(khumbula sikhulume ngalokhu esigabeni esidlule?). Iphinde inwebe indlela serializeForUpdate()
, ngoba sidinga ukufaka izinkambu ezengeziwe zomdlali kusibuyekezo somdlalo.
Ukutholakala kwekilasi lesisekelo Object
- isinyathelo esibalulekile sokugwema ukuphindaphinda kwekhodi. Isibonelo, ngaphandle kwekilasi Object
yonke into yegeyimu kufanele ibe nokusetshenziswa okufanayo distanceTo()
, futhi ukukopisha-ukunamathisela konke lokhu kuqaliswa kuwo wonke amafayela amaningi kungaba yiphupho elibi. Lokhu kubaluleka ikakhulukazi kumaphrojekthi amakhulu, lapho inani elandayo Object
amakilasi ayakhula.
4. Ukutholwa kokushayisana
Okuwukuphela kwento esele okufanele siyenze ukubona lapho ama-projectiles eshaya abadlali! Khumbula la mazwibela ekhodi asuka kundlela update()
ekilasini Game
:
umdlalo.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),
);
// ...
}
}
Kudingeka sisebenzise indlela applyCollisions()
, ebuyisela wonke ama-projectiles ashaya abadlali. Ngenhlanhla, lokhu akunzima ukukwenza ngoba
- Zonke izinto ezishayisanayo ziyiziyingi, futhi lesi yisimo esilula kakhulu sokuqalisa ukutholwa kokushayisana.
- Sesivele sinendlela
distanceTo()
, esisisebenzise ekilasini esigabeni esidluleObject
.
Nansi indlela ukuqaliswa kwethu kokuthola ukushayisana kubukeka ngayo:
ukungqubuzana.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;
}
Lokhu kutholwa kokushayisana okulula kusekelwe eqinisweni lokuthi imibuthano emibili ishayisana uma ibanga phakathi kwezikhungo zazo lingaphansi kwesamba se-radii yazo. Nasi isimo lapho ibanga eliphakathi kwezikhungo zemibuthano emibili lilingana ncamashi nesamba serediya yazo:
Lapha udinga ukunaka kakhulu izici ezimbalwa ezengeziwe:
- I-projectile akumele ishaye umdlali oyidalile. Lokhu kungafezwa ngokuqhathanisa
bullet.parentID
Ρplayer.id
. - I-projectile kufanele ishaye kanye kuphela esimweni esibi kakhulu sokushaya abadlali abaningi ngesikhathi esisodwa. Sizoxazulula le nkinga sisebenzisa opharetha
break
: Uma umdlali oshayisane ne-projectile etholakele, siyayeka ukusesha bese sidlulela ku-projectile elandelayo.
I-ΠΠΎΠ½Π΅Ρ
Yilokho kuphela! Siphathe konke odinga ukukwazi ukuze udale igeyimu yewebhu ye-.io. Yini elandelayo? Yakha eyakho igeyimu ye-.io!
Yonke ikhodi eyisibonelo ingumthombo ovulekile futhi ithunyelwe kuwo
Source: www.habr.com