using System; using System.Threading; using KitsuneCafe.Extension; using KitsuneCafe.Sys; using KitsuneCafe.Sys.Attributes; using R3; using UnityEngine; using Unit = R3.Unit; namespace KitsuneCafe.Event { public class FlickeringLight : MonoBehaviour { private const string EmissionColor = "_EmissionColor"; [Header("References")] [Tooltip("Light Source")] [SerializeField] private new Light light; [SerializeField] private bool hasEmitter = false; [Tooltip("Emission renderer")] [DrawIf(nameof(hasEmitter), true)] [SerializeField] private Renderer emitter; [Tooltip("Material index if mesh have more then 1 material")] [DrawIf(nameof(hasEmitter), true)] [SerializeField] private int materialIndex = 0; [Space(3f)] [Header("Options")] [Tooltip("How much to smooth out the randomness; lower values = sparks, higher = lantern")] [Range(1, 50)] [SerializeField] private int smoothing = 5; [SerializeField] private bool autostart = true; [SerializeField, DrawIf(nameof(autostart), true)] private bool loop = true; [Tooltip("Initial delay")] [SerializeField] private Duration delay = TimeSpan.Zero; [Tooltip("Delay between iterations")] [SerializeField] private Duration interval = TimeSpan.FromMilliseconds(5); [Tooltip("Duration of iterations")] [SerializeField] private Duration duration = TimeSpan.FromMilliseconds(5); private float maxIntensity; private float minIntensity = 0; private ExponentialMovingAverage ema; private float colorIntensity; private Color color; private float factor; private Material material; private CancellationTokenSource disableCancellationTokenSource; private CancellationToken disableCancellationToken => disableCancellationTokenSource.Token; private void Awake() { if (hasEmitter && emitter != null) { material = emitter.sharedMaterials[materialIndex]; color = emitter.materials[materialIndex].GetColor(EmissionColor); colorIntensity = (color.r + color.g + color.b) / 3f; } maxIntensity = light.intensity; ema = new(smoothing); } private void OnEnable() { disableCancellationTokenSource = new(); if (light == null || !autostart) { return; } if (loop) { LoopFlicker(interval, duration, delay); } else { Flicker(); } } private void OnDisable() { disableCancellationTokenSource?.Cancel(); light.intensity = maxIntensity; } public IDisposable LoopFlicker() => LoopFlicker(interval, duration); public IDisposable LoopFlicker(TimeSpan interval, TimeSpan duration) => LoopFlicker(interval, duration, UnityFrameProvider.Update); public IDisposable LoopFlicker(TimeSpan interval, TimeSpan duration, TimeSpan delay) => LoopFlicker(interval, duration, delay, UnityFrameProvider.Update); public IDisposable LoopFlicker(TimeSpan interval, TimeSpan duration, FrameProvider frameProvider) => LoopFlicker(delay, interval, duration, frameProvider, disableCancellationToken); public IDisposable LoopFlicker(TimeSpan interval, TimeSpan duration, TimeSpan delay, FrameProvider frameProvider) => LoopFlicker(interval, duration, delay, frameProvider, disableCancellationToken); public IDisposable LoopFlicker(TimeSpan interval, TimeSpan duration, TimeSpan delay, FrameProvider frameProvider, CancellationToken token) => Observable.Timer(delay, token) .SelectSwitch(_ => Observable.Interval(interval, token) .Merge(Observable.Return(Unit.Default)) .SelectMany(_ => Duration(duration, frameProvider, token)) ) .Subscribe( _ => Tick(), onCompleted: SetMaxIntensity ) .RegisterTo(disableCancellationToken); public IDisposable Flicker() => Flicker(duration); public IDisposable Flicker(TimeSpan duration) => Flicker(duration, UnityFrameProvider.Update); public IDisposable Flicker(TimeSpan duration, FrameProvider frameProvider) => Flicker(duration, frameProvider, disableCancellationToken); public IDisposable Flicker(TimeSpan duration, FrameProvider frameProvider, CancellationToken token) => Duration(duration, frameProvider, token) .Subscribe( _ => Tick(), onCompleted: SetMaxIntensity ) .RegisterTo(disableCancellationToken); private Observable Duration(TimeSpan duration) => Duration(duration, UnityFrameProvider.Update); private Observable Duration(TimeSpan duration, FrameProvider frameProvider) => Duration(duration, frameProvider, disableCancellationToken); private Observable Duration(TimeSpan duration, FrameProvider frameProvider, CancellationToken token) => Observable.EveryUpdate(frameProvider, token) .Select(_ => Time.deltaTime) .Scan((acc, dt) => acc + dt) .TakeUntil(t => t >= duration.TotalSeconds); private void SetMaxIntensity(R3.Result result) { light.intensity = maxIntensity; } private void Tick() { light.intensity = ema.NextValue(UnityEngine.Random.Range(minIntensity, maxIntensity)); factor = light.intensity / colorIntensity; if (hasEmitter && emitter != null) { material.SetColor( EmissionColor, new Color(color.r * factor, color.g * factor, color.b * factor) ); } } } }