Створюємо розраховану на багато користувачів браузерну гру. Частина друга. Розбираємо ігровий фреймворк


Дізнайтесь більше про нові кар'єрні можливості в EchoUA. Цікаві проекти, ринкова оплата, гарний колектив. Надсилайте резюме та приєднуйтеся до нас.

Розповідає Алвін Лін, розробник програмного забезпечення з Нью-Йорка


У цій статті я продовжую тему створення розрахованої на багато користувачів браузерної гри. Тут я з технічної точки зору спробую у загальних рисах пояснити, як затримка може впливати на гру і як із цим впоратися. Я стисло розповім про фреймворки і поясню, як додати в гру звук.

Затримка і запізнювання

Отже, давайте розберемося, що ж таке затримка? Якщо Ви раніше грали в онлайн-ігри, то швидше за все стикалися з тим, що при натисненні на клавішу або спробі щось зробити деякий час в грі нічого не відбувається. Таке явище називають “лагом”, і воно дуже заважає при грі в розраховані на багато користувачів шутери.

Затримка відбувається через фізичну віддаленість Вашого комп’ютера від сервера, до якого Ви підключені. У попередній статті я розповідав про визначення повноважного сервера. Лаг – це проміжок часу між відправкою даних на сервер і отриманням підтвердження від сервера.

Взаємодія клієнта і сервера

Щоб впоратися з лагом, є концепція клієнтського прогнозування – екстраполяція. Це поняття припускає промальовування дії на стороні клієнта до того, як його розпізнає сервер. Таким чином досягається емуляція плавного перебігу гри. Фактично клієнт передбачає стан сервера після підтвердження дії.

Цей підхід особливо легко реалізувати для пересування гравців, якщо Ви використовуєте детерміністичну модель ігрового світу. Якщо все у Вашому ігровому всесвіті відбувається згідно з прогнозованою і розрахованою фізикою, то клієнтська сторона може робити зважені припущення про коректність вхідних даних. Таким чином, промальовування пересування гравця може початися до того, як буде отримано підтвердження від сервера.

Прогнозування зі сторони клієнта

Фреймворки

Тепер перейдемо до розгляду фреймворків, першим з яких буде incheon.gg. Це класний фреймворк, який дозволяє робити багато чого в іграх з дискретною і безперервною фізикою, при цьому він бере на себе велику частину логіки синхронізації клієнта і сервера. Рекомендую використати цей фреймворк спершу, якщо Ви не хочете реалізовувати мережеву взаємодію самостійно. У нього дуже грамотна і зрозуміла документація.

Як я написав власний фреймворк?

На одній з конференцій в Технологічному інституті Рочестера, в якій я брав участь, наша команда представила розраховану на багато користувачів гру. Моїм завданням було написання фреймворка для гри.

Я давно займаюсь розрахованими на багато користувачів іграми. Цей фреймворк – квінтесенція досвіду, накопиченого мною за ці роки. На відміну від incheon.gg, мій фреймворк призначений під ігри з відкритим світом, і в ньому немає вищезгаданої мережевої функціональності. Він більше підходить тим, хто хоче вивчити архітектуру гри шляхом написання кожної частини коду самостійно.

Хоча я все ще працюю над ним, можна сказати, що мій фреймворк близький до архітектури Node.js, на якому він написаний, і дозволяє кастомізувати гру. Якщо Ви новачок у розробці ігор або в Node.js і щойно прочитали першу частину цього циклу, то я б порекомендував Вам подивитись на цей код і розібратися, що до чого.

Репозиторій на GitHub містить перероблену, забезпечену коментарями, приведену в порядок версію моделі, яку я демонстрував в першій частині. Далі я детально розповім про кожну частину цього фреймворка. Пропоную скопіювати цей архів, якщо Ви хочете дізнатися більше.

git clone https://github.com/omgimanerd/game-framework

Структура game – framework

