[Из песочницы] Игра HellWorm. История разработки


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

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

Управление достаточно простое и описывается лишь одной фразой:
«Куда кликнешь, туда и ползет».
4cc6c7ccde9e43b9a4814530b1841b83.gif

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

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

Стыковка происходит не по центральным линиям, а по углам сегментов: по левому или по правому, где сторона выбирается в зависимости от того, по какую сторону от прямой AB лежит новая точка P.
(Bx — Ax) * (Py — Ay) — (By — Ay) * (Px — Ax), если выражение >0 то точка P лежит по левую сторону.

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

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

bea49d87b78f42ddba2fe479da3288e5.PNG

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

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

2690594b3d894d149190c625756db651.PNG

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

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

Решение оказалось вполне рабочим, и, на удивление, с первого раза все заработало. Математические шаги следующие:
  • определяем в какую сторону смотрит новый сегмент относительно предыдущего (в левую или в правую)
  • находим угловые точки 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 градусов, а также уменьшить допустимую длину сегмента для разворота. Результат изображен ниже:
6d567f09b8384ce996c0970f132c4f91.PNG

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

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

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

cb840974f2534e00959904f448edc3fb.PNG

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

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

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

d7914336bd1b43e68e1ef74ea35848b0.PNG

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

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

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

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

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

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

  • 3 января 2017 в 15:00

    0

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

    • 3 января 2017 в 15:25

      +1

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

    0

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


    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, которые самостоятельно прописываются для спрайта (не знаю, насколько хорошо делать его текстовым или привязываться к имени самого спрайта). В этом случае скин игрока не поедет при добавлении нового элемента в начало.

    • 3 января 2017 в 15:24

      0

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

        0

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

        • 3 января 2017 в 15:37

          0

          Да, разумно. Спасибо!
  • 3 января 2017 в 15:16

    0

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

    +1

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

      0

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

© Habrahabr.ru