How to Debug Flow Graphs with Breakpoints and Step-by-Step Execution in Ceres
Use the editor's Debug toolbar for visual breakpoint debugging, or implement FlowGraphTracker for programmatic control over node execution, logging, and profiling.
Ceres provides a robust visual scripting system called Flow graphs that requires sophisticated debugging capabilities. Whether you prefer interactive debugging in the Unity Editor or automated analysis at runtime, you can debug Flow graphs with breakpoints and step-by-step execution using two complementary approaches that share the same underlying execution engine.
Editor-Based Debugging
The Flow editor provides a visual debugging interface accessible directly from the toolbar. This method requires no code and allows you to inspect node execution in real-time.
Enabling Debug Mode
To begin debugging, click the Debug button in the upper-right toolbar of the Flow editor. This activates the debugging session and pauses execution after each node evaluation, allowing you to control the flow manually.
Stepping Through Execution
Once Debug Mode is active, use the Next Frame (►) button to execute the current node and pause before the next one. This step-by-step execution lets you observe how data transforms through each node in the graph.
Setting Breakpoints
For targeted debugging, right-click any node and select Add Breakpoint. After setting breakpoints, use the Next Breakpoint (⏭) button on the toolbar to run the graph until it hits the first breakpoint, then continue stepping from that point.
Inspecting Port Values
While the graph is paused at a breakpoint, hover over any input port to view a tooltip displaying the current value. This allows you to verify data integrity at specific execution points without modifying the graph.
Hot-Reload Support
Enable Hot Reload in the toolbar to edit FlowGraphObject assets while the game is playing. Changes apply instantly without restarting the scene, enabling rapid iteration during debugging sessions.
The complete editor debugging guide is available in the repository documentation at Documentation~/docs/flow_debugging.md.
Runtime Debugging with FlowGraphTracker
When you need programmatic control, automated logging, or conditional breakpoints, implement the FlowGraphTracker API. This approach is defined in Runtime/Flow/Models/FlowGraphTracker.cs and provides hooks into every node execution.
Basic Tracker Pattern
The FlowGraphTracker uses an auto-scope pattern to ensure proper cleanup. Wrap your graph execution in a using statement with tracker.Auto() to automatically handle tracker lifecycle:
using Ceres.Graph.Flow;
using Cysharp.Threading.Tasks;
public async UniTask RunWithTracker(FlowGraph graph)
{
// Create a concrete tracker (e.g., the built-in dependency logger)
var tracker = new FlowGraphDependencyTracker(graph);
// Auto-scope guarantees Dispose() runs even on exceptions
using (tracker.Auto())
{
// Trigger the event you want to observe
await graph.ExecuteEventAsync(context: null, eventName: "Start", evt: null);
} // <- tracker.Dispose() called here; prints log summary
}
The FlowGraphDependencyTracker logs each node entry/exit and identifies missing dependencies, helping you trace execution flow programmatically.
Custom Breakpoint Tracker
You can halt Unity's editor by implementing a custom tracker that calls Debug.Break() when specific conditions are met:
using Ceres.Graph.Flow;
using Cysharp.Threading.Tasks;
using UnityEngine;
public class ConditionalBreakpointTracker : FlowGraphTracker
{
private readonly Func<ExecutableNode, bool> _condition;
public ConditionalBreakpointTracker(Func<ExecutableNode, bool> condition)
{
_condition = condition;
}
public override UniTask EnterNode(ExecutableNode node)
{
if (_condition(node))
{
Debug.Break(); // Pops up Unity's debugger pause
}
return UniTask.CompletedTask;
}
}
// Usage:
using (new ConditionalBreakpointTracker(n => n.GetTypeName().Contains("Log")).Auto())
{
await graph.ExecuteEventAsync(null, "Start", null);
}
This approach allows you to set conditional breakpoints based on node type, GUID, or custom metadata without modifying the visual graph.
Performance Profiling
Build a lightweight profiler by tracking timing data in EnterNode and ExitNode:
public class PerfTracker : FlowGraphTracker
{
private readonly Dictionary<string, NodeProfile> _profiles = new();
private readonly Stack<NodeProfile> _stack = new();
public override UniTask EnterNode(ExecutableNode node)
{
if (!_profiles.TryGetValue(node.Guid, out var p))
{
p = new NodeProfile { NodeName = node.GetTypeName(), NodeGuid = node.Guid };
_profiles[node.Guid] = p;
}
p.Stopwatch.Restart();
_stack.Push(p);
return UniTask.CompletedTask;
}
public override UniTask ExitNode(ExecutableNode node)
{
var p = _stack.Pop();
p.Stopwatch.Stop();
p.TotalTicks += p.Stopwatch.ElapsedTicks;
return UniTask.CompletedTask;
}
public override void Dispose()
{
foreach (var kv in _profiles)
{
var avgMs = (kv.Value.TotalTicks / (double)Stopwatch.Frequency) /
kv.Value.ExecutionCount * 1000;
Debug.Log($"{kv.Value.NodeName}: {kv.Value.ExecutionCount} runs, Avg {avgMs:F3} ms");
}
base.Dispose();
}
}
This profiler tracks average execution time per node, helping you identify bottlenecks in complex Flow graphs.
How Execution Tracking Works
The debugging system relies on a unified execution model defined in Runtime/Flow/FlowGraphObject.cs and Runtime/Flow/Models/FlowGraphTracker.cs.
When a graph executes via FlowGraphObjectBase, the runtime queries FlowGraphTracker.GetActiveTracker() to retrieve the current tracker. If no tracker is active, a no-op Empty tracker is returned, ensuring zero overhead in production builds.
The active tracker receives EnterNode and ExitNode callbacks for every ExecutableNode in the graph. This design allows the editor's visual debugger to function as a specialized tracker that pauses execution and renders port values, while custom code implementations can perform logging, profiling, or conditional breakpoints using the same callbacks.
Summary
- Editor debugging provides visual breakpoints, step-by-step execution, and port inspection through the Flow editor toolbar without writing code.
- Runtime debugging uses the
FlowGraphTrackerAPI inRuntime/Flow/Models/FlowGraphTracker.csto implement custom breakpoints, logging, and profiling. - Auto-scope pattern with
tracker.Auto()ensures proper cleanup and exception safety when using trackers programmatically. - Unified execution model means both editor and code debugging use the same
EnterNode/ExitNodecallbacks, ensuring consistent state representation.
Frequently Asked Questions
How do I set a breakpoint on a specific node in the Flow editor?
Right-click the target node in the Flow editor and select Add Breakpoint from the context menu. Once set, use the Next Breakpoint (⏭) button in the toolbar to execute the graph until it reaches that node, or step through manually using Next Frame (►) to observe execution flow.
Can I debug Flow graphs at runtime without using the editor?
Yes. Implement the FlowGraphTracker class from Runtime/Flow/Models/FlowGraphTracker.cs and override the EnterNode and ExitNode methods to add logging, profiling, or conditional breakpoints. Wrap your graph execution in a using statement with tracker.Auto() to ensure proper lifecycle management and automatic cleanup.
What is the difference between FlowGraphDependencyTracker and a custom FlowGraphTracker?
FlowGraphDependencyTracker is a built-in implementation that logs node execution entry/exit points and identifies missing dependencies between nodes. A custom FlowGraphTracker allows you to implement specialized behavior such as performance profiling, conditional breakpoints using Debug.Break(), or integration with external logging systems by overriding the virtual EnterNode and ExitNode methods.
Does enabling debug mode affect game performance?
The editor's debug mode pauses execution after each node, which inherently slows down execution to human-interactive speeds. For runtime debugging, the FlowGraphTracker system uses a no-op Empty tracker when no tracker is active, ensuring zero overhead in production builds. Only when a tracker is explicitly instantiated and registered via GetActiveTracker() does the runtime incur the cost of the EnterNode/ExitNode callbacks.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →