372 lines
11 KiB
C#
372 lines
11 KiB
C#
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 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");
|
|
|
|
[SerializeField] private int sortingOrder = 120;
|
|
|
|
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();
|
|
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)
|
|
{
|
|
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));
|
|
|
|
return blinkTween;
|
|
}
|
|
|
|
private void ShowOverlay(EyeVisualSettings settings)
|
|
{
|
|
if (settings == null || !settings.enableViewportOverlay)
|
|
{
|
|
ExitModule();
|
|
return;
|
|
}
|
|
|
|
PrepareHierarchyIfNeeded();
|
|
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 (hierarchyReady && overlayCanvas != null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (Application.isPlaying && !lifecycleReady)
|
|
{
|
|
ScheduleDeferredPrepare();
|
|
return;
|
|
}
|
|
|
|
EnsureOverlayHierarchy();
|
|
hierarchyReady = overlayCanvas != null;
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
private void EnsureOverlayHierarchy()
|
|
{
|
|
if (overlayCanvas != null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var canvasGo = new GameObject("EyeViewportOverlayCanvas");
|
|
canvasGo.transform.SetParent(transform, false);
|
|
|
|
overlayCanvas = canvasGo.AddComponent<Canvas>();
|
|
overlayCanvas.renderMode = RenderMode.ScreenSpaceOverlay;
|
|
overlayCanvas.sortingOrder = sortingOrder;
|
|
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();
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|