577 lines
18 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
}
|