Ну как первая… Если не считать релизы только под Android и с десяток заброшенных проектов у финиша, то да, это первая наша игра с замахом больше чем на одну платформу. Как же всё начиналось? А всё просто, работали мы значит над другим проектом, назовем его «проект А», работали уже долгое время и решили, а не сделать ли нам за пару месяцев игру и потренировать на ней наши маркетинговые навыки, а «проект А» выпустим сразу после с большим опытом в продвижении игр. Но звезды не сошлись, петух не свистнул и «проект А» залег на дно ровно на год. Но эта история не о нем, а о логической игре под названием «Cubicity: Slide puzzle».



По первому плану задумывалось следующее: минимум графики, минимум 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)


  1. Anton23
    08.03.2019 20:01
    +3

    Спасибо за историю, интересно было почитать.


    1. thepirateparrot Автор
      08.03.2019 21:53

      Спасибо. Приятно как Хабр тепло нас принимает.


  1. Bookvarenko
    08.03.2019 22:05
    +1

    Отличный ход с тенями и облаками!


    1. thepirateparrot Автор
      08.03.2019 22:10

      Спасибо! Возможно позже составим статью с лайвхаками подобного рода. За годы разработки на Unity накопилось уже.


      1. Bookvarenko
        09.03.2019 07:32

        Было бы интересно ознакомиться, спасибо!


  1. vassabi
    08.03.2019 23:08

    почитал про сложности с хранением данных.
    А почему не сделать сериализацию сложных данных в строку (Json <-> String) на своей стороне, а в Firebase передавать пару (ключ, строка)?


    1. thepirateparrot Автор
      08.03.2019 23:31

      На самом деле мы именно так и делаем и сериализируем используя newtonsoft.json и записываем через SetRawJson однако, у firebase на этот счёт свои мысли и похоже происходит ещё и преобразование на стороне firebase :)
      Про передачу ключ строка не думали так как дальше планируем добавлять возможность для игрока смотреть прогресс друзей из того же facebook и это будет не так удобно в итоге. Ну и бд на то и бд что бы использовать ее как бд :)
      Плюс эффективность в трафике. Я не ручаюсь за это, но похоже что исходя из того что я вижу (объем передаваемого трафика), firebase имеет свой протокол который так или иначе уменьшает объем передаваемых данных исходя из структуры json. Если же использовать строку, это скорее всего сойдёт на нет. Не добавлял это в статью так как пока это на уровне домыслов и сопоставлений с тем что вижу.


      1. vassabi
        09.03.2019 13:27

        ну, дело ваше, тем более что в комментариях ниже советуют firestore.
        Просто я для себя давно вывел, что если данных немного (меньше сотни килобайт), то быстрее паковать в строку\из строки самому (начиная от элементарного «в массив чисел» и заканчивая msgpack и zip), чем морочиться с особенностями интерпретации объектов.


        1. thepirateparrot Автор
          09.03.2019 13:34

          Про msgpack не слышал раньше. Пожалуй пойду покурю про него. На первый взгляд интересная штука.


      1. Prizrak
        09.03.2019 20:54
        +1

        А почему для сериализации вы не используете стандартные компоненты JsonUtility?


        1. thepirateparrot Автор
          10.03.2019 00:04

          JsonUtility имеют свои ограничения вроде отсутствия сериализации для dictionary, null, свойств, полиморфных типов и кучу других ограничений. Да и по факту это бы никак не повлияло на проблему с преобразованиями внутри фаербейз.


          1. Prizrak
            10.03.2019 00:12

            Я правильно понял про newtonsoft.json. И оно платное?

            Я о нем не знал, но попробую. Спасибо.

            dictionary да не работает. Но я его сохраняю через OnBeforeSerialize/OnAfterDeserialize

            null по логике, это пустота, зачем её сохранять. Ну ситуации разные.

            Может я со сложными элементами при сохранении не сталкивался.

            А если в фаербейз передавать json как простую текстовую строку. А потом текстовую строку получать обратно и преобразовывать во что нужно. Или в массив или в объект.


            1. thepirateparrot Автор
              10.03.2019 01:50

              Да, про него. Он opensource. Единственное что для Unity при работе на il2cpp AOT, там есть проблемы. Я использую этот плагин который по сути является форком с улучшеной совместимостью (Там вообще какая то чихарда с именованиями, так что могу слегка приврать. По крайней мерее там точно есть Newtonsoft.dll). Он так же бесплатный.

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


  1. mrigi
    09.03.2019 00:44

    Я может не совсем пойму в чем у вас трабла с dictionary, но если вы прям в таком виде скармливаете его в api firebase, то в попытке сериализовать этот dictionary первым делом найдется его enumerator и естественно преобразуется в массив.


    1. thepirateparrot Автор
      09.03.2019 01:09

      Сериализация происходит на нашей стороне силами newtonsoft json. На самом деле, это не так проблема, как неожиданность. Когда пишешь массив, ожидаешь считать массив. То есть например массив:
      { new Something(), null, new Something() } после сериализации newtonsoft таки массив. После передачи в firebase и считывания обратно, это уже будет объект:
      0: Something
      2: Something


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


      Спасибо за замечание.


      1. mrigi
        09.03.2019 02:20
        +1

        Мы сейчас тоже делаем проект на firebase, но с использованием firestore, а не realtime database. Массивы всегда массивы, а объекты всегда объекты. Может и вам стоит попробовать.


        1. thepirateparrot Автор
          09.03.2019 02:44

          Я хотел, однако firestore на тот момент был в бета тесте. Не хотелось иметь больше проблем. С другой стороны вероятно их было бы меньше. В следующем обязательно firestore.


  1. AlexBashkankov
    09.03.2019 11:01

    "… довольно большой скачек"


    1. thepirateparrot Автор
      09.03.2019 14:09

      Спасибо, исправили.


    1. nidalee
      09.03.2019 16:32

      Если вы пропустили.

      1. Выделяете фрагмент текста с ошибкой (любую — грамматическую или пунктуационную, на ваше усмотрение) в публикации;
      2. Жмём хоткей CTRL+Enter (или CMD+Enter);
      3. В нижней части экрана появляется форма, в которой будет процитирован выделенный ранее (в п.1) текст, а также поле для опционального пояснения.


      1. justhabrauser
        09.03.2019 19:09

        … и пытаемся пройти квест с бестолковой капчей.


        1. nidalee
          09.03.2019 19:40

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


      1. SantaCluster
        10.03.2019 12:19

        многие читают Хабр с мобильных устройств.


  1. Vov4ikpa
    09.03.2019 11:01
    -1

    Спасибо за статью, как вам програмировать в паре с девушкой?


  1. abmanimenja
    09.03.2019 14:02
    +2

    Ну а где про девушку-то?
    Кроме заголовка.


    1. thepirateparrot Автор
      09.03.2019 14:05

      Роли в разработке мы не освещали, но в целом речь идет от нас обоих. Фактического разделения в статье нет, но каждый говорил за себя и свою часть.


  1. Thoth777
    09.03.2019 19:40
    +4

    У автора есть девушка, похвально!