시간을 세팅하자
아침을 몇시로 세팅 할 지, 저녁은 몇 시로 할 것인지, 흐르는 시간의 배율 등을 세팅할 수 있게 도와주는 SO를 만들어보겠습니다.
using UnityEngine;
[CreateAssetMenu(fileName = "TimeSettings", menuName = "TimeSettings")]
public class TimeSettings : ScriptableObject {
public float timeMultiplier = 2000;
public float startHour = 12;
public float sunriseHour = 6;
public float sunsetHour = 18;
}
시간의 흐름과 낮/밤의 변화를 관리하자
using System;
using UnityEngine;
public class TimeService {
readonly TimeSettings settings;
DateTime currentTime;
readonly TimeSpan sunriseTime;
readonly TimeSpan sunsetTime;
public DateTime CurrentTime => currentTime;
public event Action OnSunrise = delegate { };
public event Action OnSunset = delegate { };
public event Action OnHourChange = delegate { };
readonly Observable<bool> isDayTime;
readonly Observable<int> currentHour;
public TimeService(TimeSettings settings) {
this.settings = settings;
currentTime = DateTime.Now.Date + TimeSpan.FromHours(settings.startHour);
sunriseTime = TimeSpan.FromHours(settings.sunriseHour);
sunsetTime = TimeSpan.FromHours(settings.sunsetHour);
isDayTime = new Observable<bool>(IsDayTime());
currentHour = new Observable<int>(currentTime.Hour);
isDayTime.ValueChanged += day => (day ? OnSunrise : OnSunset)?.Invoke();
currentHour.ValueChanged += _ => OnHourChange?.Invoke();
}
public void UpdateTime(float deltaTime) {
currentTime = currentTime.AddSeconds(deltaTime * settings.timeMultiplier);
isDayTime.Value = IsDayTime();
currentHour.Value = currentTime.Hour;
}
public float CalculateSunAngle() {
bool isDay = IsDayTime();
float startDegree = isDay ? 0 : 180;
TimeSpan start = isDay ? sunriseTime : sunsetTime;
TimeSpan end = isDay ? sunsetTime : sunriseTime;
TimeSpan totalTime = CalculateDifference(start, end);
TimeSpan elapsedTime = CalculateDifference(start, currentTime.TimeOfDay);
double percentage = elapsedTime.TotalMinutes / totalTime.TotalMinutes;
return Mathf.Lerp(startDegree, startDegree + 180, (float) percentage);
}
//현재 시간이 낮인지 여부를 판단
bool IsDayTime() => currentTime.TimeOfDay > sunriseTime && currentTime.TimeOfDay < sunsetTime;
//두 시간 간의 차이를 계산하는 메서드
TimeSpan CalculateDifference(TimeSpan from, TimeSpan to) {
TimeSpan difference = to - from;
return difference.TotalHours < 0 ? difference + TimeSpan.FromHours(24) : difference;
}
}
TimeService 메서드는 시간의 흐름과 낮/밤의 변화를 관리하고, 이에 따른 이벤트를 발생시키는 역할을 합니다.
옵저버 패턴
using System;
using System.Collections.Generic;
[Serializable]
public class Observable<T> {
private T value;
public event Action<T> ValueChanged;
public T Value {
get => value;
set => Set(value);
}
public static implicit operator T(Observable<T> observable) => observable.value;
public Observable(T value, Action<T> onValueChanged = null) {
this.value = value;
if (onValueChanged != null)
ValueChanged += onValueChanged;
}
public void Set(T value) {
if (EqualityComparer<T>.Default.Equals(this.value, value))
return;
this.value = value;
Invoke();
}
public void Invoke() {
ValueChanged?.Invoke(value);
}
public void AddListener(Action<T> handler) {
ValueChanged += handler;
}
public void RemoveListener(Action<T> handler) {
ValueChanged -= handler;
}
public void Dispose() {
ValueChanged = null;
value = default;
}
}
옵저버 패턴에 대해서는 나중에 다뤄보겠습니다. 간단하게 설명하자면 특정 객체를 계속 바라보면서 그 객체가 변하는지 안변하는지를 관찰하는 패턴이라고 생각하면 됩니다. 예를 들어, 객체가 아침이 되었는지 밤이 되었는지 상태의 변화를 관찰할 때 사용하면 좋은 패턴입니다.
SkyBox Shader
Shader "Skybox/NightDay"
{
Properties
{
_Texture1("Texture1", 2D) = "white" {}
_Texture2("Texture2", 2D) = "white" {}
_Blend("Blend", Range(0, 1)) = 0.5
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float3 texcoord : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _Texture1;
sampler2D _Texture2;
float _Blend;
v2f vert(appdata v)
{
v2f o;
o.texcoord = v.vertex.xyz;
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
float2 ToRadialCoords(float3 coords)
{
float3 normalizedCoords = normalize(coords);
float latitude = acos(normalizedCoords.y);
float longitude = atan2(normalizedCoords.z, normalizedCoords.x);
const float2 sphereCoords = float2(longitude, latitude) * float2(0.5 / UNITY_PI, 1.0 / UNITY_PI);
return float2(0.5, 1.0) - sphereCoords;
}
fixed4 frag(v2f i) : SV_Target
{
float2 tc = ToRadialCoords(i.texcoord);
fixed4 tex1 = tex2D(_Texture1, tc);
fixed4 tex2 = tex2D(_Texture2, tc);
return lerp(tex1, tex2, _Blend);
}
ENDCG
}
}
}
매우 간단한 코드입니다. 두 개의 스카이박스 텍스쳐를 블렌더시켜주는 쉐이더입니다.
쉐이더 코드를 작성한 후 메테리얼을 만들어주고 밤/낮 텍스쳐를 할당해주면 됩니다.
Time Manager
using System;
using UnityEngine;
using TMPro;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
public class TimeManager : MonoBehaviour {
[SerializeField] TextMeshProUGUI timeText;
[SerializeField] Light sun;
[SerializeField] Light moon;
[SerializeField] AnimationCurve lightIntensityCurve;
[SerializeField] float maxSunIntensity = 1;
[SerializeField] float maxMoonIntensity = 0.5f;
[SerializeField] Color dayAmbientLight;
[SerializeField] Color nightAmbientLight;
[SerializeField] Volume volume;
[SerializeField] Material skyboxMaterial;
[SerializeField] RectTransform dial;
float initialDialRotation;
ColorAdjustments colorAdjustments;
[SerializeField] TimeSettings timeSettings;
public event Action OnSunrise {
add => service.OnSunrise += value;
remove => service.OnSunrise -= value;
}
public event Action OnSunset {
add => service.OnSunset += value;
remove => service.OnSunset -= value;
}
public event Action OnHourChange {
add => service.OnHourChange += value;
remove => service.OnHourChange -= value;
}
TimeService service;
void Start() {
service = new TimeService(timeSettings);
volume.profile.TryGet(out colorAdjustments);
OnSunrise += () => Debug.Log("Sunrise");
OnSunset += () => Debug.Log("Sunset");
OnHourChange += () => Debug.Log("Hour change");
initialDialRotation = dial.rotation.eulerAngles.z;
}
void Update() {
UpdateTimeOfDay();
RotateSun();
UpdateLightSettings();
UpdateSkyBlend();
}
void UpdateSkyBlend() {
float dotProduct = Vector3.Dot(sun.transform.forward, Vector3.up);
float blend = Mathf.Lerp(0, 1, lightIntensityCurve.Evaluate(dotProduct));
skyboxMaterial.SetFloat("_Blend", blend);
}
void UpdateLightSettings() {
float dotProduct = Vector3.Dot(sun.transform.forward, Vector3.down);
float lightIntensity = lightIntensityCurve.Evaluate(dotProduct);
sun.intensity = Mathf.Lerp(0, maxSunIntensity, lightIntensity);
moon.intensity = Mathf.Lerp(maxMoonIntensity, 0, lightIntensity);
if (colorAdjustments == null) return;
colorAdjustments.colorFilter.value = Color.Lerp(nightAmbientLight, dayAmbientLight, lightIntensity);
}
void RotateSun() {
float rotation = service.CalculateSunAngle();
sun.transform.rotation = Quaternion.AngleAxis(rotation, Vector3.right);
dial.rotation = Quaternion.Euler(0, 0, rotation + initialDialRotation);
}
void UpdateTimeOfDay() {
service.UpdateTime(Time.deltaTime);
if (timeText != null) {
timeText.text = service.CurrentTime.ToString("hh:mm");
}
}
}
위에서 작성한 모든 코드들을 사용 및 관리하는 스크립트입니다.
TimeManager 스크립트를 붙여주신 다음, Sun, Moon 등 해당하는 프로퍼티들을 할당해줍니다.
Sun과 Moon의 경우 각도를 90,0,0으로 초기화해줍니다.
Volume의 경우 Color Adjusments를 오버라이드 해준 다음 Color Filter를 활성화해줍니다.
결과
게임이 엄청 사실적으로 변했네요😁
VR 게임같은데서 사용하면 더 몰입이 잘되겠죠?! 바로 적용해보러 가겠습니다 뿅~