Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Любая игра представляет собой набор файлов: изображений, звуков и.т.д. и программы, которая эти файлы воспроизводит по заданным алгоритмам. Звуки проигрываются, а изображения обрезаются в нужных пропорциях и воспроизводятся на экране в нужном порядке, как в кино, или мультипликации с той лишь разницей, что тут процессом можно управлять, используя прикладные интерфейсы — клавиатуру, мышь, джойстик, экран мобильного телефона и т.п. Управлять, не значит только переключать сцены, а управлять актерами, или даже группами актеров, влияя на сюжет или события, насколько это позволяет задумка автора.
Загрузка файлов
Для браузерной игры файлы используемые в программе, нужно сначала подгрузить в память. Для этого был написан отдельный модуль assetsm. Он позволяет добавлять файлы, ставит их в очередь и при загрузке рапортует о промежуточных результатах. Можно грузить изображения, звуки, тайлмапы и тайлсеты из редактора Tiled. А также можно расширять модуль добавляя любые другие файлы и загрузчики к ним, если нужно.
Мультиплеер
Игровой опыт по сети с другими игроками более богатый и захватывающий, чем одиночное прохождение, так что мультиплеер одна из важнейших частей любой современной игры.
Для сообщения между игроками(мультплеера), необходим отдельный веб-сервер, либо способ установки соединения напрямую. Для этого был написан еще один модуль - gameserver. Это мини-сервер с комнатами. Его также можно использовать для чатов, и как signaling для установления RTCPeerConnection.
Дирижирование и вывод на экран
Движок связывает все части воедино, позволяет подгрузить нужные файлы и задать логику для их рендера, т.е. вывода их на экран. А также имеет отправную точку для написания пользовательской логики.
Для движка была построена ООП иерархия, с применением принципов SOLID, где это возможно и использована последняя фитча js – приватные классы # и getters/setters.
Иерархия классов идет сверху вниз. т.е. класс System на самом верху регистрирует и хранит пользовательскую логику и данные, далее идет ISystem который запускает/останавливает рендер страниц и осуществляет взаимодействие между компонентами.
Пользовательская логика игры создается в унаследованных от GameStage классах, загрузка которого имеет жизненный цикл: после инициализации класса идет стадия регистрации в приложении, где можно добавить нужные модули и ресурсы(ассеты), далее - инициализации и запуска, в которых ресурсы уже будут доступны к использованию и остановка — для отчистки класса.
жизненный цикл GameStage
Классы выполняют какую-то одну функцию. Например, классы для отрисовки(DrawObjects), содержат только информацию о размерах, повороте и позиции и т.п. То, как они должны рисоваться, инкапсулируется в класс IRender и далее в дочерний класс WebGLEngine при инициализации приложения с помощью модульной системы.
Первоначально рендер делался на canvas2d, но пришлось отказаться и полностью перейти на webgl из-за невозможности в canvas2d рисовать изображения группой. Также я пробовал сделать многослойную структуру, но идея себя не оправдала. Все мержится в один слой, а если нужны эффекты наложения — можно пользоваться масками, или WebGL blend эффектами.
WebGL
WebGL – это браузерное API, надстройка над OpenGL, т.е. API другого API, внутри используется язык шейдеров(GLSL). Работать и отлаживать его в сравнении с canvas довольно сложно, но результат того стоит, на клиенте формируется бинарные данные и сразу отправляются на видеоадаптер, где по ним создается картинка. С бинарными данными также можно работать из WebAssembly, что открывает интересные перспективы для оптимизации, я делал кое-какие эксперименты в этом направлении, но это уже тема для отдельной статьи.
Схемы и примеры можно посмотреть в обучении, ссылки в конце статьи под катом.
Как работать с движком (jsge@1.1.0).
Нужно создать instance класса System передавая туда опции:
import { System, SystemSettings } from "jsge";
const app = new System(SystemSettings);
Создаем страницы будущей игры с помощью наследования от GameStage:
import { GameStage } from "jsge";
class CustomStage extends GameStage{
...
}
Описываем логику и элементы будущего окружения:
class CustomStage extends GameStage {
register() {
this.iLoader.addImage("image_key", "/images.jpg"); // добавляем изображение
}
init() {
this.player = this.draw.image(100, 200, 16, 28, "image_key"); // создаем спрайт на основе загруженного изображения
}
}
Регистрируем в системном классе:
app.registerStage("CustomStageKey", CustomStage);
Подгружаем добавленные на страницах assets и запускаем рендер:
app.preloadAllData().then(() => {
app.iSystem.startGameStage("CustomStageKey");
});
После запуска вашего instance GameStage, запускается рендер с данными хранимыми в StageData запущенной страницы.
Что сейчас реализовано.
рисование примитивов. Прямоугольники, многоугольники, круги, конусы. Можно менять цвет, прозрачность.
Можно создавать instance классов и добавлять на карту с помощью
GameStage.addRenderObject()
, либо создавать объекты используя factory класс, который сделает это за вас:
// полупрозрачный красный конус, радусом 120 пикселей рисуем и добавляем на экран
this.fireRange = this.draw.conus(55, 250, 120, "rgba(255, 0,0, 0.4)", Math.PI/8);
рисование текстов. Можно менять шрифт, цвет и обводку:
this.navItemBack = this.draw.text(200, 30, "Main menu", "18px sans-serif", "black");
рисование спрайтов(изображений). Для спрайтов поддерживаются границы, которые могут быть в виде многоугольника, либо круга, также можно задать индекс изображения, если картинка - тайлсет:
// рисуем картинку размером 16х16 пикселей, индексом 84 и границей в виде круга с радиусом 8 пикселей
this.player = this.draw.image(55, 250, 16, 16, "tilemap_packed", 84, {r: 8});
риcование tilemap. Tilemap, это файл генерируемый opensource редактором Tiled, содержащий информацию о карте и тайлсетах. Для увеличения производительности, маппинг обрезается по видимым границам, рисуется только то что видимо на экране в данный момент:
this.draw.tiledLayer("background", this.tilemapKey);
this.draw.tiledLayer("walls", this.tilemapKey);
почти все объекты можно перемещать и вращать.
можно считывать границы слоя, которые в дальнейшем используются для подсчета коллизий с границами спрайтов:
this.draw.tiledLayer("walls", this.tilemapKey, true);
...
if (!this.isBoundariesCollision(newCoordX, newCoordY, person)) {
person.x = newCoordX;
person.y = newCoordY;
}
Можно центрировать карту. Те если у вас tilemap больше размеров экрана, вам нужно будет смещать видимую/рисуемую область, например, при движении персонажа:
this.stageData.centerCameraPosition(x,y);
для спрайтов есть поддержка покадровой анимации, можно задавать порядок фреймов и скорость анимации:
const f = this.draw.image(this.player.x, this.player.y, 16, 16, this.fireImagesKey, 406, {r:4});
f.addAnimation("ANIMATION_FIREMOVE", [406, 407, 408, 409, 500], true, 5);
f.emit("ANIMATION_FIREMOVE");
Помимо изображений и tilemap, можно также загружать и проигрывать звуки и музыку:
register() {
this.iLoader.addAudio("audio_key", "./audio.mp3");
}
init () {
const track = page.audio.getAudio("audio_key");
track.play();
track.pause();
}
Модульная система позволяет добавять любые другие объекты и их рендер.
Помимо стандартной покадровой, в движок интегрирован рендер spine-анимаций с помощью модуля:
import SpineModuleInitialization from "jsge/modules/spine/dist/bundle.js";
class CustomPage extends GameStage {
register() {
// муодуль расширяет iLoader добавляя возможность грузить другие типы
// файлов(.skel, .atlas), а также добавляет новые объекты для рисования и их рендер
this.iSystem.installModule("spineModule", SpineModuleInitialization, "./spine/spine-assets");
this.iLoader.addSpineBinary(SPINE.SpineBinary, "./spineboy-pro.skel");
this.iLoader.addSpineAtlas(SPINE.SpineAtlas, "./spineboy-pma.atlas");
}
init () {
const spineDrawObject = this.draw.spine(-300, -300, SPINE.SpineText, SPINE.SpineAtlas);
spineDrawObject.scale(0.5);
spineDrawObject.animationState.setAnimation(0, "run", true);
}
}
Есть возможность использовать примитивные типы как маску, чтобы скрывать/показывать какие-либо области:
init() {
// создаем черную непрозрачную круг-маску
this.sightView = this.draw.circle(55, 250, 150, "rgba(0, 0, 0, 1)");
// Добавляем окружение видимое только в области круга-маски
this.draw.tiledLayer("background", this.tilemapKey, false, this.sightView);
this.draw.tiledLayer("walls", this.tilemapKey, true, this.sightView);
this.draw.tiledLayer("decs", this.tilemapKey, false, this.sightView);
const monster1 = new DrawImageObject(255, 250, 16, 16, "tilemap_packed", 108);
monster1.setMask(this.sightView);
}
Встроен интерфейс для мультиплеера, а также написана серверная часть с комнатами. Для работы, достаточно запустить серверную часть, а на клиенте поставить в опции адрес сервера
{ address: "https://your.gameserver.ru:9009" }
, настроить listeners и отправку нужных данных:
this.iSystem.iNetwork.addEventListener(CONST.EVENTS.WEBSOCKET.SERVER_CLIENT.CONNECTION_STATUS_CHANGED, this.#onConnectionStatusChanged);
this.iSystem.iNetwork.addEventListener(CONST.EVENTS.WEBSOCKET.SERVER_CLIENT.ROOMS_INFO, this.#onRoomsInfo);
this.iSystem.iNetwork.addEventListener(CONST.EVENTS.WEBSOCKET.SERVER_CLIENT.SERVER_MESSAGE, this.#onServerMessage);
this.iSystem.iNetwork.addEventListener(CONST.EVENTS.WEBSOCKET.SERVER_CLIENT.FULL, this.#onRoomIsFool); this.system.network.addEventListener(CONST.EVENTS.WEBSOCKET.SERVER_CLIENT.CREATED, this.#onCreatedNewRoom);
this.iSystem.iNetwork.addEventListener(CONST.EVENTS.WEBSOCKET.SERVER_CLIENT.JOINED, this.#onJoinedToRoom);
this.iSystem.iNetwork.sendGatherRoomsInfo();
this.iSystem.iNetwork.sendCreateOrJoinRoom(this.#selectedRoomName, {});
this.iSystem.iNetwork.sendMessage(message);
Какие игры можно делать
Движок можно использовать для разработки игр с видом сверху, либо изометрических. Для аркады с видом сбоку, нужно писать либо интегрировать расчет гравитации.
Оптимизация
В сети встречал статьи, что в популярных движках бывают сложности с рендерингом, когда карта большая. Пишут костыли, чтобы обойти эту проблему и/или дробят карту. В моем движке можно рендерить карту любого размера, при условии стандартного увеличения(scale). Для демонстрации подготовил специально большую карту tiled 800x800 ячеек по 16х16 пикселей и включил все фишки: слои маски, считывание границ слоя, детектор коллизий и протестировал работу на стареньком компе и мобильном телефоне.
Долговато загружается, файл почти 4мб., но последующий рендер укладывается в 60 fps(16 мс на полный цикл последовательной отрисовки).
Результаты на стареньком 4х-ядерном Xeon(Ubuntu, 27 дюймовый экран):
карта очень примитивная, но особой роли это не играет.
Результаты на мобильном(Galaxy a52):
На мобильнике может рендерится и быстрее, но скорость ограничена ~60 fps в настройках.
Ссылки
CodePen с примера: https://codepen.io/yaalfred/pen/zYegGGb
Менеджер файлов assetsm: https://github.com/ALapinskas/assetsm
Серверная часть gameserver: https://github.com/ALapinskas/gameserver
Сам движок jsge: https://github.com/ALapinskas/jsge
Обучение с примерами и документация API: https://jsge.reslc.ru/
Редактор Tiled: https://www.mapeditor.org/