Как стать автором
Обновить

Трагикомедия в NaN актах: как мы cделали игру на JS и выпустили ее в Steam

Время на прочтение 13 мин
Количество просмотров 28K
“Эка невидаль”, — скажете вы, — “В топ-100 вашей игры нет, так что нещитово”. Тоже правда. Зато за год разработки Protolife мы поднакопили какой-никакой опыт, которым можем поделиться с потенциальными будущими игроделами. Ветераны индустрии, боюсь, ничего интересного для себя не найдут. Но, может быть, хоть повеселитесь от души.


Что за игра-то? И кто “мы”?


Мы — это команда из трех человек (GRaAL, A333, icxon), волею судеб названная Volcanic Giraffe без какого либо умысла. Работали долгое время вместе, несколько раз втроем участвовали в Ludum Dare (соревнования по написанию игр за выходные), и однажды решившие довести до релиза одну из наших поделок под названием Protolife.

Если коротко: это необычная tower defense, где надо бегать героем-курсором и выстраивать оборону из блоков против постоянно растущей красной биомассы.

Из комментариев к черновику статьи:
icxon: надо хоть немного про геймплей сначала написать. А то какие-то скриншоты на которых хрен пойми что происходит

Если расписать подробнее, то что есть в игре:

  1. Есть набор уровней, на каждом уровне есть наша база и наш аватар — робот-строитель.
  2. Часть уровня заполнена красной растущей биомассой, которая извергает из себя тонны мобов.
  3. Мобы, ясно дело, бегут к базе и пытаются её сокрушить. А мы строим оборону из башен и сделать это не даем.

“Но что же тут необычного?” — спросите вы. А основные отличия от большинства tower defense таковы:

  1. Роботом-строителем мы управляем напрямую с клавиш, т.е. надо вручную носиться по уровню и успевать все строить/чинить.
  2. Все что умеет робот — это строить и демонтировать синие блоки. Один такой блок не делает ничего полезного, но несколько блоков, выложенные по определенному шаблону, превращаются в полезное строение. Примеры шаблонов:


  • Шаблон 1: простая пушка
  • Шаблон 2: стена
  • Шаблон 3: АА-пушка. здесь приходится задействовать еще и желтые кристаллы, которые добываются уже посложнее
  • Шаблон 4: пулемёт

Так и проходит игра: бегаем, строим, получаем ответку, спешно чиним. Прямо визуализация процесса разработки игры получается.

Но такой игра получилась не сразу. В далеком апреле 2017 года, на Ludum Dare 38 она выглядела так:

Если не обращать внимания на разницу в графике, то изменилось вроде бы не так уж и много. Уже на LD-версии был курсор-строитель, постройка башен из блоков и противостояние с красной биомассой. Это мы оставили, потому что это работало и это нравилось людям.

Но на самом деле под капотом изменилось очень многое помимо концентрации графония на квадратный пиксель. На пути длиной в год нам приходилось принимать различные решения — геймплейные, технические, организационные, и даже маркетинговые. Некоторые были удачными, некоторые — не очень.

Вот о таких решениях я и хотел бы рассказать.

Сразу оговорюсь — статей, вероятно, будет несколько, т.к. материала, если начать копать, вылезает много. В конце этой статьи вы найдете небольшой опрос — какие темы вам были бы наиболее интересны. Ну и ссылки на саму игру тоже.

Скучная история создания.


Не думаю, что реально кому-то интересно как игра вообще придумалась, поэтому уберу под спойлер.

Заходят как-то Конвей, Мэтисон и Петри в бар…
А бармен и говорит: undefined is not a function.

Накануне LD38 выяснилось ужасное: наш коллега и единственный художник A333 будет весь LD лететь над атлантикой, и помочь нам не сможет. Поэтому надо было оставшимися силами сделать игру максимально нетребовательную к графике.

