Explore the code of the Visual Novel Director sample
Custom runtime
Graph Toolkit doesn't come with a runtime, so this sample demonstrates how to create a simple custom runtime.
This custom runtime can be found in the Runtime folder (Unity.GraphToolkit.Samples.VisualNovelDirector.asmdef assembly) and is divided into two parts:
- The runtime data model:
VisualNovelRuntimeGraph, which stores a list ofVisualNovelRuntimeGraphNodes. - The runtime execution engine:
VisualNovelDirectorandVisualNovelRuntimeGraphNodeExecutor, which execute the graph at runtime.
Runtime data model
The Graph Toolkit data model has references to the editor and can't be used directly at runtime. This means you have to implement your own data model.
In the Visual Novel Director example runtime, the VisualNovelRuntimeGraph is a [ScriptableObject] that stores a list of VisualNovelRuntimeGraphNodes using [SerializeReference].
This custom runtime model allows you:
- to store the minimal data required for the runtime without unnecessary authoring information, such as node positions.
- As an example, the
WaitForInputNodeis completely empty because it acts as a marker in theVisualNovelRuntimeGraphwithout any actual state.
- As an example, the
- to use any preferred loading and serialization mechanism (for example MonoBehaviours or ECS).
- As an example,
VisualNovelDirectorsample uses aMonoBehaviour(theVisualNovelDirector) which stores a direct reference to theVisualNovelRuntimeGraph. This lets Unity handle the loading of the asset at runtime.
- As an example,
- to create complex, modular runtime behaviour from simple authoring nodes (i.e. authoring and runtime doesn't have to be 1:1).
Runtime execution
Graph Toolkit doesn't have a graph-based runtime engine. This means you have to implement your own runtime, which can be node-based or not.
In this sample, the VisualNovelDirector MonoBehaviour executes nodes linearly. For simplicity, the sample excludes branching or choices. The VisualNovelDirector uses the loaded VisualNovelRuntimeGraph, public configuration fields, and asset references for all required UI elements (background image, dialogue text box, etc).
This custom runtime executes the graph from any defined entry-point. The VisualNovelDirector is a MonoBehaviour and uses Unity events. On Start, it instantiates VisualNovelRuntimeGraphNodeExecutors and calls them in sequence, using the data in the runtime nodes.
Sample runtime executors execute one or more specific node types. For example, SetBackgroundExecutor executes SetBackgroundNode.
For more details on how the example runtime works (accepting input, etc), review the code and its comments.
Debug view: visualizing runtime execution in the graph editor
While the visual novel plays in Play Mode, the sample uses the Unity.GraphToolkit.Editor.GraphVisualization API to drive a live debug view in the graph editor. As nodes execute, the active node animates, the wire currently being followed animates, and the ports of the executing nodes display the live values being applied to the scene. This makes the runtime flow inside the graph editor visible without breakpoints or extra logging.
All visualization code is guarded by #if UNITY_EDITOR so the runtime keeps building outside the editor.
The visualization Context
The visual novel runtime creates a single visualization Context at the start of execution and disposes of it when execution ends:
using var debugContext = Registry.CreateVisualizationContext(RuntimeGraph.ID);
The Context is the entry point to every visualization feature used in the sample:
debugContext.GetNodeReference(nodeID)anddebugContext.Motion.Play/Stopfor node animations.debugContext.GetPortReference(portID).SetPreview/ClearPreviewfor port previews.debugContext.GetWireReference(outputPortID, inputPortID)together withWireReferenceproperties (IsDashed,Opacity,WidthOverride) anddebugContext.Motion.Play/Stopfor wire styling and animations.debugContext.NodeCustomizationEnabled,debugContext.PortPreviewEnabledto toggle the entire debug view on or off.debugContext.ClearAllVisualization()to reset every visual override applied through the context.
To make the visualization features available to the runtime nodes, the importer stores the relevant port and wire IDs (InputPortIDs, OutputPortIDs, NextWireSourceId, NextWireDestinationId, and the option-specific port IDs on the TwoOptionRuntimeNode) on the runtime nodes during graph import. The runtime executor then resolves these IDs into the NodeReference, PortReference, and WireReference values used by the visualization API.
Node animations
While a SetDialogueRuntimeNode is being executed (its dialogue is being typed out), the sample plays a looping accent animation on the corresponding node in the graph:
debugContext.Motion.Play(debugContext.GetNodeReference(dialogueNode.ID), 1f);
When the player advances past the dialogue with a WaitForInputRuntimeNode, the animation is stopped:
debugContext.Motion.Stop(debugContext.GetNodeReference(prevDialogueNode.ID));
This is the same pattern you can reuse for any long-running operation on a node, to visually indicate that the node is currently active.
Port previews
Port previews display a short string next to a port in the graph editor. The sample uses them to mirror the runtime values being applied to the scene, so the graph editor doubles as a debugger.
SetBackgroundRuntimeNodeshows the name of the sprite currently assigned to the background image on its background input port.SetDialogueRuntimeNodeshows the actor name, the actor sprite name, the location (Left/Right), and a truncated preview of the dialogue text on the matching input ports.TwoOptionRuntimeNodeshows the two option labels on its output ports. Once the player picks one, the non-selected option is wrapped in<s>...</s>so it appears struck through, which leaves a visible trace of the branch that was not taken.
When the runtime reaches a WaitForInputRuntimeNode, the previews on every node that contributed to the current beat are cleared so the previews always reflect the current dialogue rather than accumulating stale state:
foreach (var nodeBeforeWaitForInput in nodesBeforeWaitForInput)
{
foreach (var inputPortID in nodeBeforeWaitForInput.InputPortIDs)
debugContext.GetPortReference(inputPortID).ClearPreview();
foreach (var outputPortID in nodeBeforeWaitForInput.OutputPortIDs)
debugContext.GetPortReference(outputPortID).ClearPreview();
}
Wire animations
To make the active execution path obvious at a glance, every execution wire is given a base style (dashed, semi-transparent) up front:
var wire = debugContext.GetWireReference(node.NextWireSourceId, node.NextWireDestinationId);
wire.Opacity = 0.5f;
wire.IsDashed = true;
As the runtime moves from one node to the next, the wire about to be followed is cleared back to its default style, given a thicker width, and animated:
nextWire.ClearCustomization();
nextWire.WidthOverride = 10f;
debugContext.Motion.Play(nextWire, 1f);
The previously animated wire keeps the thicker width but stops animating, which leaves a visible trail of the path that has already been traversed. To avoid clearing the animation that was just (re)started, the sample only performs this transition when the active wire actually changes from one iteration to the next; some runtime node types (such as SetDialogueRuntimeNode) don't update the active wire because the conceptual "current wire" stays the same across the dialogue and its trailing WaitForInputRuntimeNode.
For the branching TwoOptionRuntimeNode, the wire to animate is chosen based on the player's selection, using the Option1/Option2 port IDs stored on the runtime node.
Toolbar buttons
The sample adds two custom toolbar elements to the graph editor (under the Editor/Toolbar folder) using [GraphToolbarElement(...)]. Both are hidden outside of Play Mode because the debug view is only meaningful while the runtime is executing.
DebugViewToggle(DebugView.cs) – anEditorToolbarTogglethat turns the whole debug view on and off by settingContext.NodeCustomizationEnabledandContext.PortPreviewEnabledon the active visualization context retrieved withRegistry.GetActiveContext(graphID).ClearDebugDataButton(ClearDebugDataButton.cs) – anEditorToolbarButtonthat callsContext.ClearAllVisualization()on the active context, which removes every node animation, port preview, and wire customization in one call.
These two buttons together let you scope and reset the debug view while iterating on a graph in Play Mode.