Customization
Custom port implementation
General
To build a custom implementation:
- Inherit from
PassageBaseorTeleportBase, or implementITransitionComponent; - Use the PortsDropdown class and the corresponding methods on
RefreshContextorEditorGraphto populate dropdowns for the current port or for any port in the graph; - Implement
GetGuid()returning the current port'sGuid; - In the editor, implement
Refresh(RefreshContext context)— the full description of the context methods is inRefreshContext.
PassageBase, TeleportBase and ITransitionComponent
PassageBase and TeleportBase are abstract base classes for passages and teleports: they already wire up PortsDropdown and related fields. In most cases inheriting from one of them and overriding a few methods (e.g. GetSpawnPosition()) is enough.
ITransitionComponent is the minimal "port on a scene" contract:
string GetGuid()— the port identifier in the graph.Vector3 GetSpawnPosition()— the character's spawn position at this port.
In the editor, a void Refresh(RefreshContext context) method is also expected — see RefreshContext.
If the base classes do not fit, you can implement only ITransitionComponent and provide your own logic (see examples).
The Refresh() method is best wrapped in #if UNITY_EDITOR, since it is only needed in the editor.
Extension methods
ITransitionComponent also has extension methods.
IsInput(manager) and IsOutput(manager) tell you whether the current component was the entry or exit port for the transition the character walked through. The manager is passed explicitly, so the methods work identically with both the built-in and a custom TransitionManager.
| Method | Purpose |
|---|---|
IsOutput(manager) | true if this is the component the transition ended at. |
IsInput(manager) | true if this is the component the transition started from. |
The parameterless overloads (IsOutput(), IsInput()) are marked [Obsolete] and internally fall back to TransitionManager.Instance. If a custom TransitionManager is enabled in project settings, they throw an exception — use the overloads that take an explicit manager.
IsShortcutOrigin() and IsShortcutDestination() tell you whether the component is the entry or exit side of a shortcut connection. Same idea for IsOneWayOrigin() and IsOneWayDestination() — for one-way connections. These methods do not take a manager and read the graph directly.
| Method | Purpose |
|---|---|
IsShortcutDestination() | true if the component is the "exit" of a shortcut. |
IsShortcutOrigin() | true if the component is the "entry" of a shortcut. |
IsOneWayDestination() | true if the component is the "exit" of a one-way link. |
IsOneWayOrigin() | true if the component is the "entry" of a one-way link. |
If the connection type does not match (e.g. calling IsShortcutOrigin() on a one-way link) — the method returns false.
RefreshContext
RefreshContext is a struct the editor passes into void Refresh(RefreshContext context) on components that implement ITransitionComponent. Editor-only. Refresh is invoked when the scene, hierarchy, or graph changes, to bring PortsDropdown and related fields back in sync with the current port Guids.
Fields:
| Name | Type | Description |
|---|---|---|
EditorGraph | EditorGraph | The graph of the current container. Use it for manual data access when the methods below are insufficient. |
Methods:
| Method | Purpose |
|---|---|
void FillPortsDropdownData(PortsDropdown dropdown) | Fills the list with the active scene's ports. |
void FillAllPortsDropdownData(PortsDropdown dropdown) | Fills the list with every port in the graph. |
TransitionManager transition methods
Task LoadSceneAsync(int buildIndex, TransitionContext context = null)
Loads a scene by build index.
Parameters:
| Name | Type | Description |
|---|---|---|
buildIndex | int | Scene build index. |
context | TransitionContext | Optional per-phase delay parameters for this transition. |
Task GoFromAsync(string currentPortGuid, bool ignoreShortcuts, ITransitionContext context = null)
Moves the player to the opposite port, using the connection defined in the graph.
Parameters:
| Name | Type | Description |
|---|---|---|
currentPortGuid | string | Guid of the current port. |
ignoreShortcuts | bool | Whether to ignore shortcuts. |
context | ITransitionContext | The built-in TransitionManager only accepts the TransitionContext type, which lets you override per-phase transition delays. |
Task GoToAsync(string currentPortGuid, string targetPortGuid, TransitionContext context = null)
Moves the player to an arbitrary target port. Does not use the connections in the graph.
Parameters:
| Name | Type | Description |
|---|---|---|
currentPortGuid | string | Guid of the current port. |
targetPortGuid | string | Guid of the target port. |
context | ITransitionContext | The built-in TransitionManager only accepts the TransitionContext type, which lets you override per-phase transition delays. |
GoToAsync / GoFromAsync / LoadSceneAsync return a Task and can be awaited. For fire-and-forget calls use _ = TransitionManager.Instance.GoFromAsync(...). The old synchronous GoTo / GoFrom / LoadScene methods are marked [Obsolete]. The built-in manager also has a GoFromAsync(currentPortGuid, context) overload without ignoreShortcuts, matching the ITransitionManager interface.
GoToAsync/GoFromAsync examples
A minimal working custom passage using GoFromAsync:
using UnityEngine;
namespace WorldGraphEditor.Examples
{
public class ExamplePassage : MonoBehaviour, ITransitionComponent
{
[SerializeField] private PortsDropdown _assignedPort = new();
private string _currentGuid;
// Any trigger, e.g. entering a collider
private void OnTriggerEnter(Collider other)
{
// Call GoFromAsync
_ = TransitionManager.Instance.GoFromAsync(_currentGuid);
}
#if UNITY_EDITOR
public void Refresh(RefreshContext context)
{
context.FillPortsDropdownData(_assignedPort);
_currentGuid = _assignedPort.GetSelectedValue();
}
#endif
public Vector3 GetSpawnPosition() => transform.position;
public string GetGuid() => _currentGuid;
}
}
A minimal working custom teleport using GoToAsync:
using UnityEngine;
namespace WorldGraphEditor.Examples
{
public class ExampleTeleport : MonoBehaviour, ITransitionComponent
{
[SerializeField] private PortsDropdown _assignedPort = new();
[SerializeField] private PortsDropdown _targetPort = new();
private string _currentGuid;
private string _targetGuid;
// Any trigger, e.g. entering a collider
private void OnTriggerEnter(Collider other)
{
// Call GoToAsync
_ = TransitionManager.Instance.GoToAsync(_currentGuid, _targetGuid);
}
#if UNITY_EDITOR
public void Refresh(RefreshContext context)
{
context.FillPortsDropdownData(_assignedPort);
context.FillAllPortsDropdownData(_targetPort);
_currentGuid = _assignedPort.GetSelectedValue();
_targetGuid = _targetPort.GetSelectedValue();
}
#endif
public Vector3 GetSpawnPosition() => transform.position;
public string GetGuid() => _currentGuid;
}
}
TransitionContext
TransitionContext is the class that lets you override per-phase transition delays for a single transition. It can be passed as an optional argument to GoToAsync, GoFromAsync, and LoadSceneAsync.
Example
_ = TransitionManager.Instance.GoFromAsync(_port.GetSelectedValue(), true,
new TransitionContext
{
PortEnteredDelay = new TransitionDelayData(DelayType.ThisFrame),
TransitionStartedDelay = new TransitionDelayData(DelayType.CustomDelay, 1.2f)
});
PortsDropdown
PortsDropdown is a class for building dropdowns bound to ports in the graph. It preserves the selected value even when ports are renamed or the port count on a node changes.
It exposes two methods:
string GetSelectedValue()
Return value:
| Type | Description |
|---|---|
string | The selected value (Guid). |
void SetData(IEnumerable<(string Guid, string DisplayName)> data)
Updates the dropdown data from Guid / display name pairs.
Parameters:
| Name | Type | Description |
|---|---|---|
data | IEnumerable<(string Guid, string DisplayName)> | Data where every entry contains a Guid and a display name. |
Low-level access: EditorGraph and PortsDropdown
In a typical scenario, filling a PortsDropdown from graph data inside Refresh(RefreshContext context) is done with context.FillPortsDropdownData(dropdown) or context.FillAllPortsDropdownData(dropdown) — see RefreshContext.
Below are the EditorGraph methods (available via context.EditorGraph or WorldGraphContainer.EditorGraph): they return raw sequences suitable for SetData when you need filtering, combining with other data, or a fully custom list assembly. The full EditorGraph surface is described in API: WorldGraph and EditorGraph.
Editor-only.
IEnumerable<(string Guid, string Name)> GetPortsDropdownData(string scenePath)
Parameters:
| Name | Type | Description |
|---|---|---|
scenePath | string | Path to the scene asset (a .unity file), same as Scene.path. |
Return value:
| Type | Description |
|---|---|
IEnumerable<(string Guid, string Name)> | Data for every port on that scene: its Guid and display name. |
Example
The recommended path is via RefreshContext:
using UnityEngine;
namespace WorldGraphEditor.Examples
{
public class MyComponentWithDropdown : MonoBehaviour, ITransitionComponent
{
[SerializeField] private PortsDropdown _dropdown = new();
private string _currentPortGuid;
#if UNITY_EDITOR
public void Refresh(RefreshContext context)
{
context.FillPortsDropdownData(_dropdown);
_currentPortGuid = _dropdown.GetSelectedValue();
}
#endif
// public Vector3 GetSpawnPosition() {}
// public string GetGuid() {}
}
}
An alternative via EditorGraph:
using UnityEngine;
using UnityEngine.SceneManagement;
namespace WorldGraphEditor.Examples
{
public class MyComponentWithDropdown : MonoBehaviour, ITransitionComponent
{
[SerializeField] private PortsDropdown _dropdown = new();
private string _currentPortGuid;
#if UNITY_EDITOR
public void Refresh(RefreshContext context)
{
var scenePath = SceneManager.GetActiveScene().path;
var scenePortsData = context.EditorGraph.GetPortsDropdownData(scenePath);
_dropdown.SetData(scenePortsData);
_currentPortGuid = _dropdown.GetSelectedValue();
}
#endif
// public Vector3 GetSpawnPosition() {}
// public string GetGuid() {}
}
}
The GetPortsDropdownData(int buildIndex) overload is marked as [Obsolete]. Prefer the scenePath variant to avoid depending on scene ordering in Build Settings.
IEnumerable<(string Guid, string Path)> GetAllPortsDropdownData()
Returns data for every port in the graph, grouped by scene.
Return value:
| Type | Description |
|---|---|
IEnumerable<(string Guid, string Path)> | Data for every port in the graph. |
Example
The recommended path is via RefreshContext:
using System;
using UnityEngine;
namespace WorldGraphEditor.Examples
{
public class MyComponentWithDropdown : MonoBehaviour, ITransitionComponent
{
[SerializeField] private PortsDropdown _dropdown = new();
private string _currentPortGuid;
#if UNITY_EDITOR
public void Refresh(RefreshContext context)
{
context.FillAllPortsDropdownData(_dropdown);
_currentPortGuid = _dropdown.GetSelectedValue();
}
#endif
// public Vector3 GetSpawnPosition() {}
// public string GetGuid() {}
}
}
An alternative via EditorGraph:
using System;
using UnityEngine;
namespace WorldGraphEditor.Examples
{
public class MyComponentWithDropdown : MonoBehaviour, ITransitionComponent
{
// Dropdown
[SerializeField] private PortsDropdown _dropdown = new();
private string _currentPortGuid;
#if UNITY_EDITOR
public void Refresh(RefreshContext context)
{
var allPortsData = context.EditorGraph.GetAllPortsDropdownData();
_dropdown.SetData(allPortsData);
_currentPortGuid = _dropdown.GetSelectedValue();
}
#endif
// public Vector3 GetSpawnPosition() {}
// public string GetGuid() {}
}
}
Push-out from passages
You can add a "push-out force" for a port by implementing IPusher and returning PushData from GetPushData().
PushData is a struct with two fields:
Force— the push forceExists— indicates whether a force is present
public readonly struct PushData {
public readonly Vector3 Force;
public readonly bool Exists;
public PushData(Vector3 force)
{
Force = force;
Exists = true;
}
}
Example
using UnityEngine;
namespace WorldGraphEditor.Examples
{
public class MyPushingPassage : PassageBase, IPusher
{
[SerializeField] private Vector2 _pushForce;
public PushData GetPushData()
{
// If the force is (0, 0), return a default PushData
if (_pushForce == Vector2.zero)
// Returns default, where Exists is false
return default;
// If there is a force, pass it to the constructor. Exists becomes true.
return new PushData(_pushForce);
}
// Other members
//...
}
}
To apply this force to the player object, read the current value from TransitionManager.Instance.PushData.
Example
using UnityEngine;
namespace WorldGraphEditor.Examples
{
public class PlayerController : MonoBehaviour
{
[SerializeField] private Rigidbody2D _rigidbody;
private void OnTransitionEnded()
{
// Read the current push force
var pushData = TransitionManager.Instance.PushData;
// Apply the force to the Rigidbody only if it exists
if (pushData.Exists)
_rigidbody.AddForce(pushData.Force, ForceMode2D.Impulse);
}
private void Awake()
{
TransitionManager.Instance.TransitionEnded += OnTransitionEnded;
}
private void OnDestroy()
{
TransitionManager.Instance.TransitionEnded -= OnTransitionEnded;
}
}
}
Replacing the built-in TransitionManager
By default the built-in TransitionManager is used, but you can replace it entirely with your own implementation — e.g. to integrate with a DI framework (Zenject, VContainer, etc.) or to plug in a different scene-loading architecture.
Enabling
- Open Edit → Project Settings → World Graph Editor (see Project Settings);
- Enable the Use Custom Transition Manager checkbox;
- Assign the same
WorldGraphContainerthat the graph uses to the newly available Container field.
Once this checkbox is on, the built-in TransitionManager is not created automatically at game start. Manager initialization becomes entirely your responsibility (e.g. via a DI container).
The ITransitionManager interface
public interface ITransitionManager
{
public IWorldGraph Graph { get; }
public Vector3 NextSpawnPosition { get; }
public ITransitionComponent? OutputTransitionComponent { get; }
public ITransitionComponent? InputTransitionComponent { get; }
public event Action TransitionStarted;
public event Action SceneLoaded;
public event Action TransitionEnded;
public Task GoToAsync(string currentPortGuid, string targetPortGuid, ITransitionContext context = null);
public Task GoFromAsync(string currentPortGuid, ITransitionContext context = null);
}
Graph— the runtime graph of the container; retrieve it withWGEProjectConfig.Instance.GetWorldGraph().OutputTransitionComponent/InputTransitionComponent— the ports between which the current or most recent transition happens. Used by theIsOutput(manager)/IsInput(manager)extension methods (see Extension methods).GoToAsync/GoFromAsync— asynchronous transition entry points:GoToAsyncis a teleport between any two ports,GoFromAsyncis a transition from the current port along a graph connection. Both return aTask; theignoreShortcutsflag was removed from the interface — theITransitionManagerimplementation itself decides whether a transition is allowed.
For graph traversal and lookups IWorldGraph (implemented by the WorldGraph class, which inherits from GraphEngine) exposes CanPassTransition(guid, ignoreShortcuts, out status), TryGetPassageTransitionData(guid, out data), GetTeleportTransitionData(currentGuid, targetGuid), TryGetOppositePassageGuid and more — the full table is in API: WorldGraph and EditorGraph. To find a port component on a loaded scene use TransitionComponentUtility.FindByGuid(portGuid).
With a custom manager enabled, the parameterless IsOutput() / IsInput() extension methods throw an exception — use IsOutput(manager) / IsInput(manager) passing your ITransitionManager instance (see Extension methods).
Example: Zenject implementation
A ready-made demo project demonstrating an ITransitionManager implementation via DI is available here.
Registering the graph and the service in the container:
public class TransitionManagerProjectInstaller : MonoInstaller
{
public override void InstallBindings()
{
var graph = WGEProjectConfig.Instance.GetWorldGraph();
Container.BindInterfacesAndSelfTo<WorldGraph>().FromInstance(graph)
.AsSingle();
Container.BindInterfacesAndSelfTo<TransitionManagerService>()
.AsSingle();
}
}
The service itself (TransitionManagerService) receives IWorldGraph via its constructor and handles the transition by switching scenes:
public class TransitionManagerService : ITransitionManager
{
public IWorldGraph Graph { get; }
public ITransitionComponent OutputTransitionComponent { get; private set; }
public ITransitionComponent InputTransitionComponent { get; private set; }
public event Action TransitionStarted;
public event Action SceneLoaded;
public event Action TransitionEnded;
public Vector3 NextSpawnPosition { get; private set; }
public TransitionManagerService(IWorldGraph graph)
{
Graph = graph;
}
public async Task GoFromAsync(string currentPortGuid, ITransitionContext context = null)
{
// Verify that the transition is allowed
if (!Graph.CanPassTransition(currentPortGuid, false, out var status))
{
Debug.LogWarning($"Can`t pass transition, reason: {status}");
return;
}
// GoFromAsync uses TryGetPassageTransitionData
if (!Graph.TryGetPassageTransitionData(currentPortGuid, out var data))
{
Debug.LogError("No transition data for this passage");
return;
}
await GoInternal(data);
}
public async Task GoToAsync(string currentPortGuid, string targetPortGuid, ITransitionContext context = null)
{
// GoToAsync uses GetTeleportTransitionData
var data = Graph.GetTeleportTransitionData(currentPortGuid, targetPortGuid);
await GoInternal(data);
}
private async Task GoInternal(RuntimeTransitionData data)
{
TransitionStarted?.Invoke();
// Use TransitionComponentUtility to find the port on the scene
InputTransitionComponent = TransitionComponentUtility
.FindByGuid(data.CurrentPassageGuid);
// Load the scene by build index
await SceneManager.LoadSceneAsync(data.TargetSceneBuildIndex);
// Use TransitionComponentUtility to find the port on the scene
OutputTransitionComponent = TransitionComponentUtility
.FindByGuid(data.TargetPassageGuid);
NextSpawnPosition = OutputTransitionComponent?
.GetSpawnPosition() ?? Vector3.zero;
SceneLoaded?.Invoke();
TransitionEnded?.Invoke();
}
}
Since GoFromAsync does not take ignoreShortcuts, you can carry the flag through your own ITransitionContext implementation when needed:
public class IgnoreShortcutsContext : ITransitionContext
{
public bool IgnoreShortcuts { get; }
private IgnoreShortcutsContext(bool ignoreShortcuts)
{
IgnoreShortcuts = ignoreShortcuts;
}
public static ITransitionContext Ignore => new IgnoreShortcutsContext(true);
public static ITransitionContext Respect => new IgnoreShortcutsContext(false);
}
Handling it in TransitionManagerService:
public async Task GoFromAsync(string currentPortGuid, ITransitionContext context = null)
{
var ignoreShortcuts = context is IgnoreShortcutsContext passContext && passContext.IgnoreShortcuts;
if (!Graph.CanPassTransition(currentPortGuid, ignoreShortcuts, out var status))
{
// ...
}
// ...
}
Calling it from an ITransitionComponent:
public void Traverse()
{
// (true)
_manager.GoFromAsync(GetGuid(), IgnoreShortcutsContext.Ignore);
// (false)
_manager.GoFromAsync(GetGuid(), IgnoreShortcutsContext.Respect);
// (false)
_manager.GoFromAsync(GetGuid());
}
TransitionComponentUtility
Use TransitionComponentUtility to look up an ITransitionComponent or a specific implementation on the scene. This is required because FindByGuid(string) and FindByGuid<T>(string) also invoke Refresh() on each candidate component, so their guids are up-to-date at comparison time.
Methods:
| Method | Description |
|---|---|
ITransitionComponent FindAny() | Returns the first ITransitionComponent on the scene. |
T FindAny<T>() | Returns the first T implementing ITransitionComponent on the scene. |
ITransitionComponent FindByGuid(string) | Returns the ITransitionComponent matching the given guid. |
T FindByGuid<T>(string) | Returns the T implementing ITransitionComponent matching the given guid. |
Manual start of the built-in TransitionManager
By default the TransitionManager is started automatically at game start, but you can create it whenever suits your project.
Disabling auto-start
- Set
AutoLoadtofalse - Call
TransitionManager.CreateInstance()when you want the manager to be created
Example
using UnityEngine;
namespace WorldGraphEditor.Examples
{
public class MyTransitionManagerLoader : MonoBehaviour
{
[SerializeField] private float _loadDelay = 2f;
private void Start()
{
Invoke(nameof(MyLoadMethod), _loadDelay);
}
private void MyLoadMethod()
{
// Create the manager
TransitionManager.CreateInstance();
}
}
}
TransitionManager also has a LoadFromResources() method, but it only returns the instance from the Resources folder — it does not create an object in DontDestroyOnLoad. That method is primarily used inside the editor to get a reference. Prefer CreateInstance() — it guards against creating multiple instances.
Events and asynchronous code
The built-in TransitionManager fires events for every transition phase:
- Initialized: raised after
TransitionManagerinitialization succeeds; - Destroyed: raised from
OnDestroy()onTransitionManager; - PortEntered: raised when
GoFromAsync()orGoToAsync()is called; - TransitionStarted: raised right before the next scene starts loading;
- SceneLoaded: raised after the scene is loaded, the "output port"
OutputTransitionComponentis found, and the player spawn positionNextSpawnPositionis determined; - TransitionEnded: raised after the player prefab is instantiated. Fires even if no prefab is assigned;
- PortLeaved: raised after
TransitionEnded.
Delays between transition phases
You can insert a delay between transition phases before invoking async methods using EventCallConfig:
- Create the asset
- RMB → Create → World Graph Editor → Event Call Config.
- Pick one of the available delay types:
- This Frame — this frame;
- Next Frame — next frame;
- Physics Update — inside
FixedUpdate(); - Custom Delay — after a specified time.
- Drag the
EventCallConfiginto the matching field on theTransitionManager.
Awaiting async methods
If you need to wait for operations like level generation, register async methods with a priority via TransitionManager.RegisterAsyncHandler(). Higher priority handlers run first.
The full call order for each transition phase is:
- The transition event fires (OnPortEntered / OnTransitionStarted / OnSceneLoaded / OnTransitionEnded);
- The
EventCallConfigdelay is awaited; - Registered async handlers are awaited.
The event type after which registered async methods run is specified with the EventType enum.
Events you can register async methods for:
public enum EventType
{
OnPortEntered,
OnTransitionStarted,
OnSceneLoaded,
OnTransitionEnded,
}
Working with async methods
static void RegisterAsyncHandler(EventType eventType, Func<CancellationToken, Task> func, int priority)
Parameters:
| Name | Type | Description |
|---|---|---|
eventType | EventType | Event after which async methods should run. |
func | Func<CancellationToken, Task> | Async method taking a CancellationToken and returning a Task. |
priority | int | Call priority. |
static void UnregisterAsyncHandler(EventType eventType, Func<CancellationToken, Task> func)
Parameters:
| Name | Type | Description |
|---|---|---|
eventType | EventType | Event to remove the handler from. |
func | Func<CancellationToken, Task> | Async method taking a CancellationToken and returning a Task. |
On OnDestroy the built-in TransitionManager automatically calls CancellationTokenSource.Cancel() and removes every registered handler.
Example
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;
namespace WorldGraphEditor.Examples
{
public class AsyncAfterSceneLoadedExample : MonoBehaviour
{
private readonly EventType _event = EventType.OnSceneLoaded;
private void Awake()
{
TransitionManager.RegisterAsyncHandler(Event, RunAfterSceneLoaded, 0);
}
private async Task RunAfterSceneLoaded(CancellationToken token)
{
await Awaitable.WaitForSecondsAsync(0.25f, token);
}
private void OnDisable()
{
TransitionManager.UnregisterAsyncHandler(Event, RunAfterSceneLoaded);
}
}
}