Хабрахабр

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

“Эка невидаль”, — скажете вы, — “В топ-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 место у нас в кармане


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

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

Если вдруг вам тоже стало обидно, что вы потратили время на это нытье, то в качестве утешения напишите мне в личку кодовое слово “скукотища”, и я пришлю вам один из 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, веб-игры получают больше фидбека, он довольно удобен и прост в изучении — сказка, а не движок.

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

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

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

Еще хуже — мы решили остаться на той же кодовой базе, т.е. В общем, решили мы остаться на 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.

И они таки уворачиваются
GRaAL: подумаешь, немного художественного вымысла

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Всем спасибо за внимание.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть