Доброго времени суток! Я хотел бы рассказать про свой опыт создания мобильной игры на 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;
На мой взгляд, про самые интересные и не очевидные моменты разработки я рассказал, но если у кого-то есть вопросы, буду рад на них ответить!
Спасибо за внимание.
P.S. Паблишера у игры нету, пытаемся раскрутиться своими силами.
ссылка на оригинал статьи https://habrahabr.ru/post/318938/
Добавить комментарий