Files
Ipseity-Demo/Assets/_Content/Scripts/Features/Hallucination/Runtime/OctoEyelidBlink.cs
T
2026-07-17 18:30:24 +02:00

296 lines
10 KiB
C#

using System;
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Video;
using OctoCore.Transitions;
using Unheimlich.Core;
namespace Unheimlich.Transitions
{
/// <summary>
/// Persistent singleton engine for "eyelid blink" transitions (eyelids + optional video overlay).
/// Lives outside the OctoCore package (this project already references LeanTween here), and
/// implements IOctoSceneTransition so OctoNavigation can drive it without the package ever
/// depending on LeanTween or this concrete type. Registers itself with OctoTransitionProvider
/// once ready so the package can find it at runtime.
/// </summary>
public class OctoEyelidBlink : Singleton<OctoEyelidBlink>, IOctoSceneTransition
{
[Header("Eyelids")]
[SerializeField] private Image _topEyelid;
[SerializeField] private Image _bottomEyelid;
[Header("Video")]
[Tooltip("RawImage displaying the shared RenderTexture. Its material is swapped per call.")]
[SerializeField] private RawImage _videoDisplay;
[SerializeField] private VideoPlayer _videoPlayer;
[SerializeField] private bool _skipOnDrop = true;
[SerializeField] private bool _logVideoDebug = false;
[Header("Positions")]
[SerializeField] private float _openOffset = 600f;
[SerializeField] private float _closedOffset = 0f;
/// <summary>True while a blink sequence is currently animating.</summary>
public bool IsBusy { get; private set; }
private int _topTweenId = -1;
private int _bottomTweenId = -1;
private int _delayTweenId = -1;
private int _videoTweenId = -1;
private bool _isWarmedUp;
private bool _videoShown;
private VideoClip _pendingClip;
private Material _defaultMaterial;
private bool HasVideo => _videoPlayer != null && _videoDisplay != null && _videoPlayer.clip != null;
protected override void OnAwake()
{
OctoTransitionProvider.Current = this;
// Only call DontDestroyOnLoad on a root object. If this component already lives
// under the persistent Octo prefab, the parent handles persistence and this is a no-op.
if (transform.parent == null)
DontDestroyOnLoad(gameObject);
if (_videoDisplay != null)
_defaultMaterial = _videoDisplay.material;
SetEyelidsOpen();
SetVideoVisible(false);
ConfigureVideoPlayer();
}
// Re-registers with the transition provider whenever Instance is restored
// (e.g. after a SubsystemRegistration reset with Domain Reload disabled).
protected override void OnEnabled()
{
OctoTransitionProvider.Current = this;
}
protected override void OnDestroyInstance()
{
if (OctoTransitionProvider.Current == (IOctoSceneTransition)this)
OctoTransitionProvider.Current = null;
}
// ---------- IOctoSceneTransition ----------
/// <summary>
/// Assigns the video clip and optional material to use during the next CloseEyes() call
/// and starts warming the clip up. Pass a null clip to perform a plain blink with no video.
/// A null material falls back to the RawImage's default material.
/// </summary>
public void SetClip(VideoClip clip, Material material)
{
if (_videoDisplay != null)
_videoDisplay.material = material != null ? material : _defaultMaterial;
if (_videoPlayer == null)
return;
_isWarmedUp = false;
_pendingClip = clip;
_videoPlayer.clip = clip;
if (clip != null)
StartCoroutine(WarmUpVideo());
}
/// <summary>Closes the eyelids over the given duration and invokes the callback on completion.</summary>
public void CloseEyes(float duration, Action onComplete)
{
IsBusy = true;
CancelCurrent();
ResetVideoToStart();
_videoShown = false;
RectTransform top = _topEyelid.rectTransform;
RectTransform bottom = _bottomEyelid.rectTransform;
_topTweenId = LeanTween.moveY(top, _closedOffset, duration)
.setEase(LeanTweenType.easeOutQuad)
.setOnComplete(() =>
{
_topTweenId = -1;
onComplete?.Invoke();
})
.id;
_bottomTweenId = LeanTween.moveY(bottom, -_closedOffset, duration)
.setEase(LeanTweenType.easeOutQuad)
.setOnComplete(() => _bottomTweenId = -1)
.id;
}
/// <summary>Opens the eyelids over the given duration and invokes the callback on completion.</summary>
public void OpenEyes(float duration, Action onComplete)
{
RectTransform top = _topEyelid.rectTransform;
RectTransform bottom = _bottomEyelid.rectTransform;
_topTweenId = LeanTween.moveY(top, _openOffset, duration)
.setEase(LeanTweenType.easeInQuad)
.setOnComplete(() =>
{
_topTweenId = -1;
onComplete?.Invoke();
})
.id;
_bottomTweenId = LeanTween.moveY(bottom, -_openOffset, duration)
.setEase(LeanTweenType.easeInQuad)
.setOnComplete(() =>
{
_bottomTweenId = -1;
IsBusy = false;
})
.id;
}
/// <summary>Stops playback, resets the video to its first frame, and hides the overlay.</summary>
public void HideVideo()
{
if (!HasVideo)
return;
_videoPlayer.Pause();
_videoPlayer.time = 0.0;
SetVideoVisible(false);
_videoShown = false;
}
/// <summary>Reveals and plays the video overlay after the given delay, once warm-up has completed.</summary>
public void ShowVideoAfterDelay(float delay)
{
if (!HasVideo)
return;
_videoTweenId = LeanTween.delayedCall(gameObject, delay, () =>
{
_videoTweenId = -1;
ShowVideo();
}).id;
}
// ---------- Extra API (not part of the interface, used by HallucinationFlash directly) ----------
/// <summary>Waits for the given duration before invoking the callback.</summary>
public void Hold(float duration, Action onComplete)
{
_delayTweenId = LeanTween.delayedCall(gameObject, duration, () =>
{
_delayTweenId = -1;
onComplete?.Invoke();
}).id;
}
// ---------- Video internals ----------
private void ConfigureVideoPlayer()
{
if (_videoPlayer == null)
return;
_videoPlayer.playOnAwake = false;
_videoPlayer.isLooping = false;
_videoPlayer.waitForFirstFrame = true;
_videoPlayer.skipOnDrop = _skipOnDrop;
}
/// <summary>
/// Prepares and briefly plays the assigned clip while hidden, forcing Unity to
/// decode the first frames ahead of time. Prevents a blank/garbage frame from
/// being visible the first time the video is actually shown.
/// </summary>
private IEnumerator WarmUpVideo()
{
if (!HasVideo)
yield break;
SetVideoVisible(false);
ConfigureVideoPlayer();
_videoPlayer.Prepare();
while (!_videoPlayer.isPrepared)
yield return null;
_videoPlayer.Play();
while (!_videoPlayer.isPlaying)
yield return null;
// Wait a few frames so the RenderTexture actually receives video data.
yield return null;
yield return null;
yield return null;
_videoPlayer.Pause();
_videoPlayer.time = 0.0;
// Wait one additional frame after seeking back to the start.
yield return null;
_isWarmedUp = true;
if (_logVideoDebug)
Debug.Log($"[OctoEyelidBlink] Warm-up complete for clip: {_pendingClip?.name}");
}
private void ShowVideo()
{
if (!HasVideo || _videoShown)
return;
// Safety check: if warm-up hasn't completed yet (e.g. clip assigned too late),
// skip showing the video rather than risk a corrupted first frame. The eyelid
// blink itself remains unaffected.
if (!_isWarmedUp)
{
if (_logVideoDebug)
Debug.LogWarning("[OctoEyelidBlink] Video not warmed up in time, skipping display.");
return;
}
_videoShown = true;
SetVideoVisible(true);
_videoPlayer.Play();
}
private void ResetVideoToStart()
{
if (!HasVideo)
return;
_videoPlayer.Pause();
_videoPlayer.time = 0.0;
}
private void SetVideoVisible(bool visible)
{
if (_videoDisplay != null)
_videoDisplay.enabled = visible;
}
// ---------- Eyelid internals ----------
private void SetEyelidsOpen()
{
_topEyelid.rectTransform.anchoredPosition =
new Vector2(_topEyelid.rectTransform.anchoredPosition.x, _openOffset);
_bottomEyelid.rectTransform.anchoredPosition =
new Vector2(_bottomEyelid.rectTransform.anchoredPosition.x, -_openOffset);
}
private void CancelCurrent()
{
if (_topTweenId != -1) { LeanTween.cancel(_topTweenId); _topTweenId = -1; }
if (_bottomTweenId != -1) { LeanTween.cancel(_bottomTweenId); _bottomTweenId = -1; }
if (_delayTweenId != -1) { LeanTween.cancel(_delayTweenId); _delayTweenId = -1; }
if (_videoTweenId != -1) { LeanTween.cancel(_videoTweenId); _videoTweenId = -1; }
}
}
}