All tools
SceneManagerAsyncShader
Scene Transition Lab
Cover, load, activate, reveal — and the .unitypackage that does it.
idle · 1570 ms
Cover
Load
Reveal
0.9
activate
0 msFade · coroutine1570 ms
AsyncOperation.progress
op.progress0.00
stalls at 0.9 — the last tenth is the activation
what the player sees0.00
op.progress / 0.9, clamped
Total
1570 ms
Covered
990 ms
screen fully hidden
Waiting on you
0 ms
min display padding
Activation
90 ms
hidden under cover
What breaks
- No known failure modes in this configuration.
Presets
The default that never looks wrong. Short, unscaled, blocks input.
Transition
Load
// Assets/SceneTransition/Runtime/SceneTransitionManager.cs
//
// Generated by burakkaan.dev/tools/scene-transition-lab
//
// One object owns the whole scene change: the cover, the async load, the
// minimum display time, the activation, and the reveal. It lives in
// DontDestroyOnLoad and bootstraps itself from Resources, so no scene has
// to carry a reference to it.
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using System.Collections;
[DisallowMultipleComponent]
public class SceneTransitionManager : MonoBehaviour
{
public static SceneTransitionManager Instance { get; private set; }
[Header("Preset")]
public TransitionPreset preset;
[Header("Refs")]
[Tooltip("Full-screen Image. Raycast Target must stay on — it is what blocks input.")]
public Image overlay;
public CanvasGroup group;
[Tooltip("Optional. Image with Type = Filled, Fill Method = Horizontal.")]
public Image progressFill;
CanvasGroup _group;
bool _busy;
/// <summary>Raised with 0..1 while a load is running. Hook your label to it.</summary>
public event System.Action<float> ProgressChanged;
/// <summary>True between the first cover frame and the last reveal frame.</summary>
public bool IsTransitioning => _busy;
/* --------------------------------------------------------------- *
* Bootstrap
* --------------------------------------------------------------- */
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
static void Bootstrap()
{
if (Instance != null) return;
var prefab = Resources.Load<GameObject>("TransitionCanvas");
if (prefab == null)
{
Debug.LogWarning("[SceneTransition] Resources/TransitionCanvas.prefab is missing. " +
"Run Tools > Scene Transition > Create Transition Canvas.");
return;
}
Instantiate(prefab);
}
void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
_group = group != null ? group : GetComponentInChildren<CanvasGroup>();
SetCover(0f);
}
/* --------------------------------------------------------------- *
* Entry points
* --------------------------------------------------------------- */
public void Load(string sceneName)
{
if (_busy)
{
// A second request mid-transition is almost always a double click.
Debug.LogWarning($"[SceneTransition] Ignored Load({sceneName}) — already transitioning.");
return;
}
StartCoroutine(RunAsync(sceneName));
}
IEnumerator RunAsync(string sceneName)
{
_busy = true;
SetBlocking(true);
yield return Cover(true);
var op = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Single);
// The load is not allowed to finish on its own: activation is what
// costs the frame, and it has to land while the screen is covered.
op.allowSceneActivation = false;
float held = 0f;
float minHold = preset != null ? preset.minDisplaySeconds : 0.300f;
// progress stalls at 0.9 with activation disabled — the last tenth IS
// the activation. Divide by 0.9 before showing it to a player.
while (op.progress < 0.9f || held < minHold)
{
held += Time.unscaledDeltaTime;
Report(op.progress);
yield return null;
}
Report(1f);
op.allowSceneActivation = true;
while (!op.isDone) { yield return null; }
// One frame of grace. Awake, Start, the first Update and shader warm-up
// all land on the activation frame; revealing on top of it shows the hitch.
yield return null;
yield return Cover(false);
SetBlocking(false);
_busy = false;
}
/* --------------------------------------------------------------- *
* Cover / reveal
* --------------------------------------------------------------- */
IEnumerator Cover(bool covering)
{
float duration = covering
? (preset != null ? preset.coverSeconds : 0.260f)
: (preset != null ? preset.revealSeconds : 0.320f);
var curve = preset != null
? (covering ? preset.coverEase : preset.revealEase)
: null;
if (duration <= 0f)
{
SetCover(covering ? 1f : 0f);
yield break;
}
float t = 0f;
while (t < duration)
{
t += Time.unscaledDeltaTime;
float k = Mathf.Clamp01(t / duration);
float eased = curve != null ? curve.Evaluate(k) : k;
SetCover(covering ? eased : 1f - eased);
yield return null;
}
SetCover(covering ? 1f : 0f);
}
/// <summary>0 = scene visible, 1 = fully covered.</summary>
public void SetCover(float amount)
{
if (_group != null) _group.alpha = amount;
}
void Report(float raw)
{
float shown = Mathf.Clamp01(raw / 0.9f);
if (progressFill != null) progressFill.fillAmount = shown;
ProgressChanged?.Invoke(shown);
}
void SetBlocking(bool on)
{
if (_group != null)
{
_group.blocksRaycasts = on;
_group.interactable = on;
}
if (overlay != null) overlay.raycastTarget = on;
}
}
The whole scene change in one file. Drop it in, run the menu item, call SceneTransitionManager.Instance.Load("Level_01").
Download
A real .unitypackage — scripts, shader, mask and the editor menu item, with GUIDs already assigned so the references survive the import.
- Runtime/SceneTransitionManager.cs
- Runtime/TransitionPreset.cs
- Editor/TransitionCanvasBuilder.cs
- README.txt
After importing, run Tools > Scene Transition > Create Transition Canvas once. The prefab is built by that script rather than shipped, so it can never land as a missing-script placeholder.