Темой кстати было “Small world”. А дальше во время брейншторма все пошло примерно так:
  • Художника нет — графика простая, примитивная
  • Small world — небольшие размеры, или может что-то микроскопическое, типа всяких микробов
  • Микробы — это типа микро-жизнь
  • А life — это такой клеточный автомат. Ну вот, клетки во всех смыслах. Давайте игрок будет воевать клеточным автоматом против другого клеточного автомата.
  • Потыкали Conway Life — не годится. Игрок, который плохо знаком с правилами автомата, скорее всего построит что-то, что само собой уничтожится. Очень сложно контролировать. Давайте по правилам life будет только противник, а мы будем строить упорядоченные структуры.
  • … но тогда противник будет то и дело самоуничтожаться. Ладно, подправим для него правила. Пусть он только растет, а уничтожать его надо уже нам.
  • Так, у нас уже есть: красные клетки противника, которые только растут, и синие наши, которые мы строим сами по заданным шаблонам. Строить мы будем башни и стены, т.е. Получается такой tower defense. Для разнообразия не хватает еще движущихся врагов (мобов).
  • Накануне я перечитывал в очередной раз “Я — легенда” Мэтисона. Так главный герой по ночам держит осаду от вампиров, а днем, когда вампиры неактивны, восстанавливает оборону, а так же расширяет сферу влияния. Это звучало как неплохой геймплейный элемент, так что в игре появились фазы дня и ночи. Ночью вражеские клетки делились и набигали домики наползали мобы, днем — все было тихо, можно было контратаковать.
  • Обзываем наши цветные пиксели “микроорганизмами” и засовываем в круглую арену — чашку Петри.
  • Берем наш любимый движок Phaser.js…
  • … и 31 место у нас в кармане

Вот такая история, не шибко интересная, как я и предупреждал.

“Но Алексей, какого черта ты её тогда написал?”. Резонный вопрос. Дело в том, что чуть ли не первый комментарий к игре звучал как “лол, вы все содрали с Creeper World”. Ознакомившись с игрой много позже собственно LD я понимаю, почему люди так думают. Но все еще немного обидно.

Если вдруг вам тоже стало обидно, что вы потратили время на это нытье, то в качестве утешения напишите мне в личку кодовое слово “скукотища”, и я пришлю вам один из 10 ключей, если к тому времени они не закончатся., к сожалению, ключи тоже закончились.


Выбор игры для полноценной реализации


Как это часто бывает, однажды нам просто подумалось всем втроем — а не пора ли уже попробовать сделать полноценную игру. Часто на этом этапе берется какой-то концепт из головы, а-ля “игра мечты”, и с ним ведется работа. А дальше как повезет угадать с желаниями аудитории.

Наша задача немного упрощалась засчёт наличия небольшого “портфолио” на Ludum Dare. Мы видели, как люди реагируют на разные игры, и могли сравнивать. Protolife имел наибольший отклик, его хвалили за оригинальность и за интересный геймплей — это при нулевом уровне графики и всего 4х уровнях!

Кроме того, принять решение нам помог itch.io, на котором мы публиковали наши поделки. Как оказалось, есть люди, которые ходят на itch.io поиграть в веб-игры. Некоторые из этих них любят жанр tower defense, и 5-10 человек заходили (и до сих пор заходят!) поиграть в тот старый Protolife каждый день.
Статистика заходов до релиза игры в Steam

Можно сказать, это было наше первое маркетинговое исследование. Мы подумали и решили, что “нестандартный tower defense” вполне может выстрелить. Tower defense-ов не так много, многие из них похожи как две капли воды, и выделиться среди них мы вполне можем.

Забегая вперед, могу сказать, что тактика себя оправдала.

Непрошеный Совет №1: Лучше не действуйте вслепую. Идеальная у вас в голове “игра мечты” может оказаться никому не нужной. Если уж не участвовать во всяких джемах, всегда есть смысл набросать геймплейный(!) прототип и потестировать его на себе, друзьях и знакомых.

Контрсовет: Если вас не пугает, что в “игру мечты” будет играть полтора человека, то какое кому дело? Делайте! Может же и повезти.

Движок: Не самое лучшее решение


Версию на Ludum Dare мы делали на движке Phaser.js. Мы неплохо знаем его, неплохо знаем javascript, веб-игры получают больше фидбека, он довольно удобен и прост в изучении — сказка, а не движок.

И перед нами встал важный вопрос: менять движок или оставить все как есть?

