Files
Echo/Assets/_Project/Scripts/Lights Control/LightsControlController.cs
T
2026-07-08 20:42:51 +08:00

577 lines
18 KiB
C#

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<Light> lights = new List<Light>();
[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<Light> cachedLights = new List<Light>();
readonly List<bool> cachedEnabled = new List<bool>();
readonly List<float> cachedIntensity = new List<float>();
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<Light> 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<Light> 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<Light>(
includeInactiveWhenSearching ? FindObjectsInactive.Include : FindObjectsInactive.Exclude,
FindObjectsSortMode.None
);
#else
found = UnityEngine.Object.FindObjectsOfType<Light>(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<Light>();
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;
}
}
}
}