Перейдемо до bower.json, який має дві залежності на стороні клієнта: jquery і howler.js. Жодна з них нам особливо не знадобиться, але jquery може спростити Вам роботу, а howler.js – класна бібліотека, яка додає в гру звук. Пізніше я розповім, як використаю howler.js.

У теці lib є файли, які зі сторони сервера управляють і зберігають стан гри та її елементів. Entity2D.js і Entity3D.js – класи ES5, які включають елементи з базовою фізикою. Якщо хочете піти далі, можете додати в клас детальнішу фізику, гравітацію і т. д.

Entity2D.prototype.update = function (deltaTime) { var currentTime =  (new Date()).getTime (); if (deltaTime) { this.deltaTime = deltaTime; } else if (this.lastUpdateTime === 0) { this.deltaTime = 0; } else { this.deltaTime =  (currentTime - this.lastUpdateTime)  / 1000; } for (var i = 0; i < DIMENSIONS; ++i) { this.position[i] += this.velocity[i] * this.deltaTime; this.velocity[i] += this.acceleration[i] * this.deltaTime; this.acceleration[i] = 0; } this.lastUpdateTime = currentTime;};

Зверніть увагу, як реалізовано незалежне оновлення частоти зміни кадрів, про яке йшлося в першій частині. Оновлення позиції і швидкості відбувається завдяки deltaTime, що призводить до плавної гри. Player.js – просто розширення Entity2D.js, яке управляє вхідними даними користувача, крім базової фізики.

Game.js трохи цікавіше.

function Game (){ this.clients = new HashMap (); this.players = new HashMap ();}

Крім цього базового конструктора, тут є методи, які допомагають у підключенні і відключенні гравців. По суті, цей клас тільки виконує оновлення і передає стан сервера.

Game.prototype.update = function (){ var players = this.getPlayers (); for (var i = 0; i < players.length; ++i) { players[i].update (); }};Game.prototype.sendState = function (){ var ids = this.clients.keys (); for (var i = 0; i < ids.length; ++i) { this.clients.get (ids[i]).emit ('update',{ self: this.players.get (ids[i]), players: this.players.values ().filter ( (player) => player.id != ids[i]) }); }};

Game.prototype.update () оновлює кожен елемент у грі, а Game.prototype.sendState () передає стан гри кожному клієнтові, підключеному до сервера. Єдине, що треба відмітити (і що відрізняє мою реалізацію від інших) – я фільтрую підключених у даний момент гравців і відправляю кожному інформацію окремо. Проте Ви можете реалізувати це по-своєму. Я вирішив зробити саме так, оскільки це полегшує роботу на стороні клієнта.

Поговоримо про теку public. Вона містить статичні файли JavaScript, які виконуються зі сторони клієнта.

Пам’ятаєте, в першій частині я говорив про те, що було б непогано виконати рефакторинг коду? Це лише один зі способів. Створення набору модулів, кожен з яких відповідає за певну частину функціонала – гарна ідея, яка відрізняє продуманий дизайн ПЗ.

Drawing.js – це об’єкт контексту Canvas, який промальовував всі послідовності в спрайті. Файл Game.js використовує window.requestAnimationFrame (), щоб запустити у клієнта цикл для відправки вхідних даних користувача на ігровий сервер, а також для виконання отриманих станів гри.

