시간 시스템

밤낮 시스템은 시간 흐름에 따라 게임 월드의 하늘이 변화하는 시스템이다.

Directional Light의 회전을 이용해 태양의 이동을 표현하고, Skybox와 Fog 값을 변경하여 아침, 낮, 저녁, 밤의 분위기를 자연스럽게 전환한다.

목차

1. 유니티 구현

     1.1. Directional Light 기반 태양 회전

    1.2. Skybox 기반 하늘 변화

    1.3. Fog 기반 환경 변화

2. 전체 코드

유니티 구현

1.1. Directional Light 기반 태양 회전

씬에는 태양 역할을 하는 Directional Light 오브젝트를 배치되어 있다.

Directional Light는 특정 위치가 아니라 방향을 기준으로 전체 씬에 빛을 비추는 조명으로, 태양과 같은 전역 광원을 표현하는 데 사용된다.

Directional Light를 X축 기준으로 회전시키면 태양의 위치가 이동하는 것처럼 보이게 된다.

이를 이용해 태양이 떠오르고 지는 시간 흐름을 표현하였다.

태양의 각도는 하루 시간의 기준이 되며, 태양의 X축 회전값을 기준으로 낮과 밤 상태를 판단하도록 구성하였다.

1.2. Skybox 기반 하늘 변화

시간 변화에 따라 하늘 색상이 자연스럽게 변화하도록 Skybox 시스템을 사용하였다.

Skybox는 씬 전체를 둘러싸는 환경 텍스처로, 하늘의 색상과 분위기를 표현하는 역할을 한다.

본 시스템에서는 여러 시간대에 맞는 Skybox Material을 미리 추가하여 리스트로 관리하였다.

Skybox Material에는 태양과 하늘 색상을 표현하기 위한 여러 셰이더 파라미터가 포함되어 있다.

Sun Disc는 태양의 색상과 밝기를 표현하며, Sun Halo는 태양 주변의 광원 효과를 표현한다.

Horizon Line은 지평선 부근의 색상을 조정하여 일출과 일몰의 분위기를 표현하고, Sky Gradient는 하늘 상단과 하단의 색상을 조정하여 시간대에 따른 하늘 색 변화를 표현한다.

이러한 파라미터들을 시간 흐름에 따라 보간(Lerp)하여 아침, 낮, 저녁, 밤의 하늘이 자연스럽게 전환되도록 구성하였다.

1.3. Fog 기반 환경 변화

밤과 낮의 분위기 차이를 표현하기 위해 Unity의 Fog 시스템을 사용하였다.

Fog는 일정 거리 이상의 오브젝트를 안개로 처리하여 환경의 깊이감과 분위기를 표현하는 기능이다.

시간 상태에 따라 Fog Density 값을 변경하여 낮에는 시야가 밝고 멀리까지 선명하게 보이도록 설정하고, 밤에는 Fog Density를 증가시켜 어두운 분위기가 느껴지도록 구성하였다.

Fog 값은 갑자기 변경되지 않도록 현재 값에서 목표 값까지 Density를 점진적으로 변화하도록 설정하여, 환경 변화가 자연스럽게 보이도록 구현하였다.

fog on
fog off

전체 코드

using System.Collections.Generic;
using UnityEngine;

public class Sun : MonoBehaviour
{
    [Header("Time Settings")]
    [SerializeField] private float msTime = 1f;
    public int day = 1;

    [Header("Fog Settings")]
    [SerializeField] private float fogDensityCalc = 0.1f;
    [SerializeField] private float nightFogDensity = 0.05f;
    [SerializeField] private float dayFogDensity = 0f;
    [SerializeField] private float currentFogDensity;

    [Header("Skybox Settings")]
    public Material skyMat; // 실제로 적용되는 스카이박스 마테리얼
    public List<Material> skyboxList = new List<Material>(); // 각 시간대별 레퍼런스

    private bool isNight = false;
    private bool previousIsNight = false;
    private int skyDay = 0;
    private bool dayTrigger = true;

    // 셰이더 프로퍼티 ID 
    private static readonly int SunDiscColor = Shader.PropertyToID("_SunDiscColor");
    private static readonly int SunDiscMult = Shader.PropertyToID("_SunDiscMultiplier");
    private static readonly int SunHaloColor = Shader.PropertyToID("_SunHaloColor");
    private static readonly int SunHaloExp = Shader.PropertyToID("_SunHaloExponent");
    private static readonly int SunHaloCont = Shader.PropertyToID("_SunHaloContribution");
    private static readonly int HorizonColor = Shader.PropertyToID("_HorizonLineColor");
    private static readonly int HorizonExp = Shader.PropertyToID("_HorizonLineExponent");
    private static readonly int HorizonCont = Shader.PropertyToID("_HorizonLineContribution");
    private static readonly int SkyTop = Shader.PropertyToID("_SkyGradientTop");
    private static readonly int SkyBottom = Shader.PropertyToID("_SkyGradientBottom");
    private static readonly int SkyExp = Shader.PropertyToID("_SkyGradientExponent");

