using System.Collections;
using DG.Tweening;
using UnityEngine;
using UnityEngine.UI;
namespace AibisDream
{
///
/// Mobile-friendly fullscreen eye viewport: elliptical sight + vertical ellipse blink.
///
[DisallowMultipleComponent]
[ExecuteAlways]
public class EyeViewportOverlay : MonoBehaviour
{
private const string OverlaySortingLayer = "StartUI";
private const float OverlayPlaneDistance = 10f;
private static readonly int EyeWidthId = Shader.PropertyToID("_EyeWidth");
private static readonly int EyeHeightId = Shader.PropertyToID("_EyeHeight");
private static readonly int EdgeSoftnessId = Shader.PropertyToID("_EdgeSoftness");
private static readonly int BlinkOpenId = Shader.PropertyToID("_BlinkOpen");
/// Persistence:本遮罩=0,UI Canvas=1,Dialog Canvas=3。
[SerializeField] private int sortingOrder;
[Tooltip("Assign in Inspector so the shader is included in builds. Falls back to Shader.Find at runtime.")]
[SerializeField] private Shader overlayShader;
private Canvas overlayCanvas;
private Image overlayImage;
private Material instanceMaterial;
private bool overlayShaderAvailable;
private bool overlayFallbackLogged;
private EyeVisualSettings activeSettings;
private Coroutine autoBlinkRoutine;
private Tween blinkTween;
private float blinkOpen = 1f;
private bool moduleActive;
private bool editorPreviewActive;
///
/// UF 等密集演出段:关掉 autoBlink 与效果过渡眨眼,仅保留 Yarn EyeBlink。
///
private bool suppressNonExplicitBlink;
private bool hierarchyReady;
private bool lifecycleReady;
private Coroutine deferPrepareRoutine;
private void Start()
{
lifecycleReady = true;
if (!Application.isPlaying)
{
return;
}
PrepareHierarchyIfNeeded();
if (!moduleActive && !editorPreviewActive)
{
SetOverlayActive(false);
}
}
private void OnDestroy()
{
StopEffects();
if (instanceMaterial != null)
{
Destroy(instanceMaterial);
}
}
public void EnterModule(EyeVisualSettings settings)
{
moduleActive = true;
editorPreviewActive = false;
ShowOverlay(settings);
}
public void ExitModule()
{
moduleActive = false;
suppressNonExplicitBlink = false;
if (editorPreviewActive)
{
return;
}
StopEffects();
SetOverlayActive(false);
activeSettings = null;
}
///
/// 抑制自动眨眼与效果过渡眨眼;显式 PlayBlink / Yarn EyeBlink 仍可用。
///
public void SetNonExplicitBlinkSuppressed(bool suppressed)
{
suppressNonExplicitBlink = suppressed;
if (suppressed)
{
StopAutoBlink();
return;
}
if (activeSettings != null
&& activeSettings.autoBlink
&& Application.isPlaying
&& (moduleActive || editorPreviewActive))
{
StartAutoBlink(activeSettings);
}
}
public void EnterEditorPreview(EyeVisualSettings settings)
{
editorPreviewActive = true;
ShowOverlay(settings);
}
public void ExitEditorPreview()
{
editorPreviewActive = false;
if (moduleActive)
{
ShowOverlay(activeSettings);
return;
}
StopEffects();
SetOverlayActive(false);
activeSettings = null;
}
public void ApplySettings(EyeVisualSettings settings)
{
if (settings == null)
{
return;
}
if (!settings.enableViewportOverlay)
{
if (editorPreviewActive)
{
ExitEditorPreview();
}
else if (moduleActive)
{
ExitModule();
}
return;
}
PrepareHierarchyIfNeeded();
if (instanceMaterial == null)
{
return;
}
activeSettings = settings;
PushViewportParams(settings);
SetBlinkOpen(blinkOpen);
if (!overlayCanvas.gameObject.activeInHierarchy)
{
return;
}
if (settings.autoBlink
&& Application.isPlaying
&& (moduleActive || editorPreviewActive)
&& !suppressNonExplicitBlink)
{
StartAutoBlink(settings);
}
else
{
StopAutoBlink();
}
}
public Tween PlayBlink(
float? totalDuration = null,
bool restartAutoBlinkAfter = false,
float presentationSpeed = 1f)
{
if (instanceMaterial == null || activeSettings == null)
{
return null;
}
blinkTween?.Kill();
float durationScale = 1f / Mathf.Max(0.01f, presentationSpeed);
float closeDuration = activeSettings.blinkCloseDuration * durationScale;
float openDuration = activeSettings.blinkOpenDuration * durationScale;
if (totalDuration.HasValue)
{
float total = Mathf.Max(0.05f, totalDuration.Value) * durationScale;
closeDuration = total * 0.42f;
openDuration = total * 0.58f;
}
blinkTween = DOTween.Sequence()
.Append(CreateBlinkTween(blinkOpen, 0f, closeDuration, Ease.InQuad))
.Append(CreateBlinkTween(0f, 1f, openDuration, Ease.OutQuad))
.OnComplete(() =>
{
SetBlinkOpen(1f);
if (restartAutoBlinkAfter)
{
RestartAutoBlinkCooldown(activeSettings);
}
});
return blinkTween;
}
///
/// 先眨眼,再在最后一次眨眼的开眼阶段触发效果(聚焦 1 次 / 想象 2 次)。
///
public IEnumerator PlayPreEffectBlinkThen(
int blinkCount,
EyeVisualSettings settings,
System.Action onEffectStart,
float effectAtOpenRatio,
float betweenBlinkGap = 0f,
float presentationSpeed = 1f)
{
if (!ShouldPlayTransitionBlink(settings))
{
onEffectStart?.Invoke();
yield break;
}
StopAutoBlink();
bool effectStarted = false;
blinkCount = Mathf.Max(1, blinkCount);
effectAtOpenRatio = Mathf.Clamp01(effectAtOpenRatio);
float durationScale = 1f / Mathf.Max(0.01f, presentationSpeed);
for (int i = 0; i < blinkCount; i++)
{
bool isLastBlink = i == blinkCount - 1;
Tween blink = PlayBlink(presentationSpeed: presentationSpeed);
if (blink == null)
{
if (isLastBlink && !effectStarted)
{
effectStarted = true;
onEffectStart?.Invoke();
}
continue;
}
if (isLastBlink)
{
float effectDelay = (settings.blinkCloseDuration + settings.blinkOpenDuration * effectAtOpenRatio)
* durationScale;
yield return new WaitForSeconds(effectDelay);
if (!effectStarted)
{
effectStarted = true;
onEffectStart?.Invoke();
}
yield return blink.WaitForCompletion();
}
else
{
yield return blink.WaitForCompletion();
if (betweenBlinkGap > 0f)
{
yield return new WaitForSeconds(betweenBlinkGap * durationScale);
}
}
}
if (!effectStarted)
{
onEffectStart?.Invoke();
}
RestartAutoBlinkCooldown(settings);
}
private bool ShouldPlayTransitionBlink(EyeVisualSettings settings)
{
return Application.isPlaying
&& moduleActive
&& !editorPreviewActive
&& !suppressNonExplicitBlink
&& overlayShaderAvailable
&& settings != null
&& settings.enableViewportOverlay;
}
private void RestartAutoBlinkCooldown(EyeVisualSettings settings)
{
if (settings == null || !settings.autoBlink || !Application.isPlaying)
{
return;
}
if (suppressNonExplicitBlink)
{
return;
}
if (!(moduleActive || editorPreviewActive))
{
return;
}
StartAutoBlink(settings);
}
private void ShowOverlay(EyeVisualSettings settings)
{
if (settings == null || !settings.enableViewportOverlay)
{
ExitModule();
return;
}
PrepareHierarchyIfNeeded();
if (!overlayShaderAvailable || instanceMaterial == null)
{
DisableOverlayFallback();
return;
}
ApplyCanvasRenderSettings();
activeSettings = settings;
ApplySettings(settings);
if (!editorPreviewActive)
{
SetBlinkOpen(1f);
}
SetOverlayActive(true);
if (settings.autoBlink && Application.isPlaying && !suppressNonExplicitBlink)
{
StartAutoBlink(settings);
}
}
private void PushViewportParams(EyeVisualSettings settings)
{
instanceMaterial.SetFloat(EyeWidthId, settings.eyeWidth);
instanceMaterial.SetFloat(EyeHeightId, settings.eyeHeight);
instanceMaterial.SetFloat(EdgeSoftnessId, settings.edgeSoftness);
}
private Tween CreateBlinkTween(float from, float to, float duration, Ease ease)
{
blinkOpen = from;
SetBlinkOpen(from);
return DOTween.To(() => blinkOpen, value =>
{
blinkOpen = value;
SetBlinkOpen(value);
}, to, duration).SetEase(ease);
}
private void StartAutoBlink(EyeVisualSettings settings)
{
StopAutoBlink();
if (!settings.autoBlink || suppressNonExplicitBlink)
{
return;
}
autoBlinkRoutine = StartCoroutine(AutoBlinkRoutine(settings));
}
private IEnumerator AutoBlinkRoutine(EyeVisualSettings settings)
{
while (enabled && overlayCanvas != null && overlayCanvas.gameObject.activeInHierarchy)
{
float min = Mathf.Max(1f, settings.autoBlinkInterval.x);
float max = Mathf.Max(min, settings.autoBlinkInterval.y);
yield return new WaitForSeconds(Random.Range(min, max));
Tween blink = PlayBlink();
if (blink != null)
{
yield return blink.WaitForCompletion();
}
}
}
private void StopEffects()
{
blinkTween?.Kill();
blinkTween = null;
StopAutoBlink();
}
private void StopAutoBlink()
{
if (autoBlinkRoutine == null)
{
return;
}
StopCoroutine(autoBlinkRoutine);
autoBlinkRoutine = null;
}
public void SetBlinkOpenPreview(float value)
{
SetBlinkOpen(value);
}
public float GetBlinkOpenPreview() => blinkOpen;
private void SetBlinkOpen(float value)
{
blinkOpen = Mathf.Clamp01(value);
instanceMaterial?.SetFloat(BlinkOpenId, blinkOpen);
}
private void SetOverlayActive(bool active)
{
if (overlayCanvas != null)
{
overlayCanvas.gameObject.SetActive(active);
}
}
private void PrepareHierarchyIfNeeded()
{
if (Application.isPlaying && !lifecycleReady)
{
ScheduleDeferredPrepare();
return;
}
EnsureOverlayHierarchy();
hierarchyReady = overlayCanvas != null;
ApplyCanvasRenderSettings();
}
private void ScheduleDeferredPrepare()
{
if (deferPrepareRoutine != null || !isActiveAndEnabled)
{
return;
}
deferPrepareRoutine = StartCoroutine(DeferredPrepareHierarchy());
}
private IEnumerator DeferredPrepareHierarchy()
{
yield return null;
deferPrepareRoutine = null;
EnsureOverlayHierarchy();
hierarchyReady = overlayCanvas != null;
if (!overlayShaderAvailable || instanceMaterial == null)
{
if (moduleActive || editorPreviewActive)
{
DisableOverlayFallback();
}
yield break;
}
if (activeSettings != null && instanceMaterial != null)
{
PushViewportParams(activeSettings);
SetBlinkOpen(blinkOpen);
}
ApplyCanvasRenderSettings();
}
private void EnsureOverlayHierarchy()
{
if (overlayCanvas == null)
{
overlayCanvas = FindExistingOverlayCanvas();
}
if (overlayCanvas == null)
{
var canvasGo = new GameObject("EyeViewportOverlayCanvas");
canvasGo.transform.SetParent(transform, false);
overlayCanvas = canvasGo.AddComponent