Доброго времени суток! Я хотел бы рассказать про свой опыт создания мобильной игры на Unity под названием HellWorm. Из названия можно понять, что игра про червяка. Ползаем, кушаем монетки, не врезаемся в препятствия. Казалось бы, клон классической игры, на которой большинство из нас выросло. Но, на самом деле, параллель со змейкой на этом заканчивается.


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

Управление достаточно простое и описывается лишь одной фразой:
“Куда кликнешь, туда и ползет”.


Первая реализация алгоритма движения наглядно продемонстрирована на схеме ниже.


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


Стыковка происходит не по центральным линиям, а по углам сегментов: по левому или по правому, где сторона выбирается в зависимости от того, по какую сторону от прямой AB лежит новая точка P.

(Bx — Ax) * (Py — Ay) — (By — Ay) * (Px — Ax), если выражение >0 то точка P лежит по левую сторону.

Как найти координату точки стыковки я расписывать не буду, т.к. ничего особенного в этом нет, простая геометрия. Лучше подробнее расскажу про сам подход.

Хочу сразу оговорить, что в данном случае максимальный угол между сегментами должен быть не более 90 градусов, иначе, как можно догадаться, углы вылезут наружу. Поэтому во время крутых (>90 градусов) разворотов происходит добавление переходного сегмента.


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

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


При создании нового сегмента данным способом нет возможности соединить центры сторон сегментов(красную и синюю точки на рисунке). А поскольку голова червя всегда привязана к центральной точке первого сегмента (а хвост привязан к центру края последнего), то происходит рывок при повороте, и чем больше угол разворота, тем сильнее заметен рывок. Конечно, на завершительных этапах разработки сталкиваться с подобным очень обидно, ведь нужно переделывать весь алгоритм. Но стремление закончить проект дошлифованным и без косяков взяло верх. Было решено вернуться к первой реализации алгоритма, т.к. вести расчет относительно ломаной сплошной линии, вокруг которой строится тело, гораздо проще и логичнее. Осталось решить проблему со стыками прямоугольников, а именно, каким образом их заполнить.


Немного поразмыслив, пришла идея продления сегментов навстречу друг другу для устранения зазора.


Решение оказалось вполне рабочим, и, на удивление, с первого раза все заработало. Математические шаги следующие:

  • определяем в какую сторону смотрит новый сегмент относительно предыдущего (в левую или в правую)
  • находим угловые точки B, C и точки на другом конце сегментов B’, C’
  • находим точку пересечения A прямых BB’ и CC’:

    Ax = ((Bx * B'y — By * B'x) * (Cx — C'x) — (Bx — B'x) * (Cx * C'y — Cy * C'x)) / ((Bx — B'x) * (Cy — C'y) — (By — B'y) * (Cx — C'x))

    Ay = ((Bx * B'y — By * B'x) * (Cy — C'y) — (By — B'y) * (Cx * C'y — Cy * C'x)) / ((Bx — B'x) * (Cy — C'y) — (By — B'y) * (Cx — C'x))

  • удлиняем сегменты на длины отрезков BA и CA соответственно

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


Вдобавок, пару слов хотелось бы рассказать о генерации окружающего мира. Все объекты сохранены в sprite sheet’ы в черно-белом виде. Белым цветом закрашены рамки, которые меняют расцветки во время игры, потому что изменяя цвет у Sprite Renderer все белые участки становятся именно выбранного цвета.


(боковые стенки с добавленным Polygon Collider 2D, и фоновое изображение)

Та же техника используется для игровых препятствий и главного персонажа.


На скриншоте с препятствиями можно заметить синие и желтые ромбики, это места возможного появления монетки (желтые) или черепка (синие). Они являются пустыми gameobject с выбранной иконкой.


Я выбрал такой подход, чтобы, во-первых, упростить задачу по добавлению монет/черепов на сцену (не заморачиваться с рандомной генерацией) и, во-вторых, чтобы они появлялись в интересных для геймплея местах. А, по-скольку, в игре около 70 разных туннелей, скал и проходов, то для игрока не так заметны предопределенные места.