Вопрос был сложный. Ни одного другого движка никто из нас не знал и не изучал на тот момент. Тратить время на изучение — штука хорошая, но так можно было и весь энтузиазм растерять. И потом — что взять? Javascript — единственный язык, который знал каждый из нас, брать C++/Java/C# движок — значит моментально лишиться половины разработчиков. Да и движки уровня Unity на тот момент казались слишком громоздкими для “простой 2D игры”.

И потом: вот же есть уже игра. Осталось обновить графен, доделать уровней — и все. Работы на пару месяцев. А тут еще изучать, переписывать…

В общем, решили мы остаться на Phaser.js. Еще хуже — мы решили остаться на той же кодовой базе, т.е. строить игру поверх прототипа Ludum Dare.

Из комментариев к черновику статьи
a333: Жалко, у меня не осталось той картинки с костылем вместо Эйфелевой Башни”

Непрошеный совет №2: никогда так не делайте! Особенно это касается переиспользования прототипа. Код на джемах всегда пишется быстро и грязно, без учета будущего развития. Там костыль, сям костыль — и вот вы понимаете, что пишете сразу legacy-код, и моментально начинаете от него страдать. Прототипы надо внимательно прочитывать, а потом сжигать в /dev/null и писать заново, только уже набело и начисто.

Контрсовет: учитывайте, однако, особенности психологии. Бывает, что промедления в пару недель хватит на то, чтобы “остынуть”. Лучше сделать игру с плохой архитектурой, чем не сделать вообще.

Из комментариев к черновику статьи:
A333: я сильно склоняюсь к этому варианту. Насколько я знаю, именно так и делается большая часть игр, потому что время и запал играют ключевую роль. Если у вас есть возможность переписать всё начисто на новом движке — хорошо, но так бывает далеко не всегда. Не бойтесь писать быстро и грязно, выкатывать ранние прототипы, копировать и вставлять куски кода, и выпускать забаго… *звуки ударов и приглушенные стоны*

Собственно, в чем проблема именно с Phaser.js

  • Движок, как можно догадаться, вебовский. Знаете, как релизить веб-игру в Steam? Правильно, выдавать ее вместе с Хромом, используя nw.js или Electron. Думаю, минусы такого подхода объяснять не надо.
  • Производительность javascript конечно весьма неплоха, но нативный код выполнялся бы быстрее, и можно было бы не экономить на спичках.
  • Очень слабый контроль за рендером. Phaser сам все рендерит, и делает это в целом неплохо, но иногда хочется влезть в процесс или дорендерить что-то на webGL своими силами. Увы, единственное что позволяет Phaser — применить fragment shader к экрану целиком или каким-то отдельным спрайтам на экране, причем тоже в меру. Работать с vertex shader он не позволяет совсем (да и вообще работать с vertices), а многие решения в игре с ними были бы куда проще.
  • Проблемы с большим числом спрайтов (активных объектов) на экране. Причем “большое число” — это пара тысяч, а не миллионы. И “активным объектом” будет даже лежащий камень без анимаций. На каждый тик Phaser будет проходить по всем объектам и делать свою особую фазеровскую магию, отжирая время у игровой логики.

Многие из этих проблем мы решали по ходу дела, некоторые так и не решили до конца. Сложно судить без опыта работы с другими движками, но мне кажется, что любой другой движок позволил бы обрабатывать тысячи игровых объектов без адских ухищрений. Впрочем, я могу и ошибаться.

Война стилей


Вы же еще помните “оригинальный дизайн” исходной версии игры?


Безусловно он имеет свой шарм, но для серьезного продукта никак не подходил. Да и наш художник как раз вернулся из командировки, что ему, без дела сидеть?

Он и не сидел, сделав несколько набросков возможного дизайна. История сохранила два стула стиля, между которыми надо было сделать выбор.

Кандидат 1: минималистичный дизайн. По сути — те же “пиксели”, только посимпатичнее. Все яркое, контрастное. Выглядит прилично, но, скажем так, несолидно. В том смысле, что выглядящая так игра хорошо будет смотреться на мобилках или ВКонтакте где-то между тетрисом и арканоидом. Зато быстро и дешево.


