canto/Assets/Scripts/Event/FlickeringLight.cs
2025-10-02 15:28:03 -04:00

176 lines
5.4 KiB
C#

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<float> Duration(TimeSpan duration) =>
Duration(duration, UnityFrameProvider.Update);
private Observable<float> Duration(TimeSpan duration, FrameProvider frameProvider) =>
Duration(duration, frameProvider, disableCancellationToken);
private Observable<float> 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)
);
}
}
}
}