По первому плану задумывалось следующее: минимум графики, минимум UI и всего что только возможно по минимуму, игра должна была быть в стиле сегодняшних казуалок, которых на рынке так же много, как и Match-3. В итоге наша цель была следующей, круглые фишки соединяются в заданную фигуру, перемещаясь свайпом в 4х направлениях. Кто уже играл в Cubicity, тот знает, что от этой задачи мы далеко не ушли, но по остальной части совершили довольно большой скачок, как для команды состоящей всего с двух человек.
Если кто-то из читателей ожидает найти здесь секрет успешной и быстрой разработки игр, то знайте, секрета нет. Мы не делимся тут большим опытом или знаниями, здесь описана только история одного проекта небольшой компании. Успешного или нет, нам еще не известно. Но для многих из вас, наши читатели — это послание из прошлого от самих разработчиков.
Возвращаясь к истории создания Cubicity, работаем мы в основном только на Unity и стандартный набор любого уважающего себя Unity разработчика здесь: Newtonsoft.json, Zenject, Cinemachine, Dotween и т.д… Как видели выше, первый прототип игры выглядел именно так, кубы и блинчики. После недели раздумий, как разнообразить игру и завлечь игроков, пришла гениальная мысль… Посмотреть на Asset store кубических или круглых персонажей. Ну и понеслась, несколько паков с персонажами были куплены без раздумий. Такая же ситуация произошла и с блоками по которым сейчас перемещаются персонажи. Также составили список новых элементов геймплея, со списка примерно в 30 новых плюшек, отобрали для начала нейтральные вещи, такие как: перенаправляющие блоки/стрелки, лифт и телепорт. Остальное решили оставить на новые уровни и внедрять их по одному в 30-35 уровней.
Честно, не можем вспомнить, что нас побудило на первых порах сделать так много уровней, но как есть, и в первый релиз пошло 95 уровней. Очень много на самом деле и мы не раз об этом пожалели. Почему пожалели? А потому, что игра была сырой и много чего изменялось по ходу. Приходилось довольно часто получать дозу «дня сурка», заходя в каждый из 95 уровней и внося изменения. На все уровни ушло 2 месяца непрерывной работы. Это не были уже на 100% готовые уровни, но очень близко. В продуктивные дни, 10 уровней не составляло особого труда переместить с головы на бумагу, а после и в сцену. Но были и те дни, когда чувствуешь себя Хенком Муди из Блудливой Калифорнии, переживающим творческий кризис, думаешь всё, иссяк, но наступает новый день и новые идеи.
Если говорить про визуальную составляющую, то тут все несколько сложнее. Отрисовка как и в большинстве игр проводится во вне экранную поверхность с разрешением меньше нативного и блитится в основную поверхность, но UI для четкости и читабельности рисуется без каких либо изменений в разрешении. Таким образом, мы получаем лучшее от двух миров – не размытый UI, но и не слишком прожорливый рендер в игре. Для сглаживания было экспериментальным путем выбрано 2x MSAA + FXAA, как те которые дают лучшую картинку при наименьших затратах ресурсов. Здраво рассудив, что логической игре ни к чему 60 кадров в секунду, мы решили не изобретать велосипед и установить лимит кадров в 30fps (чего уж говорить, даже консоли обычно этим занимаются). Установка лимита кадров позитивно сказывается не только на потреблении энергии, но и на нагреве телефона, что в свою очередь не дает телефону тротлить из-за перегрева.
Нелегкое решение ждало нас впереди, и это Finish points. Поскольку при каждом запуске уровня персонажи выбирались рандомно из доступных игроку, то рисовать какую либо миниатюру фигуры из персонажей было бы проблематично. Можете не верить, но именно эту задачу решали дольше всего и оттягивали на потом. Кубы на финишах не казались тогда столь жуткой идеей, и бумажная живопись помогала пройти уровень и довести каждого на свое место. После было принято решение вместо кубов использовать тех же персонажей но поменьше, стало лучше, но только для нас. Еще спустя несколько дней, этих персонажей развернули и подсветили, стало гораздо понятнее кто есть кто, но все еще не удовлетворительно. Окончательный вариант был принят еще через месяц, методом проб и ошибок, и еще пара недель уходит на создание иконок для финишей. До свидания лето, скоро с тобой вновь встретимся!
На наш скромный взгляд, тучи у нас получились довольно приятными на вид. Но по факту это простейший и не очень грязный хак. Когда только решили добавить тучи, то первая мысль была, сделать задник 360 видео. Этот подход не оправдал себя, так как для мобильных платформ желательно уместить игру в лимит размера для скачивания по LTE. Чтобы видео выглядело чуть лучше, чем отданное на растерзание шакалам сжатия, ему самому нужно было выделить 10-15 Мбайт, что в сочетании с наличием в игре ночных уровней со своими тучами, слишком много (весь конечный билд игры на Android занимает 61 Мб). Вторым желанием было написать свою систему для облаков, это было заманчиво как для разработчика, но как для человека, который хочет закончить игру поскорее это не подходило. Решение пришло в виде создания текстуры для облака и создания системы частиц с бесконечным временем жизни частицы, и также ограниченным количеством частиц в общем. После добавили случайные размеры между двумя константами на ряду со случайным вращением. Результат был более чем удовлетворительный – наше небо заполнилось облаками, которые были миловидны и не вызывали у нас желание плакать глядя на них.
Тени в игре (в мобильной версии) полностью состоят из квадов, которые просто расставлены вручную, так как не хотелось добавлять реальные тени в мобильную версию. Одной из причин является отсутствие мягких теней на мобильных платформах с OpenGLES 2.0, ну и конечно деградация производительности на слабых устройствах.
Как говорили ранее, для сглаживания мы использовали 2x MSAA + FXAA, но это еще не все! Также к нашему процессу пост обработки добавлен AmplifyColor – отличный ассет за свои деньги, позволяющий применять разные Lut-ы на пост обработке. При правильно подобранном lut, картинка становится лучше. В процессе разработки, мы пробовали разные подходы, включая стандартный unity post processing stack, но в билде его шейдеры и варианты занимали столько, что ни в сказке сказать, ни пером описать. Некоторые решения были очень красивы, но работали крайне плохо на телефонах не первой свежести (поверьте, если вы думаете, что у всех сейчас хотя бы ‘нормальные’ телефоны – вы ошибаетесь. Огромное количество людей, до сих пор ходят с китайцем за 40$ и жалуются вам в комментах, что на их микроволновке ваш DOOM не идет).
Баланс игры — это всегда не просто и даже сейчас всплывают мысли, а не слишком ли сложные уровни, а не часто ли сложные уровни выпадают и т.д. Отбалансив, как могли одной левой ногой, решили внедрить инструменты для облегчения жизни игроку (Ход назад, Бомба, Ледяной блок, Телепорт), и да, стало жить проще, но не нам, а только будущим игрокам. У нас же работы и багов прибавилось.
Добрались к меню игры, силы и нервы на пределе, творческая натура ударила по тормозам, и не будем утаивать, пришлось вдохновляться другими играми, за что им огромное спасибо. И вот «На утро вышла черепаха!». Не прямо на следующее утро, но вышла, UI был готов по предварительно созданным макетам.
Желание быть стильным, модным, молодежным не обошло и нас. Мы решили добавить облачные сохранения и в целом не пожалели об этом. Это не было самой простой задачей, так как на разных платформах, разные провайдеры облачного сохранения. На Steam — это Steamworks, для мобильных – GooglePlay и GameRoom. Так что пришлось унифицировать систему сохранения для возможности подмены для нужной платформы. Для начала мы решили использовать EasyMobile для этих целей, но увы, рано или поздно отказались от этой идеи. Плагин сам по себе хорош, и имеет огромное количество возможностей, но сама специфика работы с нативными облачными хранилищами нам не очень понравилась. Как результат выбор пал на Firebase Realtime Database и аутентификацию через Facebook. Если коротко, то пришлось пройти 7 кругов ада, чтобы это все заработало (и тут дело не в программировании, а скорей в 100500 настройках, которые нужно сделать в 100500 местах приложения и кабинетах в Facebook, Firebase и т.д.). Так же в базе есть лимиты по трафику и чтобы экономить его, мы каждый раз при записи создаем GUID и записываем его как в базу так и на устройстве. Таким образом если мы видим что GUIDы на устройстве и в облаке совпадают, мы можем быть уверенными, что не нужно вычитывать все данные из облака, а можно пользоваться локальной копией данных. В результате синхронизация была добавлена, но… Одним из самых странных для нас багов, было неочевидное поведение Firebase Database в некоторых случаях. Так как мы используем Json, мы сериализируем классы предназначенные для хранения состояния, но Firebase иногда ведет себя несколько странно.
Если мы передаем Firebase для записи объект-словарь, например такого вида:
var dict = new Dictionary<int, SlotState> { { 0, new SlotState() }, { 1, new SlotState() }, { 2, new SlotState() };
Когда мы будем считывать его из базы, мы получим не объект Json, а массив Json (What?)
Ну вроде, понятно, будем использовать везде списки и не будем испытывать проблем, да? Но не тут-то было.
Если мы запишем в Firebase:
var dict = new Dictionary<int, SlotState> { { 0, new SlotState() }, { 1, new SlotState() }, { 100500, new SlotState() };
Или даже:
var dict = new Dictionary<int, SlotState> { { 0, new SlotState() }, { 1, null }, { 2, new SlotState() };
Когда мы будем читать его из базы, мы таки получим Json объект с ключами и значениями.
Понять логику разработчиков в какой то степени можно, но это может привести к багам, которые возможно проявятся лишь через время (Помните про вышесказанные GUID добавленные для сохранения? Как результат редкие чтения из базы при относительно частых записях в нее).
Когда релиз? Этот вопрос слышали чаще всего. Но нужно было основательно подготовиться к этому дню. Составить список маркетов, выбрать дату релиза, избегать крупных распродаж, довольно много нюансов, из-за которых релиз сдвинулся, как минимум на 2 месяца. Послушав совета одной статьи, выбрали вторник и среду для релиза. Решили точно заказать обзор на 4pda, закинуть новость о игре на несколько форумов и бомбить соц.сети в частности Instagram (конечно же платно). Что из всего этого сработало, мы с вами узнаем во второй части этой истории, но уже позднее.
Что имеем в итоге? Создать быстро игру – это не всегда быстро. И не исключено, что ожидаемые сроки создания игры придется умножить на 5. Обзаведитесь людьми, которые смогут помочь вам дельным советом в незнакомых вам отраслях. Расслабляйтесь при любой возможности, так как создание чего либо, не только игр, забирает много сил. Негоже подбираясь к релизу быть вялой сосиской и быть менее полезным, чем на старте проекта. Ну и деньги, ищите деньги, они вам понадобятся. А от нас, спасибо за внимание, удачи и до встречи в следующей статье.
Комментарии (27)
Bookvarenko
08.03.2019 22:05+1Отличный ход с тенями и облаками!
thepirateparrot Автор
08.03.2019 22:10Спасибо! Возможно позже составим статью с лайвхаками подобного рода. За годы разработки на Unity накопилось уже.
vassabi
08.03.2019 23:08почитал про сложности с хранением данных.
А почему не сделать сериализацию сложных данных в строку (Json <-> String) на своей стороне, а в Firebase передавать пару (ключ, строка)?thepirateparrot Автор
08.03.2019 23:31На самом деле мы именно так и делаем и сериализируем используя newtonsoft.json и записываем через SetRawJson однако, у firebase на этот счёт свои мысли и похоже происходит ещё и преобразование на стороне firebase :)
Про передачу ключ строка не думали так как дальше планируем добавлять возможность для игрока смотреть прогресс друзей из того же facebook и это будет не так удобно в итоге. Ну и бд на то и бд что бы использовать ее как бд :)
Плюс эффективность в трафике. Я не ручаюсь за это, но похоже что исходя из того что я вижу (объем передаваемого трафика), firebase имеет свой протокол который так или иначе уменьшает объем передаваемых данных исходя из структуры json. Если же использовать строку, это скорее всего сойдёт на нет. Не добавлял это в статью так как пока это на уровне домыслов и сопоставлений с тем что вижу.vassabi
09.03.2019 13:27ну, дело ваше, тем более что в комментариях ниже советуют firestore.
Просто я для себя давно вывел, что если данных немного (меньше сотни килобайт), то быстрее паковать в строку\из строки самому (начиная от элементарного «в массив чисел» и заканчивая msgpack и zip), чем морочиться с особенностями интерпретации объектов.thepirateparrot Автор
09.03.2019 13:34Про msgpack не слышал раньше. Пожалуй пойду покурю про него. На первый взгляд интересная штука.
Prizrak
09.03.2019 20:54+1А почему для сериализации вы не используете стандартные компоненты JsonUtility?
thepirateparrot Автор
10.03.2019 00:04JsonUtility имеют свои ограничения вроде отсутствия сериализации для dictionary, null, свойств, полиморфных типов и кучу других ограничений. Да и по факту это бы никак не повлияло на проблему с преобразованиями внутри фаербейз.
Prizrak
10.03.2019 00:12Я правильно понял про newtonsoft.json. И оно платное?
Я о нем не знал, но попробую. Спасибо.
dictionary да не работает. Но я его сохраняю через OnBeforeSerialize/OnAfterDeserialize
null по логике, это пустота, зачем её сохранять. Ну ситуации разные.
Может я со сложными элементами при сохранении не сталкивался.
А если в фаербейз передавать json как простую текстовую строку. А потом текстовую строку получать обратно и преобразовывать во что нужно. Или в массив или в объект.thepirateparrot Автор
10.03.2019 01:50Да, про него. Он opensource. Единственное что для Unity при работе на il2cpp AOT, там есть проблемы. Я использую этот плагин который по сути является форком с улучшеной совместимостью (Там вообще какая то чихарда с именованиями, так что могу слегка приврать. По крайней мерее там точно есть Newtonsoft.dll). Он так же бесплатный.
Ваш подход возможен, но не очень правилен с точки зрения работы с базой данных. Я чуть выше другому человеку уже коментировал почему не стал так делать. Не буду еще раз дублировать. Но он бы стработал :)
mrigi
09.03.2019 00:44Я может не совсем пойму в чем у вас трабла с dictionary, но если вы прям в таком виде скармливаете его в api firebase, то в попытке сериализовать этот dictionary первым делом найдется его enumerator и естественно преобразуется в массив.
thepirateparrot Автор
09.03.2019 01:09Сериализация происходит на нашей стороне силами newtonsoft json. На самом деле, это не так проблема, как неожиданность. Когда пишешь массив, ожидаешь считать массив. То есть например массив:
{ new Something(), null, new Something() } после сериализации newtonsoft таки массив. После передачи в firebase и считывания обратно, это уже будет объект:
0: Something
2: Something
Если знать про это поведение, его обыграть не составляет труда.
Я пожалуй не лучший пример привел в статье. Подправлю этот момент.
Спасибо за замечание.
mrigi
09.03.2019 02:20+1Мы сейчас тоже делаем проект на firebase, но с использованием firestore, а не realtime database. Массивы всегда массивы, а объекты всегда объекты. Может и вам стоит попробовать.
thepirateparrot Автор
09.03.2019 02:44Я хотел, однако firestore на тот момент был в бета тесте. Не хотелось иметь больше проблем. С другой стороны вероятно их было бы меньше. В следующем обязательно firestore.
AlexBashkankov
09.03.2019 11:01"… довольно большой скачек"
nidalee
09.03.2019 16:321. Выделяете фрагмент текста с ошибкой (любую — грамматическую или пунктуационную, на ваше усмотрение) в публикации;
2. Жмём хоткей CTRL+Enter (или CMD+Enter);
3. В нижней части экрана появляется форма, в которой будет процитирован выделенный ранее (в п.1) текст, а также поле для опционального пояснения.justhabrauser
09.03.2019 19:09… и пытаемся пройти квест с бестолковой капчей.
nidalee
09.03.2019 19:40Я уже с десяток сообщений написал, капчи пока не видел. Может, вам с сетью не везет?
abmanimenja
09.03.2019 14:02+2Ну а где про девушку-то?
Кроме заголовка.thepirateparrot Автор
09.03.2019 14:05Роли в разработке мы не освещали, но в целом речь идет от нас обоих. Фактического разделения в статье нет, но каждый говорил за себя и свою часть.
Anton23
Спасибо за историю, интересно было почитать.
thepirateparrot Автор
Спасибо. Приятно как Хабр тепло нас принимает.