Files
A-Fairytale-Gone-Bad-Briar-…/Assets/Scripts/Framework/Effects/UIEdgeDarken.cs

503 lines
13 KiB
C#

using System;
using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks;
using PrimeTween;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace BriarQueen.Framework.Effects
{
[ExecuteAlways]
public class UIEdgeDarken : MonoBehaviour
{
private static readonly int _amountId = Shader.PropertyToID("_Amount");
private static readonly int _centerDarknessId = Shader.PropertyToID("_CenterDarkness");
private static readonly int _colorId = Shader.PropertyToID("_Color");
private static readonly int _edgeDarknessId = Shader.PropertyToID("_EdgeDarkness");
private static readonly int _edgeWidthId = Shader.PropertyToID("_EdgeWidth");
private static readonly int _rectPivotId = Shader.PropertyToID("_RectPivot");
private static readonly int _rectSizeId = Shader.PropertyToID("_RectSize");
private static readonly int _softnessId = Shader.PropertyToID("_Softness");
[Header("References")]
[SerializeField]
private Graphic _graphic;
[SerializeField]
private CanvasGroup _canvasGroup;
[SerializeField]
private Material _edgeDarkenMaterialTemplate;
[Header("Targeting")]
[SerializeField]
private bool _targetChildGraphics;
[SerializeField]
private bool _includeInactiveChildGraphics = true;
[SerializeField]
private bool _includeTextGraphics;
[Header("Darken")]
[SerializeField]
[Range(0f, 1f)]
private float _amount = 1f;
[SerializeField]
private Color _color = new(0f, 0f, 0f, 0.65f);
[SerializeField]
[Range(0f, 1f)]
private float _edgeDarkness = 1f;
[SerializeField]
[Range(0f, 1f)]
private float _centerDarkness;
[SerializeField]
[Range(0.001f, 0.5f)]
private float _edgeWidth = 0.22f;
[SerializeField]
[Range(0.001f, 0.5f)]
private float _softness = 0.18f;
[Header("Tween")]
[SerializeField]
private float _duration = 0.35f;
[SerializeField]
private Ease _ease = Ease.InOutSine;
[SerializeField]
private bool _useUnscaledTime = true;
[Header("Editor Preview")]
[SerializeField]
private bool _previewInEditMode;
[SerializeField]
[Range(0f, 1f)]
private float _previewAmount = 1f;
private readonly List<Graphic> _targetGraphics = new();
private readonly List<Material> _originalMaterials = new();
private readonly List<Material> _runtimeMaterials = new();
private CancellationTokenSource _darkenCts;
private bool _hasOriginalMaterials;
private bool _isPreviewMaterial;
private Sequence _darkenSequence;
public float Amount
{
get => _amount;
set => SetAmount(value);
}
private void Awake()
{
ResolveReferences();
if (Application.isPlaying)
{
CreateRuntimeMaterial(false);
ApplyMaterialProperties(_amount);
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()
{
CancelTween();
if (Application.isPlaying)
{
DestroyRuntimeMaterial(false);
return;
}
#if UNITY_EDITOR
RestoreEditModeMaterial();
#endif
}
private void OnValidate()
{
ResolveReferences();
if (Application.isPlaying)
{
ApplyMaterialProperties(_amount);
return;
}
#if UNITY_EDITOR
RestoreEditModeMaterial();
RefreshEditModePreview();
#endif
}
public UniTask DarkenIn()
{
return TweenAmount(0f, 1f, _duration);
}
public UniTask DarkenOut()
{
return TweenAmount(1f, 0f, _duration);
}
public async UniTask TweenAmount(float from, float to, float duration)
{
if (_runtimeMaterials.Count == 0)
{
CreateRuntimeMaterial(false);
}
CancelTween();
_darkenCts = new CancellationTokenSource();
SetAmount(from);
_darkenSequence = Sequence.Create(useUnscaledTime: _useUnscaledTime)
.Group(Tween.Custom(
from,
to,
Mathf.Max(0f, duration),
SetAmount,
_ease,
useUnscaledTime: _useUnscaledTime));
try
{
await _darkenSequence.ToUniTask(cancellationToken: _darkenCts.Token);
SetAmount(to);
}
catch (OperationCanceledException)
{
// Interrupted by another darken request or object destruction.
}
finally
{
_darkenSequence = default;
if (_darkenCts != null)
{
_darkenCts.Dispose();
_darkenCts = null;
}
}
}
public void SetAmount(float amount)
{
_amount = Mathf.Clamp01(amount);
ApplyMaterialProperties(_amount);
}
public void CancelTween()
{
if (_darkenSequence.isAlive)
{
_darkenSequence.Stop();
_darkenSequence = default;
}
if (_darkenCts != null)
{
_darkenCts.Cancel();
_darkenCts.Dispose();
_darkenCts = null;
}
}
private void ResolveReferences()
{
if (_graphic == null)
{
_graphic = GetComponent<Graphic>();
}
if (_canvasGroup == null)
{
_canvasGroup = GetComponent<CanvasGroup>();
}
}
private void ResolveTargetGraphics()
{
ResolveReferences();
_targetGraphics.Clear();
if (_targetChildGraphics)
{
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 (_graphic != null)
{
_targetGraphics.Add(_graphic);
}
}
private bool IsValidTargetGraphic(Graphic graphic)
{
if (graphic == null)
{
return false;
}
// TMP needs a TMP-compatible 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 = _edgeDarkenMaterialTemplate != null
? _edgeDarkenMaterialTemplate
: _targetGraphics[0].material;
if (sourceMaterial == null)
{
return;
}
if (_runtimeMaterials.Count > 0)
{
ApplyRuntimeMaterialToTargets();
return;
}
SaveOriginalMaterials();
_isPreviewMaterial = isPreviewMaterial;
foreach (var graphic in _targetGraphics)
{
if (graphic == null)
{
_runtimeMaterials.Add(null);
continue;
}
var runtimeMaterial = Instantiate(sourceMaterial);
runtimeMaterial.name = isPreviewMaterial
? $"{nameof(UIEdgeDarken)} Preview Material"
: $"{nameof(UIEdgeDarken)} Runtime Material";
if (isPreviewMaterial)
{
runtimeMaterial.hideFlags = HideFlags.DontSaveInEditor;
}
_runtimeMaterials.Add(runtimeMaterial);
}
ApplyRuntimeMaterialToTargets();
}
private void ApplyRuntimeMaterialToTargets()
{
var count = Mathf.Min(_targetGraphics.Count, _runtimeMaterials.Count);
for (var i = 0; i < count; i++)
{
var graphic = _targetGraphics[i];
if (graphic == null)
{
continue;
}
graphic.material = _runtimeMaterials[i];
graphic.SetMaterialDirty();
}
}
private void ApplyMaterialProperties(float amount)
{
if (_runtimeMaterials.Count == 0)
{
return;
}
var count = Mathf.Min(_targetGraphics.Count, _runtimeMaterials.Count);
for (var i = 0; i < count; i++)
{
var material = _runtimeMaterials[i];
var graphic = _targetGraphics[i];
if (material == null || graphic == null)
{
continue;
}
var rectTransform = graphic.rectTransform;
material.SetFloat(_amountId, Mathf.Clamp01(amount));
material.SetColor(_colorId, _color);
material.SetFloat(_edgeDarknessId, _edgeDarkness);
material.SetFloat(_centerDarknessId, _centerDarkness);
material.SetFloat(_edgeWidthId, _edgeWidth);
material.SetFloat(_softnessId, _softness);
material.SetVector(_rectSizeId, rectTransform.rect.size);
material.SetVector(_rectPivotId, rectTransform.pivot);
}
SetTargetsMaterialDirty();
}
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 (_runtimeMaterials.Count == 0)
{
return;
}
RestoreOriginalMaterials();
foreach (var runtimeMaterial in _runtimeMaterials)
{
if (runtimeMaterial == null)
{
continue;
}
if (immediate)
{
DestroyImmediate(runtimeMaterial);
}
else
{
Destroy(runtimeMaterial);
}
}
_runtimeMaterials.Clear();
_isPreviewMaterial = false;
}
#if UNITY_EDITOR
private void RefreshEditModePreview()
{
if (Application.isPlaying)
{
return;
}
if (!_previewInEditMode)
{
RestoreEditModeMaterial();
return;
}
CreateRuntimeMaterial(true);
ApplyMaterialProperties(_previewAmount);
}
private void RestoreEditModeMaterial()
{
if (!_isPreviewMaterial)
{
return;
}
DestroyRuntimeMaterial(true);
}
#endif
}
}