Кандидат 2: реалистичный, фантастический, суровый. Здесь можно уже увидеть некую историю, контекст. Неожиданно хорошо работает контраст не только цветов, но и форм — наши здания более упорядоченные и квадратные, вражеские строения — круглые, неправильной формы.



Нельзя сказать, что мы очень долго колебались. Мы представили себе, в какой из “скриншотов” нам хотелось бы поиграть больше, и у первого кандидата просто не было шансов выжить. Мы понятия не имели что у нас будет за сюжет, где все это происходит, что происходит — но выбрали вариант два.

Конечно, сравнить не с чем, но мне кажется, что это был удачный вариант. Да, первый вариант мы бы доделали быстрее, он был бы менее требователен к производительности и графике, и наверное его даже удалось бы портировать на мобилки.

Но мы хотели играть во второй вариант.

Так микроорганизмы и чашка Петри сменились на далекую планету, роботов, и таинственный инопланетный организм.

Непрошеный банальный совет №3: делайте то, во что хотите играть сами.

Контрсовет: если у вас 3 ипотеки и нет другой работы, вас никто не будет винить за то, что вы сделаете что-то хорошо продающееся, но от чего хочется помыть глаза и руки с мылом.

Крадущаяся биомасса, затаившийся червяк


Я упоминал контраст цветов и форм, и если сравнить тот скетч и конечный вариант — контраст только усилился. Сравните — квадратики наших строений, круглые вражеские строения, и общая округлость красной массы.


Кстати, красная масса, как и почти все в игре, где-то там внутри игровой логики укладывается в сетку так же, как и блоки. Но выглядит она при этом хаотично, так, как будто она отвязана от сетки.

У меня сохранилась демка, где я отлаживал внешний вид биомассы. Там все устроено следующим образом: берется условный “квадрат” и заполняется кругами разного размера как можно более плотно. Ниже — пример такого заполнения. В самой игре заполнение более плотное (и оттого хуже читаемое).

Между кругами есть линии — связи. Если биомассе надо вырасти в какие-то клетки, то подбираются подходящие круги, пересекающиеся с нужными клетками, и рост происходит в них по линиям.


Вот как это выглядит в движении:


Из комментариев к черновику статьи:
icxon: впервые вижу эту демку, лол
В самой игре все работает точно так же, разве что все размеры, цвета и скорости подогнаны нашим художником, чтобы смотрелось получше.


Ой, я кажется забыл рассказать откуда взялось именно такое решение, как проходили этапы обсуждения, отметание кандидатов…

А нет, не забыл. Их не было. Изначально мы планировали все делать как в версии LD, т.е. рост квадратиками. Просто как-то вечером мне стало скучно, и я набросал эту демку, чтобы потренироваться. А парням зашло. Так и сделали.

Непрошеный Совет №4: планы-планами, но не бойтесь пробовать что-то новое. Интуиция может вам подсказать какое-то удачное решение.

Контрсовет: если у вас уже назначена дата релиза и подписан контракт с издателем — возможно, экспериментировать не стоит.

Анимация биомассы


На гифках выше вы могли заметить idle-анимацию биомассы — даже в покое она усиленно “дышит”, красные бутоны как будто раскрываются и обратно закрываются. В идеальном мире это были бы заботливо нарисованные художником спрайты с анимацией, которые расставлены по той схеме с кругами. В реальности же это были бы тысячи игровых объектов, с которыми Phaser.js просто не справился бы. Причем это уже проверенный факт — в версии с Ludum Dare я уже сталкивался с адскими тормозами, когда биомасса заполняла хотя бы половину карты, а ведь там даже не было никакой idle анимации.

На помощь приходит шейдер, который крутится там себе на GPU и не занимает процессор. Еще лучше, если этот шейдер будет реально простой — он тогда и видеокарту не будет сильно нагружать.

Шейдеру надо как-то рассказать о том, что и как ему рисовать. Какие есть способы передачи информации шейдеру:

  • Хардкод в самом коде шейдера. В нашем случае не подходит, но вообще иногда такой вариант тоже есть смысл рассматривать.
  • Через uniform переменные (это переменные, одинаковые для любого пикселя изображения)
  • Через varying переменные (переменные, которые интерполируются между двумя вершинами)
  • Через текстуры (кодируя цветом какие-то значения)

