This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

GS_GraphCanvas

A framework for building custom visual graph editors in O3DE — descriptors, nodes, variables, execution engines, and editor infrastructure.

GS_GraphCanvas is a higher-level framework built on top of O3DE’s GraphCanvas and GraphModel gems. It lets you create fully-featured visual graph editors — dialogue trees, audio event graphs, state machines, or any domain-specific tool — without touching the complex boilerplate of the underlying engine systems. You provide a descriptor, a context, and your nodes; the framework gives you a complete editor.

 

Contents


Architecture

GS_GraphCanvas sits between your gem and the engine’s graph infrastructure. The layer model:

[Your Gem]              (gs_cinematics, gs_audio, gs_unit, or your own)
   Provides: GraphSystemDescriptor, GraphContext subclass, Node definitions
        |
[gs_graphcanvas]        (STATIC LIB  framework layer)
   Provides: MainWindow, save/load, undo/redo, variables, inspector,
             node registry, palette, execution engines, multi-doc, pages
        |
[O3DE GraphModel]       (engine  data model)
   Provides: Graph, Node, Slot, Connection, DataType, GraphContext
        |
[O3DE GraphCanvas]      (engine  visual rendering)
   Provides: QGraphicsScene rendering, node visuals, slot widgets,
             connection rendering, selection, copy/paste, bookmarks

Downstream gems inherit from the framework’s MainWindow and extend it without fighting the base infrastructure. The base MainWindow is a fully functional single-graph editor. Downstream editors add domain-specific behavior (database containers, sequence sidebars, layer systems) on top.

Build Model

gs_graphcanvas is a static library. Each downstream gem links against it and gets its own compiled copy. This means reflected types can be registered multiple times from different DLLs — all Reflect() methods must include FindClassData guards to prevent duplicate registration crashes.


Descriptor & Topology

The GraphSystemDescriptor is the identity card for a graph editor type. It tells the framework what kind of editor to build — its name, file extension, topology, and enabled features.

The topology is the most important decision. It determines connection style, execution model, and slot types:

TopologyUse CaseConnectionsExecution Model
FlowGraphSequential processes, dialogue treesDirectional FlowIn/FlowOutStep/wait/resume
DataFlowGraphSignal processing, audio, shadersDirectional data slotsTopological dirty-propagation
StateMachineGraphState machines, HFSM, behavior treesMultidirectional perimeter arrowsTick-based state transitions

See Descriptor & Topology for full field reference and the GraphContext data type system.


Nodes

All gs_graphcanvas nodes inherit from BaseNode. The framework provides macros for slot registration, auto-registration into the node palette, and rapid node creation for simple cases.

Three tiers of complexity:

  • Macro node (~5 lines) — DEFINE_SIMPLE_NODE_WITH_SLOTS generates the entire class
  • Helper macro node (~15 lines) — Use slot macros with a custom class
  • Full custom node (30+ lines) — Full control over slots, properties, and behavior

See Nodes for the node system, slot macros, and auto-registration.


Editor Window

The MainWindow class provides a complete editor out of the box:

  • Multi-document tabs with dirty tracking and close-with-save prompts
  • File menu (New, Open, Save, Save As, Open Recent)
  • Edit menu (Undo/Redo, Cut/Copy/Paste)
  • Node palette with drag-drop creation
  • Inspector panel for node and connection properties
  • Variable panel (when enabled)
  • Full-window page system for non-graph pages (e.g., Performers, Settings)
  • In-memory graph operations for container/database editors

Downstream editors subclass MainWindow and override virtual hooks to add domain-specific behavior.

See Editor Window for virtual hooks, page system, and container editor patterns.


Variables

When variablesEnabled = true in the descriptor, the framework provides a complete variable system:

  • Variable Panel dock widget for declaring typed variables
  • Get/Set Variable nodes automatically included in the palette
  • Convert to Reference — right-click any input slot to bind it to a variable
  • Drag to Canvas — drag a variable from the panel onto the canvas to create a Get or Set node
  • Rename propagation — renaming a variable updates all referencing nodes

