add : flash transitions between scenes

This commit is contained in:
2026-07-20 18:46:56 +02:00
parent 50f65e99a7
commit a0cfb31079
14 changed files with 347 additions and 150 deletions
@@ -32,6 +32,11 @@ namespace Unheimlich.Transitions
[SerializeField] private float _openOffset = 600f;
[SerializeField] private float _closedOffset = 0f;
// Set per-call via SetLayerSwapEnabled() rather than serialized here, since this
// is a shared singleton: different callers (HallucinationFlash instances, scene
// transitions) may want different behavior for the same shared component.
private bool _swapLayerOrderOnTransition;
/// <summary>True while a blink sequence is currently animating.</summary>
public bool IsBusy { get; private set; }
@@ -42,11 +47,36 @@ namespace Unheimlich.Transitions
private bool _isWarmedUp;
private bool _videoShown;
private bool _showRequested;
private VideoClip _pendingClip;
private Material _defaultMaterial;
private bool HasVideo => _videoPlayer != null && _videoDisplay != null && _videoPlayer.clip != null;
/// <summary>
/// Logs the individual state of every condition HasVideo depends on, plus the
/// warm-up/shown flags. Use this to pinpoint exactly why the video overlay isn't
/// appearing (missing reference, clip not assigned, warm-up not finished, etc.).
/// </summary>
private void LogVideoState(string context)
{
if (!_logVideoDebug)
return;
Debug.LogError(
$"[OctoEyelidBlink] VideoState @ {context}"
+ $" | _videoPlayer={(_videoPlayer != null ? "OK" : "NULL")}"
+ $" | _videoDisplay={(_videoDisplay != null ? "OK" : "NULL")}"
+ $" | clip={(_videoPlayer != null && _videoPlayer.clip != null ? _videoPlayer.clip.name : "NULL")}"
+ $" | HasVideo={HasVideo}"
+ $" | _isWarmedUp={_isWarmedUp}"
+ $" | _videoShown={_videoShown}"
+ $" | _videoDisplay.enabled={(_videoDisplay != null ? _videoDisplay.enabled.ToString() : "N/A")}"
+ $" | _videoPlayer.isPrepared={(_videoPlayer != null ? _videoPlayer.isPrepared.ToString() : "N/A")}"
+ $" | _videoPlayer.isPlaying={(_videoPlayer != null ? _videoPlayer.isPlaying.ToString() : "N/A")}"
);
}
protected override void OnAwake()
{
OctoTransitionProvider.Current = this;
@@ -92,14 +122,40 @@ namespace Unheimlich.Transitions
if (_videoPlayer == null)
return;
// Avoid restarting an already-completed or in-progress warm-up for the same clip.
// This lets a caller pre-warm a clip early (e.g. HallucinationFlash.Prewarm()) and
// then safely call SetClip again with the same clip right before CloseEyes without
// losing the warm-up progress already made.
if (clip == _pendingClip && _videoPlayer.clip == clip)
{
LogVideoState("SetClip (skipped, same clip already set)");
return;
}
_isWarmedUp = false;
_pendingClip = clip;
_videoPlayer.clip = clip;
LogVideoState("SetClip");
if (clip != null)
StartCoroutine(WarmUpVideo());
}
/// <summary>
/// Enables or disables the eyelid/video layer swap for the next CloseEyes()/OpenEyes()
/// sequence: when enabled, the eyelid bars render behind the video overlay while closing
/// (video shows on top), then swap to render in front of it while opening (video
/// disappears behind the still-closed-looking bars instead of staying visible during
/// the reveal). Call this before CloseEyes() to opt a specific sequence in; it resets
/// to disabled automatically once the sequence finishes opening, so it never leaks into
/// unrelated callers (e.g. scene transitions) that don't set it explicitly.
/// </summary>
public void SetLayerSwapEnabled(bool enabled)
{
_swapLayerOrderOnTransition = enabled;
}
/// <summary>Closes the eyelids over the given duration and invokes the callback on completion.</summary>
public void CloseEyes(float duration, Action onComplete)
{
@@ -107,6 +163,10 @@ namespace Unheimlich.Transitions
CancelCurrent();
ResetVideoToStart();
_videoShown = false;
_showRequested = false;
if (_swapLayerOrderOnTransition)
SetEyelidsBehindVideo();
RectTransform top = _topEyelid.rectTransform;
RectTransform bottom = _bottomEyelid.rectTransform;
@@ -129,6 +189,9 @@ namespace Unheimlich.Transitions
/// <summary>Opens the eyelids over the given duration and invokes the callback on completion.</summary>
public void OpenEyes(float duration, Action onComplete)
{
if (_swapLayerOrderOnTransition)
SetEyelidsInFrontOfVideo();
RectTransform top = _topEyelid.rectTransform;
RectTransform bottom = _bottomEyelid.rectTransform;
@@ -147,16 +210,28 @@ namespace Unheimlich.Transitions
{
_bottomTweenId = -1;
IsBusy = false;
_swapLayerOrderOnTransition = false;
})
.id;
}
/// <summary>
/// True if the given clip is the one currently assigned and has finished warming up.
/// Lets a caller (e.g. HallucinationFlash) check readiness after calling Prewarm-style
/// SetClip ahead of time, without having to guess a fixed delay.
/// </summary>
public bool IsClipReady(VideoClip clip)
{
return _isWarmedUp && _pendingClip == clip;
}
/// <summary>Stops playback, resets the video to its first frame, and hides the overlay.</summary>
public void HideVideo()
{
if (!HasVideo)
return;
_showRequested = false;
_videoPlayer.Pause();
_videoPlayer.time = 0.0;
SetVideoVisible(false);
@@ -166,6 +241,8 @@ namespace Unheimlich.Transitions
/// <summary>Reveals and plays the video overlay after the given delay, once warm-up has completed.</summary>
public void ShowVideoAfterDelay(float delay)
{
LogVideoState("ShowVideoAfterDelay (before HasVideo check)");
if (!HasVideo)
return;
@@ -236,28 +313,46 @@ namespace Unheimlich.Transitions
_isWarmedUp = true;
if (_logVideoDebug)
Debug.Log($"[OctoEyelidBlink] Warm-up complete for clip: {_pendingClip?.name}");
Debug.LogError($"[OctoEyelidBlink] Warm-up complete for clip: {_pendingClip?.name}");
// The requested reveal may have fired before warm-up finished and bailed out.
// Catch up now instead of leaving the overlay stuck hidden for the whole sequence.
if (_showRequested && !_videoShown)
{
if (_logVideoDebug)
Debug.LogError("[OctoEyelidBlink] Warm-up finished after a pending show request, showing video now.");
ShowVideo();
}
}
private void ShowVideo()
{
LogVideoState("ShowVideo (entry)");
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.
_showRequested = true;
// Safety check: if warm-up hasn't completed yet (e.g. clip assigned too late,
// or the reveal delay is shorter than warm-up takes), skip showing the video for
// now rather than risk a corrupted first frame. WarmUpVideo() will catch up and
// call ShowVideo() again as soon as it finishes, since _showRequested is still set.
if (!_isWarmedUp)
{
if (_logVideoDebug)
Debug.LogWarning("[OctoEyelidBlink] Video not warmed up in time, skipping display.");
Debug.LogError("[OctoEyelidBlink] Video not warmed up in time, skipping display for now (will retry once warm-up completes).");
return;
}
_showRequested = false;
_videoShown = true;
SetVideoVisible(true);
_videoPlayer.Play();
LogVideoState("ShowVideo (after SetVideoVisible(true))");
}
private void ResetVideoToStart()
@@ -271,8 +366,43 @@ namespace Unheimlich.Transitions
private void SetVideoVisible(bool visible)
{
if (_videoDisplay != null)
_videoDisplay.enabled = visible;
if (_videoDisplay == null)
{
if (_logVideoDebug)
Debug.LogError("[OctoEyelidBlink] SetVideoVisible called but _videoDisplay is NULL.");
return;
}
_videoDisplay.enabled = visible;
if (_logVideoDebug)
Debug.LogError($"[OctoEyelidBlink] SetVideoVisible({visible}) applied. _videoDisplay.enabled is now {_videoDisplay.enabled} on '{_videoDisplay.gameObject.name}'.");
}
// ---------- Layering internals ----------
/// <summary>
/// Moves both eyelid bars to the first sibling position, so any other UI element
/// under the same parent (in particular the video overlay) renders on top of them.
/// Used when closing, so the video appears layered above the bars.
/// </summary>
private void SetEyelidsBehindVideo()
{
_topEyelid.transform.SetAsFirstSibling();
_bottomEyelid.transform.SetAsFirstSibling();
}
/// <summary>
/// Moves both eyelid bars to the last sibling position, so they render in front of
/// any other UI element under the same parent (in particular the video overlay).
/// Used when opening, so the video disappears behind the still-closed-looking bars
/// instead of staying visible while the scene is revealed.
/// </summary>
private void SetEyelidsInFrontOfVideo()
{
_topEyelid.transform.SetAsLastSibling();
_bottomEyelid.transform.SetAsLastSibling();
}
// ---------- Eyelid internals ----------