Метод 3 нам мог бы пригодиться, но в случае с Phaser.js он нам недоступен. Через uniform много не передашь (скажем, массив всех кружочков с их радиусами в uniform-ы не влезет — там есть ограничения). Остается текстура.

Трюк вот в чем: я сначала рисую одно состояние (скажем, закрытые бутоны) синим цветом:


Потом второе состояние (открытые бутоны) — красным:


Если их сложить, то получается фиолетовое месиво:


Шейдер же видит текстуру, видит текущее время, и с определенным периодом показывает нам то “синее” состояние, то “красное”, плавно перетекая между ними. Ну и само собой применяя нужную палитру цветов. Получается вот так:


Из комментариев к черновику статьи:
a333: Ах вот как эта б***я е***а работает
icxon: это классно, потому что я всё ещё не понял как она работает

То же самое, но на примере прямоугольников:

Текстура:

Итоговая анимация:

Текстура обновляется только по мере роста/разрушения, в остальное время работает gpu-only анимация.

Забота об игроке


В процессе разработки мы старались не забывать о игроках, которые видят нашу игру впервые. Это не так просто, как кажется — через некоторое время глаз замыливается, и начинаешь считать очевидным все, что тебе, как разработчику, уже известно.

Мы это понимали, и поэтому первый бета-тест был проведен аж за 8 месяцев до релиза — как только у нас было готово первые 10 уровней и 40-50% от контента. Тот бета-тест дал отличный фидбек по дизайну уровней, про который я расскажу как-нибудь в следующий раз. Одновременно мы узнали, что и сами неплохо предугадали некоторые моменты.

Ситуация: есть база игрока, которую надо защищать — её разрушение приводит к провалу миссии. Игрок же не следит за базой постоянно — он следит за своим аватаром-роботом. Причем следит постоянно — темп игры довольно быстрый, стоять и созерцать времени особо нет. В итоге что мы, что тестеры, иногда не замечали зашедшего в тыл врага, бомбящего базу.

Решение: во-первых, покажем урон по базе концентрическими кругами, идущими через пол-карты.


Во-вторых, если база повреждена значительно, то она самостоятельно наносит ответный удар, сметающий врагов, который хорошо заметен и озвучен. Это привлекает внимание игрока к проблеме, и дает время принять меры.


Ситуация: как в типичном Tower defense, враги идут проторенным маршрутом. Однако этот маршрут не всегда угадывается. А понимать маршрут — очень важно для успешной обороны: некоторые башни стреляют очень недалеко, а слишком близко поставленная башня будет снесена следующей волной.

Решение: рисуем кровь после убийства врагов. Через некоторое время на земле появляется четкий след, на который можно ориентироваться:


Вопрос: а, собственно, почему проторенный маршрут? Вы что, не умеете А* реализовывать?

Ответ: реализовывали кучу раз :) мы честно экспериментировали с AI противника. У нас были умные ищущие дорогу червяки, и слаймы, уворачивающиеся от пуль. Играть становилось невозможно. Игрок не мог построить эффективную оборону и не мог порадоваться, наблюдая как она работает. Удовольствие от игры резко падало. Это не значит, что “умные” враги — это плохо. Просто для выбранной механики — когда наши строения статичны — такие враги не подходили. Для “умных врагов” нужно то ли роботу приделать пулемет, то ли башням — ноги. А это уже совсем другая игра — не та, что понравилась людям на LD и itchio.
Из комментариев к черновику статьи:
icxon: Но слаймы до сих пор есть. И они таки уворачиваются
GRaAL: подумаешь, немного художественного вымысла

Они и правда есть, но уворачиваются куда ленивее, чем в изначальном варианте

Ситуация: у противника есть дальнобойные пушки (мортиры), которые могут находиться за пределами видимости игрока. В самом начале почти все поле скрыто за туманом войны, из-за чего прилет вражеских снарядов мог пройти незамеченным — они вылетали из темноты и тут же наносили урон. Если игрок смотрел в этот момент в другую сторону, он мог так и не понять что же произошло.

Решение: летящий снаряд видно над туманом войны. Это дает игроку возможность заметить угрозу и принять меры.