See Variables for the variable system and runtime bindings.


Execution Engines

gs_graphcanvas includes three execution engines matching the three topologies:

EngineTopologyPattern
FlowGraphEvaluatorFlowGraphStep/wait/resume along FlowIn/FlowOut connections
DataFlowGraphEvaluatorDataFlowGraphTopological ordering, dirty-propagation, re-evaluate only changed nodes
StateMachineEvaluatorStateMachineGraphTick-based with OnEnter/OnTick/OnExit lifecycle and transition conditions

All engines share GraphExecutionContext for variable storage and value resolution. GraphInstance provides independent runtime copies of a graph for safe concurrent execution.

See Execution Engines for engine details and the runtime pipeline.


Inspector & Save/Load

Inspector Panel

The inspector auto-generates UI from node EditContext reflection. Select a node to edit its properties; in state machine graphs, click a transition line to edit its priority and conditions.

Save/Load

Graphs are saved as GraphDocumentAsset — a plain struct (not AZ::Data::AssetData) containing the graph as a binary byte buffer wrapped in XML. This avoids ObjectStream and AssetManager interaction issues.

Undo/Redo

Snapshot-based: each undo point captures the entire graph state as a byte buffer. Per-graph undo stacks. Ctrl+Z / Ctrl+Y.


Installation

In your gem’s editor module CMakeLists.txt, add gs_graphcanvas as a build dependency:

ly_add_target(
    NAME GS_MyTool.Editor.Static STATIC
    ...
    BUILD_DEPENDENCIES
        PRIVATE
            Gem::GraphCanvas.Editor.Static
            Gem::GraphModel.Editor.Static
            Gem::GS_GraphCanvas.Editor.Static
            ...
)

See Also

1 - Descriptor & Topology

GraphSystemDescriptor fields, topology selection, and GraphContext data type registration.

The GraphSystemDescriptor tells gs_graphcanvas what kind of editor to build. It is a plain struct that you fill out and pass to the MainWindow constructor.

 

GraphSystemDescriptor Fields

Identity

FieldTypeDescription
systemIdconst char*Unique ID string (e.g., "dialogue", "audiograph", "unitaction"). Used for node filtering.
systemNameconst char*Display name shown in the editor title bar.
fileExtensionconst char*File extension for graph files (e.g., ".dialogue", ".audiograph").
mimeTypeconst char*MIME type for drag-drop from the node palette.
saveIdentifierconst char*QSettings key for persisting window layout state.
editorIdGraphCanvas::EditorIdUnique editor ID for the GraphCanvas system.

Topology

FieldTypeDefaultDescription
topologyGraphTopologyFlowGraphDetermines connection style, slot types, and execution model.

Topology options:

  • FlowGraph — Directional flow slots (FlowIn/FlowOut), sequential step/wait/resume execution. Best for: dialogue trees, scripting, sequential processes.
  • DataFlowGraph — No flow slots, data-only connections with dirty-propagation evaluation. Best for: audio, shaders, procedural generation, signal processing.
  • StateMachineGraph — Multidirectional perimeter connections with tick-based state transitions. Best for: HFSM, behavior trees, character controllers.

See Execution Engines for detailed Topology Flow reference and internal API for runtime execution.

 

Feature Flags

FieldTypeDefaultDescription
variablesEnabledboolfalseEnables the variable panel, Get/Set variable nodes, and reference bindings.
allowConnectionLoopbackboolfalseWhether cycles are allowed in the graph.

 

State Machine Features

These fields are only relevant when topology == StateMachineGraph:

FieldTypeDefaultDescription
hierarchicalStatesboolfalseEnable compound/nested states.
parallelLayersboolfalseEnable parallel layer evaluation.
transitionConditionsboolfalseEnable polymorphic transition conditions on connections.

 

UI

