Сегодня давайте закончим нашу систему улучшения юнитов!

⬅️ Урок № 26. Повышение уровня наших юнитов! 1/2| ТОЦ | Урок № 28. Добавление ярлыков ➡️

🚀 Найдите код этой серии руководств на моем Github!

В прошлый раз мы начали работать над системой прокачки наших юнитов. Мы объяснили, что вы можете покупать улучшения для своих юнитов, используя внутриигровые ресурсы, что модернизация юнита улучшает его параметры производства и/или атаки, а уровни становятся все более и более дорогостоящими.

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

Простое создание пользовательских функций эволюции: определение трендов с помощью кривых

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

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

Это работает и имеет широкие возможности настройки, но требует, чтобы вы взялись за код и записали результат для всех уровней. Что, если вы хотите линейную прогрессию? Тогда вам нужно будет написать, что для уровня 1 значение равно 1; для уровня 2 значение равно 2; и так далее…

В нашем случае есть два лучших решения:

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

Подождите — «анимационные» кривые?

Я знаю, это может звучать странно: зачем нам использовать кривые анимации?

На самом деле ничто не заставляет вас использовать эти кривые для анимации. Просто название говорит об этом. По правде говоря, эти кривые просто сопоставляют заданное числовое значение (называемое «временем», но это ваше значение X) с другим (вашим значением Y). Это очень полезно, если вы хотите, чтобы какое-то значение компонента развивалось на протяжении всей анимации, вот почему это любимый вариант использования, но вы можете использовать его и по-другому!

И приятно то, что эти кривые автоматически получают небольшой редактор кривых в инспекторе Unity :)

Вот, например, допустим, мы добавляем некоторые переменные к нашим глобальным параметрам игры:

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

Теперь в Инспекторе, если я выбираю экземпляр Scriptable Object для глобальных параметров игры, у меня есть 4 новых слота, где я могу установить значение:

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

Идея этих кривых заключается в том, что вы определяете два или более ключевых кадра (все еще терминология из мира анимации, но вы не ограничены этим вариантом использования!), которые определяют точные пары (x, y). — т. е. при этих конкретных значениях X у вас есть эти конкретные значения Y. Затем остальная часть кривой интерполируется для автоматического «смешивания» между этими фиксированными точками.

В зависимости от типа интерполяции вы можете получить совершенно разные кривые и, следовательно, совершенно разные значения для промежуточных (интерполированных) значений:

Unity позволяет нам довольно много играть с формой нашей кривой, используя небольшие предопределенные тренды в верхнем левом углу или вручную настраивая маркеры на наших ключевых кадрах; в общем, это позволяет визуально создать небольшую функцию, которая сопоставляет поплавок с другим.

Затем кривая автоматически заполняется после вашего последнего ключевого кадра, и вы можете щелкнуть этот ключевой кадр правой кнопкой мыши, чтобы выбрать нужный тип заливки:

Теперь — как мы должны выбирать между двумя решениями (функциями в коде или кривыми анимации)?

Я думаю, это зависит от нескольких параметров:

  • насколько сложными/конкретными вы хотите, чтобы ваши функции были (если они простые, вы могли бы также записать их, а не перетаскивать маркеры повсюду)
  • хотите ли вы, чтобы гейм-дизайнеры (которые не привыкли к навигации по сценариям) настраивали эти значения, и в этом случае визуальные редакторы, вероятно, лучше
  • самое главное, независимо от того, есть ли у вас максимальный уровень: кривые анимации могут иметь эту функцию автозаполнения, но вы по-прежнему контролируете только части с ключевыми кадрами. Если вы хотите, чтобы ваша игра имела бесконечное количество уровней и чтобы эти значения вели себя странно в более высоких диапазонах, вам, вероятно, потребуется определить свою функцию в коде.

В моем случае я знаю, что мне нужен максимальный уровень 4 для моих юнитов. Итак, что я сделаю, так это определю кривые тренда для моих четырех переменных в диапазоне значений X от 1 до 4 (даже если на самом деле значения для 1 не будут использоваться, поскольку мы уже находимся на уровне 1…), а затем , в моем файле GameGlobalParameters.cs используйте одну из этих кривых в качестве ссылки, чтобы получить мой максимальный уровень юнитов:

Я использую инструмент C# Linq, чтобы легко получить ключевой кадр с самым высоким значением X на моей кривой эволюции опыта, что соответствует максимальному уровню для моих юнитов. Обратите внимание, что я выбрал эту кривую произвольно, и это требует, чтобы все кривые определяли один и тот же диапазон значений X; но это все еще довольно удобно, так что я не думаю, что это так уж важно;)

