253 lines
9.9 KiB
C#
253 lines
9.9 KiB
C#
using FishNet.Managing.Timing;
|
|
using FishNet.Object;
|
|
using FishNet.Object.Synchronizing;
|
|
using UnityEngine;
|
|
|
|
namespace Ashwild.Environment
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[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
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
private readonly SyncVar<double> startServerTime = new SyncVar<double>();
|
|
|
|
#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; }
|
|
|
|
/// <summary>
|
|
/// Current normalized time of day (0..1, midnight → midnight), identical on every client.
|
|
/// </summary>
|
|
public float TimeOfDay01 { get; private set; }
|
|
|
|
#endregion
|
|
|
|
#region Unity Lifecycle
|
|
|
|
/// <summary>
|
|
/// Validates the editor references and drives the editor preview when the game is not running.
|
|
/// </summary>
|
|
private void OnEnable()
|
|
{
|
|
WarnIfUnassigned();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
private void OnValidate()
|
|
{
|
|
dayLengthSeconds = Mathf.Max(1f, dayLengthSeconds);
|
|
ambientRefreshInterval = Mathf.Max(0.01f, ambientRefreshInterval);
|
|
if (!Application.isPlaying) ApplyEditorPreview(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Claims the singleton and binds the skybox material + sun so every client renders the sky.
|
|
/// </summary>
|
|
public override void OnStartNetwork()
|
|
{
|
|
base.OnStartNetwork();
|
|
Instance = this;
|
|
BindRenderSettings();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public override void OnStartServer()
|
|
{
|
|
base.OnStartServer();
|
|
startServerTime.Value = base.TimeManager.TicksToTime(TickType.Tick);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears the singleton — mirrors OnStartNetwork.
|
|
/// </summary>
|
|
public override void OnStopNetwork()
|
|
{
|
|
base.OnStopNetwork();
|
|
if (Instance == this) Instance = null;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Public API
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Assigns the procedural sky to the lighting settings and registers the sun for URP ambient.
|
|
/// </summary>
|
|
private void BindRenderSettings()
|
|
{
|
|
if (skyboxMaterial != null) RenderSettings.skybox = skyboxMaterial;
|
|
if (sunLight != null) RenderSettings.sun = sunLight;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
private void ApplyEditorPreview(bool rotateSun)
|
|
{
|
|
BindRenderSettings();
|
|
TimeOfDay01 = editorPreviewTime;
|
|
ApplyVisuals(editorPreviewTime, editorPreviewTime * dayLengthSeconds, rotateSun);
|
|
|
|
#if UNITY_EDITOR
|
|
if (!Application.isPlaying) UnityEditor.SceneView.RepaintAll();
|
|
#endif
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Refreshes skybox-based ambient and reflections on a fixed cadence — UpdateEnvironment is
|
|
/// expensive, so it must never run every frame.
|
|
/// </summary>
|
|
private void RefreshAmbientThrottled(float dt)
|
|
{
|
|
ambientTimer += dt;
|
|
if (ambientTimer < ambientRefreshInterval) return;
|
|
ambientTimer = 0f;
|
|
DynamicGI.UpdateEnvironment();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Logs a clear error when the inspector references are missing so the sky never fails silently.
|
|
/// </summary>
|
|
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
|
|
}
|
|
}
|