FieldTypeDescription
autoSpawnNodeTypesAZStd::vector<AZ::TypeId>Node types automatically created in every new graph (e.g., entry nodes).
stylesheetPathconst char*Optional path to a custom QSS stylesheet for node styling.
windowTitleconst char*Window title text.
menuCategoryconst char*Menu path in the O3DE editor (e.g., "GS Tools").

GraphContext

Each graph system needs a GraphContext subclass that registers the data types available in that system. The framework provides a set of built-in CommonDataTypes available to all systems:

TypeEnumC++ Type
BoolGS_Boolbool
IntGS_Intint
FloatGS_Floatfloat
StringGS_TextAZStd::string
Vector2GS_Vec2AZ::Vector2
Vector3GS_Vec3AZ::Vector3
ColorGS_ColorAZ::Color
EntityIdGS_EntityIdAZ::EntityId

If your tool only uses these built-in types, your GraphContext subclass may be trivial. Domain-specific types (e.g., AudioRoute for audio graphs) are registered by calling context->RegisterDataType(...) in your subclass.


Topology Decision Matrix

ConsiderationFlowGraphDataFlowGraphStateMachineGraph
Flow slots (FlowIn/FlowOut)YesNoNo
Connection directionLeft-to-rightLeft-to-rightMultidirectional (perimeter)
Execution modelStep/wait/resumeTopological dirty-propagationTick-based state transitions
Node interfaceIExecutableNodeIDataFlowNodeIStateMachineNode
EvaluatorFlowGraphEvaluatorDataFlowGraphEvaluatorStateMachineEvaluator
Cycles allowedOptionalNo (DAG required)Yes (inherent)
Transition conditionsN/AN/ATransitionCondition base class
Example downstreamDialogue EditorAudio Event GraphUnit Action Graph

2 - Nodes

BaseNode, slot macros, auto-registration, rapid node creation, and built-in node types.

All gs_graphcanvas nodes inherit from BaseNode, which extends GraphModel::Node. BaseNode provides slot registration helpers, flow slot support, inspector property reflection, and transition descriptor management for state machines.

 

Slot Registration

Slots are registered in the RegisterSlots() override using helper macros:

GS_INPUT_SLOT_TYPED("slot_name", "Display Name", DataTypeEnum, DefaultValue)
GS_OUTPUT_SLOT_TYPED("slot_name", "Display Name", DataTypeEnum)
GS_INPUT_SLOT_CONNECTION("slot_name", "Display Name")        // Connection-only, no inline editor
GS_MULTI_INPUT_SLOT_CONNECTION("slot_name", "Display Name")  // Multiple connections allowed

All input slots use editableOnNode=true by default, providing inline value editing directly on the node in the graph canvas.

For flow-based graphs, call RegisterFlowSlots() to add FlowIn and FlowOut slots. Override HasFlowIn() or HasFlowOut() to control which flow slots appear (e.g., a Start node has no FlowIn).

For state machine graphs, call RegisterTransitionSlots() to add transition_in and transition_out perimeter slots. Override HasTransitionIn() / HasTransitionOut() to control slot presence.


Auto-Registration

Nodes self-register into the node palette via macros:

// Register for ALL graph systems:
GS_AUTO_REGISTER_NODE(MyNode)

// Register for a specific system only:
GS_AUTO_REGISTER_NODE_FOR(MyNode, "dialogue")

Each registered node includes a category string (from CATEGORY constant), display name (from TITLE), and a creation lambda. The NodeRegistry singleton manages all registrations and handles MIME event reflection for drag-drop.


Rapid Node Creation

For simple nodes with no custom logic, a single macro generates the entire class:

DEFINE_SIMPLE_NODE_WITH_SLOTS(
    MyNode,
    "{UNIQUE-UUID}",
    "My Node",
    "My Category",
    {INPUT_SLOT("input", GS_Text, "Input text")},
    {OUTPUT_SLOT("output", GS_Text, "Output text")}
)

