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

519 lines
15 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;
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;
}
/// <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
&& 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<Canvas>();
overlayCanvas.pixelPerfect = false;
var scaler = canvasGo.AddComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920f, 1080f);
scaler.matchWidthOrHeight = 0.5f;
canvasGo.AddComponent<GraphicRaycaster>();
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<Image>();
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<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;
}
}
}