An sake shi a cikin 2015 Agar.io ya zama magabata na sabon salo games .iowanda ya karu cikin farin jini tun lokacin. Ni da kaina na fuskanci haɓakar shaharar wasannin .io: a cikin shekaru uku da suka gabata, ina da halitta da sayar da wasanni biyu na wannan nau'in..
Idan baku taɓa jin labarin waɗannan wasannin ba, waɗannan wasannin gidan yanar gizo ne masu yawa masu sauƙin kunnawa (babu asusu da ake buƙata). Yawancin lokaci suna fuskantar 'yan wasa da yawa masu adawa da juna a fage daya. Wasu shahararrun wasannin .io: Slither.io и Diep.io.
A cikin wannan post, za mu bincika yadda ƙirƙirar wasan .io daga karce. Don wannan, ilimin Javascript kawai zai isa: kuna buƙatar fahimtar abubuwa kamar syntax ES6, keyword this и alkawura. Ko da ilimin ku na Javascript bai cika ba, har yanzu kuna iya fahimtar yawancin post ɗin.
Misalin wasan .io
Don taimakon ilmantarwa, za mu koma zuwa misali game .io. Yi ƙoƙarin kunna shi!
Wasan yana da sauƙi: kuna sarrafa jirgi a cikin fage tare da wasu 'yan wasa. Jirgin naku yana harba manyan injina ta atomatik kuma kuna ƙoƙarin buga wasu 'yan wasa yayin da kuke guje wa kayan aikin su.
Layukan da suka fi muhimmanci a nan su ne kamar haka:
src/client/index.js shine wurin shigarwa na abokin ciniki na Javascript (JS). Fakitin gidan yanar gizo zai fara daga nan kuma yana neman sauran fayilolin da aka shigo da su akai-akai.
Fitowar JS na ginin fakitin gidan yanar gizon mu zai kasance a cikin kundin adireshi dist/. Zan kira wannan fayil ɗin mu kunshin js.
Muna amfani Babel, da kuma musamman tsarin @babel/preset-env don fassara lambar JS ɗin mu don tsofaffin masu bincike.
Muna amfani da plugin don fitar da duk CSS da fayilolin JS suka yi nuni kuma mu haɗa su wuri ɗaya. Zan kira shi namu css kunshin.
Wataƙila kun lura da bakon sunaye na fakitin '[name].[contenthash].ext'. Sun ƙunshi maye gurbin sunan fayil Shafin yanar gizo: [name] za a maye gurbinsu da sunan wurin shigarwa (a cikin yanayinmu, wannan game), da [contenthash] za a maye gurbinsu da zanta na abubuwan da ke cikin fayil ɗin. Mun yi shi don inganta aikin don hashing - za ku iya gaya wa masu bincike su cache fakitinmu na JS har abada, saboda idan kunshin ya canza, to sunan fayil ɗin shima yana canzawa (canje-canje contenthash). Sakamakon ƙarshe shine sunan fayil ɗin duba game.dbeee76e91a97d0c7207.js.
fayil webpack.common.js shine fayil ɗin sanyi na tushe wanda muke shigo da shi cikin haɓakawa da gama tsarin saitin aikin. Misali, ga tsarin ci gaba:
Don dacewa, muna amfani da tsarin ci gaba webpack.dev.js, kuma ya canza zuwa webpack.prod.jsdon inganta girman fakiti lokacin turawa zuwa samarwa.
Saitin gida
Ina ba da shawarar shigar da aikin a kan injin gida don ku iya bin matakan da aka jera a cikin wannan sakon. Saitin yana da sauƙi: na farko, dole ne a shigar da tsarin kumburi и NPM. Na gaba kuna buƙatar yin
$ git clone https://github.com/vzhou842/example-.io-game.git
$ cd example-.io-game
$ npm install
kuma kuna shirye ku tafi! Don fara uwar garken ci gaba, kawai gudu
$ npm run develop
kuma je zuwa mashigar yanar gizo localhost: 3000. Sabar ci gaba za ta sake gina fakitin JS da CSS ta atomatik yayin da lambar ta canza - kawai sabunta shafin don ganin duk canje-canje!
3. Abubuwan Shiga Abokin Ciniki
Bari mu gangara zuwa lambar wasan kanta. Da farko muna buƙatar shafi index.html, lokacin ziyartar shafin, mai binciken zai fara loda shi. Shafin namu zai kasance mai sauqi qwarai:
index.html
Misali .io game WASA
An sauƙaƙa wannan misalin lambar don tsabta, kuma zan yi daidai da yawancin sauran misalan post. Ana iya duba cikakken lambar koyaushe a Github.
Wannan na iya zama kamar rikitarwa, amma babu abin da ke faruwa a nan:
Ana shigo da wasu fayilolin JS da yawa.
Shigowar CSS (don haka Webpack ya san ya haɗa su a cikin kunshin CSS ɗin mu).
Kaddamarwa connect() don kafa haɗi tare da uwar garken da gudu downloadAssets() don zazzage hotunan da ake buƙata don yin wasan.
Bayan kammala mataki na 3 Ana nuna babban menu (playMenu).
Saita mai sarrafa don danna maɓallin "PLAY". Lokacin da aka danna maɓallin, lambar ta fara wasan kuma ta gaya wa uwar garken cewa a shirye muke mu yi wasa.
Babban “nama” na dabarar sabar abokin cinikinmu yana cikin waɗancan fayilolin da fayil ɗin ya shigo da su index.js. Yanzu za mu yi la'akari da su duka a cikin tsari.
4. Musanya bayanan abokin ciniki
A cikin wannan wasan, muna amfani da sanannen ɗakin karatu don sadarwa tare da uwar garken socket.io. Socket.io yana da goyon baya na asali shafukan yanar gizo, waɗanda suka dace da sadarwa ta hanyoyi biyu: za mu iya aika saƙonni zuwa uwar garken и uwar garken na iya aiko mana da saƙon akan haɗin kai ɗaya.
Za mu sami fayil ɗaya src/client/networking.jswanda zai kula kowa da kowa sadarwa tare da uwar garken:
Gudanar da albarkatun ba shine wahalar aiwatarwa ba! Babban batu shine adana abu assets, wanda zai ɗaure maɓallin sunan fayil ɗin zuwa ƙimar abin Image. Lokacin da aka ɗora kayan aiki, muna adana shi a cikin wani abu assets don saurin shiga nan gaba. Yaushe ne za a ba wa kowane kayan aiki damar saukewa (wato, duk albarkatun), mun yarda downloadPromise.
Bayan zazzage albarkatun, zaku iya fara nunawa. Kamar yadda aka fada a baya, don zana kan shafin yanar gizon, muna amfani HTML5 Canvas (<canvas>). Wasan mu abu ne mai sauƙi, don haka kawai muna buƙatar zana abubuwa masu zuwa:
Bayan Fage
Jirgin ruwa mai wasa
Sauran 'yan wasa a wasan
Harsashi
Anan ga mahimman snippets src/client/render.js, wanda ke mayar da daidai abubuwa huɗu da aka jera a sama:
kawo.js
import { getAsset } from './assets';
import { getCurrentState } from './state';
const Constants = require('../shared/constants');
const { PLAYER_RADIUS, PLAYER_MAX_HP, BULLET_RADIUS, MAP_SIZE } = Constants;
// Get the canvas graphics context
const canvas = document.getElementById('game-canvas');
const context = canvas.getContext('2d');
// Make the canvas fullscreen
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
function render() {
const { me, others, bullets } = getCurrentState();
if (!me) {
return;
}
// Draw background
renderBackground(me.x, me.y);
// Draw all bullets
bullets.forEach(renderBullet.bind(null, me));
// Draw all players
renderPlayer(me, me);
others.forEach(renderPlayer.bind(null, me));
}
// ... Helper functions here excluded
let renderInterval = null;
export function startRendering() {
renderInterval = setInterval(render, 1000 / 60);
}
export function stopRendering() {
clearInterval(renderInterval);
}
Hakanan an gajarta wannan lambar don tsabta.
render() shine babban aikin wannan fayil. startRendering() и stopRendering() sarrafa kunna madauki na madauki a 60 FPS.
Ƙaddamar da aiwatar da ayyukan taimakon kai tsaye (misali. renderBullet()) ba su da mahimmanci, amma ga misali ɗaya mai sauƙi:
kawo.js
function renderBullet(me, bullet) {
const { x, y } = bullet;
context.drawImage(
getAsset('bullet.svg'),
canvas.width / 2 + x - me.x - BULLET_RADIUS,
canvas.height / 2 + y - me.y - BULLET_RADIUS,
BULLET_RADIUS * 2,
BULLET_RADIUS * 2,
);
}
Lura cewa muna amfani da hanyar getAsset(), wanda a baya aka gani a ciki asset.js!
Idan kuna sha'awar koyo game da wasu mataimaka masu bayarwa, to ku karanta sauran. src/abokin ciniki/render.js.
6. Shigar abokin ciniki
Lokaci yayi da za a yi wasa m! Tsarin sarrafawa zai zama mai sauƙi: don canza yanayin motsi, zaka iya amfani da linzamin kwamfuta (a kan kwamfuta) ko taɓa allon (akan na'urar hannu). Don aiwatar da wannan, za mu yi rajista Masu Sauraron Taro don abubuwan Mouse da Touch.
Zai kula da duk wannan src/client/input.js:
shigar.js
import { updateDirection } from './networking';
function onMouseInput(e) {
handleInput(e.clientX, e.clientY);
}
function onTouchInput(e) {
const touch = e.touches[0];
handleInput(touch.clientX, touch.clientY);
}
function handleInput(x, y) {
const dir = Math.atan2(x - window.innerWidth / 2, window.innerHeight / 2 - y);
updateDirection(dir);
}
export function startCapturingInput() {
window.addEventListener('mousemove', onMouseInput);
window.addEventListener('touchmove', onTouchInput);
}
export function stopCapturingInput() {
window.removeEventListener('mousemove', onMouseInput);
window.removeEventListener('touchmove', onTouchInput);
}
onMouseInput() и onTouchInput() su ne Event Listeners cewa kira updateDirection() (na networking.js) lokacin da abin shigarwa ya faru (misali, lokacin da aka motsa linzamin kwamfuta). updateDirection() yana sarrafa saƙo tare da uwar garken, wanda ke sarrafa taron shigar da kuma sabunta yanayin wasan daidai.
7. Matsayin abokin ciniki
Wannan sashe shine mafi wahala a sashin farko na sakon. Kada ku karaya idan ba ku fahimce shi ba a karon farko da kuka karanta shi! Kuna iya ma tsallake shi kuma ku dawo gare shi daga baya.
Yanki na ƙarshe na wasan wasa da ake buƙata don kammala abokin ciniki/lambar uwar garken shine jihar. Ka tuna snippet code daga sashen Rendering Client?
kawo.js
import { getCurrentState } from './state';
function render() {
const { me, others, bullets } = getCurrentState();
// Do the rendering
// ...
}
getCurrentState() ya kamata ya iya ba mu halin yanzu na wasan a cikin abokin ciniki a kowane lokaci dangane da sabuntawa da aka samu daga uwar garken. Ga misalin sabunta wasan da uwar garken zata iya aikawa:
Kowane sabunta wasan ya ƙunshi fage guda biyar iri ɗaya:
t: Tambarin lokaci na uwar garken yana nuna lokacin da aka ƙirƙiri wannan ɗaukakawar.
me: Bayani game da mai kunnawa da ke karɓar wannan sabuntawa.
wasu: Tsari na bayanai game da sauran 'yan wasan da ke shiga wasa ɗaya.
harsasai: tsararrun bayanai game da majigi a cikin wasan.
leaderboard: Bayanan jagora na yanzu. A cikin wannan sakon, ba za mu yi la'akari da su ba.
7.1 Halin butulci na abokin ciniki
aiwatar da butulci getCurrentState() kawai zai iya dawo da bayanan sabunta wasan da aka karɓa kwanan nan.
butulci-jihar.js
let lastGameUpdate = null;
// Handle a newly received game update.
export function processGameUpdate(update) {
lastGameUpdate = update;
}
export function getCurrentState() {
return lastGameUpdate;
}
Da kyau kuma bayyananne! Amma idan kawai ya kasance mai sauƙi. Ɗaya daga cikin dalilan wannan aiwatarwa yana da matsala: yana iyakance ƙimar firam ɗin bayarwa zuwa ƙimar agogon uwar garken.
Matsakaicin Tsari: adadin firam (watau kira render()) a sakan daya, ko FPS. Wasanni yawanci suna ƙoƙarin cimma aƙalla 60 FPS.
Tick Rate: Mitar da uwar garken ke aika sabuntawar wasanni ga abokan ciniki. Yawancin lokaci yana ƙasa da ƙimar firam. A cikin wasanmu, uwar garken yana gudana a ticks 30 a sakan daya.
Idan kawai muka yi sabon sabuntawar wasan, to FPS da gaske ba za su iya wuce 30 ba saboda ba mu taɓa karɓar sabuntawa sama da 30 a sakan daya daga uwar garken ba. Ko da mun kira render() Sau 60 a cikin dakika guda, to rabin waɗannan kiran za su sake zana abu ɗaya kawai, da gaske ba za su yi komai ba. Wata matsala tare da aiwatar da butulci ita ce ta batun jinkiri. Tare da ingantaccen saurin Intanet, abokin ciniki zai karɓi sabuntawar wasa daidai kowane 33ms (30 a sakan daya):
Abin takaici, babu abin da yake cikakke. Hoton da ya fi dacewa zai kasance:
Aiwatar da butulci a zahiri shine mafi munin lamarin idan ya zo ga jinkiri. Idan an karɓi sabuntawar wasa tare da jinkiri na 50ms, to rumbun abokin ciniki ta ƙarin 50ms saboda har yanzu yana sanya yanayin wasan daga sabuntawar da ya gabata. Kuna iya tunanin yadda wannan bai dace ba ga mai kunnawa: saboda raguwar sabani, wasan zai yi kama da mara kyau da rashin kwanciyar hankali.
7.2 Inganta yanayin abokin ciniki
Za mu yi wasu gyare-gyare ga aiwatar da butulci. Da farko, muna amfani yin jinkiri ku 100 ms. Wannan yana nufin cewa yanayin "na yanzu" na abokin ciniki koyaushe zai kasance baya bayan yanayin wasan akan sabar ta 100ms. Misali, idan lokacin akan uwar garken ne 150, to abokin ciniki zai sanya yanayin da uwar garken yake a lokacin 50:
Wannan yana ba mu madaidaicin 100ms don tsira lokacin sabunta wasan da ba a iya faɗi ba:
Sakamakon wannan zai kasance na dindindin rashin jin daɗi ku 100 ms. Wannan ƙaramar sadaukarwa ce don wasa mai santsi - yawancin 'yan wasa (musamman 'yan wasa na yau da kullun) ba za su ma lura da wannan jinkiri ba. Yana da sauƙi ga mutane su daidaita zuwa tsayin daka na 100ms fiye da yin wasa tare da jinkirin da ba a iya faɗi ba.
Za mu iya amfani da wata dabara mai suna tsinkayar gefen abokin ciniki, wanda ke yin aiki mai kyau na rage jinkirin da aka sani, amma ba za a tattauna a cikin wannan sakon ba.
Wani cigaban da muke amfani dashi shine layika interpolation. Saboda jinkirin bayarwa, yawanci muna aƙalla sabuntawa guda ɗaya kafin lokacin yanzu a cikin abokin ciniki. Lokacin da aka kira getCurrentState(), za mu iya cika layika interpolation tsakanin sabunta wasan kafin da kuma bayan lokaci na yanzu a cikin abokin ciniki:
Wannan yana magance matsalar ƙimar firam: yanzu za mu iya yin firam na musamman a kowane ƙimar firam ɗin da muke so!
7.3 Aiwatar da ingantaccen yanayin abokin ciniki
Misalin aiwatarwa a src/client/state.js yana amfani da jinkirin ba da jinkiri da tsaka-tsakin layi, amma wannan ba ya daɗe. Mu karya lambar zuwa kashi biyu. Ga na farko:
state.js part 1
const RENDER_DELAY = 100;
const gameUpdates = [];
let gameStart = 0;
let firstServerTimestamp = 0;
export function initState() {
gameStart = 0;
firstServerTimestamp = 0;
}
export function processGameUpdate(update) {
if (!firstServerTimestamp) {
firstServerTimestamp = update.t;
gameStart = Date.now();
}
gameUpdates.push(update);
// Keep only one game update before the current server time
const base = getBaseUpdate();
if (base > 0) {
gameUpdates.splice(0, base);
}
}
function currentServerTime() {
return firstServerTimestamp + (Date.now() - gameStart) - RENDER_DELAY;
}
// Returns the index of the base update, the first game update before
// current server time, or -1 if N/A.
function getBaseUpdate() {
const serverTime = currentServerTime();
for (let i = gameUpdates.length - 1; i >= 0; i--) {
if (gameUpdates[i].t <= serverTime) {
return i;
}
}
return -1;
}
Mataki na farko shine gano menene currentServerTime(). Kamar yadda muka gani a baya, kowane sabuntawar wasa ya haɗa da tambarin lokutan sabar sabar. Muna son yin amfani da latency don sanya hoton 100ms a bayan sabar, amma ba za mu taɓa sanin lokacin yanzu akan sabar ba, saboda ba za mu iya sanin tsawon lokacin da aka ɗauka don kowane sabuntawa ya isa gare mu ba. Intanet ba shi da tabbas kuma saurin sa na iya bambanta sosai!
Don magance wannan matsala, za mu iya amfani da ma'auni mai ma'ana: mu yi kamar sabuntawa na farko ya zo nan take. Idan wannan gaskiya ne, to da mun san lokacin uwar garken a wannan lokacin! Muna adana tambarin sabar a ciki firstServerTimestamp kuma kiyaye mu na gida (abokin ciniki) timestamp a lokaci guda a ciki gameStart.
Oh jira. Shin bai kamata ya zama lokacin uwar garken = lokacin abokin ciniki ba? Me yasa muke bambanta tsakanin "tambarin sabar" da "tambarin abokin ciniki"? Wannan babbar tambaya ce! Sai ya zama ba haka ba ne. Date.now() zai dawo daban-daban tambura a cikin abokin ciniki da uwar garken, kuma ya dogara da abubuwan gida zuwa waɗannan injina. Kar a taɓa ɗauka cewa tambarin lokaci zai kasance iri ɗaya akan duk injina.
Yanzu mun fahimci abin da ya aikata currentServerTime(): yana dawowa sabar timestamp na lokacin bayarwa na yanzu. A takaice dai, wannan shine lokacin uwar garken na yanzu (firstServerTimestamp <+ (Date.now() - gameStart)) rage jinkiri (RENDER_DELAY).
Yanzu bari mu kalli yadda muke tafiyar da sabunta wasanni. Lokacin da aka karɓi sabuntawa daga uwar garken, ana kiran shi processGameUpdate()kuma muna ajiye sabon sabuntawa zuwa tsararru gameUpdates. Sa'an nan, don bincika amfanin ƙwaƙwalwar ajiya, muna cire duk tsofaffin abubuwan da suka gabata tushe updatedomin bama bukatarsu kuma.
Menene "sabuntawa na asali"? Wannan sabuntawa na farko da muke samu ta hanyar komawa baya daga lokacin uwar garken na yanzu. Ka tuna wannan zane?
Sabunta wasan kai tsaye zuwa hagu na "Lokacin Sadar da Abokin Ciniki" shine sabunta tushe.
Menene sabuntawar tushe da ake amfani dashi? Me yasa za mu iya sauke sabuntawa zuwa tushe? Don fahimtar wannan, bari mu a ƙarshe la'akari da aiwatarwa getCurrentState():
state.js part 2
export function getCurrentState() {
if (!firstServerTimestamp) {
return {};
}
const base = getBaseUpdate();
const serverTime = currentServerTime();
// If base is the most recent update we have, use its state.
// Else, interpolate between its state and the state of (base + 1).
if (base < 0) {
return gameUpdates[gameUpdates.length - 1];
} else if (base === gameUpdates.length - 1) {
return gameUpdates[base];
} else {
const baseUpdate = gameUpdates[base];
const next = gameUpdates[base + 1];
const r = (serverTime - baseUpdate.t) / (next.t - baseUpdate.t);
return {
me: interpolateObject(baseUpdate.me, next.me, r),
others: interpolateObjectArray(baseUpdate.others, next.others, r),
bullets: interpolateObjectArray(baseUpdate.bullets, next.bullets, r),
};
}
}
Muna gudanar da shari'o'i uku:
base < 0 yana nufin cewa babu sabuntawa har sai lokacin bayarwa na yanzu (duba aiwatarwa na sama getBaseUpdate()). Wannan na iya faruwa daidai a farkon wasan saboda jinkirin yin wasa. A wannan yanayin, muna amfani da sabon sabuntawa da aka karɓa.
base shine sabon sabuntawa da muke dashi. Wannan na iya faruwa saboda jinkirin hanyar sadarwa ko rashin haɗin intanet. A wannan yanayin kuma muna amfani da sabon sabuntawa da muke da shi.
Muna da sabuntawa duka kafin da bayan lokacin bayarwa na yanzu, don haka zamu iya interpolate!
Duk abin da ya rage a ciki state.js shine aiwatar da tsaka-tsakin layi mai sauƙi (amma m) lissafi. Idan kana son gano shi da kanka, to bude state.js a kan Github.
Part 2. Backend Server
A wannan bangare, za mu duba baya Node.js wanda ke sarrafa namu .io wasan misali.
1. Wurin Shiga Sabar
Don sarrafa sabar gidan yanar gizo za mu yi amfani da sanannen tsarin gidan yanar gizo don Node.js da ake kira Express. Za a saita shi ta fayil ɗin shigarwar uwar garken mu src/server/server.js:
server.js part 1
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const webpackConfig = require('../../webpack.dev.js');
// Setup an Express server
const app = express();
app.use(express.static('public'));
if (process.env.NODE_ENV === 'development') {
// Setup Webpack for development
const compiler = webpack(webpackConfig);
app.use(webpackDevMiddleware(compiler));
} else {
// Static serve the dist/ folder in production
app.use(express.static('dist'));
}
// Listen on port
const port = process.env.PORT || 3000;
const server = app.listen(port);
console.log(`Server listening on port ${port}`);
Ka tuna cewa a kashi na farko mun tattauna Webpack? Anan ne za mu yi amfani da saitunan Webpack ɗin mu. Za mu yi amfani da su ta hanyoyi biyu:
Bayan samun nasarar kafa haɗin socket.io zuwa uwar garken, mun kafa masu gudanar da taron don sabon soket. Masu gudanar da taron suna sarrafa saƙonnin da aka karɓa daga abokan ciniki ta hanyar ba da izini zuwa abu guda ɗaya game:
server.js part 3
const Game = require('./game');
// ...
// Setup the Game
const game = new Game();
function joinGame(username) {
game.addPlayer(this, username);
}
function handleInput(dir) {
game.handleInput(this, dir);
}
function onDisconnect() {
game.removePlayer(this);
}
Muna ƙirƙirar wasan .io, don haka kwafi ɗaya kawai muke buƙata Game ("Wasan") - duk 'yan wasa suna wasa a fage ɗaya! A sashe na gaba, za mu ga yadda wannan ajin ke aiki. Game.
2. Wasan sabobin
Класс Game ya ƙunshi mafi mahimmanci dabaru a gefen uwar garken. Yana da manyan ayyuka guda biyu: sarrafa player и wasan kwaikwayo na wasan kwaikwayo.
Bari mu fara da aikin farko, sarrafa ɗan wasa.
game.js, part 1
const Constants = require('../shared/constants');
const Player = require('./player');
class Game {
constructor() {
this.sockets = {};
this.players = {};
this.bullets = [];
this.lastUpdateTime = Date.now();
this.shouldSendUpdate = false;
setInterval(this.update.bind(this), 1000 / 60);
}
addPlayer(socket, username) {
this.sockets[socket.id] = socket;
// Generate a position to start this player at.
const x = Constants.MAP_SIZE * (0.25 + Math.random() * 0.5);
const y = Constants.MAP_SIZE * (0.25 + Math.random() * 0.5);
this.players[socket.id] = new Player(socket.id, username, x, y);
}
removePlayer(socket) {
delete this.sockets[socket.id];
delete this.players[socket.id];
}
handleInput(socket, dir) {
if (this.players[socket.id]) {
this.players[socket.id].setDirection(dir);
}
}
// ...
}
A cikin wannan wasa, za mu tantance ’yan wasa ta fili id socket.io socket (idan kun rikice, to koma zuwa server.js). Socket.io da kansa yana ba kowane soket na musamman iddon haka bai kamata mu damu da hakan ba. Zan kira shi ID mai kunnawa.
Tare da wannan a zuciya, bari mu bincika misalai masu canji a cikin aji Game:
sockets wani abu ne da ke ɗaure ID ɗin mai kunnawa zuwa soket ɗin da ke da alaƙa da mai kunnawa. Yana ba mu damar samun dama ga soket ta ID ɗin ɗan wasan su a cikin lokaci akai-akai.
players abu ne da ke ɗaure ID ɗin mai kunnawa zuwa lambar> Abun ɗan wasa
bullets tsararrun abubuwa ne Bullet, wanda ba shi da takamaiman tsari. lastUpdateTime shine tambarin lokaci na ƙarshe lokacin da aka sabunta wasan. Za mu ga yadda ake amfani da shi ba da jimawa ba. shouldSendUpdate madaidaicin taimako ne. Za mu kuma ga amfani da shi nan ba da jimawa ba.
Hanyoyi addPlayer(), removePlayer() и handleInput() babu buƙatar bayyana, ana amfani da su a ciki server.js. Idan kana buƙatar sabunta ƙwaƙwalwar ajiyarka, koma baya kadan sama.
Layin ƙarshe constructor() farawa sabunta sake zagayowar wasanni (tare da mitar sabuntawa / s 60):
game.js, part 2
const Constants = require('../shared/constants');
const applyCollisions = require('./collisions');
class Game {
// ...
update() {
// Calculate time elapsed
const now = Date.now();
const dt = (now - this.lastUpdateTime) / 1000;
this.lastUpdateTime = now;
// Update each bullet
const bulletsToRemove = [];
this.bullets.forEach(bullet => {
if (bullet.update(dt)) {
// Destroy this bullet
bulletsToRemove.push(bullet);
}
});
this.bullets = this.bullets.filter(
bullet => !bulletsToRemove.includes(bullet),
);
// Update each player
Object.keys(this.sockets).forEach(playerID => {
const player = this.players[playerID];
const newBullet = player.update(dt);
if (newBullet) {
this.bullets.push(newBullet);
}
});
// Apply collisions, give players score for hitting bullets
const destroyedBullets = applyCollisions(
Object.values(this.players),
this.bullets,
);
destroyedBullets.forEach(b => {
if (this.players[b.parentID]) {
this.players[b.parentID].onDealtDamage();
}
});
this.bullets = this.bullets.filter(
bullet => !destroyedBullets.includes(bullet),
);
// Check if any players are dead
Object.keys(this.sockets).forEach(playerID => {
const socket = this.sockets[playerID];
const player = this.players[playerID];
if (player.hp <= 0) {
socket.emit(Constants.MSG_TYPES.GAME_OVER);
this.removePlayer(socket);
}
});
// Send a game update to each player every other time
if (this.shouldSendUpdate) {
const leaderboard = this.getLeaderboard();
Object.keys(this.sockets).forEach(playerID => {
const socket = this.sockets[playerID];
const player = this.players[playerID];
socket.emit(
Constants.MSG_TYPES.GAME_UPDATE,
this.createUpdate(player, leaderboard),
);
});
this.shouldSendUpdate = false;
} else {
this.shouldSendUpdate = true;
}
}
// ...
}
Hanyar update() ya ƙunshi watakila mafi mahimmancin ɓangaren dabaru na gefen uwar garken. Ga abin da yake yi, domin:
Yana ƙididdige tsawon lokacin dt wuce tun daga karshe update().
Yana wartsakar da kowane majigi kuma yana lalata su idan ya cancanta. Za mu ga aiwatar da wannan aikin daga baya. A yanzu, ya ishe mu sanin haka bullet.update()ya dawo true, idan majigin dole ne a lalata (ya fita wajen fage).
Yana sabunta kowane ɗan wasa kuma yana haifar da majigi idan ya cancanta. Za mu kuma ga wannan aiwatarwa daga baya - player.update()iya mayar da abu Bullet.
Bincika don samun karo tsakanin majigi da 'yan wasa tare da applyCollisions(), wanda ke dawo da jerin gwanon da ke buga ƴan wasa. Ga kowane majigi da aka dawo, muna ƙara maki na ɗan wasan da ya kori shi (amfani da player.onDealtDamage()), sa'an nan kuma cire projectile daga tsararru bullets.
Yana sanar da lalata duk 'yan wasan da aka kashe.
Yana aika sabuntawar wasa ga duk 'yan wasa kowane daƙiƙa lokuta idan aka kira update(). Maɓallin ƙarin da aka ambata a sama yana taimaka mana bin wannan shouldSendUpdate. Domin update() da ake kira sau 60/s, muna aika sabuntawar wasanni sau 30/s. Don haka, mitar agogo Agogon uwar garken shine 30 clocks/s (mun yi magana game da ƙimar agogo a ɓangaren farko).
Me yasa aika sabuntawar wasa kawai ta lokaci ? Don ajiye tashar. Sabunta wasanni 30 a sakan daya yana da yawa!
Me yasa ba kawai kira ba update() Sau 30 a sakan daya? Don inganta wasan kwaikwayo. Mafi sau da yawa ake kira update(), mafi daidaitaccen simintin wasan zai kasance. Amma kar a shaku da yawan ƙalubalen. update(), saboda wannan aiki ne mai tsadar lissafi - 60 a kowace daƙiƙa ya isa.
Sauran ajin Game ya ƙunshi hanyoyin taimako da ake amfani da su a ciki update():
getLeaderboard() Yana da sauƙi mai sauƙi - yana rarraba ƴan wasa da maki, yana ɗaukar manyan biyar, kuma yana mayar da sunan mai amfani da maki ga kowane.
createUpdate() amfani dashi update() don ƙirƙirar sabuntawar wasan da aka rarraba wa 'yan wasa. Babban aikinsa shine kiran hanyoyin serializeForUpdate()aiwatar da azuzuwan Player и Bullet. Lura cewa kawai yana canja wurin bayanai zuwa kowane ɗan wasa game da mafi kusa 'yan wasa da majigi - babu buƙatar watsa bayanai game da abubuwan wasan da ke da nisa daga mai kunnawa!
3. Abubuwan wasa akan uwar garken
A cikin wasanmu, majigi da ƴan wasa a haƙiƙa suna kama da juna: abubuwa ne na zahiri, zagaye, abubuwan wasan motsa jiki. Don amfanuwa da wannan kamanceceniya tsakanin ƴan wasa da majigi, bari mu fara da aiwatar da ajin tushe Object:
Babu wani abu mai rikitarwa da ke faruwa a nan. Wannan ajin zai zama kyakkyawan ma'ana don tsawaitawa. Bari mu ga yadda ajin Bullet amfani Object:
harsashi.js
const shortid = require('shortid');
const ObjectClass = require('./object');
const Constants = require('../shared/constants');
class Bullet extends ObjectClass {
constructor(parentID, x, y, dir) {
super(shortid(), x, y, dir, Constants.BULLET_SPEED);
this.parentID = parentID;
}
// Returns true if the bullet should be destroyed
update(dt) {
super.update(dt);
return this.x < 0 || this.x > Constants.MAP_SIZE || this.y < 0 || this.y > Constants.MAP_SIZE;
}
}
Aiwatarwa Bullet gajere sosai! Mun kara zuwa Object kawai kari masu zuwa:
Amfani da kunshin shortid domin bazuwar tsara id tsinkaya.
Ƙara filin parentIDdomin ku iya bin diddigin dan wasan da ya kirkiro wannan aikin.
Ƙara ƙimar dawowa zuwa update(), wanda yake daidai da trueidan majigi yana waje da fage (tuna mun yi magana game da wannan a cikin sashe na ƙarshe?).
Mu ci gaba zuwa Player:
mai wasa.js
const ObjectClass = require('./object');
const Bullet = require('./bullet');
const Constants = require('../shared/constants');
class Player extends ObjectClass {
constructor(id, username, x, y) {
super(id, x, y, Math.random() * 2 * Math.PI, Constants.PLAYER_SPEED);
this.username = username;
this.hp = Constants.PLAYER_MAX_HP;
this.fireCooldown = 0;
this.score = 0;
}
// Returns a newly created bullet, or null.
update(dt) {
super.update(dt);
// Update score
this.score += dt * Constants.SCORE_PER_SECOND;
// Make sure the player stays in bounds
this.x = Math.max(0, Math.min(Constants.MAP_SIZE, this.x));
this.y = Math.max(0, Math.min(Constants.MAP_SIZE, this.y));
// Fire a bullet, if needed
this.fireCooldown -= dt;
if (this.fireCooldown <= 0) {
this.fireCooldown += Constants.PLAYER_FIRE_COOLDOWN;
return new Bullet(this.id, this.x, this.y, this.direction);
}
return null;
}
takeBulletDamage() {
this.hp -= Constants.BULLET_DAMAGE;
}
onDealtDamage() {
this.score += Constants.SCORE_BULLET_HIT;
}
serializeForUpdate() {
return {
...(super.serializeForUpdate()),
direction: this.direction,
hp: this.hp,
};
}
}
'Yan wasa sun fi na'urori masu sarkakiya, don haka ya kamata a adana wasu filaye a cikin wannan ajin. Hanyarsa update() yana yin ƙarin aiki, musamman mayar da sabon aikin da aka ƙirƙira idan babu sauran fireCooldown (ka tuna mun yi magana game da wannan a sashin da ya gabata?). Hakanan yana fadada hanyar serializeForUpdate(), saboda muna buƙatar haɗa ƙarin filayen don mai kunnawa a cikin sabunta wasan.
Samun tushe aji Object - muhimmin mataki don guje wa maimaita lambar. Misali, ba tare da aji ba Object kowane abu na wasa dole ne ya kasance yana aiwatar da iri ɗaya distanceTo(), kuma kwafin duk waɗannan aiwatarwa a cikin fayiloli da yawa zai zama abin tsoro. Wannan ya zama mahimmanci ga manyan ayyuka.lokacin da yawan fadadawa Object azuzuwan suna girma.
4. Gano karo
Abinda kawai ya rage mana shine mu gane lokacin da majigi ya bugi 'yan wasan! Tuna wannan yanki na lambar daga hanyar update() a cikin aji Game:
game.js
const applyCollisions = require('./collisions');
class Game {
// ...
update() {
// ...
// Apply collisions, give players score for hitting bullets
const destroyedBullets = applyCollisions(
Object.values(this.players),
this.bullets,
);
destroyedBullets.forEach(b => {
if (this.players[b.parentID]) {
this.players[b.parentID].onDealtDamage();
}
});
this.bullets = this.bullets.filter(
bullet => !destroyedBullets.includes(bullet),
);
// ...
}
}
Muna buƙatar aiwatar da hanyar applyCollisions(), wanda ke mayar da duk abubuwan da suka faru da 'yan wasa. Sa'ar al'amarin shine, ba shi da wuya a yi saboda
Duk abubuwan da suke karo da'ira ne, kuma wannan shine mafi sauƙin siffa don aiwatar da gano karo.
Mun riga muna da hanya distanceTo(), wanda muka aiwatar a cikin sashin da ya gabata a cikin aji Object.
Ga yadda aiwatar da aikin mu na gano karo yayi kama:
karo.js
const Constants = require('../shared/constants');
// Returns an array of bullets to be destroyed.
function applyCollisions(players, bullets) {
const destroyedBullets = [];
for (let i = 0; i < bullets.length; i++) {
// Look for a player (who didn't create the bullet) to collide each bullet with.
// As soon as we find one, break out of the loop to prevent double counting a bullet.
for (let j = 0; j < players.length; j++) {
const bullet = bullets[i];
const player = players[j];
if (
bullet.parentID !== player.id &&
player.distanceTo(bullet) <= Constants.PLAYER_RADIUS + Constants.BULLET_RADIUS
) {
destroyedBullets.push(bullet);
player.takeBulletDamage();
break;
}
}
}
return destroyedBullets;
}
Wannan saurin gano karon ya dogara ne akan gaskiyar cewa dawafi biyu suna karo idan tazarar da ke tsakanin cibiyoyinsu bai kai adadin radiyoyinsu ba. Ga lamarin inda tazarar da ke tsakanin cibiyoyin da’irori biyu daidai yake da jimlar radiyoyinsu:
Anan kuna buƙatar kula sosai ga wasu ƙarin bangarorin biyu:
Dole ne majigi ya bugi ɗan wasan da ya ƙirƙira shi. Ana iya samun wannan ta hanyar kwatanta bullet.parentID с player.id.
Dole ne majigi ya buga sau ɗaya kawai a cikin iyakance yanayin 'yan wasa da yawa da suka yi karo a lokaci guda. Za mu magance wannan matsala ta amfani da mai aiki break: da zaran an sami mai kunnawa da ya yi karo da majigi, sai mu dakatar da binciken kuma mu matsa zuwa na gaba.
Ƙarshen
Shi ke nan! Mun rufe duk abin da kuke buƙatar sani don ƙirƙirar wasan yanar gizon .io. Menene na gaba? Gina wasan .io naku!
Duk samfurin samfurin buɗaɗɗen tushe ne kuma an buga shi Github.