This generates the class with TITLE, CATEGORY, Reflect(), RegisterSlots(), and the constructor.

Three Tiers of Node Complexity

TierLinesWhen to Use
Macro node~5Simple pass-through or data-holding nodes
Helper macro node~15Nodes with custom properties but standard slot patterns
Full custom node30+Nodes with custom execution logic, dynamic slots, or complex behavior

Reflect Pattern

Every node’s Reflect() method must include a FindClassData guard because gs_graphcanvas is a static library and types can be registered from multiple DLLs:

void MyNode::Reflect(AZ::ReflectContext* context)
{
    if (auto* sc = azrtti_cast<AZ::SerializeContext*>(context))
    {
        if (sc->FindClassData(azrtti_typeid<MyNode>())) { return; }  // MANDATORY guard
        sc->Class<MyNode, GS_GraphCanvas::BaseNode>()
            ->Version(1)
            ->Field("MyProperty", &MyNode::m_myProperty)
            ;
    }
    if (auto* ec = azrtti_cast<AZ::EditContext*>(context))
    {
        ec->Class<MyNode>("My Node", "Description")
            ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
                ->Attribute(AZ::Edit::Attributes::AutoExpand, true)
            ->DataElement(AZ::Edit::UIHandlers::Default, &MyNode::m_myProperty,
                "My Property", "Tooltip")
            ;
    }
}

Built-in Nodes

gs_graphcanvas includes several utility nodes available to all graph systems:

NodeCategoryPurpose
IfNodeRoutingRoutes data based on a boolean condition
SwitchNodeRoutingRoutes data based on a selector value
GetVariableNodeVariablesOutputs the value of a declared variable
SetVariableNodeVariablesWrites a value to a declared variable

Variable nodes are automatically included in the palette when variablesEnabled = true in the descriptor.

3 - Editor Window

MainWindow features, virtual hooks, page system, and container editor patterns.

The MainWindow class provides a complete graph editor out of the box. Downstream editors subclass it and override virtual hooks to add domain-specific behavior.

 

Out-of-the-Box Features

Every MainWindow instance includes:

  • Multi-document tabs — Each graph opens in its own tab with dirty tracking (asterisk * in tab title) and close-with-save prompts
  • File menu — New, Open, Save, Save As, Open Recent (tracks last 10 files)
  • Edit menu — Undo/Redo (Ctrl+Z / Ctrl+Y), Cut/Copy/Paste
  • Node palette — Searchable list of available nodes with drag-drop creation
  • Inspector panel — Auto-generated property editor from node EditContext reflection
  • Variable panel — Variable declarations, type selection, and default value editing (when enabled)

Virtual Hooks

MainWindow provides virtual methods for downstream customization:

File Menu Overrides

virtual void AddFileNewAction();
virtual void AddFileOpenAction();
virtual void AddFileSaveAction();
virtual void AddFileSaveAsAction();

Override these to replace per-file behavior with container-level behavior (e.g., the Dialogue Editor overrides these to save the entire database instead of individual graphs).

Lifecycle Hooks

HookCalled When
OnEditorOpened()A graph tab is opened
OnEditorClosing()A graph tab is being closed
OnActiveGraphChanged()The user switches between graph tabs
OnNewGraphRequested(QString& outTitle)Before a new graph tab is created. Return false to cancel.
OnNodeDoubleClicked(BaseNode* node)A node is double-clicked. Used to open sub-graph editors.

Full-Window Page System

The page system lets you add non-graph pages alongside the graph editor in the same window:

AddFullWindowPage("Performers", m_performersWidget);
AddFullWindowPage("System", m_systemWidget);
SetGraphPageTitle("Sequences");  // Rename the default graph tab
SetActivePage(index);            // Switch between pages

The base class creates a tab bar on the first call. When switching to a non-graph page, the graph canvas and all docks are hidden; the page widget fills the entire window. Dock layout is preserved between switches.

