using System; using System.Collections; using System.Collections.Generic; using UnityEngine; namespace Test_2022.LightsControl { [DisallowMultipleComponent] public sealed class LightsControlController : MonoBehaviour { public enum ControlMode { Manual = 0, TimedToggle = 1, HorrorFlicker = 2 } public enum TimedToggleMode { FixedOnOffDurations = 0, DurationAndToggleCount = 1 } [Serializable] public struct TimedToggleSettings { public TimedToggleMode mode; public bool startOn; public bool endOn; public bool loop; public float onSeconds; public float offSeconds; public int cycles; public float durationSeconds; public int toggleCount; [Range(0f, 1f)] public float jitter; public static TimedToggleSettings Default => new TimedToggleSettings { mode = TimedToggleMode.FixedOnOffDurations, startOn = true, endOn = true, loop = false, onSeconds = 0.15f, offSeconds = 0.12f, cycles = 8, durationSeconds = 2f, toggleCount = 10, jitter = 0.1f }; } [Serializable] public struct HorrorFlickerSettings { public bool startOn; public bool endOn; public bool loop; public float durationSeconds; public bool affectIntensity; [Min(0f)] public float intensityMinMultiplier; [Min(0f)] public float intensityMaxMultiplier; [Min(0f)] public float subtleNoiseSpeed; [Range(0f, 1f)] public float subtleNoiseStrength; [Min(0f)] public float steadyOnMinSeconds; [Min(0f)] public float steadyOnMaxSeconds; [Min(0f)] public float offBlipMinSeconds; [Min(0f)] public float offBlipMaxSeconds; [Min(0f)] public float blackoutMinSeconds; [Min(0f)] public float blackoutMaxSeconds; [Range(0f, 1f)] public float blackoutChance; [Min(0)] public int burstToggleMin; [Min(0)] public int burstToggleMax; [Min(0f)] public float burstIntervalMinSeconds; [Min(0f)] public float burstIntervalMaxSeconds; [Range(0f, 1f)] public float steadyWeight; [Range(0f, 1f)] public float offBlipWeight; [Range(0f, 1f)] public float burstWeight; public bool useFixedSeed; public int seed; public static HorrorFlickerSettings Default => new HorrorFlickerSettings { startOn = true, endOn = true, loop = false, durationSeconds = 6f, affectIntensity = true, intensityMinMultiplier = 0.6f, intensityMaxMultiplier = 1.05f, subtleNoiseSpeed = 1.2f, subtleNoiseStrength = 0.35f, steadyOnMinSeconds = 0.35f, steadyOnMaxSeconds = 1.25f, offBlipMinSeconds = 0.03f, offBlipMaxSeconds = 0.18f, blackoutMinSeconds = 0.25f, blackoutMaxSeconds = 1.2f, blackoutChance = 0.06f, burstToggleMin = 4, burstToggleMax = 14, burstIntervalMinSeconds = 0.02f, burstIntervalMaxSeconds = 0.11f, steadyWeight = 0.55f, offBlipWeight = 0.25f, burstWeight = 0.20f, useFixedSeed = false, seed = 12345 }; } [Header("Lights")] [SerializeField] List lights = new List(); [SerializeField] bool includeInactiveWhenSearching = true; [Header("Control")] [SerializeField] ControlMode mode = ControlMode.Manual; [SerializeField] bool manualOn = true; [SerializeField] bool autoStartOnEnable = true; [SerializeField] bool restoreBaseStateOnDisable = true; [SerializeField] TimedToggleSettings timedToggle = TimedToggleSettings.Default; [SerializeField] HorrorFlickerSettings horrorFlicker = HorrorFlickerSettings.Default; Coroutine running; readonly List cachedLights = new List(); readonly List cachedEnabled = new List(); readonly List cachedIntensity = new List(); public ControlMode Mode { get => mode; set => mode = value; } public bool ManualOn { get => manualOn; set => manualOn = value; } public bool AutoStartOnEnable { get => autoStartOnEnable; set => autoStartOnEnable = value; } public IList Lights => lights; void OnEnable() { if (!autoStartOnEnable) { if (mode == ControlMode.Manual) ApplyManual(); return; } if (mode == ControlMode.Manual) ApplyManual(); else StartControl(); } void OnDisable() { StopControl(restoreBaseStateOnDisable); } public void ApplyManual() { StopControl(true); SetAllEnabled(manualOn); } public void ForceOn() { StopControl(false); SetAllEnabled(true); } public void ForceOff() { StopControl(false); SetAllEnabled(false); } public void StartControl() { StopControl(false); CacheBaseState(); switch (mode) { case ControlMode.Manual: ApplyManual(); return; case ControlMode.TimedToggle: running = StartCoroutine(RunTimedToggle(timedToggle)); return; case ControlMode.HorrorFlicker: running = StartCoroutine(RunHorrorFlicker(horrorFlicker)); return; default: ApplyManual(); return; } } public void StopControl(bool restoreBaseState) { if (running != null) { StopCoroutine(running); running = null; } if (restoreBaseState) RestoreBaseState(); } public void SetAllEnabled(bool enabled) { for (var i = 0; i < lights.Count; i++) { var l = lights[i]; if (l == null) continue; l.enabled = enabled; } } public void SetAllIntensityMultiplier(float multiplier) { if (cachedLights.Count == 0) CacheBaseState(); for (var i = 0; i < cachedLights.Count; i++) { var l = cachedLights[i]; if (l == null) continue; l.intensity = cachedIntensity[i] * multiplier; } } public void ClearLights() { lights.Clear(); } public bool AddLight(Light light) { if (light == null) return false; if (lights.Contains(light)) return false; lights.Add(light); return true; } public int AddLights(IEnumerable addLights) { if (addLights == null) return 0; var added = 0; foreach (var l in addLights) { if (l == null) continue; if (lights.Contains(l)) continue; lights.Add(l); added++; } return added; } public int RemoveNullLights() { return lights.RemoveAll(l => l == null); } public int FindAllLightsInScene(bool clearFirst, Transform root = null) { Light[] found; #if UNITY_2022_2_OR_NEWER found = UnityEngine.Object.FindObjectsByType( includeInactiveWhenSearching ? FindObjectsInactive.Include : FindObjectsInactive.Exclude, FindObjectsSortMode.None ); #else found = UnityEngine.Object.FindObjectsOfType(includeInactiveWhenSearching); #endif if (clearFirst) lights.Clear(); var added = 0; for (var i = 0; i < found.Length; i++) { var l = found[i]; if (l == null) continue; if (root != null && !l.transform.IsChildOf(root)) continue; if (lights.Contains(l)) continue; lights.Add(l); added++; } return added; } void CacheBaseState() { cachedLights.Clear(); cachedEnabled.Clear(); cachedIntensity.Clear(); var seen = new HashSet(); for (var i = 0; i < lights.Count; i++) { var l = lights[i]; if (l == null) continue; if (!seen.Add(l)) continue; cachedLights.Add(l); cachedEnabled.Add(l.enabled); cachedIntensity.Add(l.intensity); } } void RestoreBaseState() { for (var i = 0; i < cachedLights.Count; i++) { var l = cachedLights[i]; if (l == null) continue; l.enabled = cachedEnabled[i]; l.intensity = cachedIntensity[i]; } } IEnumerator RunTimedToggle(TimedToggleSettings s) { do { if (lights.Count == 0) yield break; SetAllEnabled(s.startOn); if (s.mode == TimedToggleMode.FixedOnOffDurations) { var cycles = Mathf.Max(0, s.cycles); for (var i = 0; i < cycles; i++) { if (s.startOn) yield return WaitOrBreak(Mathf.Max(0f, s.onSeconds)); else yield return WaitOrBreak(Mathf.Max(0f, s.offSeconds)); SetAllEnabled(!s.startOn); if (s.startOn) yield return WaitOrBreak(Mathf.Max(0f, s.offSeconds)); else yield return WaitOrBreak(Mathf.Max(0f, s.onSeconds)); SetAllEnabled(s.startOn); } } else { var count = Mathf.Max(0, s.toggleCount); if (count <= 0) yield break; var baseInterval = Mathf.Max(0f, s.durationSeconds) / count; var current = s.startOn; for (var i = 0; i < count; i++) { var interval = baseInterval; if (s.jitter > 0f) { var j = Mathf.Clamp01(s.jitter); interval *= UnityEngine.Random.Range(1f - j, 1f + j); } yield return WaitOrBreak(interval); current = !current; SetAllEnabled(current); } } SetAllEnabled(s.endOn); } while (s.loop); } IEnumerator WaitOrBreak(float seconds) { if (seconds <= 0f) yield break; yield return new WaitForSeconds(seconds); } IEnumerator RunHorrorFlicker(HorrorFlickerSettings s) { var previousRandomState = UnityEngine.Random.state; if (s.useFixedSeed) UnityEngine.Random.InitState(s.seed); try { do { if (lights.Count == 0) yield break; SetAllEnabled(s.startOn); var untilTime = s.durationSeconds > 0f ? Time.unscaledTime + s.durationSeconds : float.PositiveInfinity; var on = s.startOn; while (Time.unscaledTime < untilTime) { var wSteady = Mathf.Max(0f, s.steadyWeight); var wOff = Mathf.Max(0f, s.offBlipWeight); var wBurst = Mathf.Max(0f, s.burstWeight); var wTotal = wSteady + wOff + wBurst; if (wTotal <= 0f) wTotal = 1f; var pick = UnityEngine.Random.value * wTotal; if (pick < wSteady) { var seg = UnityEngine.Random.Range( Mathf.Max(0f, s.steadyOnMinSeconds), Mathf.Max(0f, s.steadyOnMaxSeconds) ); if (!on) { on = true; SetAllEnabled(true); } var end = Time.unscaledTime + seg; while (Time.unscaledTime < end && Time.unscaledTime < untilTime) { ApplySubtleIntensityNoise(s); yield return null; } } else if (pick < wSteady + wOff) { if (UnityEngine.Random.value < Mathf.Clamp01(s.blackoutChance)) { if (on) { on = false; SetAllEnabled(false); } var blackout = UnityEngine.Random.Range( Mathf.Max(0f, s.blackoutMinSeconds), Mathf.Max(0f, s.blackoutMaxSeconds) ); yield return WaitOrBreak(Mathf.Min(blackout, untilTime - Time.unscaledTime)); if (Time.unscaledTime < untilTime) { on = true; SetAllEnabled(true); } } else { if (on) { on = false; SetAllEnabled(false); } var off = UnityEngine.Random.Range( Mathf.Max(0f, s.offBlipMinSeconds), Mathf.Max(0f, s.offBlipMaxSeconds) ); yield return WaitOrBreak(Mathf.Min(off, untilTime - Time.unscaledTime)); if (Time.unscaledTime < untilTime) { on = true; SetAllEnabled(true); } } } else { var minT = Mathf.Max(0, s.burstToggleMin); var maxT = Mathf.Max(minT, s.burstToggleMax); var toggles = UnityEngine.Random.Range(minT, maxT + 1); for (var i = 0; i < toggles && Time.unscaledTime < untilTime; i++) { on = !on; SetAllEnabled(on); ApplySubtleIntensityNoise(s); var interval = UnityEngine.Random.Range( Mathf.Max(0f, s.burstIntervalMinSeconds), Mathf.Max(0f, s.burstIntervalMaxSeconds) ); yield return WaitOrBreak(Mathf.Min(interval, untilTime - Time.unscaledTime)); } if (Time.unscaledTime < untilTime) { on = true; SetAllEnabled(true); } } } SetAllEnabled(s.endOn); } while (s.loop); } finally { if (s.useFixedSeed) UnityEngine.Random.state = previousRandomState; } } void ApplySubtleIntensityNoise(HorrorFlickerSettings s) { if (!s.affectIntensity) return; if (cachedLights.Count == 0) CacheBaseState(); var min = Mathf.Max(0f, s.intensityMinMultiplier); var max = Mathf.Max(min, s.intensityMaxMultiplier); var t = Time.unscaledTime * Mathf.Max(0f, s.subtleNoiseSpeed); var strength = Mathf.Clamp01(s.subtleNoiseStrength); for (var i = 0; i < cachedLights.Count; i++) { var l = cachedLights[i]; if (l == null) continue; var n = Mathf.PerlinNoise((s.seed * 0.001f) + i * 13.37f, t); var target = Mathf.Lerp(min, max, n); var blended = Mathf.Lerp(1f, target, strength); l.intensity = cachedIntensity[i] * blended; } } } }