Using GraphCanvas
⚠️ Heads Up!
This is incomplete and a work in progress. Think of it as a "getting started" collection of notes rather than proper docs
Credits
- Shauna: For her notes to get started on Graph Canvas.
Getting started
You'll need to create a gem with a template that gives you a QWidget in your
editor tools lke CppToolGem.
We need to add a couple of deps to your CMakeLists.txt in the Code directory
of your gem.
This wont list out everything so dont copy and paste this, just add what isn't there.
ly_add_target(
NAME ${gem_name}.Editor.Private.Object STATIC
#...
BUILD_DEPENDENCIES
PRIVATE
Gem::GraphModel.Editor.Static
PUBLIC
Gem::GraphCanvasWidgets
)
ly_add_target(
NAME ${gem_name}.Editor GEM_MODULE
#...
BUILD_DEPENDENCIES
PUBLIC
Gem::GraphCanvas.Editor
PRIVATE
Gem::GraphModel.Editor.Static
RUNTIME_DEPENDENCIES
Gem::GraphCanvas.Editor
Gem::GraphModel.Editor
)
Header file
A couple of things you need in your widget header
#if !defined(Q_MOC_RUN)
#include <GraphModel/Integration/EditorMainWindow.h>
#endif
namespace Ui
{
class GraphCanvasEditorDockWidget;
}
namespace MyGem
{
inline const GraphCanvas::EditorId THINGCANVAS_EDITOR_ID = AZ_CRC_CE("ThingCanvas");
class MyGemWidget : public QWidget
{
Q_OBJECT
//....
private:
GraphCanvas::StyleManager m_styleManager;
GraphCanvas::EditorId m_editorId;
GraphCanvas::GraphId m_graphId;
AZ::Entity* m_sceneEntity = nullptr;
AZStd::unique_ptr<GraphCanvas::GraphCanvasGraphicsView> m_graphicsView;
}
}
Source file
#include "AzQtComponents/Components/StyleManager.h"
#include "GraphCanvas/GraphCanvasBus.h"
#include "GraphCanvas/Components/Nodes/NodeBus.h"
#include "GraphCanvas/Components/Nodes/NodeTitleBus.h"
#include "GraphCanvas/Components/Nodes/Comment/CommentBus.h"
#include "GraphCanvas/Components/Slots/Extender/ExtenderSlotBus.h"
#include "GraphCanvas/Widgets/GraphCanvasGraphicsView/GraphCanvasGraphicsView.h"
#include "UI/PropertyEditor/EntityIdQLineEdit.h"
namespace MyGem
{
MyGemWidget::MyGemWidget(QWidget* parent)
: QWidget(parent)
, m_graphicsView(new GraphCanvas::GraphCanvasGraphicsView())
, m_editorId(THINGCANVAS_EDITOR_ID)
, m_styleManager(THINGCANVAS_EDITOR_ID, "MaterialCanvas/StyleSheet/materialcanvas_style.json")
{
m_graphicsView->SetEditorId(m_editorId);
GraphCanvas::GraphCanvasRequestBus::BroadcastResult(m_sceneEntity, &GraphCanvas::GraphCanvasRequests::CreateSceneAndActivate);
m_graphId = m_sceneEntity->GetId();
GraphCanvas::SceneRequestBus::Event(m_graphId, &GraphCanvas::SceneRequests::SetEditorId, m_editorId);
m_graphicsView->SetScene(m_graphId);
AzQtComponents::StyleManager::setStyleSheet(this, QStringLiteral("style:Editor.qss"));
AzQtComponents::StyleManager::setStyleSheet(this, QStringLiteral(":/GraphView/GraphView.qss"));
QVBoxLayout* mainLayout = new QVBoxLayout(this);
mainLayout->addWidget(m_graphicsView.get());
setLayout(mainLayout);
}
}
Now you should have a basic grid that you can zoom and move around in 🙂
Nodes
Let's add a node to your graph, this is actually pretty straightforward.
//Call this in your constructor after you've set your layout.
void CreateNode()
{
const char* nodeStyle = "";
AZ::Entity* nodeEntity = nullptr;
GraphCanvas::GraphCanvasRequestBus::BroadcastResult(nodeEntity, &GraphCanvas::GraphCanvasRequests::CreateGeneralNodeAndActivate, nodeStyle);
const AZ::EntityId nodeId = nodeEntity->GetId();
GraphCanvas::NodeTitleRequestBus::Event(nodeId, &GraphCanvas::NodeTitleRequests::SetTitle, "My Node");
GraphCanvas::NodeTitleRequestBus::Event(nodeId, &GraphCanvas::NodeTitleRequests::SetSubTitle, "Does stuff");
//Now lets give it a slot!
{
AZ::Entity* slotEntity = nullptr;
GraphCanvas::ExtenderSlotConfiguration extenderConfig;
extenderConfig.m_extenderId = AZ_CRC_CE("TLS_SID"); //?
extenderConfig.m_name = "Exec";
extenderConfig.m_tooltip = "Exec line";
extenderConfig.m_connectionType = GraphCanvas::ConnectionType::CT_Input;
extenderConfig.m_slotGroup = GraphCanvas::SlotGroups::ExecutionGroup;
GraphCanvas::GraphCanvasRequestBus::BroadcastResult(slotEntity, &GraphCanvas::GraphCanvasRequests::CreateSlot, nodeId, extenderConfig);
slotEntity->Init();
slotEntity->Activate();
GraphCanvas::NodeRequestBus::Event(nodeId, &GraphCanvas::NodeRequests::AddSlot, slotEntity->GetId());
}
//Add the node to our graph
GraphCanvas::SceneRequestBus::Event(m_graphId, &GraphCanvas::SceneRequests::AddNode, nodeId, AZ::Vector2{}, false);
}
Overview
Now that you have the code, let's go over it.
We don't have anything handling the GraphCanvas::GraphModelRequestBus so connections won't be possible and same with
things like undo/redo, the only class that implements this is the higher level stuff
in GraphModelIntegration::GraphController or you can implement the GraphModelRequestBus yourself.
Notes
m_styleManagerSeems to be a helper class that helps with loading JSON style files with hot reload support, it talks over EBus to apply the style using your editor id.- GraphCanvas uses the entity system of the engine a lot, in fact you can iterate the components on your entities even connections are entities.
- If you use the higher level API that uses this it pretty much just manages these entities, creates the slots automatically and handles asset stuff.
Higher level Sorcery!
If you want the ease of use of the higher-level system without the asset editing part, then here's some code for ya.
If you're using the
CreateNodefunction from the Getting Started section, you'll need to stop using it.
You'll need some new variables in your header
#include <GraphModel/Integration/EditorMainWindow.h>
#include "GraphModel/Integration/GraphController.h"
enum MyDataTypeEnum : GraphModel::DataType::Enum
{
MyDT_Float,
};
class MyGraphContext : public GraphModel::GraphContext
{
public:
MyGraphContext();
}
class MyGemWidget{
///....
private:
GraphModelIntegration::GraphController* m_controller;
GraphModel::GraphPtr m_graph;
AZStd::shared_ptr<MyGraphContext> m_graphCtx;
}
And here's how you use it
#include "AzQtComponents/Components/StyleManager.h"
#include "GraphCanvas/GraphCanvasBus.h"
#include "GraphCanvas/Components/Nodes/NodeBus.h"
#include "GraphCanvas/Components/Nodes/NodeTitleBus.h"
#include "GraphCanvas/Components/Nodes/Comment/CommentBus.h"
#include "GraphCanvas/Components/Slots/Extender/ExtenderSlotBus.h"
#include "GraphCanvas/Widgets/GraphCanvasGraphicsView/GraphCanvasGraphicsView.h"
#include "UI/PropertyEditor/EntityIdQLineEdit.h"
namespace MyGem
{
MyGraphContext::MyGraphContext() :
GraphContext("MyGem", ".mygemext", {})
{
const auto floatType = azrtti_typeid<float>();
m_dataTypes.push_back(
AZStd::make_shared<GraphModel::DataType>(MyDT_Float, floatType, AZStd::any{}, "Float", "float"));
}
//Just putting the node code here for simplicity
class SimpleNode : public GraphModel::Node
{
public:
static const char* SN_THING_INPUT = "simpleNode_thing_in";
AZ_RTTI(SimpleNode, "{UUID}", Node);
static void Reflect(AZ::ReflectContext* context)
{
auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context);
if (serializeContext)
{
serializeContext->Class<SimpleNode, GraphModel::Node>()
->Version(0);
}
}
SimpleNode() = default;
SimpleNode(GraphModel::GraphPtr graph, MyGraphContext* ctx) : Node(graph),
m_ctx(ctx)
{
RegisterSlots();
CreateSlotData();
}
const char* GetTitle() const override
{
return "SimpleNode";
}
void RegisterSlots() override
{
GraphModel::DataTypePtr floatDataType = ctx->GetDataType(MyDT_Float);
//Look at SlotDefinition's constructor for what these do :)
RegisterSlot(AZStd::make_shared<GraphModel::SlotDefinition>(
GraphModel::SlotDirection::Output,
GraphModel::SlotType::Data,
SN_THING_INPUT,
"Thing",
"A thingy",
GraphModel::DataTypeList{floatDataType},
AZStd::any(0.5f),
0,
0,
AZStd::string{},
AZStd::string{},
AZStd::vector<AZStd::string>{},
true,
true
));
}
MyGraphContext* ctx;
}
MyGemWidget::MyGemWidget(QWidget* parent)
: QWidget(parent)
, m_graphicsView(new GraphCanvas::GraphCanvasGraphicsView())
, m_editorId(THINGCANVAS_EDITOR_ID)
, m_styleManager(THINGCANVAS_EDITOR_ID, "MaterialCanvas/StyleSheet/materialcanvas_style.json")
{
m_graphicsView->SetEditorId(m_editorId);
GraphCanvas::GraphCanvasRequestBus::BroadcastResult(m_sceneEntity, &GraphCanvas::GraphCanvasRequests::CreateSceneAndActivate);
m_graphId = m_sceneEntity->GetId();
GraphCanvas::SceneRequestBus::Event(m_graphId, &GraphCanvas::SceneRequests::SetEditorId, m_editorId);
m_graphicsView->SetScene(m_graphId);
AzQtComponents::StyleManager::setStyleSheet(this, QStringLiteral("style:Editor.qss"));
AzQtComponents::StyleManager::setStyleSheet(this, QStringLiteral(":/GraphView/GraphView.qss"));
QVBoxLayout* mainLayout = new QVBoxLayout(this);
mainLayout->addWidget(m_graphicsView.get());
setLayout(mainLayout);
m_graphCtx = AZStd::make_shared<MyGraphContext>();
m_graph = AZStd::make_shared<GraphModel::Graph>(m_graphCtx);
m_controller = new GraphModelIntegration::GraphController(m_graph, m_graphId);
//Now lets add some nodes!
m_controller->AddNode(AZStd::make_shared<SimpleNode>(m_graph, m_graphCtx));
}
}
Handling things myself!
If you don't want to use the higher level stuff heres a thing you can copy and paste, so you have all the pure virtual functions exposed.
void RequestUndoPoint() override;
void RequestPushPreventUndoStateUpdate() override;
void RequestPopPreventUndoStateUpdate() override;
void TriggerUndo() override;
void TriggerRedo() override;
void DisconnectConnection(const GraphCanvas::ConnectionId& connectionId) override;
bool CreateConnection(const GraphCanvas::ConnectionId& connectionId, const GraphCanvas::Endpoint& sourcePoint, const GraphCanvas::Endpoint& targetPoint) override;
bool IsValidConnection(const GraphCanvas::Endpoint& sourcePoint, const GraphCanvas::Endpoint& targetPoint) const override;
AZStd::string GetDataTypeString(const AZ::Uuid& typeId) override;
void OnSaveDataDirtied(const AZ::EntityId& savedElement) override;
void OnRemoveUnusedNodes() override;
void OnRemoveUnusedElements() override;
void ResetSlotToDefaultValue(const GraphCanvas::Endpoint& endpoint) override;