    private void Start()
    {
        dayFogDensity = RenderSettings.fogDensity;
        currentFogDensity = dayFogDensity;
    }

    private void Update()
    {
        float rotationSpeed = isNight ? 0.2f : 0.1f;
        transform.Rotate(Vector3.right, rotationSpeed * msTime * Time.deltaTime);

        float angleX = transform.eulerAngles.x;
        float angleZ = transform.eulerAngles.z;

        UpdateNightState(angleX);
        UpdateDayCycle();
        UpdateFog(angleX);
        UpdateSkybox(angleX, angleZ);

        previousIsNight = isNight;
    }

    void UpdateNightState(float angleX)
    {
        if (angleX >= 340)
            isNight = false;
        else if (angleX >= 170)
            isNight = true;
    }

    void UpdateDayCycle()
    {
        if (previousIsNight && !isNight)
        {
            day = (day % 7) + 1; // 1~7일 반복
            if (day == 1) RaidManager.instance.SpawnStart();
        }
    }

    void UpdateFog(float angleX)
    {
        float targetDensity;

        // 170도(밤 시작) 근처에서 갑자기 변하지 않도록 
        // 일몰 구간(120도 ~ 170도)부터 서서히 목표 농도를 높임
        if (angleX > 120 && angleX < 170)
        {
            // 120도일 때 0, 170도일 때 nightFogDensity가 되도록 보간
            float t = (angleX - 120) / (170 - 120);
            targetDensity = Mathf.Lerp(0, nightFogDensity, t);
        }
        else if (isNight)
        {
            targetDensity = nightFogDensity;
        }
        else
        {
            targetDensity = 0f;
        }

        currentFogDensity = Mathf.MoveTowards(currentFogDensity, targetDensity, fogDensityCalc * Time.deltaTime);
        RenderSettings.fogDensity = currentFogDensity;
    }

    void UpdateSkybox(float angleX, float angleZ)
    {
        Material bef, aft;
        bool isNightSide = angleZ > 90;

        if (isNightSide)
        {
            if (angleX > 270) 
            { 
                bef = GetSky(16, 4); 
                aft = GetSky(11, 5); 
            } // Night -> Morning
            else 
            { 
                bef = GetSky(11, 5); 
                aft = GetSky(4, 7); 
            } // Sunset -> Night
        }
        else
        {
            if (angleX > 270) 
            { 
                bef = GetSky(16, 4); 
                aft = GetSky(0, 4); 
               
                if (!dayTrigger) dayTrigger = true; 
            } // Night -> Morning
            else
            {
                if (dayTrigger) 
                { 
                    dayTrigger = false; 
                    skyDay++; 
                }

                bef = GetSky(0, 4, 3); 
                aft = GetSky(4, 7); // Morning -> Day
            }
        }

        ApplyShaderLerp(bef, aft, (angleX % 90) / 90f);
    }

    Material GetSky(int startIdx, int range, int offset = 0) => skyboxList[startIdx + (skyDay + offset) % range];

    void ApplyShaderLerp(Material b, Material a, float t)
    {
        skyMat.SetColor(SunDiscColor, Color.Lerp(b.GetColor(SunDiscColor), a.GetColor(SunDiscColor), t));
        skyMat.SetFloat(SunDiscMult, Mathf.Lerp(b.GetFloat(SunDiscMult), a.GetFloat(SunDiscMult), t));

        skyMat.SetColor(SunHaloColor, Color.Lerp(b.GetColor(SunHaloColor), a.GetColor(SunHaloColor), t));
        skyMat.SetFloat(SunHaloExp, Mathf.Lerp(b.GetFloat(SunHaloExp), a.GetFloat(SunHaloExp), t));
        skyMat.SetFloat(SunHaloCont, Mathf.Lerp(b.GetFloat(SunHaloCont), a.GetFloat(SunHaloCont), t));

        skyMat.SetColor(HorizonColor, Color.Lerp(b.GetColor(HorizonColor), a.GetColor(HorizonColor), t));
        skyMat.SetFloat(HorizonExp, Mathf.Lerp(b.GetFloat(HorizonExp), a.GetFloat(HorizonExp), t));
        skyMat.SetFloat(HorizonCont, Mathf.Lerp(b.GetFloat(HorizonCont), a.GetFloat(HorizonCont), t));

        skyMat.SetColor(SkyTop, Color.Lerp(b.GetColor(SkyTop), a.GetColor(SkyTop), t));
        skyMat.SetColor(SkyBottom, Color.Lerp(b.GetColor(SkyBottom), a.GetColor(SkyBottom), t));
        skyMat.SetFloat(SkyExp, Mathf.Lerp(b.GetFloat(SkyExp), a.GetFloat(SkyExp), t));
    }
}