399 lines
14 KiB
C#
399 lines
14 KiB
C#
using System;
|
|
using System.Threading;
|
|
using BriarQueen.Framework.Services.Destruction;
|
|
using Cysharp.Threading.Tasks;
|
|
using PrimeTween;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
using VContainer;
|
|
|
|
namespace BriarQueen.Game.Effects
|
|
{
|
|
[ExecuteAlways]
|
|
public class UIFogReveal : MonoBehaviour
|
|
{
|
|
// ── Shader property IDs ───────────────────────────────────────
|
|
private static readonly int _fogAmountId = Shader.PropertyToID("_FogAmount");
|
|
private static readonly int _fogColorId = Shader.PropertyToID("_FogColor");
|
|
private static readonly int _fogIntensityId = Shader.PropertyToID("_FogIntensity");
|
|
private static readonly int _edgeSoftnessId = Shader.PropertyToID("_EdgeSoftness");
|
|
private static readonly int _noiseScaleId = Shader.PropertyToID("_NoiseScale");
|
|
private static readonly int _driftSpeedId = Shader.PropertyToID("_DriftSpeed");
|
|
private static readonly int _densityVariationId = Shader.PropertyToID("_DensityVariation");
|
|
private static readonly int _aspectRatioId = Shader.PropertyToID("_AspectRatio");
|
|
private static readonly int _fogMotionId = Shader.PropertyToID("_FogMotion");
|
|
private static readonly int _useSpriteId = Shader.PropertyToID("_UseSprite");
|
|
|
|
// ── Inspector ─────────────────────────────────────────────────
|
|
[Header("References")]
|
|
[SerializeField] private Image _image;
|
|
[SerializeField] private Material _fogMaterialTemplate;
|
|
|
|
[Header("Fog Settings")]
|
|
[SerializeField] private Color _fogColor = new(0.18f, 0.20f, 0.26f, 1f);
|
|
[SerializeField][Range(0f, 1f)] private float _fogIntensity = 0.95f;
|
|
[SerializeField][Range(0.05f, 1f)] private float _edgeSoftness = 0.55f;
|
|
[SerializeField][Range(1f, 20f)] private float _noiseScale = 5f;
|
|
[SerializeField][Range(0f, 0.5f)] private float _driftSpeed = 0.04f;
|
|
[SerializeField][Range(0f, 1f)] private float _densityVariation = 0.28f;
|
|
[SerializeField] private bool _fogMotion = true;
|
|
[SerializeField] private bool _useSprite = false;
|
|
|
|
[Header("Fog Range")]
|
|
[SerializeField][Range(0f, 1f)] private float _startFog = 0f;
|
|
[SerializeField][Range(0f, 1f)] private float _maxFog = 0.6f;
|
|
|
|
[Header("Screen")]
|
|
[SerializeField] private bool _autoAspectRatio = true;
|
|
[SerializeField] private float _aspectRatio = 1.777f;
|
|
|
|
[Header("Tween")]
|
|
[SerializeField] private float _duration = 1.5f;
|
|
[SerializeField] private Ease _ease = Ease.InOutSine;
|
|
[SerializeField] private bool _useUnscaledTime = true;
|
|
|
|
[Header("Delay")]
|
|
[SerializeField][Range(0f, 5f)] private float _fogInDelay = 0f;
|
|
[SerializeField][Range(0f, 5f)] private float _fogOutDelay = 0f;
|
|
|
|
[Header("Editor Preview")]
|
|
[SerializeField] private bool _previewInEditMode;
|
|
[SerializeField][Range(0f, 1f)] private float _previewFogAmount;
|
|
|
|
// ── Runtime state ─────────────────────────────────────────────
|
|
private Material _runtimeMaterial;
|
|
private bool _isPreviewMaterial;
|
|
private Sequence _fogSequence;
|
|
private CancellationTokenSource _fogCts;
|
|
private DestructionService _destructionService;
|
|
|
|
// ── Public properties ─────────────────────────────────────────
|
|
public float FogAmount
|
|
{
|
|
get => _runtimeMaterial != null ? _runtimeMaterial.GetFloat(_fogAmountId) : 0f;
|
|
set => SetFogAmount(value);
|
|
}
|
|
|
|
public float FogInDuration => _fogInDelay + _duration;
|
|
public float FogOutDuration => _fogOutDelay + _duration;
|
|
|
|
public bool FogMotion
|
|
{
|
|
get => _fogMotion;
|
|
set
|
|
{
|
|
_fogMotion = value;
|
|
_runtimeMaterial?.SetFloat(_fogMotionId, value ? 1f : 0f);
|
|
}
|
|
}
|
|
|
|
public bool UseSprite
|
|
{
|
|
get => _useSprite;
|
|
set
|
|
{
|
|
_useSprite = value;
|
|
_runtimeMaterial?.SetFloat(_useSpriteId, value ? 1f : 0f);
|
|
}
|
|
}
|
|
|
|
public float MaxFog
|
|
{
|
|
get => _maxFog;
|
|
set => _maxFog = value;
|
|
}
|
|
|
|
// ── DI ────────────────────────────────────────────────────────
|
|
[Inject]
|
|
public void Construct(DestructionService destructionService)
|
|
{
|
|
_destructionService = destructionService;
|
|
}
|
|
|
|
// ── Unity lifecycle ───────────────────────────────────────────
|
|
private void Awake()
|
|
{
|
|
ResolveReferences();
|
|
|
|
if (Application.isPlaying)
|
|
{
|
|
CreateRuntimeMaterial(false);
|
|
SetFogAmount(_startFog);
|
|
return;
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
RefreshEditModePreview();
|
|
#endif
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
#if UNITY_EDITOR
|
|
if (!Application.isPlaying)
|
|
RefreshEditModePreview();
|
|
#endif
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
#if UNITY_EDITOR
|
|
if (!Application.isPlaying)
|
|
RestoreEditModeMaterial();
|
|
#endif
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
if (!Application.isPlaying) return;
|
|
if (!_autoAspectRatio) return;
|
|
|
|
var canvas = GetComponentInParent<Canvas>();
|
|
if (canvas == null) return;
|
|
|
|
var rt = canvas.GetComponent<RectTransform>();
|
|
var ratio = rt.rect.width / Mathf.Max(rt.rect.height, 0.001f);
|
|
_runtimeMaterial?.SetFloat(_aspectRatioId, ratio);
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
CancelFog();
|
|
|
|
if (Application.isPlaying)
|
|
{
|
|
DestroyRuntimeMaterial(false);
|
|
return;
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
RestoreEditModeMaterial();
|
|
#endif
|
|
}
|
|
|
|
private void OnValidate()
|
|
{
|
|
ResolveReferences();
|
|
|
|
if (Application.isPlaying)
|
|
{
|
|
ApplyShaderSettings();
|
|
return;
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
RestoreEditModeMaterial();
|
|
RefreshEditModePreview();
|
|
#endif
|
|
}
|
|
|
|
// ── Public API ────────────────────────────────────────────────
|
|
|
|
/// <summary>Animate fog in (startFog → maxFog). Respects _fogInDelay.</summary>
|
|
public UniTask FogIn() => TweenFogWithDelay(_startFog, _maxFog, _duration, _fogInDelay);
|
|
|
|
/// <summary>Animate fog in over a custom duration, no delay.</summary>
|
|
public UniTask FogIn(float duration) => TweenFog(_startFog, _maxFog, duration);
|
|
|
|
/// <summary>Animate fog out (maxFog → startFog). Respects _fogOutDelay.</summary>
|
|
public UniTask FogOut() => TweenFogWithDelay(_maxFog, _startFog, _duration, _fogOutDelay);
|
|
|
|
/// <summary>Animate fog out over a custom duration, no delay.</summary>
|
|
public UniTask FogOut(float duration) => TweenFog(_maxFog, _startFog, duration);
|
|
|
|
/// <summary>Animate fog to an arbitrary target amount, no delay.</summary>
|
|
public UniTask FogTo(float target, float duration) => TweenFog(FogAmount, target, duration);
|
|
|
|
/// <summary>Snap fog to a value immediately, cancels any running tween.</summary>
|
|
public void FogSet(float amount)
|
|
{
|
|
CancelFog();
|
|
SetFogAmount(amount);
|
|
}
|
|
|
|
/// <summary>Snap fog to startFog immediately.</summary>
|
|
public void FogReset()
|
|
{
|
|
CancelFog();
|
|
SetFogAmount(_startFog);
|
|
}
|
|
|
|
public void CancelFog()
|
|
{
|
|
if (_fogSequence.isAlive)
|
|
{
|
|
_fogSequence.Stop();
|
|
_fogSequence = default;
|
|
}
|
|
|
|
if (_fogCts != null)
|
|
{
|
|
_fogCts.Cancel();
|
|
_fogCts.Dispose();
|
|
_fogCts = null;
|
|
}
|
|
}
|
|
|
|
public async UniTask TweenFog(float from, float to, float duration)
|
|
{
|
|
await TweenFogWithDelay(from, to, duration, 0f);
|
|
}
|
|
|
|
public async UniTask TweenFogWithDelay(float from, float to, float duration, float delay)
|
|
{
|
|
if (_runtimeMaterial == null)
|
|
CreateRuntimeMaterial(false);
|
|
|
|
CancelFog();
|
|
|
|
_fogCts = new CancellationTokenSource();
|
|
SetFogAmount(from);
|
|
|
|
try
|
|
{
|
|
if (delay > 0f)
|
|
{
|
|
await UniTask.Delay(
|
|
TimeSpan.FromSeconds(delay),
|
|
ignoreTimeScale: _useUnscaledTime,
|
|
cancellationToken: _fogCts.Token);
|
|
}
|
|
|
|
_fogSequence = Sequence.Create(useUnscaledTime: _useUnscaledTime)
|
|
.Group(Tween.Custom(
|
|
from,
|
|
to,
|
|
Mathf.Max(0f, duration),
|
|
SetFogAmount,
|
|
_ease,
|
|
useUnscaledTime: _useUnscaledTime));
|
|
|
|
await _fogSequence.ToUniTask(cancellationToken: _fogCts.Token);
|
|
SetFogAmount(to);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// Interrupted — normal, swallow it.
|
|
}
|
|
finally
|
|
{
|
|
_fogSequence = default;
|
|
|
|
if (_fogCts != null)
|
|
{
|
|
_fogCts.Dispose();
|
|
_fogCts = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Internal ──────────────────────────────────────────────────
|
|
|
|
private void SetFogAmount(float amount)
|
|
{
|
|
if (_runtimeMaterial == null) return;
|
|
_runtimeMaterial.SetFloat(_fogAmountId, Mathf.Clamp01(amount));
|
|
_image?.SetMaterialDirty();
|
|
}
|
|
|
|
private void ApplyShaderSettings()
|
|
{
|
|
if (_runtimeMaterial == null) return;
|
|
|
|
_runtimeMaterial.SetColor(_fogColorId, _fogColor);
|
|
_runtimeMaterial.SetFloat(_fogIntensityId, _fogIntensity);
|
|
_runtimeMaterial.SetFloat(_edgeSoftnessId, _edgeSoftness);
|
|
_runtimeMaterial.SetFloat(_noiseScaleId, _noiseScale);
|
|
_runtimeMaterial.SetFloat(_driftSpeedId, _driftSpeed);
|
|
_runtimeMaterial.SetFloat(_densityVariationId, _densityVariation);
|
|
_runtimeMaterial.SetFloat(_fogMotionId, _fogMotion ? 1f : 0f);
|
|
_runtimeMaterial.SetFloat(_useSpriteId, _useSprite ? 1f : 0f);
|
|
|
|
if (!_autoAspectRatio)
|
|
_runtimeMaterial.SetFloat(_aspectRatioId, _aspectRatio);
|
|
|
|
_image?.SetMaterialDirty();
|
|
}
|
|
|
|
private void ResolveReferences()
|
|
{
|
|
if (_image == null)
|
|
_image = GetComponent<Image>();
|
|
}
|
|
|
|
private void CreateRuntimeMaterial(bool isPreviewMaterial)
|
|
{
|
|
if (_image == null) return;
|
|
|
|
var source = _fogMaterialTemplate != null
|
|
? _fogMaterialTemplate
|
|
: _image.material;
|
|
|
|
if (source == null) return;
|
|
|
|
if (_runtimeMaterial != null)
|
|
{
|
|
_image.material = _runtimeMaterial;
|
|
_image.SetMaterialDirty();
|
|
return;
|
|
}
|
|
|
|
_runtimeMaterial = Instantiate(source);
|
|
_runtimeMaterial.name = isPreviewMaterial
|
|
? $"{nameof(UIFogReveal)} Preview Material"
|
|
: $"{nameof(UIFogReveal)} Runtime Material";
|
|
|
|
_isPreviewMaterial = isPreviewMaterial;
|
|
|
|
if (isPreviewMaterial)
|
|
_runtimeMaterial.hideFlags = HideFlags.DontSaveInEditor;
|
|
|
|
_image.material = _runtimeMaterial;
|
|
_image.SetMaterialDirty();
|
|
|
|
ApplyShaderSettings();
|
|
}
|
|
|
|
private void DestroyRuntimeMaterial(bool immediate)
|
|
{
|
|
if (_runtimeMaterial == null) return;
|
|
|
|
if (_image != null)
|
|
{
|
|
_image.material = null;
|
|
_image.SetMaterialDirty();
|
|
}
|
|
|
|
if (immediate)
|
|
DestroyImmediate(_runtimeMaterial);
|
|
else
|
|
Destroy(_runtimeMaterial);
|
|
|
|
_runtimeMaterial = null;
|
|
_isPreviewMaterial = false;
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
private void RefreshEditModePreview()
|
|
{
|
|
if (Application.isPlaying) return;
|
|
|
|
if (!_previewInEditMode)
|
|
{
|
|
RestoreEditModeMaterial();
|
|
return;
|
|
}
|
|
|
|
CreateRuntimeMaterial(true);
|
|
ApplyShaderSettings();
|
|
SetFogAmount(_previewFogAmount);
|
|
}
|
|
|
|
private void RestoreEditModeMaterial()
|
|
{
|
|
if (!_isPreviewMaterial) return;
|
|
DestroyRuntimeMaterial(true);
|
|
}
|
|
#endif
|
|
}
|
|
} |