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(); canvasGo.AddComponent(); canvasGo.AddComponent(); } NormalizeCanvasTransform(overlayCanvas.transform); overlayCanvas.pixelPerfect = false; var scaler = overlayCanvas.GetComponent(); if (scaler == null) { scaler = overlayCanvas.gameObject.AddComponent(); } scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.referenceResolution = new Vector2(1920f, 1080f); scaler.matchWidthOrHeight = 0.5f; if (overlayCanvas.GetComponent() == null) { overlayCanvas.gameObject.AddComponent(); } EnsureOverlayImage(); if (overlayImage != null && instanceMaterial == null && !TrySetupOverlayMaterial()) { SetOverlayActive(false); } ApplyCanvasRenderSettings(); } private Canvas FindExistingOverlayCanvas() { foreach (var canvas in GetComponentsInChildren(true)) { if (canvas != null && canvas.gameObject.name == "EyeViewportOverlayCanvas") { return canvas; } } return null; } private void EnsureOverlayImage() { if (overlayImage == null && overlayCanvas != null) { Transform existing = overlayCanvas.transform.Find("Overlay"); if (existing != null) { overlayImage = existing.GetComponent(); } } if (overlayImage == null) { var imageGo = new GameObject("Overlay", typeof(RectTransform)); imageGo.transform.SetParent(overlayCanvas.transform, false); overlayImage = imageGo.AddComponent(); } var rect = overlayImage.rectTransform; rect.anchorMin = Vector2.zero; rect.anchorMax = Vector2.one; rect.offsetMin = Vector2.zero; rect.offsetMax = Vector2.zero; rect.localScale = Vector3.one; overlayImage.raycastTarget = false; overlayImage.color = Color.black; } private static void NormalizeCanvasTransform(Transform canvasTransform) { if (canvasTransform == null) { return; } canvasTransform.localPosition = Vector3.zero; canvasTransform.localRotation = Quaternion.identity; canvasTransform.localScale = Vector3.one; } private void DisableOverlayFallback() { StopEffects(); SetOverlayActive(false); if (overlayFallbackLogged) { return; } overlayFallbackLogged = true; Debug.LogWarning( "[EyeViewportOverlay] 遮罩 shader 不可用,已跳过眼洞遮罩(视觉模块仍可正常使用,只是没有椭圆暗角/眨眼)。", this); } private bool TrySetupOverlayMaterial() { overlayShaderAvailable = false; Shader shader = overlayShader != null ? overlayShader : Shader.Find("UI/EyeViewportOverlay"); if (shader == null) { Debug.LogError("[EyeViewportOverlay] 找不到 UI/EyeViewportOverlay shader。", this); return false; } if (!shader.isSupported) { Debug.LogError("[EyeViewportOverlay] UI/EyeViewportOverlay shader 在当前平台不受支持。", this); return false; } instanceMaterial = new Material(shader) { name = "EyeViewportOverlay (Instance)", hideFlags = HideFlags.DontSave }; overlayImage.material = instanceMaterial; overlayShaderAvailable = true; return true; } private void ApplyCanvasRenderSettings() { if (overlayCanvas == null) { return; } overlayCanvas.renderMode = RenderMode.ScreenSpaceCamera; Camera uiCamera = ResolveUICamera(); overlayCanvas.worldCamera = uiCamera; 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; } } }