И таким образом теперь, если я решу переместить свой ключевой кадр вправо или добавить новый в эту кривую, вся игра автоматически адаптируется для увеличения максимального уровня юнитов!

Выборка кривых в нашем коде

Конечно, теперь нам нужно сэмплировать эти кривые, чтобы фактически извлекать значения и использовать их в нашем коде. Для этого нам достаточно вызвать метод Evaluate() наших анимационных кривых со значением X (в нашем случае — нового уровня) в качестве параметра:

Если вы хотите попробовать это, вы можете определить явно преувеличенную тенденцию, например, такую, как эта, для множителя улучшения производства вашего юнита (то есть переменной productionMultiplierCurve):

Затем начните игру и улучшите исходное здание Дома:

Вы видите, что его производство внезапно удвоилось, потому что кривая множителя была выбрана при x = 2 (поскольку мы обновлялись до уровня 2), и она получила значение 2 на нашей кривой.

В моем проекте я в конце концов определил большинство трендов для перехода от 1 к 2, но с различными формами кривых, чтобы избежать слишком резкого увеличения; однако стоимость опыта увеличивается от 1 до 10! И, конечно же, это зависит от вашей игры :)

Небольшие обновления пользовательского интерфейса: отображение параметров атаки и влияние обновления.

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

Получите масштаб холста, сохраните его в экземпляре GameManager, а затем разделите все мои преобразованные векторы экрана в мир на этот коэффициент, чтобы правильно разместить полосу здоровья, индикатор мини-карты и производство строительных ресурсов, которое скоро будет размещено. подсказка.

Поэтому также изменилась функция Awake() (иерархия объектов немного отличается, поэтому мне пришлось обновить пути Find()) и почему нет кода для установки размера выбранных подэлементов панели юнитов: это делается с помощью Unity’s UI Layout Element и другие утилиты пользовательского интерфейса :)

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

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

Ладно: приступим!

Отображение параметров атаки

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

Еще раз: я не буду вдаваться во все детали настройки моих игровых объектов UI в сцене. По сути, это набор вложенных вертикальных макетов и элементов макета, которые вместе отображают «контент» (верхняя часть меню с информацией только для чтения) и «кнопки» (навыки, если они есть, «обновление» и кнопки «уничтожить»).

Если вы хотите увидеть, как именно он настроен, вы можете проверить проект на Github 🚀

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

Для этого требуются различные переменные и настройки (большинство из них у нас уже есть, но я напомню их, чтобы вы увидели, насколько это похоже на новую ссылку _selectedUnitAttackParametersParent):

Затем в нашем _SetSelectedUnitMenu() мы можем отобразить значения под информацией о производстве:

Теперь, если вы запустите игру и выберете юнита, вы также увидите его силу атаки и дальность :)

Отображение влияния обновления

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

  • стоимость следующего уровня
  • новое производство юнита, если он модернизируется
  • новый урон от атаки юнита, если он апгрейдится
  • новая дальность атаки юнита, если он модернизируется

Благодаря этой структуре данных мы сможем вычислить все эти данные один раз (когда мы обновим наше производство или когда мы повысим уровень), а затем перечитать их несколько раз намного быстрее!

Мы можем определить в нашем файле Unit.cs над классом:

А затем мы просто добавим переменную и метод внутри класса Unit для хранения данных для обновления следующего уровня и для фактического вычисления этих данных:

По сути, я извлек середину моей функции LevelUp() в метод _GetLevelUpData() util, а затем я вызываю эту функцию в разных местах во время своей модульной процедуры, чтобы убедиться, что она актуальна и действительна для следующего уровня.

Теперь, когда наши данные готовы, давайте используем их в нашем UIManager! Во-первых, давайте заменим нашу переменную _selectedUnitNextLevelCost: вместо этого мы будем напрямую обращаться к LevelUpData.cost, которая определена в нашем экземпляре _selectedUnit:

А затем давайте обновим нашу функцию _SetSelectedUnitMenu() так, чтобы:

  • он принимает новый необязательный параметр showUpgrade, который по умолчанию равен false: если этот параметр активен, то панель будет отображать значения единицы улучшения; в противном случае он будет отображать текущие значения
  • он использует этот параметр для изменения отображения параметров производства и атаки.

Мы даже можем сделать так, чтобы обновленные значения отображались светло-зеленым цветом (чтобы четко видеть, что это определенный режим отображения), используя функцию Unity форматированный текст, которая позволяет нам размещать HTML-подобные теги в строке для дальнейшей настройки ее отображения:

Здесь я просто использую шестнадцатеричный формат цвета, чтобы указать пользовательскому интерфейсу использовать светло-зеленый цвет для обновленных значений.

