Как я делал руку с картами для своей карточной игры

Зона, с которой игрок чаще всего взаимодействует в карточной игре — это его рука (не путать с рукой в играх с 18+ контентом). Рука — это набор карт, которые игрок может разыграть в текущий ход. И раз это одно из самых важных мест, я решил уже на этапе прототипа сделать руку приятной и красивой.

Дисклеймер: я тут описываю свой говнокод. Программировать я совсем не умею, так что не советую использовать как инструкцию.

Вот как раз во время работы с рукой (не путать с «во время работы рукой») я понял отличие качественной игры от поделки на уровне прототипа для тестирования геймплея. Все дело в мелочах и деталях. Посмотрите, как ведут себя карты в руке в Slay the Spire:

Теперь посмотрите на те же карты на видосе у чела, который пишет в названии ролика, что он сделал Slay the Spire за неделю (9:11):

Теперь у меня это название вызывает горький смех, потому что как раз из-за таких небольших фишек с анимациями запрограммировать всё из Slay the Spire не удастся за неделю, даже при условии, что у тебя будет готов абсолютно весь арт, геймдизайн и другой контент.

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

Как я делал руку с картами

Первую руку я сделал по туториалу, с которого делал первый прототип. В нём за формирование руки отвечал компонент Grid, который расставляет объекты по сетке. Grid лишает вас необходимости писать код, который отвечает за расстановку карт: вы просто задаете настройки и карты сами встают ровно при любом количестве, когда попадают в нужный родительский объект.

<p>Интерфейс компонента: как видите, все отступы, расположение и расстояния можно настроить тут, не нужно писать код</p>

Интерфейс компонента: как видите, все отступы, расположение и расстояния можно настроить тут, не нужно писать код

Проблема с Grid была в том, что делалось это некрасиво: карты телепортировались в сетку, а мне хотелось, чтобы они плавно переходили.

Недолго погуглив, я нашёл в Ассет Сторе бесплатный ассет Smooth Grid. Там были желанные плавные переходы. Если я вытаскивал карту, она потом вставала в конец — ну да и ладно, решил я.

<p>В настройках все то же самое, только еще есть плавность: насколько быстро карты переходят в свое место в сетке</p>

В настройках все то же самое, только еще есть плавность: насколько быстро карты переходят в свое место в сетке

Проблемы со Smooth Grid появились, когда я решил сделать руку похожей на руку в реальной жизни и в других карточных играх — чтобы карты слегка наезжали друг на друга — и после этого стал думать, как мне сделать выделение карты по наведению курсора. Сначала карты просто не хотели выезжать. Когда я разобрался с выездом, понял, что карта не хочет выходить на передний план. Всплыла проблема с тем, что эта плавная сетка перемещала карты влево или вправо в зависимости от положения объекта с картой в инспекторе. А значит, если я буду выводить карту на передний план с помощью перемещения в инспекторе, карта будет перескакивать со своей позиции в самую крайнюю в руке. Такое мне точно не подходило.

Я понял, что в 2D в Unity не было способов перетащить объект на задний и передний план кроме свойства Sorting Layer. Вот только применить его можно только к объектам с компонентом Sprite Renderer. Он отвечает, например, за то, чтобы у карточки выводилась нужная картинка. А у меня все объекты имели компонент Image — я выводил изображение карточки с помощью него.

Так вот, чтобы выводить карточку на передний план, мне нужно было менять компонент Image на компонент Sprite. И это привело к своим сюрпризам. Оказывается, все объекты внутри Canvas с компонентом Image будут перекрывать любой другой объект. И когда я создаю объект Background, который задумывал как фон, он перекрывает спрайты карточек, и поверх фона ничего не видно. И это довольно логично: любой объект внутри Canvas Unity распознает как часть интерфейса: а интерфейс как бы должен быть поверх всего остального в игре. Но…на осознание этих правил у меня ушло много времени. А еще много времени на то, чтобы разобраться, как выставить настройки у спрайтов, поправить поехавшую камеру и сделать кучу мелких действий.

Следующее открытие: Event Trigger, компонент, с помощью которого я делал выведение карточек на передний план при наведении мыши и разыгрывании карточек, не хотел работать со спрайтами. Один и тот же скрипт у меня работал на объекте с Image и переставал работать на объекте со Sprite Renderer. Оказалось, есть еще один способ регистрировать наведение мышки: с помощью методов MouseEnter, MouseExit и MouseOver.

Дальше меня ждали новые проблемы. Я использовал метод Lerp, чтобы карточки красиво залетали в руку и красиво выдвигались, когда наводишь на них курсор. Все отлично работало ровно до тех пор, пока я не совмещал анимации карточек с их добавлением в Smooth Grid. Тогда начиналась котовасия.

Дело в том, что Smooth Grid тоже построен на Lerp. А Lerp — это такая функция, которая как резиночка тянет объект на родительское место. И вот проблема была в том, что Lerp в сетке и мой Lerp спорили между собой, а карточки в итоге трясло и кидало в разные стороны. Я пытался это решить попеременным отключением Lerp’ов — не сработало. Все начинало ехать еще больше.

В итоге пришлось отказаться от компонента Grid совсем: и от Smooth, и от обычного. Я сам сделал простенький вывод на нужную позицию с помощью цикла for, максимально костыльно.

<p>Смотрю и даже стыдно смотреть...</p>

Смотрю и даже стыдно смотреть...

Вот что у меня получилось в итоге

Скорее всего, прокатит для трейлера. Но для реальной игры нужно будет править, чтобы, например, карточки выравнивались по центру и расстояние между ними менялось, когда карточек в ряду становится больше или меньше. А еще наведение курсора не всегда регистрируется, и при желании порядок выдачи и улетания карт в сброс можно сломать.

Что думаю про сишарп

Несмотря на кучу проблем, мне нравится работать с сишарпом. Но по сравнению со всякими JS и Python синтаксис у него выглядит более избыточным:

Как я делал руку с картами для своей карточной игры

Не напоминает ли это вам этот момент из «Кролика ДжоДжо» (0:18):

Хотелось бы конечно сделать так же сочно, как в Slay the Spire, но это капец сколько ещё ковыряться. Вообще при разработке стараюсь максимально отсекать всё лишнее. Например, когда я понял сейчас, что надо много всего перепиливать, я отсек вообще всю логику, которая не нужна была для той задачи, которую я поставил себе здесь, в телеге. Этим путём я следую и при разработке части игры для трейлера: максимальный говнокод (даже по моим меркам, а я тот ещё программист), максимум бутафорских штук. Например, у меня карточки тупо вылазят из края экрана, а там они изначально лежат перед запуском экрана.

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

Этот пост входит в цикл постов про игру, которую я потихоньку делаю уже несколько месяцев. Я делюсь всем производственным процессом: какие решения я принимаю в разработке, геймдизайне, интерфейсе, арте и других сферах. В моем телеграм-канале вы можете посмотреть другие посты и узнать, когда ждать следующие: @nigylamchan.

3232
18 комментариев

Спасибо!

1

Вполне неплохо получилось.

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

1

О, я тоже пробовал делать текстовые квесты на Twine. А карточную игру сначала хотел делать на Gamemaker. В итоге пришел к C# и Unity. Мне просто нравится сидеть и погружаться в C#, хотя понимаю, что есть наверняка более простые инструменты для не-программиста

1

Почему бы не закешировать компонент и не обращаться к нему без постоянного геткомпонента?

1

Стыдно спрашивать, но как это лучше сделать? Во время работы я об это споткнулся, и у меня не перезаписывалось значение переменной. В итоге забил

А где рука то?

1