Files

500 lines
13 KiB
C#

using System;
using System.Collections.Generic;
using System.Threading;
using BriarQueen.Framework.Services.Destruction;
using Cysharp.Threading.Tasks;
using PrimeTween;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using VContainer;
namespace BriarQueen.Framework.Effects
{
[ExecuteAlways]
public class UIDissolveImage : MonoBehaviour
{
private static readonly int _dissolveAmountId = Shader.PropertyToID("_DissolveAmount");
private static readonly int _reverseDirectionId = Shader.PropertyToID("_ReverseDirection");
[Header("References")]
[SerializeField]
private Image _image;
[SerializeField]
private CanvasGroup _canvasGroup;
[SerializeField]
private Material _dissolveMaterialTemplate;
[Header("Targeting")]
[SerializeField]
private bool _dissolveChildGraphics;
[SerializeField]
private bool _includeInactiveChildGraphics = true;
[SerializeField]
private bool _includeTextGraphics;
[Header("Tween")]
[SerializeField]
private float _duration = 0.75f;
[SerializeField]
private Ease _ease = Ease.InOutSine;
[SerializeField]
private bool _useUnscaledTime = true;
[Header("Direction")]
[SerializeField]
private bool _reverseDirection;
[Header("Destruction")]
[SerializeField]
private bool _destroyWhenFullyDissolved = true;
[Header("Editor Preview")]
[SerializeField]
private bool _previewInEditMode;
[SerializeField]
[Range(0f, 1f)]
private float _previewDissolveAmount;
private readonly List<Graphic> _targetGraphics = new();
private readonly List<Material> _originalMaterials = new();
private CancellationTokenSource _dissolveCts;
private DestructionService _destructionService;
private bool _hasOriginalMaterials;
private bool _isPreviewMaterial;
private Material _runtimeMaterial;
private Sequence _dissolveSequence;
public float DissolveAmount
{
get
{
if (_runtimeMaterial == null)
{
return 0f;
}
return _runtimeMaterial.GetFloat(_dissolveAmountId);
}
set => SetDissolveAmount(value);
}
[Inject]
public void Construct(DestructionService destructionService)
{
_destructionService = destructionService;
}
private void Awake()
{
ResolveReferences();
if (Application.isPlaying)
{
CreateRuntimeMaterial(false);
SetDissolveAmount(0f);
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 OnDestroy()
{
CancelDissolve();
if (Application.isPlaying)
{
DestroyRuntimeMaterial(false);
return;
}
#if UNITY_EDITOR
RestoreEditModeMaterial();
#endif
}
private void OnValidate()
{
ResolveReferences();
if (Application.isPlaying)
{
SetReverseDirection(_reverseDirection);
return;
}
#if UNITY_EDITOR
RestoreEditModeMaterial();
RefreshEditModePreview();
#endif
}
public UniTask DissolveIn()
{
gameObject.SetActive(true);
return TweenDissolve(1f, 0f, _duration, false);
}
public UniTask DissolveOut()
{
return TweenDissolve(0f, 1f, _duration, _destroyWhenFullyDissolved);
}
public UniTask DissolveOut(bool destroyWhenComplete)
{
return TweenDissolve(0f, 1f, _duration, destroyWhenComplete);
}
public UniTask DissolveOutAndDestroy()
{
return TweenDissolve(0f, 1f, _duration, true);
}
public UniTask TweenDissolve(float from, float to, float duration)
{
return TweenDissolve(from, to, duration, false);
}
public async UniTask TweenDissolve(float from, float to, float duration, bool destroyWhenComplete)
{
if (_runtimeMaterial == null)
{
CreateRuntimeMaterial(false);
}
CancelDissolve();
_dissolveCts = new CancellationTokenSource();
SetDissolveAmount(from);
_dissolveSequence = Sequence.Create(useUnscaledTime: _useUnscaledTime)
.Group(Tween.Custom(
from,
to,
Mathf.Max(0f, duration),
SetDissolveAmount,
_ease,
useUnscaledTime: _useUnscaledTime));
try
{
await _dissolveSequence.ToUniTask(cancellationToken: _dissolveCts.Token);
SetDissolveAmount(to);
if (destroyWhenComplete && Mathf.Approximately(to, 1f))
{
await DestroyAfterDissolve();
}
}
catch (OperationCanceledException)
{
// Interrupted by another dissolve request or object destruction.
}
finally
{
_dissolveSequence = default;
if (_dissolveCts != null)
{
_dissolveCts.Dispose();
_dissolveCts = null;
}
}
}
public void SetDissolveAmount(float amount)
{
if (_runtimeMaterial == null)
{
return;
}
_runtimeMaterial.SetFloat(_dissolveAmountId, Mathf.Clamp01(amount));
SetTargetsMaterialDirty();
}
public void SetReverseDirection(bool reverseDirection)
{
_reverseDirection = reverseDirection;
if (_runtimeMaterial == null)
{
return;
}
_runtimeMaterial.SetFloat(_reverseDirectionId, _reverseDirection ? 1f : 0f);
SetTargetsMaterialDirty();
}
public void CancelDissolve()
{
if (_dissolveSequence.isAlive)
{
_dissolveSequence.Stop();
_dissolveSequence = default;
}
if (_dissolveCts != null)
{
_dissolveCts.Cancel();
_dissolveCts.Dispose();
_dissolveCts = null;
}
}
private async UniTask DestroyAfterDissolve()
{
if (_destructionService != null)
{
await _destructionService.Destroy(gameObject);
return;
}
Debug.LogWarning($"[{nameof(UIDissolveImage)}] Missing {nameof(DestructionService)}. Destroying directly.");
Destroy(gameObject);
}
private void ResolveReferences()
{
if (_image == null)
{
_image = GetComponent<Image>();
}
if (_canvasGroup == null)
{
_canvasGroup = GetComponent<CanvasGroup>();
}
}
private void ResolveTargetGraphics()
{
ResolveReferences();
_targetGraphics.Clear();
if (_dissolveChildGraphics)
{
var root = _canvasGroup != null ? _canvasGroup.transform : transform;
var graphics = root.GetComponentsInChildren<Graphic>(_includeInactiveChildGraphics);
foreach (var graphic in graphics)
{
if (IsValidTargetGraphic(graphic))
{
_targetGraphics.Add(graphic);
}
}
return;
}
if (_image != null)
{
_targetGraphics.Add(_image);
}
}
private bool IsValidTargetGraphic(Graphic graphic)
{
if (graphic == null)
{
return false;
}
// TMP needs a TMP-compatible dissolve shader; replacing its SDF material breaks text rendering.
if (!_includeTextGraphics && graphic is TMP_Text)
{
return false;
}
return true;
}
private void CreateRuntimeMaterial(bool isPreviewMaterial)
{
ResolveTargetGraphics();
if (_targetGraphics.Count == 0)
{
return;
}
var sourceMaterial = _dissolveMaterialTemplate != null
? _dissolveMaterialTemplate
: _targetGraphics[0].material;
if (sourceMaterial == null)
{
return;
}
if (_runtimeMaterial != null)
{
ApplyRuntimeMaterialToTargets();
return;
}
SaveOriginalMaterials();
_runtimeMaterial = Instantiate(sourceMaterial);
_runtimeMaterial.name = isPreviewMaterial
? $"{nameof(UIDissolveImage)} Preview Material"
: $"{nameof(UIDissolveImage)} Runtime Material";
_isPreviewMaterial = isPreviewMaterial;
if (isPreviewMaterial)
{
_runtimeMaterial.hideFlags = HideFlags.DontSaveInEditor;
}
ApplyRuntimeMaterialToTargets();
SetReverseDirection(_reverseDirection);
}
private void ApplyRuntimeMaterialToTargets()
{
foreach (var graphic in _targetGraphics)
{
if (graphic == null)
{
continue;
}
graphic.material = _runtimeMaterial;
graphic.SetMaterialDirty();
}
}
private void SaveOriginalMaterials()
{
_originalMaterials.Clear();
foreach (var graphic in _targetGraphics)
{
_originalMaterials.Add(graphic != null ? graphic.material : null);
}
_hasOriginalMaterials = true;
}
private void RestoreOriginalMaterials()
{
if (!_hasOriginalMaterials)
{
return;
}
var count = Mathf.Min(_targetGraphics.Count, _originalMaterials.Count);
for (var i = 0; i < count; i++)
{
var graphic = _targetGraphics[i];
if (graphic == null)
{
continue;
}
graphic.material = _originalMaterials[i];
graphic.SetMaterialDirty();
}
_originalMaterials.Clear();
_hasOriginalMaterials = false;
}
private void SetTargetsMaterialDirty()
{
foreach (var graphic in _targetGraphics)
{
if (graphic != null)
{
graphic.SetMaterialDirty();
}
}
}
private void DestroyRuntimeMaterial(bool immediate)
{
if (_runtimeMaterial == null)
{
return;
}
RestoreOriginalMaterials();
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);
SetReverseDirection(_reverseDirection);
SetDissolveAmount(_previewDissolveAmount);
}
private void RestoreEditModeMaterial()
{
if (!_isPreviewMaterial)
{
return;
}
DestroyRuntimeMaterial(true);
}
#endif
}
}