Доброго времени суток всем хаброжителям. Хочу рассказать вам о том, как я вернулся в любительский геймдев спустя 3+ года, кардинально сменив инструмент (а попутно — и свое мировоззрение), и что из этого вышло. Под катом вас ожидает:
- Краткая диспозиция всех фактов в начале пути. Как картинка «ДО» в дешёвой интернет-рекламе «ДО» и «ПОСЛЕ».
- Добровольный нырок в современный frontend в стиле «Где деньги, Лебовски?!»
- Легкий зуд в интимной точке, переходящий в жгучее желание изучить что-то новое, сделав что-нибудь старое.
- Осознание собственной беспомощности
- Преодоление
- Приятное окончание, ну совсем как в этих ваших фильмах.
Дисклеймер
Эта статья лежала в черновиках на github-е два года, ее почти полная версия была написана в конце 2017 года по горячим следам от событий.
Гражданин, представьтесь!
В своей предыдущей статье (https://habrahabr.ru/post/244417/) я немного писал о себе, а точнее о том, как связан я и геймдев. Де юро — никак, де факто — пишу игры и движки для удовольствия и участия в теплых и ламповых конкурсах. С тех пор мало что изменилось.
Для дальнейшего повествования и нагнетения необходимой атмосферы важно то, что на момент начала этого сказа я являюсь профессиональным .net web developer, который умеет в html5, css3 и… jquery. Для укрепления неожиданного «вот это поворот»-а добавим, что имеется недоверие и брезгливость к JavaScript, приправленная:
- откровенным непониманием всего этого хайпа вокруг языка;
- набором шуток про типизацию, ускоренное устаревание фреймворков, number+string-неожиданностей и прочим. Знаете, такой вполне стандартный, джентльменский набор разработчика, использующего более взрослый и устоявшийся язык (эй, я знаю, что платформа .net моложе js, но "взрослый" не в прямом смысле).
Пройдемте!
Сентябрь 2017. Я вышел на новую теплую и ламповую работу со свежим стеком технологий (.net core web api + angular 4) и моей задачей был только бекенд. Слово Angular звучало для меня ругательно, npm и nodejs прочно ассоциировались со смузи и гироскутером. Тимлид, видя это и относясь с пониманием, провел мне короткий курс о том, как запускать все это шайтанство. Аккурат как в том анекдоте про чукчу и собак в космосе. Я запоминаю нужные для запуска bat-ники (запоминание npm run start я посчитал крайне излишним для своей нежной натуры), и погружаюсь в теплый и ламповый светодиодный .net core.
Октябрь 2017. Первый звоночек. Тимлид говорит, что наш фронтедщик зашивается на 4-х проектах сразу и предлагает впилить какой-то там раздел в админке мне. Мотивируя, что, мол, там несложно, просто повтори как сделано для сущности N, только теперь для M. Под смешки коллег, убегающих за гироскутером и вейпом для меня, наспех прочитав какой-то короткий ликбез по Angular, путем копипаста и везения, я рожаю какой-то раздел, который, с оговорками, работает. Тимлид доволен, я в шоке, и вот вроде бы и задачки по бекенду на доске появились...
Но нет. Бекендщиков много, задач для них уже мало, а на фронте — непаханное поле. Новая задача, уже непосредственно на портале, заставляет меня взяться за дело всерьез. Коллеги норовят подвернуть мне штаны и отвести в барбершоп, а я на недельку погружаюсь в чтение материалов по Angular, TypeScript, npm, webpack и прочему. Кстати, отлично помогла статья про современный Javascript для динозавров — https://habrahabr.ru/company/mailru/blog/340922/.
Спустя некоторое время я смог закрывать несложные фронтенд-задачи, чувствуя себя как привратник в отсутствие горничной. Все еще посмеиваясь над шутками про фронтенд, я предлагаю коллегам-бекендщикам собраться и послушать мой бананово-клубничный опыт. Все смеются, но в тайне каждому хочется понять, что за бурление происходит в этом стремительном фронтенде (на самом деле, нет).
Я, не мудрствуя лукаво, пересказываю вышеупомянутую статью с хабра, приправляя ее своими базовыми знаниями по Angular. Митап заходит на ура, и, впоследствии, я провожу целую серию таких — о фронтенде для бекендщиков, что, впрочем, уже не так важно для нашего сказа. Важно лишь то, что их проведение сподвигло меня самого на более глубокое изучение современного стека технологий фронтенда.
Зуд
На работе все складывалось, в семейной жизни тоже наступило довольно спокойное время, и появился зуд. Такой, знаете ли, маленький, еще совсем неокрепший и живущий где-то на границе подсознания. В голове мелькали мысли, мол, три года назад я практически полностью перестал заниматься своим хобби — программированием игр. А что если совместить свежие знания и?..
Я давно смотрел на WebGL, но ранее меня отпугивало отсутствие всякой помощи (типизация, автодополнение) при его использовании. А тут, пожалуйста — несуществующая серебряная пуля во плоти, Тот самый молоток, при взятии которого все становится гвоздями. TypeScript.
Мне, как человеку, познавшему боль с callback hell, var self = this, bind(this), undefined is not a function… Так вот, мне TypeScript показался даже не глотком свежего воздуха, а эдаким волшебником на голубом звездолете, который, спустившись с небес, лениво бросил мне — сынок, забудь все, что ты знал о js раньше, и я покажу тебе, насколько глубока кроличья нора. Аналогия выглядит сумбурной и вообще какой-то солянкой из других аналогий? Верно, как и сам TypeScript с первого взгляда.
Классы, интерфейсы, типизация, async-и, отлавливаемые при компиляции ошибки и опечатки, автодополнения в коде, работающие без бубна, понятное поведение let вместо обескураживающего fuck-the-scope var. Конечно, спустя время, я осознал, что спонсором большей части этого счастья является вовсе не TypeScript, а сам JavaScript, в его современной редакции (ES6 и выше). Но на тот момент казалось, что я нашел пророка, что приведет фронтенд к светлому, коммунистическому будущему. Добавьте к этому достаточно серьезный прогресс различных IDE в области Intellisense и помощи разработчику, и, может быть, поймете мой щенячий восторг.
Беспомощность
Первый серьезный удар под дых нанес мне сам TypeScript. Попытки прикрутить его к пустому проекту дали мне понять — я ни черта не смыслю за пределами angular и angular-cli, которые делают за меня кучу грязной работы. Мне нужен компилятор? Окей, прикручиваем его в package.json, делаем npm install, запускаем tsc и… ничего. Ах, его надо установить еще глобально? Опуская унылые подробности моей войны со всем этим добром, скажу, что некоторое время спустя я научился превращать main.ts в main.js. Но впереди меня ждала возня с webpack.
Да, сейчас я осознаю, что можно было обойтись и без него. Но когда в неумелых руках Typescript, то все вокруг похоже на angular-cli. Это уже после внедрения webpack я узнаю, что и сам компилятор TypeScript умеет «следить» за изменениями в файлах, что проблему с import/export можно решить без webpack-а.
Преодоление
Спустя примерно неделю я смог создать проект, в котором я писал TypeScript-код, нажимал «Сохранить» и все получалось так, как надо — webpack автоматически пересобирал все мои файлы в единый бандл, предварительно прогоняя по ним компилятор TypeScript, а затем загонял билд в отдельную папку, куда копировал прочие статические вещи, а в браузере в этот момент lite-server перезагружал страничку. Я сказал, что все происходило автоматически? Нет, все происходило автомагически. Радости не было предела, и я сел писать простенький аренашутер.
С чего начинается родинаигра для меня? Конечно, с базовых вещей, вроде математики векторов и матриц. Синдром Not Invented Here я успешно преодолел на работе, но в хобби он никуда не делся. Мне не хотелось готовых библиотек, поэтому я сел писать свою математику. Нет, это слишком громко сказано. Я открыл свой предыдущий фреймворк на FreePascal (https://github.com/perfectdaemon/tiny-glr/) и начал конвертить математику оттуда.
Забегая вперед, скажу, что и в целом весь мой новый фреймворк является конвертацией старого из Free Pascal в TypeScript. Учитывая более чем трехлетний перерыв в геймдеве, я не смог придумать архитектуру лучше, или даже вспомнить, какие минусы были у прошлой.
Через некоторое время я начал выдыхаться: мотивация естественным образом снижалась, нагрузка на работе возрастала. А тут на igdc объявили очередной конкурс. А конкурс, поверьте — это отличный внешний мотиватор, устанавливающий тематические, технические и временные ограничения.
Конкурс
Я уже говорил в прошлой своей статье, что такое безумиеigdc, но кратко повторюсь здесь — это такое теплое, ламповое сообщество, проводящее короткие или средней длины конкурсы по разработке игр на заданную тему. Без денежных призов, именитых спонсоров и гарантий трудоустройства. Ах да, еще и без рекламы, на энтузиазме и с привлечением добровольного доната на хостинг и домен. И так уже почти 15 лет.
Тема конкурса — Garbage Game. Дан обширный и разнородный пак графических ресурсов. Задача участников — сделать игру, используя только его. Жанр и тема не ограничены, допускается использование любых звуков и музыки, так как в паке только графика. Есть историческое техническое ограничение, связанное с запуском строго оффлайн, без инсталляторов, докачивания и прочего.
Последнее является небольшой проблемой, ввиду того, что Chrome и компания много чего запрещают, когда пользователь открывает html-файл локально. WebGL может не включиться, скрипты не захотят подтягиваться, не говоря уже о графических ресурсах. Выход есть — делаем локальный супер маленький веб-сервер и скрипт запуска для пользователя, который его поднимет и откроет пользователю его любимый браузер по нужному адресу.
Задача несложная, я уложился в 6-килобайтный exe на C# и bat-ник рядом, определяющий корневую папку для сервера и порт.
Но, довольно об этой маленькой помехе на пути к большой и чистой цели — написанию игры для конкурса.
Игра на конкурс
Взглянув на ресурсы, я понял, что аренашутер с их использованием получится вполне добротный.
Закодив базовые вещи, я приступил к выбору необходимых мне ресурсов. Итоговая нарезка в виде текстурного атласа выглядит так:
К слову, некоторые вещи, запеченные на атласе, так и не были использованы в игре. После загрузки ресурсов я приступил к новому для себя функционалу — спрайтовой анимации. Радовался как ребёнок, когда всё получилось:
Своя физика
Подключать чужую физику с учётом безудержного желания изобретения всего своего выглядело, по меньшей мере, кощунственной идей, и, как мне кажется, даже не рассматривалось моим полным энтузиазма организмом (я только что победил спрайтовую анимацию, помните?). К тому же, что может быть сложного в простой обработке столкновений на основе прямоугольников (Axis-aligned bounding box, AABB) и окружностей? Много чего. Начиная от застревания персонажа невидимыми углами и заканчивая тем фактом, что вы приводите все объекты своего мира к прямоугольникам и окружностям.
Решить все проблемы не удалось, но какая-никакая физика всё-таки появилась:
Звуки
Подспудно ожидал, что будет сложно. На деле вышло как-то подозрительно просто и работоспособно. Создали аудиоконтекст, создали аудиобуфер, положили туда музыку/звуки в любом приемлемом для браузера формате (я воспользовался wav), создали node и сказали, из какого буфера играть.
Сами звуки я генерировал с помощью bfxr (https://www.bfxr.net/).
Хаки
Костыли, ad hoc решения, подхачивания — сложно найти человека, которому эти слова знакомы только понаслышке. К концу конкурса я вставил пару знатных подпорок.
Не было вывода текста — использовал html
К концу конкурса стало ясно, что перенести рендеринг текста в движок я не успею. Даже без генератора. Вариантов было много, вот четыре основных:
- Рендеринг текста на прозрачном 2D-канвасе, который располагается поверх канваса с WebGL.
- Сделать игру без текста
- Запечь необходимые слова и оперировать ими как спрайтами
- Выводить HUD в html
По условиям конкурса, можно было использовать только предоставленный кастомный пикселявый шрифт, поэтому первый вариант (рендиринг текста на 2D-канвасе) отпал. Рендерить в канвас можно, используя только системные шрифты. Шрифты, загруженные в css через fontface, работают крайне нестабильно.
Игра без текста — это отличный и креативный вариант. Жаль, что с креативностью на тот момент у меня было уже плохо.
Необходимый минимум запаса слов оказался большим и раздувал размер ассетов. К тому же, показ чисел все равно требовал определенной логики.
И, наконец, мы добрались до настоящего костыля, который я и использовал — я просто реализовал HUD в виде html-разметки под канвасом, подключил @fontface в css, а обновление организовал через стандартное DOM API (getElementById).
Рестарт игры — document.location.reload()
Когда игрок погибает, то он хочет или закрыть игру, или начать ее заново. К сожалению, если не предусматривать некий restart-механизм с самого начала и размазать все состояние игры по куче плохо организованных классов — никакой красивый restart не получить. Всегда будет волшебное место, которое не было обнулено.
Отличным костылем в такой случае получилась банальная перезагрузка страницы через упомянутый в заголовке document.location.reload(). В условиях малого размера итогового билда (менее 300 кб, включая все ресурсы) и того, что игра работает локально, скоростью перезагрузки можно пренебречь.
Что получилось
Попробовать онлайн можно тут: https://perfectdaemon.github.io/151/index.html
Ссылка на репозиторий: https://github.com/perfectdaemon/ts-game
Версия репозитория с исходным кодом этой игры: https://github.com/perfectdaemon/ts-game/releases/tag/0.1.0
Заключение оно же — приятное окончание
В заключение хотелось бы кратко обрисовать субъективные плюсы и минусы написания игр на WebGL + TypeScript.
Плюсы
- Write once, play/debug everywhere. Очень круто, что я могу просто выложить игру на github.io, и в нее может поиграть кто угодно с любого устройства с браузером. Естественно с рядом оговорок.
- После определенных настроек и добавления TypeScript-а, работать с современным JS не так уж и плохо.
- Удобно создавать контекст (по сравнению с Win API и OpenGL)
- Есть удобный инструмент (Visual Studio Code), который отлично комплектуется плагинами и действительно помогает в написании кода через подсказки, сниппеты и отметки о проблемных местах.
Минусы
- Javascript работает в один поток. И проблема даже не в том, что хочется считать физику или логику в отдельных потоках. Профилирование показало, что вызовы WebGL — блокирующие, т.е. они дожидаются возврата управления из драйвера видеокарты, прежде чем приступать к исполнению кода дальше. Хотя большая часть вызовов WebGL не возвращает ничего, кроме void. Десктопные реализации OpenGL (в некоторых драйверах) у многих функций возвращают управление мгновенно, что значительно повышает скорость рендеринга.
- Небольшой шаг влево/вправо — и вы совсем одни. Возникла проблема с Angular и Typescript? Вам помогут первые две ссылки на stack overflow. Возникло желание подружиться с Typescript отдельно и возник странный баг/вопрос? Готовьтесь к тому, что вы вполне можете оказаться в одиночестве. Конечно, я, как человек ранее писавший на очень популярном (нет) стеке FreePascal + OpenGL, к этому привычен. Но в случае зрелого языка, вроде C++, всегда найдется индус, который уже решал такую проблему. А затем и китаец, который решил ее с помощью контроллера от Guitar Hero и Arduino.