Кстати про снаряды. Отвлечемся от геймплейно-UX проблем.

Обработка коллизий


В LD версии движущихся объектов было не так уж и много — десяток пулек да десяток червячков в каждый момент времени. В игре этого оказалось мало. Чтобы ощущался challenge, приходилось повышать количество противников, и одновременно давать игроку скорострельное оружие для противодействия. Так что в игре не редкость такая ситуация:


Для всех игровых объектов нужно считать коллизии. Пульки врезаются в червяков, червяки врезаются в блоки, и всех их на экране — иногда несколько сотен. А еще пушкам надо бы выбирать себе цели в радиусе действия. Если в LD-версии мы могли себе позволить итерироваться по всем врагам в поисках подходящих, то здесь это уже начинало тормозить.

В целом, у этой проблемы есть решения, на хабре я не раз встречал статьи на эту тему (вот одна из них). Так как у нас есть логическая сетка на экране, а большинство активных объектов размером не превышают 2х2 игровых клетки, мы решили использовать эту сетку для определения коллизий.

С каждой клеткой связан список объектов, которые находятся в этой клетке. Есть объекты статичные (типа блоков или камней), которые занимают ровно одну клетку целиком и никуда не перемещаются. И есть динамические, которые движутся по сетке, “перетекая” из одной клетки в другую.


Т.к. объект может находиться где-то на границе двух клеток (хотя его центр однозначно лежит внутри только одной), то при учете коллизий осматриваются все соседние клетки. Т.е. берем мы скажем пульку с координатами X, Y, и смотрим что лежит в клетках (X,Y), (X+1,Y), (X-1,Y), (X,Y+1), (X,Y-1). Если там есть объекты, с которыми пулька может взаимодействовать, то уже для каждого из них точно рассчитывается коллизия исходя из формы и размера.

Чтобы два раза не вставать, эта же сетка используется для выбора целей башнями. Башня после создания “подписывается” на определенные клетки сетки. Если в клетку попадает что-то, что может быть подстрелено, башня получает уведомление (и обычно после этого стреляет). Таким образом если тысяча врагов ползает заведомо далеко от башни, башня не будет тратить время на подсчет расстояния до них.


Если карта большая, то клеток в сетке становится тоже много, и все апдейты и проверки начинают занимать больше времени. Также проблемы начинаются если есть какие-то здания/юниты размером в несколько клеток — для них приходится проверять больше клеток вокруг.

Для нивелирования этих моментов у нас поверх мелкой сетки положена крупная, размером 16х16 клеток. У нее считается все то же самое, и она используется для быстрого отсева ситуаций, когда в радиусе нескольких клеток вокруг объектов ничего нет и коллизии можно не проверять.

Обещанные ссылки и опрос


Статья получилась уже достаточно длинной, а мой список тем в блокноте уменьшился дай бог на четверть. Скажите, о чем вам интереснее было бы услышать в следующей статье? Можно голосовать за несколько пунктов.

Ну и если вдруг есть какие-то конкретные вопросы о том как сделано, или почему сделано — пишите в комментариях. Ответим, если сами вспомним :)

Ссылки на игру:


Всем спасибо за внимание.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
О чем сделать следующую статью?
56.99% Про геймдизайн — дизайн уровней, управление, вовлеченность игрока, вот это все. 208
61.1% Про техническую часть — оптимизации, Phaser-specific моменты, особенности релиза js-игры в Steam 223
32.88% Давайте лучше про маркетинг — как продвигали, как сочиняли промо-материалы, кому проплачивали за хвалебные рецензии 120
21.37% Лучше про организацию — как принимались решения? SCRUM, Agile? Тирания, демократия? Кранчи? Скандалы? Интриги? 78
4.66% Не пишите больше ничего, задрали пиксельные инди 17
Проголосовали 365 пользователей. Воздержались 67 пользователей.
Теги:
Хабы:
+87
Комментарии 51
Комментарии Комментарии 51

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн
PG Bootcamp 2024
Дата 16 апреля
Время 09:30 – 21:00
Место
Минск Онлайн
EvaConf 2024
Дата 16 апреля
Время 11:00 – 16:00
Место
Москва Онлайн