Найти тему
ElandGames

Делаем свою игру на Unity и изучаем язык программирования C# (Часть 6)

Оглавление

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

Движение камеры за игроком

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

Давайте создадим скрипт CameraMove и объявим в нём два поля playerTransform и cameraPosition. Первое поле это будет ссылка на компонент Transform нашего персонажа, чтобы брать оттуда координаты игрока. А второе поле это будут координаты камеры относительно игрока - фактически значения смещения вверх и назад.

Скрипт CameraMove мы повесим на нашу главную камеру (MainCamera) и будем управлять её позицией в пространстве, задавая её компоненту Transform каждый кадр в методе Update() позицию игрока с учётом смещения. Вот и весь скрипт!

Поскольку скрипт висит на объекте камеры, то обращаться к её компоненту Transform можно напрямую с помощью обращения transform.
Поскольку скрипт висит на объекте камеры, то обращаться к её компоненту Transform можно напрямую с помощью обращения transform.

Чтобы всё это заработало первым делом мы должны сохранить скрипт и повесить его на объект MainCamera в окне Иерархии, либо кликнуть на MainCamera и перетащить скрипт в окно инспектора выше или ниже имеющихся компонентов (но выше компонента Transform свой скрипт перетащить не получится).

После этого нужно обязательно не забыть перетащить в поле playerTransform объект Player, чтобы сделать ссылку именно на его координаты и задать смещение камеры в cameraPosition. Поскольку игрок у нас пока находится в нулевых координатах, то и смещение камеры будет равно её координатам.

Я предпочитаю вешать свои скрипты сразу после компонента Transform.
Я предпочитаю вешать свои скрипты сразу после компонента Transform.

Жмём Play и проверяем. Камера двигается вместе с игроком и теперь мы можем без проблем исследовать весь лес и даже дойти до края земли!

Так вот ты какой, край земли!
Так вот ты какой, край земли!

Добавляем в игру параметр времени

Бесконечно ходить по лесу и собирать грибы не особо интересно, поэтому чтобы создать для игрока дополнительный интерес предлагаю добавить время игрового дня. Задачей игрока становится дожить до конца дня. Учитывая, что мы делаем казуальную игру, то такая простая задача думаю вполне подойдёт.

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

Поскольку игра казуальная, то предлагаю считать не реальный световой день с 5 утра до 7 вечера условно и плюс ещё ночь, а упростить всё просто до определенного промежутка времени от 0 до 24 часов. Тогда наша задача будет просто посчитать эти 24 часа в определённом масштабе - например 1 игровой час это будет 1 реальная минута.

Давайте создадим новый скрипт TimeCounter и объявим в нём поля для счетчика времени time и счётчика дней day. Также сделаем поле timeScale для изменения масштаба времени в инспекторе. Если мы в каждом кадре будем увеличивать переменную time на Time.deltaTime, то мы будем считать реальные секунды. Умножим их на наш масштаб 60 и получится, что каждую секунду мы будем отсчитывать одну игровую минуту. Остается только теперь проверять, что наш time не перевалил за 24 * 60 = 1440 минут.

В консоль мы выводим результат сложения трёх строк, причём day имеет тип int, но если его складывать со стройкой, то он автоматически преобразуется в тип string.
В консоль мы выводим результат сложения трёх строк, причём day имеет тип int, но если его складывать со стройкой, то он автоматически преобразуется в тип string.

Когда игровой день закончился, то обнуляем наше время и вызываем специально обученный нами метод NewDay(), который сообщит нам в консоли, сколько дней мы прожили и увеличит счётчик дней на единицу.

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

Наглядное отображение игровых параметров

Давайте сделаем отображение времени дня в интерфейсе. Поскольку мы не привязывались к реалистичному времени суток (утро, день, вечер, ночь), а просто упростили всё до таймера, который отсчитывает 24 часа, то мы можем легко отобразить время в виде шкалы. Заполненная шкала будет означать закончившийся день. Что-то типа полоски здоровья. Но можно пойти еще дальше и сделать заполняющийся круг.

