Time progression, directional light rotation, and event triggers for dawn and dusk.
A robust Day/Night cycle system decoupled from rendering. It rotates a Directional Light to simulate the sun, tracks in-game time (0-24 hours), and fires C# events when time-of-day phases change (e.g., Dawn, Noon, Dusk, Midnight).
┌─────────────────────────────────────────────────────────────┐
│ DAY NIGHT CYCLE │
├─────────────────────────────────────────────────────────────┤
│ │
│ TIME TRACKER LIGHT CONTROLLER │
│ ┌────────────────┐ ┌───────────────────────────┐ │
│ │ Tick() │──────────▶│ UpdateRotation() │ │
│ │ CurrentTime │ │ UpdateIntensity() │ │
│ │ OnDayPassed │ └───────────────────────────┘ │
│ └────────────────┘ │
│ │ │
│ ▼ (Events) │
│ ┌────────────────┐ │
│ │ World Events │ (Spawn monsters, close shops) │
│ └────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
[Test]
public void TimePasses_AccordingToTimeScale()
{
var timeSystem = new TimeSystem(dayLengthInSeconds: 60f);
timeSystem.Tick(15f); // Advance 1/4 of a day
Assert.AreEqual(6f, timeSystem.CurrentHour); // 6 AM
}
[Test]
public void TimePassesMidnight_RaisesDayPassedEvent()
{
var timeSystem = new TimeSystem(dayLengthInSeconds: 60f);
timeSystem.SetTime(23f); // 11 PM
bool eventRaised = false;
timeSystem.OnDayPassed += () => eventRaised = true;
timeSystem.Tick(5f); // Pass midnight
Assert.IsTrue(eventRaised);
Assert.AreEqual(1f, timeSystem.CurrentHour); // 1 AM
}
// --- TimeSystem.cs ---
using System;
using UnityEngine;
public class TimeSystem : MonoBehaviour
{
[Header("Settings")]
[Tooltip("How many real-world minutes a full in-game day takes.")]
[SerializeField] private float _dayLengthInMinutes = 24f;
[SerializeField] private Transform _sunLight;
public float CurrentHour { get; private set; } = 8f; // Start at 8 AM
public int CurrentDay { get; private set; } = 1;
public event Action OnDayPassed;
public event Action<float> OnHourChanged;
private float _lastHourBroadcast = -1f;
private void Update()
{
Tick(Time.deltaTime);
}
public void Tick(float deltaSeconds)
{
float timeScale = 24f / (_dayLengthInMinutes * 60f);
CurrentHour += deltaSeconds * timeScale;
if (CurrentHour >= 24f)
{
CurrentHour -= 24f;
CurrentDay++;
OnDayPassed?.Invoke();
}
int intHour = Mathf.FloorToInt(CurrentHour);
if (intHour != _lastHourBroadcast)
{
_lastHourBroadcast = intHour;
OnHourChanged?.Invoke(CurrentHour);
}
UpdateSunRotation();
}
private void UpdateSunRotation()
{
if (_sunLight == null) return;
// 6 AM = 0 degrees (sunrise), 12 PM = 90 degrees (noon), 6 PM = 180 degrees (sunset)
float sunAngle = (CurrentHour - 6f) / 24f * 360f;
_sunLight.rotation = Quaternion.Euler(sunAngle, 0f, 0f);
}
public void SetTime(float newHour) => CurrentHour = Mathf.Repeat(newHour, 24f);
}
OnHourChanged, OnDayPassed) so other systems can react without polling.@save-load-serialization — Save the CurrentDay and CurrentHour.@event-bus-system — Broadcast time changes globally without tight coupling.