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; private Canvas overlayCanvas; private Image overlayImage; private Material instanceMaterial; private EyeVisualSettings activeSettings; private Coroutine autoBlinkRoutine; private Tween blinkTween; private float blinkOpen = 1f; private bool moduleActive; private bool editorPreviewActive; 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; if (editorPreviewActive) { return; } StopEffects(); SetOverlayActive(false); activeSettings = null; } 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)) { StartAutoBlink(settings); } else { StopAutoBlink(); } } public Tween PlayBlink(float? totalDuration = null, bool restartAutoBlinkAfter = false) { if (instanceMaterial == null || activeSettings == null) { return null; } blinkTween?.Kill(); float closeDuration = activeSettings.blinkCloseDuration; float openDuration = activeSettings.blinkOpenDuration; if (totalDuration.HasValue) { float total = Mathf.Max(0.05f, totalDuration.Value); 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) { if (!ShouldPlayTransitionBlink(settings)) { onEffectStart?.Invoke(); yield break; } StopAutoBlink(); bool effectStarted = false; blinkCount = Mathf.Max(1, blinkCount); effectAtOpenRatio = Mathf.Clamp01(effectAtOpenRatio); for (int i = 0; i < blinkCount; i++) { bool isLastBlink = i == blinkCount - 1; Tween blink = PlayBlink(); if (blink == null) { if (isLastBlink && !effectStarted) { effectStarted = true; onEffectStart?.Invoke(); } continue; } if (isLastBlink) { float effectDelay = settings.blinkCloseDuration + settings.blinkOpenDuration * effectAtOpenRatio; 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); } } } if (!effectStarted) { onEffectStart?.Invoke(); } RestartAutoBlinkCooldown(settings); } private bool ShouldPlayTransitionBlink(EyeVisualSettings settings) { return Application.isPlaying && moduleActive && !editorPreviewActive && settings != null && settings.enableViewportOverlay; } private void RestartAutoBlinkCooldown(EyeVisualSettings settings) { if (settings == null || !settings.autoBlink || !Application.isPlaying) { return; } if (!(moduleActive || editorPreviewActive)) { return; } StartAutoBlink(settings); } private void ShowOverlay(EyeVisualSettings settings) { if (settings == null || !settings.enableViewportOverlay) { ExitModule(); return; } PrepareHierarchyIfNeeded(); ApplyCanvasRenderSettings(); activeSettings = settings; ApplySettings(settings); if (!editorPreviewActive) { SetBlinkOpen(1f); } SetOverlayActive(true); if (settings.autoBlink && Application.isPlaying) { 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) { 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 (activeSettings != null && instanceMaterial != null) { PushViewportParams(activeSettings); SetBlinkOpen(blinkOpen); } ApplyCanvasRenderSettings(); } private void EnsureOverlayHierarchy() { if (overlayCanvas == null) { var canvasGo = new GameObject("EyeViewportOverlayCanvas"); canvasGo.transform.SetParent(transform, false); overlayCanvas = canvasGo.AddComponent(); overlayCanvas.pixelPerfect = false; var scaler = canvasGo.AddComponent(); scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.referenceResolution = new Vector2(1920f, 1080f); scaler.matchWidthOrHeight = 0.5f; canvasGo.AddComponent(); var imageGo = new GameObject("Overlay", typeof(RectTransform)); imageGo.transform.SetParent(canvasGo.transform, false); var rect = (RectTransform)imageGo.transform; rect.anchorMin = Vector2.zero; rect.anchorMax = Vector2.one; rect.offsetMin = Vector2.zero; rect.offsetMax = Vector2.zero; overlayImage = imageGo.AddComponent(); overlayImage.raycastTarget = false; overlayImage.color = Color.black; SetupMaterial(); } ApplyCanvasRenderSettings(); } private void SetupMaterial() { Shader shader = Shader.Find("UI/EyeViewportOverlay"); if (shader == null) { Debug.LogError("[EyeViewportOverlay] 找不到 UI/EyeViewportOverlay shader。", this); return; } instanceMaterial = new Material(shader) { name = "EyeViewportOverlay (Instance)", hideFlags = HideFlags.DontSave }; overlayImage.material = instanceMaterial; } private void ApplyCanvasRenderSettings() { if (overlayCanvas == null) { return; } overlayCanvas.renderMode = RenderMode.ScreenSpaceCamera; overlayCanvas.worldCamera = ResolveUICamera(); overlayCanvas.planeDistance = OverlayPlaneDistance; overlayCanvas.sortingLayerName = OverlaySortingLayer; overlayCanvas.sortingOrder = sortingOrder; SetOverlayUILayer(overlayCanvas.gameObject); } private static void SetOverlayUILayer(GameObject root) { int uiLayer = LayerMask.NameToLayer("UI"); if (uiLayer < 0) { uiLayer = 5; } foreach (Transform node in root.GetComponentsInChildren(true)) { node.gameObject.layer = uiLayer; } } private static Camera ResolveUICamera() { if (UIManager.Instance != null && UIManager.Instance.UICamera != null) { return UIManager.Instance.UICamera; } var uiManager = Object.FindObjectOfType(); if (uiManager != null && uiManager.UICamera != null) { return uiManager.UICamera; } return Camera.main; } } }