Для этого во-первых давайте создадим ещё один Canvas внутри имеющегося и назовём его TimeCanvas. Это позволит нам оптимальнее пользоваться нашим интерфейсом, потому что все что находится на одном канвасе перерисовывается каждый раз, когда происходит изменения хотя бы в одном элементе. Т.е. каждый раз когда наш индикатор в каждом кадре будет заполняться, то все индикаторы параметров будут перерисовываться, а это повлияет на производительность игры. Отдельные же канвасы, даже вложенные друг в друга перерисовываются независимо по мере изменения. Вот мы и создадим новый вложенный канвас специально для нашего таймера.

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

Давайте создадим внутри TimeCanvas объект Image, кликнув в окне иерархии по объекту TimeCanvas правой кнопкой мыши и выбрав UI - Image. Созданную картинку назовём Timer и в её настройках в Инспекторе выберем в поле Source Image, кликнув по кружку выбора объектов данного типа, стандартную картинку Knob - белый круг.

В проекте по умолчанию присутствуют стандартные картинки для простейших элементов интерфейса
В проекте по умолчанию присутствуют стандартные картинки для простейших элементов интерфейса

И теперь в настройках Image появится новое поле Image Type, в котором нам нужно выбрать Filled, чтобы картинка стал заполняемой. А в настройке Fill Origin давайте выберем Top, чтобы индикатор заполнялся сверху, напоминая движение стрелки часов от 12 и до 12.

Компонент Image имеет много удобных и полезных настроек
Компонент Image имеет много удобных и полезных настроек

Создадим также текстовый объект DayText (UI - Text) и разместим их вместе с индикатором времени вверху экрана, не забыв выбрать привязку якорей к верхней границе посередине, выбрав иконку Top-Center. В настройках текстового объекта в текстовое окно впишем "День 1", чтобы прикинуть как это будет выглядеть.

Якоря объектов интерфейса можно выставлять вручную, а можно просто выбрать один из вариантов кликнув на иконку с красными гранями.
Якоря объектов интерфейса можно выставлять вручную, а можно просто выбрать один из вариантов кликнув на иконку с красными гранями.

В настройках нашего индикатора времени, мы можем изменять значение Fill Amount и посмотреть как индикатор будет меняться в зависимости от значения.

Очень удобно часто изменяемые параметры выводить в инспектор в виде изменяемой шкалы.
Очень удобно часто изменяемые параметры выводить в инспектор в виде изменяемой шкалы.

Было бы неплохо добавить нашему "циферблату" фон, чтобы он не выглядел как кусок пирога. Для этого давайте выберем в иерархии объект таймер и нажмём CTRL+D, чтобы продублировать его. В его настройках выберем другой цвет, например серый и типа изображения Simple. Назовём его Timer Background и наш Timer в иерархии перетащим на него. Теперь всё это выглядит интереснее.

Теперь это кусок пирога, на серой тарелке!
Теперь это кусок пирога, на серой тарелке!

Осталось изменять значение таймера Fill Amount из кода, в соответствии со значением нашей переменной time.

Изменение изображений из кода

Давайте в нашем скрипте UI, который управляет интерфейсом, сделаем ссылку на компонент Image нашего таймера, настройками которого нам предстоит управлять. Вот только по умолчанию наш скрипт ни про какие компоненты Image даже не слышал.

Если VisualStudio сигнализирует об ошибке, то можно навести курсор на подчёркнутое слово, чтобы понять, что он имеет в виду
Если VisualStudio сигнализирует об ошибке, то можно навести курсор на подчёркнутое слово, чтобы понять, что он имеет в виду

Воспользуемся подсказками VisualStudio, кликнув на "Показать возможные решения" во всплывающем окне указав курсором на подчеркнутое слово. Ну и добавим предложенную стандартную библиотеку UI или иными словами пространство имён UI. Причём наш класс никак не будет конфликтовать с этим пространством имён, не смотря на идентичные названия. Это совершенно разные сущности. Теперь мы можем пользоваться типом Image и давайте сделаем метод для его изменения.

По логике разделения ответственности скрипт отображения интерфейса не должен заниматься расчётами
По логике разделения ответственности скрипт отображения интерфейса не должен заниматься расчётами