Расскажу еще пару слов о черве, а именно его голове. У игрока есть возможность ее менять (как и прочие части тел), и, в том числе, цвет кожи и глаз.


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


Имеется prefab головы worm_head, в котором находятся все головы head+n, состоящие из объектов head (спрайт головы белого цвета) и eyes (спрайт с глазами, расположенный поверх головы, который также белого цвета). По умолчанию, все объекты head+n невидимы. Во время старта игры происходит проверка номера установленной головы, чтобы сделать объект head+n видимым. Вместе с этим применяется выбранная игроком цветовая схема: основной цвет задается для спрайта головы, тела, хвоста, а дополнительные цвета задаются для глаз и шипов.

head.GetComponent<SpriteRenderer>().color = headColor;
eyes.GetComponent<SpriteRenderer>().color = eyesColor;

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

Спасибо за внимание. Поиграть в игру можно в Google Play.

P.S. Паблишера у игры нету, пытаемся раскрутиться своими силами.
Поделиться с друзьями
-->

Комментарии (20)


  1. nightrain912
    03.01.2017 15:00
    +2

    Спасибо за хорошую статью, их по unity3d не так уж много теперь.
    Не думали использовать сплайны для создания тела червя? Ключевые точки все известны, можно построить Mesh используя их, а в unity3d есть удобный [AnimationCurve](https://docs.unity3d.com/ScriptReference/AnimationCurve.html, с помощью которого можно рассчитать сплайн.


    1. ian_phobos
      03.01.2017 15:25

      Рад, что понравилось :)
      О сплайнах, к сожалению, не слышал. Но буду иметь в виду, спасибо!
      На момент начала разработки хотелось скорее написать рабочий прототип, поэтому был выбран первый пришедший в голову подход.


      1. vtulin
        04.01.2017 12:52
        +3

        О сплайнах, к сожалению, не слышал.

        Сразили прямо в сердце!(


        1. ian_phobos
          05.01.2017 21:16
          +1

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


  1. nightrain912
    03.01.2017 15:12

    И в отдельной ветке:
    Про скины к червю и их реализации. Есть более красивое решение, которое, к тому-же, аккуратнее кастомизируется и не подразумевает создание кучи объектов:


    1. Создаем новый класс WormSkins, наследуемый от ScriptableObject'а
    2. Прописываем атрибут CreateAssetMenuAttribute, чтобы его можно было в редакторе создать
    3. В WormSkins делаем SerializeField массивы для голов, хвостов и тд:
      [SerializeField] Sprite[] heads;
    4. Делаем геттеры (точнее, функции-геттеры, т.к по индексу берем):
      Sprite GetHead(int index)
    5. Создаем в ассетах экземпляр этого класса (пункт 2)
    6. Прописываем в нём все головы, хвосты и т.д.
    7. Оставляем ссылку на этот экземпляр в префабе червя, в том классе, где у вас сейчас кастомизация происходит (или как-то более красиво, тут много вариантов)
    8. Оставляем в черве только одну голову, один хвост и тд, без указанного спрайта
    9. При запуске берем индекс из пользовательских настроек, получаем спрайт из WormSkins и засовываем его в нужный SpriteRenderer

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


    1. ian_phobos
      03.01.2017 15:24

      Я понял ваш ход мыслей, в принципе у меня так реализовано для хвостов и шипов.
      А для голов проблема в том, что у них еще есть глаза как отдельный объект, которые размещены в разных местах относительно спрайта головы. Поэтому просто менять sprite у SpriteRenderer в данном случае не вариант. Приходится где-то хранить группу с головой и размещенными для нее глазами.


      1. nightrain912
        03.01.2017 15:33

        Ну, тогда можно сделать каждую голову (уже с глазами) отдельным префабом, а в WormSkins хранить массив этих префабов. Идея в том, чтобы не хранить все головы в префабе, который будет создан на сцене.


        1. ian_phobos
          03.01.2017 15:37

          Да, разумно. Спасибо!


  1. ian_phobos
    03.01.2017 15:16

    Рад, что понравилось :)
    О сплайнах, к сожалению, не слышал. Но буду иметь в виду, спасибо!
    На момент начала разработки хотелось скорее написать рабочий прототип, поэтому был выбран первый пришедший в голову подход.


  1. Maklaud
    03.01.2017 15:43

    Понравился стиль игры — простенький, но интересный, а что еще нужно?..
    Какую монетизацию используете? Покупка игровых денег за реальные? Реклама?
    Что делаете для продвижения? Похоже, обычных статей на хабре, постов на форуме уже становится недостаточно.
    Успехов с игрой!


    1. ian_phobos
      03.01.2017 15:54

      Приятно слышать, спасибо! :)
      Сам играю на телефоне только в простые игры с короткой сессией, пока еду в метро.
      Встроили рекламу в игру + видео за монетки (rewarded video). Стандартно можно отключить рекламу или купить монет за реальные деньги. Еще можно приобрести уникальный скин для червя.
      Для продвижения был скомпонован пресс-кит на английском и русском языках. Два раза разослали в игровую прессу. Только 1 сайт написал полноценный обзор, который привлек около 50 игроков. Остальные просят денюжки или игнорируют из-за огромного кол-ва писем. Сегодня про игру написали на белорусском онлайнере, будем надеятся это поможет дальнейшей раскрутке.


      1. Kavabunga
        08.01.2017 11:32

        Попробуйте выложить игру на форуме 4pda. Мне в прошлом году это принесло чуть меньше трех сотен установок за вечер. Но там специфическая аудитория — будьте готовы к читерам и паре-тройке единиц на сотню установок. Мне тогда читеры подпортили таблицу рекордов.


        1. ian_phobos
          08.01.2017 11:32

          Выложил в первый же день публикации игры. Увы, ни одной установки это не принесло.


          1. Kavabunga
            08.01.2017 23:02

            А в какой день недели/время публиковали? Возможно тема просто потерялась в недрах форума. У меня игрушка была гораздо слабее, но я видимо попал в струю. Еще через пару недель после апдейта приложения написал в теме и это принесло еще несколько десятков установок. Сейчас нашел тему, это был март 2015 года, может с тех пор форум перестал быть таким полезным.


        1. m0sk1t
          13.01.2017 10:50
          +8

          комментарий миллионник =)


        1. ComodoHacker
          13.01.2017 17:23

          Вот это гет так гет.


    1. afrokick
      03.01.2017 16:51
      +2

      Еще нужно везение) Не в обиду автору, но таких игр куча, с короткой сессией, милой графикой и простым геймплеем. Сейчас качество продукта — не опциональный параметр, а по умолчанию. А вот чтобы игру заметили, нужен хороший пиар и везение. Удачи автору!


      1. Tutanhomon
        03.01.2017 17:52

        Что верно то верно. Не обязательно это крупные денежные вложения в рекламу. Но нужно попотеть, потыкаться везде, где только можно заявить о себе и о игре. Удачи, коллега!


        1. ian_phobos
          04.01.2017 11:05

          Абсолютно верно, именно этим мы и занимаемся. :) Пока деньги на рекламу и не пытаемся вкладывать, только после какого-либо успеха можно будет попробовать вложиться. Спасибо!


        1. afrokick
          05.01.2017 23:10

          У меня был опыт получения 100к установок (выход в топы20 по категории) без единого цента, лишь на ключевых запросах. Причем игра — готовый проект купленный за гроши(20$ вроде) + рекламная сетка от appodeal.

          И вот вижу кучу прилаг в магазе — ну херь херью, клоны, а набирают по 1млн загрузок.

          Вопрос напрашивается: куда катиться мирстор?)

          PS не в коем разе не призываю делать подделки на коленках