Examples in practice:

  • Dialogue Editor: Sequences (graph) + Performers + System
  • Unit Action Editor: Layers (graph) + Layer Details

Container Editor Pattern

For editors where multiple graphs live inside a single file (like dialogue sequences in a database, or state machine layers in a unit action asset), MainWindow provides in-memory graph operations:

Opening Sub-Graphs

OpenGraphFromAsset(GraphDocumentAsset& asset, const QString& title, AZ::SerializeContext* sc);

Deserializes a graph from an asset’s byte buffer and opens it as a new tab.

Capturing Sub-Graphs

CaptureGraphToAsset(GraphModel::GraphId graphId, GraphDocumentAsset& outAsset, AZ::SerializeContext* sc);

Serializes an open graph back into the asset’s byte buffer. Used before saving the container.

Creating Empty Graphs

OpenNewGraphTab(const QString& title);

Opens a blank graph tab for a new sub-object.

Clearing Dirty State

ClearAllDocumentDirty();

Clears dirty markers on all open tabs. Called after a container-level save so that individual tab dirty states don’t linger.

Typical Container Save Flow

  1. Iterate all open graph tabs
  2. Call CaptureGraphToAsset() for each to write graphs back to the container
  3. Serialize the container to disk
  4. Call ClearAllDocumentDirty() to clear all tab markers

4 - Variables

Variable declarations, Get/Set nodes, reference bindings, and drag-to-canvas interactions.

When variablesEnabled = true in the GraphSystemDescriptor, the framework provides a complete variable system for storing and reading named values within a graph.

 

Variable Panel

The Variable Panel is a dock widget with a table listing all declared variables. Each variable has:

  • Name — User-defined identifier
  • Data Type — Selected from a dropdown of registered types (Bool, Int, Float, String, etc.)
  • Default Value — Type-specific editor (checkbox for Bool, spinner for numeric, text field for String, multi-field for vectors, color picker for Color)

Variables are per-graph. Add or remove variables using the panel controls. Undo/redo integration is automatic.


Get / Set Variable Nodes

When variables are enabled, GetVariableNode and SetVariableNode are automatically included in the node palette under the “Variables” category.

  • GetVariableNode — Outputs the current value of a named variable
  • SetVariableNode — Writes a value to a named variable

Both nodes reference a variable by name. If the variable is renamed, all referencing Get/Set nodes update automatically.


Convert to Reference

Right-click any typed input slot on a node to convert it to a variable reference. A dropdown appears listing all declared variables that match the slot’s data type. Once bound:

  • The slot displays the variable name instead of an inline value
  • The slot reads the variable’s value at evaluation time
  • Right-click again and select “Convert to Value” to unbind

Reference bindings are persisted as VariableBinding structs (NodeId + SlotName + VariableId) and restored automatically on load.


Drag to Canvas

Drag a variable from the Variable Panel directly onto the graph canvas. A popup menu asks whether to create a Get or Set node for that variable. The node is automatically configured with the variable name and data type.


Runtime Variable Access

At runtime, variables live in the GraphExecutionContext blackboard. The execution context provides:

  • Read/write access by variable name
  • InitializeBindings() resolves VariableId references to names
  • Variable changes can trigger re-evaluation (in DataFlowGraph, MarkDirty(variableName) propagates to dependent nodes)

Downstream gems expose variable control through their own APIs. For example, the Audio Event Graph provides SetAudioGraphVariable(instanceId, name, value) to control graph variables from gameplay code.

5 - Execution Engines

FlowGraphEvaluator, DataFlowGraphEvaluator, StateMachineEvaluator, GraphExecutionContext, and GraphInstance.

gs_graphcanvas provides three execution engines — one for each topology. All share a common GraphExecutionContext for variable storage and value resolution, and GraphInstance for creating independent runtime copies.

 

FlowGraphEvaluator

Flow Evaluator in Graph Tool

Topology: FlowGraph