Game.prototype.animate = function (){ this.animationFrameId = window.requestAnimationFrame ( Util.bind (this, this.update));};Game.prototype.update = function (){ if (this.selfPlayer) { // Слухаємо події гравця this.socket.emit ('player - action',{ keyboardState: { left: Input.LEFT, right: Input.RIGHT, up: Input.UP, down: Input.DOWN } }); // Малюємо стан в canvas this.draw (); } this.animate ();};

Цей фрагмент коду показує, як працює вищезгадана функція. Ось посилання на документацію requestAnimationFrame, якщо Ви не знаєте, для чого вона потрібна. Util.bind () створює функцію, яка зв’язує контекст об’єкта Game з функцією update (), щоб він міг бути викликаний за допомогою requestAnimationFrame (). Це дивний прийом, який стосується технічних особливостей JavaScript, тому я не обговорюватиму його. Ви легко можете добитися того самого результату за допомогою анонімної функції.

Файл Input.js я дуже часто використовую у своїх іграх (з деякими змінами). Він зв’язує обробники певних подій, щоб відстежувати вхідні дані від клавіатури/миші, і зберігає їх для швидкого доступу. Коли я виділяю цю функцію і зберігаю її в одному класі, мені не доводиться турбуватися про те, щоб отримувати вхідні дані користувача у будь-якому іншому місці.

Рухаємося далі. Тека shared містить тільки один файл – Util.js.

Util.js – ще один з тих файлів, які я часто використовую у своїх проектах. Він містить багато допоміжних функцій, які використовуються як на стороні клієнта, так і на стороні сервера. Тека shared служить для статики, тому я можу підключити скрипт з файлу HTML і використати його з допомогою require () на стороні сервера, коли мені це знадобиться. Ви можете переміщувати цей файл, якщо хочете, просто така організація зручна для мене.

Крім цього, я використовую теку shared для зберігання ігрових констант, до яких необхідно мати доступ як серверу, так і клієнтові. Наприклад, у мене є клас:

function Constants (){}Constants.PLAYER_HITBOX = 10;Constants.WORLD_SIZE = 2500;if (typeof (module)  === 'object') { module.exports = Constants;} else { window.Util = Util;}

Цей код дозволяє легко обробляти клас на стороні клієнта або сервера. Тека views не дуже цікава і містить шаблон для демонстрації моделі.

doctype htmlhtml head title Game! body canvas (id='canvas' width='800px' height='600px') script(src='/socket.io/socket.io.js?x18050') script(src='/public/bower/jquery/dist/jquery.min.js?x18050') script(src='/shared/Util.js?x18050') script(src='/public/js/game/Drawing.js?x18050') script(src='/public/js/game/Game.js?x18050') script(src='/public/js/game/Input.js?x18050')
script(src='/public/js/game/Sound.js?x18050') script(src='/public/js/client.js?x18050')

Я використовую шаблонізатор pug, але Ви легко можете підключити будь-який інший. І, нарешті, в кореневій теці у нас є файл server.js. Це стандартний сервер Node.js, який використовує веб-фреймворк express.

const FPS = 60;io.on ('connection', (socket) => { socket.on ('player - join', () => {
game.addNewPlayer (socket); }); socket.on ('player - action',  (data)  => { game.updatePlayerOnInput (socket.id, data); }); socket.on ('disconnect', () => { game.removePlayer (socket.id); })});setInterval (() => { game.update (); game.sendState ();}, 1000 / FPS);

У першій частині я розповідав про обробник подій сокета, і це перероблений, впорядкований код, де я використовую методи, які були визначені в класі Game раніше. Цикл setInterval () просто запускає оновлення гри і передачу стану знову і знову з частотою близько 60 кадрів на секунду.

Що далі?

Рекомендую Вам попрацювати з цим фреймворком і розібратися в ньому. Переробіть його під себе, напишіть власний код, роздрукуйте його і спаліть – загалом, робіть все, що хочете. Спробуйте оновити фізику або написати власний анімаційний движок. Сподіваюся Ви дізнаєтесь про щось нове і зробите що-небудь неординарне.

Дякую за те, що прочитали!

Переклад статті “How To Build A Multiplayer Browser Game (Part 2) “

Київ, Харків, Одеса, Дніпро, Запоріжжя, Кривий Ріг, Вінниця, Херсон, Черкаси, Житомир, Хмельницький, Чернівці, Рівне, Івано-Франківськ, Кременчук, Тернопіль, Луцьк, Ужгород, Кам'янець-Подільський, Стрий - за статистикою саме з цих міст програмісти найбільше переїжджають працювати до Львова. А Ви розглядаєте relocate?


Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *