Система суточного цикла (день-ночь) Unity HDRP 2022

Суточный цикл - что получится в итоге

Шепард на мостике!

Вы наверное видели что я сейчас разрабатываю проект, любовное послание EX-Machina - Road Citizen, в нем одна из ключевых механик суточный цикл и я хочу поделиться с вами своим опытом создания системы управления циклом дня и ночи в Unity с использованием HDRP.

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

Шаг 1: Создание ScriptableObject для настроек

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

Создаем DayCycleSettings.cs

using UnityEngine; [CreateAssetMenu(fileName = "DayCycleSettings", menuName = "ScriptableObjects/DayCycleSettings", order = 1)] public class DayCycleSettings : ScriptableObject { [Header("Настройки времени")] [Range(0, 24)] public float timeSpeed = 1f; // Скорость течения времени [Header("Настройки продолжительности дня и ночи")] [Range(0, 24)] public float dayStartTime = 6f; // Время начала дня [Range(0, 24)] public float dayEndTime = 18f; // Время окончания дня [Range(0, 24)] public float nightStartTime = 18f; // Время начала ночи [Range(0, 24)] public float nightEndTime = 6f; // Время окончания ночи [Header("Настройки солнца")] [Range(0, 90)] public float sunLatitude = 20f; [Range(-100, 100)] public float sunLongitude = -90f; public float sunIntensity = 1f; public AnimationCurve sunIntensityMultiply; public AnimationCurve sunTemperatureMultiplier; [Header("Настройки луны")] [Range(0, 90)] public float moonLatitude = 40f; [Range(-100, 100)] public float moonLongitude = 90f; public float moonIntensity = 1f; public AnimationCurve moonIntensityMultiply; public AnimationCurve moonTemperatureMultiplier; [Header("Настройки звездного неба")] [Range(0, 90)] public float PolarStarLatitude = 40f; [Range(-100, 100)] public float PolarStarLongitude = 90f; public float starsIntensity = 1f; public AnimationCurve starsCurve; }

Что здесь происходит?

  • [CreateAssetMenu]: Эта атрибута позволяет нам создать экземпляр этого ScriptableObject через меню Unity.
  • Параметры времени: Мы задаем текущее время и скорость его течения.
  • Продолжительность дня и ночи: Здесь я добавила возможность настраивать время начала и окончания дня и ночи. Это дает гибкость в управлении циклом.
  • Настройки солнца и луны: Задаем параметры для позиции, интенсивности и температуры цвета, а также кривые для их изменения в течение времени.
  • Настройки звездного неба: Аналогично, задаем параметры для управления звездами.

Шаг 2: Создание основного менеджера цикла дня и ночи

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

using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.HighDefinition; public class CircadianSystem : MonoBehaviour { [Range(0, 24)] public float currentTime = 12f; // Текущее время public DayCycleSettings settings; // Ссылка на ScriptableObject с настройками [Header("Ссылки на объекты сцены")] public Light sunLight; // Ссылка на источник света солнца public Light moonLight; // Ссылка на источник света луны public VolumeProfile VolumeProfile; // Профиль объемного освещения для HDRP [Header("Текущее время")] [SerializeField] private string currentTimeString; // Строковое представление текущего времени public bool SunActive; public bool MoonActive; public bool isDay = false; private HDAdditionalLightData sunLightData; private HDAdditionalLightData moonLightData; private Light SunComponent; private Light MoonComponent; private PhysicallyBasedSky skySettings; private float previousTime = -1f; /// <summary> /// Вызывается при изменении значений в инспекторе Unity /// </summary> #if UNITY_EDITOR private void OnValidate() { VolumeProfile.TryGet(out skySettings); InitializeComponents(); UpdateLight(); CheckShadowsStatus(); SkyStars(); UpdateTimeText(); } #endif /// <summary> /// Вызывается при старте скрипта /// </summary> private void Start() { InitializeComponents(); VolumeProfile.TryGet(out skySettings); UpdateTimeText(); CheckShadowsStatus(); SkyStars(); } /// <summary> /// Инициализация компонентов и данных /// </summary> private void InitializeComponents() { if (sunLight != null) { SunComponent = sunLight.GetComponent<Light>(); sunLightData = sunLight.GetComponent<HDAdditionalLightData>(); } if (moonLight != null) { MoonComponent = moonLight.GetComponent<Light>(); moonLightData = moonLight.GetComponent<HDAdditionalLightData>(); } } /// <summary> /// Вызывается каждый кадр /// </summary> private void Update() { // Обновляем текущее время с учетом скорости времени currentTime += Time.deltaTime * settings.timeSpeed; if (currentTime >= 24) { currentTime = 0; // Сбрасываем время после 24 часов } float currentFloorTime = Mathf.Floor(currentTime); // Обновляем текст времени только при изменении целого часа if (currentFloorTime != Mathf.Floor(previousTime)) { UpdateTimeText(); } UpdateLight(); // Обновляем параметры света // Проверяем, нужно ли обновить статус теней и активности солнца/луны if (ShouldUpdateShadowsStatus(currentTime, previousTime)) { CheckShadowsStatus(); } SkyStars(); // Обновляем параметры звездного неба previousTime = currentTime; // Обновляем предыдущее время } /// <summary> /// Обновляет параметры звездного неба /// </summary> private void SkyStars() { if (skySettings == null) return; float normalizedTime = currentTime / 24.0f; // Нормализуем время от 0 до 1 // Обновляем интенсивность звезд в зависимости от времени skySettings.spaceEmissionMultiplier.value = settings.starsCurve.Evaluate(normalizedTime) * settings.starsIntensity; // Обновляем вращение звездного неба skySettings.spaceRotation.value = (Quaternion.Euler(90 - settings.PolarStarLatitude, settings.PolarStarLongitude, 0) * Quaternion.Euler(0, normalizedTime * 360.0f, 0)).eulerAngles; } /// <summary> /// Проверяет, нужно ли обновить статус теней и активности источников света /// </summary> private bool ShouldUpdateShadowsStatus(float currentTime, float previousTime) { // Обновляем состояние теней, если текущий час отличается от предыдущего return Mathf.Floor(currentTime) != Mathf.Floor(previousTime); } /// <summary> /// Проверяет и обновляет статус теней и активность солнца и луны /// </summary> private void CheckShadowsStatus() { float currentSunRotation = currentTime; // Определяем, является ли текущее время днем bool isDayTime = IsTimeBetween(currentTime, settings.dayStartTime, settings.dayEndTime); // Определяем, должно ли солнце быть активным bool sunShouldBeActive = IsTimeBetween(currentTime, settings.dayStartTime - 0.3f, settings.dayEndTime + 0.3f); // Определяем, должна ли луна быть активной bool moonShouldBeActive = !IsTimeBetween(currentTime, settings.nightEndTime, settings.nightStartTime); // Включаем или отключаем тени для солнца и луны if (sunLightData != null) sunLightData.EnableShadows(isDayTime); if (moonLightData != null) moonLightData.EnableShadows(!isDayTime); isDay = isDayTime; // Обновляем флаг дня // Включаем или отключаем объект солнца if (sunLight != null) { sunLight.gameObject.SetActive(sunShouldBeActive); SunActive = sunShouldBeActive; } // Включаем или отключаем объект луны if (moonLight != null) { moonLight.gameObject.SetActive(moonShouldBeActive); MoonActive = moonShouldBeActive; } } /// <summary> /// Обновляет параметры света для солнца и луны /// </summary> private void UpdateLight() { float normalizedTime = currentTime / 24f; // Нормализуем время от 0 до 1 float sunRotation = normalizedTime * 360f; // Вычисляем вращение солнца // Вычисляем базовые вращения для солнца и луны Quaternion sunBaseRotation = Quaternion.Euler(settings.sunLatitude - 90, settings.sunLongitude, 0); Quaternion moonBaseRotation = Quaternion.Euler(90 - settings.moonLatitude, settings.moonLongitude, 0); Quaternion timeRotation = Quaternion.Euler(0, sunRotation, 0); // Вращение по времени // Применяем вращение к солнцу if (sunLight != null) sunLight.transform.localRotation = sunBaseRotation * timeRotation; // Применяем вращение к луне if (moonLight != null) moonLight.transform.localRotation = moonBaseRotation * timeRotation; // Вычисляем интенсивности по кривым float sunIntensityCurve = settings.sunIntensityMultiply.Evaluate(normalizedTime); float moonIntensityCurve = settings.moonIntensityMultiply.Evaluate(normalizedTime); // Обновляем интенсивность солнца if (sunLightData != null) { sunLightData.intensity = sunIntensityCurve * settings.sunIntensity; } // Обновляем интенсивность луны if (moonLightData != null) { moonLightData.intensity = moonIntensityCurve * settings.moonIntensity; } // Вычисляем температуры цвета по кривым float sunTemperature = settings.sunTemperatureMultiplier.Evaluate(normalizedTime) * 10000f; float moonTemperature = settings.moonTemperatureMultiplier.Evaluate(normalizedTime) * 10000f; // Обновляем температуру цвета солнца if (SunComponent != null) { SunComponent.colorTemperature = sunTemperature; } // Обновляем температуру цвета луны if (MoonComponent != null) { MoonComponent.colorTemperature = moonTemperature; } } /// <summary> /// Обновляет строковое представление текущего времени /// </summary> private void UpdateTimeText() { float hours = Mathf.Floor(currentTime); // Целые часы float minutes = (currentTime - hours) * 60; // Минуты // Форматируем строку времени в формате "HH:MM" currentTimeString = hours.ToString("00") + ":" + minutes.ToString("00"); } /// <summary> /// Проверяет, находится ли текущее время между двумя значениями /// </summary> private bool IsTimeBetween(float time, float startTime, float endTime) { if (startTime < endTime) { return time >= startTime && time <= endTime; } else { // Если время начала больше времени окончания (ночь через полночь) return time >= startTime || time <= endTime; } } }

Пояснения к коду:

  • Ссылки на объекты сцены: Поскольку ScriptableObject не может содержать ссылки на объекты сцены, мы держим ссылки на солнце, луну и профиль объема в основном скрипте.
  • InitializeComponents(): Здесь мы получаем компоненты света и дополнительные данные HDRP для солнца и луны.
  • Update(): В этом методе мы обновляем время, свет, статус теней и звездное небо каждый кадр.
  • SkyStars(): Обновляем интенсивность и вращение звездного неба в зависимости от времени суток.
  • CheckShadowsStatus(): Определяем, является ли текущее время днем или ночью, и соответственно включаем или отключаем тени и объекты солнца и луны.
  • UpdateLight(): Обновляем параметры света (интенсивность, температуру цвета, позицию) для солнца и луны на основе текущего времени и настроек.
  • UpdateTimeText(): Обновляем строковое представление текущего времени для отображения или отладки.
  • IsTimeBetween(): Утилитарный метод для проверки, находится ли текущее время между двумя заданными значениями, учитывая переход через полночь.

Шаг 3: Настройка и использование

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

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

1. Создаем экземпляр DayCycleSettings:

  • В Unity переходим в меню Assets > Create > ScriptableObjects > DayCycleSettings.
  • Настраиваем параметры в созданном объекте: устанавливаем кривые, интенсивности, времена начала и окончания дня и ночи и т.д.

Настройка настроек (0_о)

мои настройки выглядят так, но вы вольны в эксперименты.
мои настройки выглядят так, но вы вольны в эксперименты.

Мои варианты кривых, но вы можете экспериментировать

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

2. Настраиваем CircadianSystem :

  • Добавляем скрипт CircadianSystem на пустой объект в сцене.
  • В инспекторе привязываем созданный ранее DayCycleSettings к полю settings.
  • Привязываем ссылки на объекты сцены: солнце (sunLight), луну (moonLight) и профиль объема (VolumeProfile).
Система суточного цикла (день-ночь) Unity HDRP 2022

С объемом есть один нюанс, после его создания нужно нажать на кнопку New строки Profile, это создаст scriptable object настроек профиля объема.

Система суточного цикла (день-ночь) Unity HDRP 2022
Система суточного цикла (день-ночь) Unity HDRP 2022

И в поле Volume Profile скрипта CircadianSystem, перетащить именно scriptable object профиля настроек объема.

3. Настраиваем объекты солнца и луны:

  • Убедитесь, что объекты солнца и луны имеют компоненты Light и HDAdditionalLightData.
  • Настройте их начальные параметры по необходимости.

Начальные настройки для солнца и луны

Солнце
Солнце
Луна
Луна

Настройка профиля объема

Обязательные компоненты

Система суточного цикла (день-ночь) Unity HDRP 2022
Система суточного цикла (день-ночь) Unity HDRP 2022

Необязательные но добавляющие красивости и тормозов)))

Система суточного цикла (день-ночь) Unity HDRP 2022
Система суточного цикла (день-ночь) Unity HDRP 2022
Система суточного цикла (день-ночь) Unity HDRP 2022

Шаг 4: Тестирование и отладка

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

Советы по отладке:

  • Проверьте кривые: Убедитесь, что кривые интенсивности и температуры цвета правильно настроены и дают ожидаемые результаты.
  • Следите за логами: При необходимости добавьте Debug.Log для отслеживания значений и поведения скрипта.
  • Изменяйте параметры на лету: Во время игры можно изменять параметры в DayCycleSettings для мгновенного эффекта и подбора нужных значений.

Заключение

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

Преимущества такого подхода:

  • Разделение данных и логики: Настройки хранятся отдельно от кода, что облегчает их управление и переиспользование.
  • Гибкость: Можно создавать различные пресеты настроек для разных сцен или условий.
  • Удобство настройки: Изменения в настройках сразу отражаются в поведении сцены, без необходимости править код.

HDRP

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

Примечание - URP

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

Напоследок

Надеюсь, мой опыт и этот рассказ помогут вам в создании собственной системы цикла дня и ночи в Unity! Если у вас есть вопросы или вы хотите поделиться своими идеями, буду рада обсудить их!
Приходите в гости в ToryGames

помните

Зачем вам один мир? Вы можете создать тысячи!

Это Шепард

Конец связи.

3131
22
11
7 комментариев

Много текста, потом прочитаю)
Но кадр с секс машиной и сменой день-ночь сексный, крут сделала

3
Ответить
1
Ответить

С помощью HDRP можно делать красиво.
И ресурсов это требует не больше, чем аналогичная картинка на других популярных движках.

1
Ответить

Просто умница и звезда! Для себя написал примерно такое же, только с
1. Астрономически корректными вычислениями положения солнца и луны в зависимости от локального времени и координат- а это и корректное время восхода и заката с временами года и всё такое. Велик не изобретал, взял готовые скрипты для солнца и луны:
https://github.com/kostebudinoski/SunCalcNet
2. Дополнительный бонус от этого, что в HDRP 17 в параметрах луны можно установить Shading -> Reflect Sun Lights и будут фазы луны из коробки и без танцев с бубном.
3. Обновления всего в Update() - не самая лучшая идея в плане производительности. Лучше это делать в таске или короутине с интервалом в 1 секунду. В самом PBS тоже можно настроить UpdateMode и UpdatePeriod (у меня это значение в настройках). Все просто летает...
4. Сюда же я добавил: управление LensFlare (активация и интенсивность), управление формой и скоростью облаков(с WindZone), обновление Reflection Probe(s) и управление сценариями Advanced Probe Volumes, погоду (раз уже есть времена года).
В общем, непаханое поле :)))

1
Ответить

Было бы неплохо увеличить яркость звёзд или же добавить млечный путь для реализма или другие кубемапы:
https://assetstore.unity.com/packages/2d/textures-materials/milky-way-skybox-94001

Ответить

Так яркость звезд настраивается соответствующей кривой 😉

Ответить

Насчёт магию согласен.)

Ответить