Files
aibis-dream/Assets/Scripts/MiniGame/Peipei/Eye/EyeViewportOverlay.cs
T

632 lines
19 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections;
using DG.Tweening;
using UnityEngine;
using UnityEngine.UI;
namespace AibisDream
{
/// <summary>
/// Mobile-friendly fullscreen eye viewport: elliptical sight + vertical ellipse blink.
/// </summary>
[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");
/// <summary>Persistence:本遮罩=0UI Canvas=1Dialog Canvas=3。</summary>
[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;
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;
}
/// <summary>
/// 先眨眼,再在最后一次眨眼的开眼阶段触发效果(聚焦 1 次 / 想象 2 次)。
/// </summary>
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
&& overlayShaderAvailable
&& 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();
if (!overlayShaderAvailable || instanceMaterial == null)
{
DisableOverlayFallback();
return;
}
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 (!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<Canvas>();
canvasGo.AddComponent<CanvasScaler>();
canvasGo.AddComponent<GraphicRaycaster>();
}
NormalizeCanvasTransform(overlayCanvas.transform);
overlayCanvas.pixelPerfect = false;
var scaler = overlayCanvas.GetComponent<CanvasScaler>();
if (scaler == null)
{
scaler = overlayCanvas.gameObject.AddComponent<CanvasScaler>();
}
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920f, 1080f);
scaler.matchWidthOrHeight = 0.5f;
if (overlayCanvas.GetComponent<GraphicRaycaster>() == null)
{
overlayCanvas.gameObject.AddComponent<GraphicRaycaster>();
}
EnsureOverlayImage();
if (overlayImage != null && instanceMaterial == null && !TrySetupOverlayMaterial())
{
SetOverlayActive(false);
}
ApplyCanvasRenderSettings();
}
private Canvas FindExistingOverlayCanvas()
{
foreach (var canvas in GetComponentsInChildren<Canvas>(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<Image>();
}
}
if (overlayImage == null)
{
var imageGo = new GameObject("Overlay", typeof(RectTransform));
imageGo.transform.SetParent(overlayCanvas.transform, false);
overlayImage = imageGo.AddComponent<Image>();
}
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<Transform>(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<UIManager>();
if (uiManager != null && uiManager.UICamera != null)
{
return uiManager.UICamera;
}
return Camera.main;
}
}
}