Метод UpdateTimer() будет получать на вход дробное значение типа float и изменять параметр fillAmount у нашего таймера. А само входное значение будем вычислять в скрипте TimeCounter, в котором надо сделать ссылку на скрипт UI.

Логично, что раз время time и временной масштаб timeScale принадлежать скрипту TimeCounter, то пусть он сам с ними и работает
Логично, что раз время time и временной масштаб timeScale принадлежать скрипту TimeCounter, то пусть он сам с ними и работает

Поскольку параметр fillAmount у компонента Image изменяется от 0 до 1, то в метод UpdateTimer() будем отправлять значение времени разделённое на количество минут в дне.

Повесим скрипт TimeCounter на объект Game и проставим в нём ссылку на скрипт UI, который висит на объекте Canvas. А в скрипте UI проставим ссылку на Timer. Теперь запустим игру и проверим. Работает, но время бежит очень быстро. За 24 секунды прошел цикл дня и в консоли появилось сообщение, что мы прожили 1 "дней".

Иногда логические ошибки бывают полезными :)
Иногда логические ошибки бывают полезными :)

Очень удобная ошибка для тестирования получилась - мы быстро проверили, что все работает, что за первым днём отсчитывается следующий и что всё отображается. Но где-то мы явно зря умножаем на 60. За день длинною в 24 секунды вряд ли наш герой успеет хоть что-то, кроме как нажраться грибов. :)

Ошибка кроется вот в этом умножении на timeScale. Получается, что за 1 реальную секунду проходит не игровая минута, а игровой час.

Значение таймера не должно ни на что умножаться, ведь он отсчитывает реальные секунды, а переводим их в игровые мы уже сами при проверке и отображении. Хотя можно и изменить логику избавив процессор от лишнего умножения timeScale на 24.
Значение таймера не должно ни на что умножаться, ведь он отсчитывает реальные секунды, а переводим их в игровые мы уже сами при проверке и отображении. Хотя можно и изменить логику избавив процессор от лишнего умножения timeScale на 24.

Удалим это умножение на timeScale и всё заработает как нам надо. Остается только обновлять текст с номером дня. Для этого в скрипте UI сделаем на него ссылку dayText и добавим метод обновления UpdateDay().

Опять складываем тип string и числовой тип int автоматически преобразую второй в string. Главное не забыть дополнительный пробел в первой строке, чтобы разделить слово и цифру.
Опять складываем тип string и числовой тип int автоматически преобразую второй в string. Главное не забыть дополнительный пробел в первой строке, чтобы разделить слово и цифру.

Остаётся проставить ссылку в инспекторе на текстовый объект с номером дня и в скрипте TimeCounter в методе NewDay() добавить строчку кода.

В этом методе сначала отображается в консоли старое значение day, потом увеличивает его на единицу, а затем в метод обновления интерфейса посылается уже новое значение day.
В этом методе сначала отображается в консоли старое значение day, потом увеличивает его на единицу, а затем в метод обновления интерфейса посылается уже новое значение day.

Теперь чтобы протестировать и не ждать 24 минуты, пока игровой день закончится, давайте изменим в инспекторе значение timeScale на 1, чтобы ускорить процесс 60 раз.

Вот так вот, благодаря изменению timeScale, наш герой прожил целый день и даже не проголодался.
Вот так вот, благодаря изменению timeScale, наш герой прожил целый день и даже не проголодался.

Всё работает. Индикатор времени циклично отсчитывает время, а индикатор дней отображает номер текущего дня, при этом в консоли отображается сообщение о количестве прожитых дней.

Что ж мы научились перемещать один объект относительно другого, поломали голову над тем как считать и отображать время в игре, начали работать с картинками из кода, научились в C# складывать строки и числа, а также научились грамотно использовать особенности обновления канвасов.

В следующей части займёмся возобновление (респауном) грибов, увеличим их разнообразие и добавим им различные свойства. Так что, подписывайтесь на канал, чтобы не пропустить продолжение, задавайте вопросы и предлагайте идеи в комментариях! Как думаете, что еще нужно добавить в игру, чтобы играть в неё было интереснее?

Вот предыдущая часть статьи:

А вот вся подборка статей по этой теме:

Учимся делать игры и программировать на C# | Сергей Эланд | Дзен