Step/wait/resume execution model for sequential graphs. The evaluator follows FlowIn/FlowOut connections from node to node.

Nodes implement the IExecutableNode interface:

FlowResult Execute(GraphExecutionContext& context);

FlowResult controls what happens after a node executes:

ResultBehavior
ContinueImmediately proceed to the next connected node
WaitPause execution until Resume() is called externally
StopEnd graph execution

Used by: Dialogue Editor — text nodes return Wait while dialogue is displayed, then Resume() advances to the next node.


DataFlowGraphEvaluator

Data Flow Evaluator in Graph Tool

Topology: DataFlowGraph

Dirty-propagation evaluator for continuously-evaluated graphs. On initialization, builds a topological ordering of all nodes via Kahn’s algorithm.

Nodes implement the IDataFlowNode interface:

void Process(GraphExecutionContext& context);

Evaluation Pipeline

  1. Initialize — Build topological order from the graph’s connection structure
  2. Track dependencies — Map which nodes depend on which variables
  3. Mark dirtyMarkDirty(variableName) marks dependent nodes dirty, then transitively marks all downstream nodes
  4. Evaluate — Iterate nodes in topological order, re-running only dirty nodes

The explicit dirty marking (not automatic) gives downstream code batching control — set multiple variables, then call Evaluate() once.

Empty Any Pattern

When a data-flow node has no valid output (gated off, no valid input), it must output AZStd::any{} (truly empty), never a default-constructed value:

if (validInput)
    context.SetOutputValue(this, "result", computedValue);
else
    context.SetOutputValueAny(this, "result", AZStd::any{});

This is critical because multi-input resolution returns the first non-empty AZStd::any. A default-constructed struct inside an any is non-empty and will mask valid values from other connections.

Used by: Audio Event Graph — entry gate nodes, filter nodes, and effect nodes all follow this pattern.


StateMachineEvaluator

State Transition Flow Evaluator in Graph Tool

Topology: StateMachineGraph

Tick-based evaluator for hierarchical finite state machines. Maintains the current active state and evaluates transitions each tick.

Nodes implement the IStateMachineNode interface:

void OnEnter(GraphExecutionContext& context);
void OnTick(GraphExecutionContext& context, float deltaTime);
void OnExit(GraphExecutionContext& context);

Tick Cycle

  1. Increment __stateTime auto-variable
  2. Gather all outgoing transitions from the current state
  3. Evaluate conditions on each transition
  4. Fire the highest-priority valid transition (OnExit on source, OnEnter on destination)
  5. Call OnTick on the active state

Transition Conditions

Each connection carries a TransitionDescriptor with a priority and a list of polymorphic TransitionCondition objects. Built-in conditions:

ConditionDescription
VariableCompareConditionCompares a variable to a value using a comparison operator
TimeElapsedConditionFires after N seconds in the current state (reads __stateTime)
VariableTrueConditionFires when a boolean variable is true

Custom conditions are created by subclassing TransitionCondition, reflecting it, and it auto-discovers via SerializeContext::EnumerateDerived().

ParallelLayerEvaluator

Wraps multiple StateMachineEvaluator instances (one per layer) with shared context synchronization. Each layer evaluates independently but shares the same variable context for cross-layer reads.

Used by: Unit Action Graph — parallel Movement, Action, and Rotation layers.


GraphExecutionContext

Shared context for all evaluator types. Provides:

  • Input value resolutionGetInputValue<T>() and GetInputValueAny() with multi-input resolution (returns first non-empty value)
  • Output valuesSetOutputValue() and SetOutputValueAny()
  • Blackboard — Variable storage by name
  • Binding initializationInitializeBindings() resolves VariableId references to variable names

GraphInstance

An independent runtime copy of a graph. GraphInstance::CreateFromAsset() deserializes a fresh graph from a GraphDocumentAsset’s byte buffer, initializes variables and bindings. Each instance is fully independent — downstream gems can pool instances for high-frequency execution paths.