Привет, Хабр! Как-то раз после работы мне захотелось взять и написать небольшую компьютерную игру. А почему бы и нет? Играть я люблю, программировать — тоже. Захотелось проверить, можно ли сделать что-то прикольное на уровне современных AAA-игр, не изучая дополнительных языков программирования, а также избежать банального повторения тех же «велосипедов», которые уже 100500 раз выложены на различных стримах и, конечно, не раз разбирались на Хабре. В этом посте я хотел бы поделиться с вами своим небольшим экспериментом в области GameDev на базе JS и обсудить возможности, которые есть у любознательного программиста с бэкграундом в сфере JavaScript.
Меня зовут Семен Брятов, я занимаюсь разработкой фронтэнда в команде Леруа Мерлен. При необходимости иногда немножко работаю fullstack-программистом на базе Node JS, но случается это пока что редко (хотя кто знает, куда меня занесет судьба завтра ????). Периодически занимаюсь всякими экспериментами и интересными пет-проектами. И хотя являюсь практически нубом в этой сфере, сегодня хочу поделиться своим опытом GameDev’а. Может быть, мой пост вдохновит ещё кого-нибудь заняться чем-то интересным в свободное от работы время.
Вообще, сама идея написать свою собственную игру давно витала где-то у меня в голове. Мало того, что я люблю видеоигры и как любой (не)нормальный программист отдыхаю тоже в коде, так еще мне нравится испытывать боль и удовольствие от удач и неудач. При этом программировать я умею лучше всего на JavaScript.
Все это привело к мысли: пора уже сделать свою собственную игру на JS! ????????????
Выбираем фреймворк
У любого разработчика с опытом при подходе к новому проекту неизбежно возникает мысль: «Зачем городить костыли и велосипеды, ведь их уже нагородили?» Нет, конечно, написать свой собственный фреймворк на JS — задача интересная, но мне хотелось получить игровой результат в какие-то более-менее адекватные сроки.
Когда я впервые подступался к подобной задаче(а было это давно, я тогда только погружался в мир веба), у меня возникли опасения, что найти подходящий движок под JS будет не так просто. На тот момент я был уже знаком с движками Unreal и Unity и даже немного пробовал в них ковыряться. Но я прекрасно понимал, что при всей своей крутости Unreal Engine сильно использует С++. Да, в нем в последнее время добавились Blueprints, которые позволяют заниматься визуальным программированием (примерно как в нашей Platformeco). Но во всем этом нужно еще разбираться и разбираться… да и все равно есть подозрение, что без С++ далеко на Unreal не уедешь.
Движок Unity плотно сидит на C#. В нем была (это ключевое слово) поддержка JS. Но, судя по всему, разработчики решили от нее отказаться. Думаю, они правильно сделали: лучше специализироваться на чем-то одном, но хорошо. Когда-то в универские годы у меня был опыт разработки как на С++, так и на С#. Но кривая дорожка судьбы привела меня в мир веба. По этой причине, отбросив два самых популярных игровых движка в геймдеве, я решил отправиться в свободное плавание…
Я знал, чувствовал в глубине души, что ситуация не может быть такой мрачной. Сейчас, будучи опытным юзером веб-технологий, я знаю что для JS написано не просто много либ, а СЛИШКОМ много либ. Среди них просто обязано быть подходящее мне решение! И выяснилось, что так оно и есть. Ассортимент действительно большой — и это поначалу даже пугает. Так сказать, рождает большую ответственность разработчика за выбор «правильного», подходящего инструмента.
Тут-то и начался рисерч. В целях первого ознакомления он был относительно недолгим. Уверен, можно раздобыть гораздо больше инфы, однако, на мой нубский взгляд, вот самые популярные фреймворки на 2022 год, которые я успел раскопать.
Движки для 2D
Один из наиболее beginner-friendly вариантов — GDevelop. В этом фреймворке есть своя IDE. В ней можно программировать не только с использованием JS, но также при помощи блоков — снова приведу в пример решение Platformeco от моей компании. Ну а если ближе к геймдеву, то здесь стоить вспомнить про упомянутую ранее Blueprints от Unreal Engine. Вообще, визуальное программирование активно развивается, и, думаю, работать с GDevelop будет удобно многим, кто уже имеет подобный опыт.
Phaser, пожалуй, самый популярный фреймворк разработки для 2D на JS. Насколько я знаю, Phaser уже лет 10 существует и развивается. И такая история обеспечивает ему огромное количество дочерних библиотек и дополнительных примочек от огромного сообщества.
Melonjs — фреймворк-конструктор, в котором просто собирать проекты из различных модулей. Его тоже активно используют разработчики 2D-игр. Глубоко в него не копал, но довольно часто слышал его упоминание в различных топ-обзорах популярных фреймворков для JS.
Движки для 3D
Когда мы говорим про 3D, сразу же на ум приходит PlayCanvas. Как мне показалось во время исследования, это настоящий гигант в мире разработки игр на веб-технологиях. На базе PlayCanvas можно прямо в браузере запустить специализированную IDE и уже в ней работать с текстурами и модельками. Платформа широко поддерживает 3D Web-рендеринг и показывает, кстати, отличную производительность. Пока я проводил свой ресерч, запускал пару демок, и на PlayCanvas получился вполне нормальный уровень графики. Это ведь в браузере! В общем, штука хорошая.
Babylon.js — крупный комбайн для работы с графикой, который позволяет подгружать разные текстуры и модельки. Под капотом есть множество опций для работы с физикой. Этот фреймворк также вполне можно использовать, если вы захотите сделать игру, которая будет сложной с графически-физической точки зрения.
ThreeJS — еще одна популярная библиотека для работы с графикой в браузере (думаю, что самая популярная). Ее можно свободно использовать, она относительно простая и, самое главное, бесплатная! Но если говорить о геймдеве, с ней придется много что писать самому с нуля. Ведь это больше инструмент рендеринга 3D для веба, а не полноценный движок с физикой, готовыми контроллерами и прочими прелестями игровых фреймворков. Так что инструмент хорош для тех, кто любит «заходить с кода и продолжать кодом», не боясь «получить Нобелевскую за изобретение очередного велосипеда». Зато какие красивые сайты-визитки можно с ее помощью делать! Я вдохновился! И вам советую посмотреть примеры с официальной страницы библиотеки https://threejs.org/.
Что выбираем?
Повторю то, что сказал в самом начале. Я — нуб. Поэтому решил выбрать что-то попроще, самое популярное и подробно разжеванное. После недолгих раздумий я решил начать с 2D, взял Phaser и погнал работать…
Но куда работать-то? Стоит ли реально закапываться или лучше сделать что-то простенькое? В голову пришел бесконечный раннер… Но вот смотришь на YouTube 1001 туториал о том, как индусы кодят тот же самый раннер, и начинаешь думать: «Какая посредственность… зачем мне это нужно? Даже для учебы брать уже как-то скучно».
Тут, к счастью, у меня состоялась беседа с коллегой, Вадимом Жуковым (руководитель разработки на одном из проектов, тоже интересуется гейм-девом), который предложил мне кое-что изменить и сделать управление в раннере… с помощью звука! И вот в этот момент у меня в голове, наконец, загорелась лампочка: «Отличная идея, нужно попробовать!»
И я начал делать свой сверхзвуковой раннер.
Под капотом — серьезные вещи!
Итак, я взял базовый сборщик на базе Vite и TypeScript, а также использовал пакеты audiomotion — analyzer v3 (помогает распознавать звуки) и phaser v3. Деплой реализовал через gh-pages (быстро-модно-молодежно). Ссылочка на проект, ридми и демку тут — https://github.com/SimonBryatov/Phaser3-RunnerGame
Итак, как все это выглядит в phaser?
Есть сам проект. Я назвал его… Phaser3-RunnerGame. Мы создаем в нем instance: определяем ширину и высоту нашего экрана, настраиваем зум. Можно добавить физику (кстати, аркадная физика лучше всего подходит для 2D), назначаем гравитацию — по оси Y, чтобы динозаврика тянуло обратно на землю.
Объект игры ссылается на сцены — это вторая core-составляющая Phaser. Сцены — это отдельные классы, в которых мы описываем логику для каждого конкретного момента.
Сцены бывают активные игровые, а бывает preloader или, к примеру, gameover. Я использую preloader, чтобы загрузить все текстуры, картинки, музыку и прочее (так было рекомендовано в туториале, и я решил следовать лучшим практикам!). По факту, если в игре ресурсы будут жирными, то надо сделать экран загрузки, где потенциальный игрок будет весело проводить время в ожидании подготовки основной сцены игры. Для моего проекта речь идет про какие-то доли секунды, однако на будущее в голове надо держать этот концепт. Плюс данная сцена — некая абстракция для инкапсуляции неигровой «скучной» логики подгрузки ассетов, чтобы не засорять лишними букафффами основную игровую сцену.
Когда все подгрузится, мы переходим на этап сцены Game:
import Phaser from "phaser";
import { AnimationKey } from "../consts/AnimationKey";
import { SceneKey } from "../consts/SceneKey";
import { TextureKey } from "../consts/TextureKey";
import AudioMotionAnalyzer from "audiomotion-analyzer";
// Наша основная сцена
export class Game extends Phaser.Scene {
private background!: Phaser.GameObjects.TileSprite;
private bush!: Phaser.GameObjects.Image;
private copCar!: Phaser.Types.Physics.Arcade.SpriteWithDynamicBody;
private prevRunVelocity = 0;
private dino!: Phaser.Types.Physics.Arcade.SpriteWithDynamicBody;
private audiomotion!: AudioMotionAnalyzer;
private isGameOver: boolean = false;
private scoreText!: Phaser.GameObjects.Text;
private music!: any;
constructor() {
super(SceneKey.game);
this.loadAudioAnalyzer();
}
private loadAudioAnalyzer = () => {
//...
// Зашружаем нашу библиотеку по работе с входным звуком
};
private setPlayer(sceneWidth: number, sceneHeight: number) {
//...
// Обрабатываем модельку персонажа и настраиваем игровую камеру
}
private setWorld(sceneWidth: number, sceneHeight: number) {
//...
// Рисуем игровой мир
}
private setSound() {
//...
// Врубаем хайповый "музон"
}
private onObstacleHit = () => {
//...
// Обрабатываем столкновения с препятствиями тут
};
private handleJumping = () => {
//...
// Обрабатываем логику прыжков
};
private handleRunning = () => {
//...
// Обрабатываем логику бега
};
private spawnDecorations = () => {
//...
// Рисуем рандомно декоративные элементы (кусты)
};
private spawnObstacles = () => {
//...
// Рисуем рандомно препятсвия (коповские тачки)
};
create() {
this.setSound();
this.prevRunVelocity = 0;
this.isGameOver = false;
const sceneWidth = this.scale.width;
const sceneHeight = this.scale.height;
this.setWorld(sceneWidth, sceneHeight);
this.setPlayer(sceneWidth, sceneHeight);
this.physics.add.collider(this.dino, this.copCar, this.onObstacleHit);
// Всякие прелести UI добавляем. Можно было б тоже в функу вынести
this.scoreText = this.add
.text(0, 0, "Score: ", {
fontSize: "16px",
color: "white",
backgroundColor: "black",
})
.setScrollFactor(0);
this.add
.text(90, 60, "Run frequencies (0-60)", {
fontSize: "10px",
color: "white",
backgroundColor: "black",
})
.setScrollFactor(0);
this.add
.text(360, 60, "Jump frequencies (2k-16k)", {
fontSize: "10px",
color: "white",
backgroundColor: "black",
})
.setScrollFactor(0);
}
// Тик-так, а вот и новый такт игры подъехал - пора обработать
update(): void {
if (this.isGameOver) {
return;
}
this.handleRunning();
this.handleJumping();
this.background.setTilePosition(this.cameras.main.scrollX);
this.spawnDecorations();
this.spawnObstacles();
this.scoreText.text =
"Score: " + Math.round(this.cameras.main.scrollX * 0.25 - 40);
}
}
В этом классе описана вся логика геймплея. Кто работал в react, тот знает lifecycle-методы — тут что-то похожее.
init — аналог on mount
create — что-то среднее между mount и первым update
update — как render, только подвязанный на знакомый многим Window.requestAnimationFrame() — каждый очередной момент времени движок вызывает данный метод, и происходит вычисление следующего состояния сцены.
В update можно как раз узнать силу входного звукового сигнала в каждый такт игры. Мы считываем ее, делаем примитивные калькуляции и даем объекту нужную скорость.
Тут же определяется вся логика взаимодействия динозаврика с миром. Если динозавр сталкивается с машиной, сцена переходит в сцену GameOver, которая кладется «поверх» сцены Game. Когда мы совершаем клик мышкой, происходит сброс основной сцены Game и таким образом производится перезапуск игры с начальными параметрами.
import Phaser from "phaser";
import { SceneKey } from "../consts/SceneKey";
// Сцена Гейм овер, "аста ла виста, Бейби"
export class GameOver extends Phaser.Scene {
constructor() {
super(SceneKey.gameOver);
}
resetGame = () => {
this.scene.get(SceneKey.game).scene.restart();
this.scene.stop();
};
create() {
this.add
.text(120, 200, "Game over, Dino!", {
fontSize: "38px",
color: "white",
fontStyle: "bold",
backgroundColor: "black",
})
.setScrollFactor(0);
this.add
.text(300, 250, "Try Again ?", {
fontSize: "20px",
color: "white",
backgroundColor: "black",
})
.setScrollFactor(0)
.setInteractive()
.on("pointerdown", this.resetGame);
}
}
Признаюсь, я сам до конца в phaser не разобрался и делал все методом тыка, поэтому не претендую на то, что мой код предельно оптимальный. Однако, он работает! ???? Так что можно сказать, сейчас делюсь с вами теми основами, которые нашел сам. Очевидно, что можно все завернуть глубже, круче и интереснее!
Ключевой момент — управление звуком ????️????️
Но вернемся к тому, что делает мой раннер чем-то особенным. И это управление звуком. Для этого я обратился к крутой либе audioMotion. Это полноценный музыкальный плеер, который содержит множество функций. Только вот сам плеер мне не был нужен и тут мне несказанно повезло. Эти ребята также выпустили отдельно пакет audioMotion-analyzer, который содержит в себе только вычислительные функции для работы с звуковым входом в браузере. На его основе можно проводить как визуализации (визуализатор работает на технологии canvas прямо из коробки!), так и получать конкретные значения амплитуды звукового сигнала в выбранном частотном диапазоне.
Предоставляемый библиотекой метод getEnergy(...) как раз таки нам и нужен. В нашем случае он вызывается при каждом вызове update-метода сцены Phaser, то есть на каждый обрабатываемый игрой такт, и возвращает «силу сигнала» в указанном диапазоне частот. Я использовал эти значения, чтобы вычислить скорость, с которой наш динозаврик должен бежать и прыгать.
Фактически, под капотом программных абстракций происходит очень интересная штука — звук от входного аудиоустройства и аудиокарты компьютера передается браузеру и библиотеке, в которой и происходит вся магия по обработке сигнала. Благо эту «магию» делаем не мы — за нас уже постарались физики, низкоуровневые ЯП и разработчики библиотеки audiomotion :) Наше дело несложное — направить «нужные циферки» в «нужное русло» игрового контроллера под управлением движка Phaser и заставить героя выполнять примитивные действия, ради нашей забавы.
Как я уже описал выше, метод GetEnergy выдает результаты от 0 до 1, оценивая «силу» входного звука (не знаю, какой радиотехнический термин тут корректнее употребить, так что прошу меня простить). При этом можно выделить только определенный диапазон частотного спектра и получить значение для конкретных типов входящих сигналов. Для движения динозавра я решил выбрать частоты от 0 до 60 Гц. Это характерный звук от, скажем, стука кулаком по столу или топанья по полу. Библиотека оценивает «среднюю энергию звука», а я уже перемножаю это число от 0 до 1 на определенный коэффициент, нормализуя его до подходящих значений для физики движка. В общем, немного уже нашей магии — и становится понятно, когда динозавр начинает бежать ????
С прыжком то же самое, но на более высоких частотах. Я выбрал диапазон от 2 до 16 КГц — это уровень звука «хлопка в ладоши». Но тест-драйв показал, что туда же ложатся крики и даже шмыганье носом. Так что можно устроить себе очень интересную игру, например, выкрикивая «Ко-ко!», чтобы динозавр прыгнул.
Что еще потребовалось?
Даже на таком простом проекте нужно еще кое-что, чтобы игра действительно заиграла. Я использовал программу Aseprite. Она пригодилась, чтоб нарисовать динозаврика, а также анимировать его. Кстати, создатели Aseprite реально старались. Прога получилась отличная, и я нарисовал отличного Дино, и даже трассу сам нарисовал (хотя и не очень старался). За этот удобный инструментарий для PixelArt я даже решил заплатить аж 350 рублей. Никаких вам «йо-хо-хо» и «бутылки рома»! ????☠️ ????
К фону я подошел проще — взял фотографию города и пикселизировал ее для пущей аутентичности. Машину и кустик просто взял из открытого доступа. Они неплохо вписались в антураж.
Что получилось?
В общем, получилась забавная игруха, в которой я даже сам пока не очень преуспел, потому что наверное... медленно шмыгаю носом и хлопаю в ладоши! Сразу скажу, что играть в нее лучше в наушниках, чтобы музыка (а там есть саундтрек просто супертематический!) не мешала библиотеке audioMotion-analyzer определять «энергию ваших звуков» правильно. Хотя если сделать музыку потише, а микрофон при этом у вас относительно нормальный, можно играть и без наушников. Как раз те самые «страшные коэффициенты» для умножения значений уровня входного звука я старался подобрать так, чтобы минимизировать ложные срабатывания. Но не переоценивайте мою экспертизу в этом вопросе — решал задачу методом тыка, конечно же!
Судя по всему, игру уже можно экспортировать как Android- и iOS-приложение — кажется, я встречал такие возможности во всех (или почти всех) упомянутых при рисерче библиотеках игровых движков для JS. Что и говорить, сейчас Web пытается встраиваться куда угодно. Может быть, скоро мой раннер получится запустить даже на «вашем тостере». Ну, вы понимаете :)
В любом случае, когда будете запускать, сделайте предварительно потише громкость в системе, чтобы нечаянно не оглохнуть) Конечно, по-хорошему нужно бы еще настраивать и настраивать эту игруху, но я свою задачу выполнил и действительно сделал свой первый раннер, причем полностью на JS. Если вам интересно или хотите сделать что-то свое по образу и подобию, дублирую ссылочку на GitHub тут. Там же в ридми есть ссылочка на демку, развернутую на gh-pages.
А вообще, спасибо за внимание, буду рад вашим комментариям!
Всем хорошего настроения и творческого подхода! ????
gpt74
Автор, а подкажите зачем и как в прикрутили к Фазер Тапскрипт - ведь врродебы Фазер на чиоом Ява-скрипт сделан, да и звуковой фрейм-ворк вроде тоже на чистом Ява-скрипт? Я смотрел на Фазер и Бабилон - на мой взгляд Бабилон имеет огромное преимущество - импорт готовых фигурок из других дизайнеров и автоматиеский "кокон" для детекции столкновений. Имхо, без этого разработка превращается в Ад. И еще как решили пробему на мобилах, где активация звуковых фейм-ворков возможна только по тапу по экрану, в фоне не работает...?
SimonBryatov Автор
Зачем тайпскрипт?
Статическая типизация typescript мне очень нравится - помогает писать более структурированный код :) Создатели phaser поддерживают типизацию на ts - у них даже туториал есть официальный с использованием ts
Babylon vs Phaser
В статье я описал, что Babylon рассчитан больше на работу с 3d, в то время как Phaser - "царь" в 2d сегменте. Если я правильно понимаю термин "автоматиеский кокон" и вы имеете ввиду коллизии, то Phaser их тоже поддерживает "из коробки"
Как решили проблему на мобилах
На мобилы не экспортил - только написал о такой потенциальной возможности. Вообще, с мобильного браузера запускается, но что-то с вводом с микрофона действительно есть проблемы + я не оптимизировал адаптивность странички. Основная задумка была под веб с ПК
gpt74
автоматический кокон я в фазере не нашел, автоматский значит например у вас есть сложная фигурка - человек с растопыренными руками, самолет и тонкими и динными крыльями, змея длинная и с извивами и т.д. - так вот в бабилон оно само автоматиески надевает на нее кокон (СЛОЖНЫЙ И ТОЧНЫЙ!!!) для детекции касаний с другими такими же сложными объектам - в фазере как я понял это адский труд самому прорисовыать ломаными линиями в 2д сложный конкон или одеваь окружность или квадрат что будет рабтать плохо и убого.
SimonBryatov Автор
https://phaser.io/news/2021/04/phaser-3-typescript-starter
вот ссылка с их сайта на демо-проект на ts