Примечание. Чтобы этот код работал правильно, необходимо убедиться, что для всех компонентов текста в используемых вами префабах включена опция «Rick text». В противном случае они не смогут интерпретировать тег <color> и он ничего не покажет ;)

Теперь нам просто нужно передать флаг в некоторых конкретных случаях, а именно, когда мы наводим/отключаем кнопку «обновить» или когда мы нажимаем на нее, и у нас все еще есть некоторые возможные уровни после этого:

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

И последнее, но не менее важное: воспроизведение звуковых эффектов и добавление визуальных эффектов!

Итак, теперь, когда мы закончили настройку фактической логики и пользовательского интерфейса, пришло время немного повеселиться и просто добавить немного звука и визуальных эффектов! :)

Мы сделаем все это в нашем UnitManager и просто создадим новую функцию LevelUp():

И, конечно же, нам придется вызывать этот метод из функции LevelUp() в нашем скрипте Unit:

Добавляем немного звука

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

Я снова обратился к некоторым бесплатным звуковым библиотекам с открытым исходным кодом (в данном случае Mixkit) и получил несколько хороших коротких звуков повышения уровня, чтобы играть вместе с моими визуальными эффектами:

Показываем визуальную анимацию!

Это действительно крутая вещь с видеоиграми: они полны всевозможных изящных визуальных эффектов! :)

Большинство этих эффектов написано благодаря шейдерам.

Краткий (очень) обзор шейдеров

Шейдеры — сложная вещь. Если вы не очень хорошо с ними знакомы, я недавно начал новую серию CG под названием «Путешествие по шейдерам», и во вводной статье есть небольшой обзор того, что такое шейдеры и как они работают.

Так что обязательно проверьте! :)

Шейдер повышения уровня моего юнита

Здесь я решил использовать «спиральный» шейдер, который обвивает мой юнит по мере повышения уровня и анимируется в течение секунды, прежде чем исчезнуть:

Если вам интересно, вот шейдер, создающий этот визуальный эффект:

Как видите, у него есть несколько входных параметров (например, используемые цвета, количество циклов, насколько они сглажены…). Важным свойством является _CurrentTime: это число с плавающей запятой, которое мы установим из нашего сценария C#, чтобы шейдерная анимация выполнялась во времени.

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

Подготовка правильной сетки

Следует отметить, однако, что этот шейдер должен быть применен к сетке с определенным набором UVs: вам нужна «полоса» внешнего цилиндра (т. е. без верхней и нижней граней) где вся полоса UV отображается в один прямоугольник.

Это отличается от встроенного цилиндра Unity, например, который разделяет «полосу» на два прямоугольника. Если я применяю свой шейдер к этим объектам, то получаю несколько странный эффект:

Поэтому я фактически использовал Blender, чтобы подготовить небольшую цилиндрическую полосу с правильным UV-картированием, экспортировал ее в формат OBJ и повторно импортировал в свой проект Unity.

Вы можете найти эту сетку OBJ в папке ресурсов Resources/Imports в репозитории Github :)

Использование этого шейдера в нашем UnitManager

В заключение давайте посмотрим, как создать экземпляр и использовать этот шейдер в функции LevelUp() нашего UnitManager. Прежде чем мы сможем использовать шейдер, нам нужно создать материал, который его использует:

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

Теперь мы можем создать небольшой префаб для нашего «эффекта повышения уровня», который берет пользовательскую импортированную сетку и применяет к ней наш новый материал:

И, наконец, мы можем обновить наш скрипт UnitManager. Мы создадим экземпляр нашего префаба, кэшируем некоторые ссылки и используем базовую сопрограмму для обновления нашего параметра _CurrentTime и «проигрывания» анимации шейдера. Мы также обязательно уничтожаем все ранее (незавершенные) префабы и удаляем объект после истечения его 1-секундного срока службы.

Чтобы установить _CurrentTime, мы можем просто вызвать метод SetFloat() для нашего материала, который отправляет эти данные его шейдеру ;)

Вывод

На прошлой неделе и в этом выпуске мы внедрили базовую систему повышения уровня для наших юнитов! Теперь мы можем покупать улучшения, чтобы увеличить их параметры производства или атаки; мы видели, как определить простые в настройке пользовательские функции для эволюции модификаторов для этой системы, и мы даже добавили немного звука и визуальных эффектов с помощью шейдеров!

Вот небольшая демонстрация того, что мы сделали (скорость x4):

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

⬅️ Урок № 26. Повышение уровня наших юнитов! 1/2| ТОЦ | Урок № 28. Добавление ярлыков ➡️

Если вам понравилась эта статья, вы можете найти другие записи в блоге о технологиях, искусственном интеллекте и программировании на мой сайт :)