using FishNet.Managing.Timing; using FishNet.Object; using FishNet.Object.Synchronizing; using UnityEngine; namespace Ashwild.Environment { /// /// Owns the synchronized day/night clock and drives the stylized procedural skybox plus the /// directional sun. The whole sky is a pure function of a single time value, so the server only /// replicates one epoch (SyncVar) and every client reconstructs the identical sky locally — no /// per-frame traffic, and late joiners snap to the correct time of day from the replicated epoch. /// Mirrors SeasonManager's global-driving / [ExecuteAlways] style but networked like the registries. /// [ExecuteAlways] [RequireComponent(typeof(NetworkObject))] [AddComponentMenu("GAME/Environment/Sky Time Manager")] public class SkyTimeManager : NetworkBehaviour { #region Serialized Fields [Header("Cycle")] [Tooltip("Durée d'une journée complète (24 h de jeu) en secondes réelles.")] [SerializeField] private float dayLengthSeconds = 1200f; [Tooltip("Heure de départ du serveur. 0 = minuit, 0.25 = aube, 0.5 = midi, 0.75 = crépuscule.")] [Range(0f, 1f)] [SerializeField] private float initialTimeOfDay = 0.3f; [Header("Rendu")] [SerializeField] private Material skyboxMaterial; [SerializeField] private Light sunLight; [Tooltip("Orientation (azimut) de l'arc du soleil, en degrés.")] [SerializeField] private float sunAzimuthDegrees = 30f; [Tooltip("Intensité de la lumière directionnelle en plein jour (s'éteint la nuit).")] [SerializeField] private float maxSunIntensity = 1.1f; [Tooltip("Intervalle (s) de rafraîchissement de l'ambient/reflection — coûteux, pas chaque frame.")] [SerializeField] private float ambientRefreshInterval = 0.25f; [Header("Aperçu Éditeur")] [Tooltip("Heure simulée hors Play pour prévisualiser le ciel dans la Scene view.")] [Range(0f, 1f)] [SerializeField] private float editorPreviewTime = 0.3f; #endregion #region Networked State /// /// Server time (synchronized seconds) captured when the cycle's zero-point was set. Written /// once on server start and only again on an explicit SetTimeOfDay. Clients derive the current /// time of day from this plus their own corrected TimeManager tick. /// private readonly SyncVar startServerTime = new SyncVar(); #endregion #region State private static readonly int TimeOfDayId = Shader.PropertyToID("_TimeOfDay"); private static readonly int SunDirId = Shader.PropertyToID("_SunDirection"); private static readonly int SkyTimeId = Shader.PropertyToID("_SkyTime"); private float ambientTimer; #endregion #region Public State public static SkyTimeManager Instance { get; private set; } /// /// Current normalized time of day (0..1, midnight → midnight), identical on every client. /// public float TimeOfDay01 { get; private set; } #endregion #region Unity Lifecycle /// /// Validates the editor references and drives the editor preview when the game is not running. /// private void OnEnable() { WarnIfUnassigned(); } /// /// Clamps tuning and refreshes the sky preview live while editing. Does not rotate the sun /// transform here — Unity forbids transform writes during OnValidate; the shader reads the sun /// direction from a global computed analytically, and Update rotates the actual Light. /// private void OnValidate() { dayLengthSeconds = Mathf.Max(1f, dayLengthSeconds); ambientRefreshInterval = Mathf.Max(0.01f, ambientRefreshInterval); if (!Application.isPlaying) ApplyEditorPreview(false); } /// /// In Play, recomputes the synced time of day from the network clock and drives the sky; out /// of Play, drives the sky from the editor preview slider without touching the network stack. /// private void Update() { if (!Application.isPlaying) { ApplyEditorPreview(true); return; } if (!base.IsSpawned || base.TimeManager == null) return; double now = base.TimeManager.TicksToTime(TickType.Tick); double elapsed = now - startServerTime.Value; TimeOfDay01 = Mathf.Repeat(initialTimeOfDay + (float)(elapsed / dayLengthSeconds), 1f); ApplyVisuals(TimeOfDay01, (float)elapsed, true); RefreshAmbientThrottled(Time.deltaTime); } #endregion #region Network Lifecycle /// /// Claims the singleton and binds the skybox material + sun so every client renders the sky. /// public override void OnStartNetwork() { base.OnStartNetwork(); Instance = this; BindRenderSettings(); } /// /// Server-only: stamps the cycle's zero-point once so the SyncVar replicates the current time /// of day (offset by initialTimeOfDay) to every present and future client. /// public override void OnStartServer() { base.OnStartServer(); startServerTime.Value = base.TimeManager.TicksToTime(TickType.Tick); } /// /// Clears the singleton — mirrors OnStartNetwork. /// public override void OnStopNetwork() { base.OnStopNetwork(); if (Instance == this) Instance = null; } #endregion #region Public API /// /// Server-only: jumps the whole world to a given time of day by rewriting the epoch, which the /// SyncVar replicates so all clients snap together. Ignored when called off the server. /// public void SetTimeOfDay(float timeOfDay01) { if (!base.IsServerInitialized) { Debug.LogWarning("[SkyTimeManager] SetTimeOfDay ignored — only the server owns the clock.", this); return; } float target = Mathf.Repeat(timeOfDay01, 1f); double now = base.TimeManager.TicksToTime(TickType.Tick); startServerTime.Value = now - (target - initialTimeOfDay) * dayLengthSeconds; } #endregion #region Internal Helpers /// /// Assigns the procedural sky to the lighting settings and registers the sun for URP ambient. /// private void BindRenderSettings() { if (skyboxMaterial != null) RenderSettings.skybox = skyboxMaterial; if (sunLight != null) RenderSettings.sun = sunLight; } /// /// Drives the sky from the editor preview slider, repainting the Scene view so the look updates /// live without entering Play (matches SeasonManager's preview). rotateSun is false when called /// from OnValidate, where rotating the Light transform is disallowed. /// private void ApplyEditorPreview(bool rotateSun) { BindRenderSettings(); TimeOfDay01 = editorPreviewTime; ApplyVisuals(editorPreviewTime, editorPreviewTime * dayLengthSeconds, rotateSun); #if UNITY_EDITOR if (!Application.isPlaying) UnityEditor.SceneView.RepaintAll(); #endif } /// /// Fades the sun intensity below the horizon, pushes the synced time/sun-direction/animation /// globals the skybox shader reads, and (when rotateSun) rotates the Light along its arc. The /// sun direction is computed analytically so the shader is correct even when the transform is /// not touched. skyTime is the continuous synced seconds driving cloud drift and shooting stars, /// so those events match across clients. /// private void ApplyVisuals(float timeOfDay01, float skyTime, bool rotateSun) { float altitude = Mathf.Sin((timeOfDay01 - 0.25f) * 2f * Mathf.PI); Quaternion sunRotation = Quaternion.Euler(altitude * 90f, sunAzimuthDegrees, 0f); Vector3 sunDirection = -(sunRotation * Vector3.forward); if (sunLight != null) { if (rotateSun) sunLight.transform.rotation = sunRotation; sunLight.intensity = maxSunIntensity * Mathf.Clamp01(altitude * 1.5f + 0.05f); } Shader.SetGlobalFloat(TimeOfDayId, timeOfDay01); Shader.SetGlobalVector(SunDirId, sunDirection); Shader.SetGlobalFloat(SkyTimeId, skyTime); } /// /// Refreshes skybox-based ambient and reflections on a fixed cadence — UpdateEnvironment is /// expensive, so it must never run every frame. /// private void RefreshAmbientThrottled(float dt) { ambientTimer += dt; if (ambientTimer < ambientRefreshInterval) return; ambientTimer = 0f; DynamicGI.UpdateEnvironment(); } /// /// Logs a clear error when the inspector references are missing so the sky never fails silently. /// private void WarnIfUnassigned() { if (skyboxMaterial == null) Debug.LogError("[SkyTimeManager] No skybox material assigned — the sky will not render.", this); if (sunLight == null) Debug.LogError("[SkyTimeManager] No directional sun Light assigned — day/night lighting will not work.", this); } #endregion } }