raynodes
is a standalone 2D node editor made using raylib
and raygui with a focus
extensibility. It aims to be an attractive tool for any node based task, and supports being integrated into
bigger projects like games, editors...
In many cases it comes close to being a node-editor SDK of sorts.
A small showcase of its major features:
- Clean, modularized and documented source code!
- Extensive and easy-to-use node and component interface allowing for endless customization!
- Custom filetype optimized for low export size
- Fast and optimized import header included for working with project exports!
- Supports both Windows and Linux (and possibly macOS)
- Plugin interface and capabilities which allows extending the node library
- Unit testing of critical parts (importing, persistence...)
- Supports user-created nodes inside the editor without touching the source code!
- Modern code base using many C++20 and above
For more infos on the design choices go to Software Design
For more information on how to use the editor look at the raynodes-wiki.
The other dependencies
are cxstructs, tinyfiledialogs
for file dialogs and catch2 for testing.
1. Installation
2. Editor Features
3. Components!
4. Plugins
5. Nodes!
6. Software Design
Just download the .zip for your operating system from the most recent release in
the release page.
Unzip and start the executable! Have fun using raynodes
.
This project uses CMake as buildsystem!
To build the project locally you just need to do 4 simple steps:
- Clone this git repository
- Create a new directory inside the clone repository (e.g. cmake-build-debug)
- Configure the build from inside the build directory with
cmake ..
- Build the project from inside the build directory with
make ..
All external dependencies are included in the source! This model is chosen based on their combined low size (only 14mb) and the provided simplicity for sharing and integrated testing.
raynodes
can be passed a path which it will try to open over the commandline.
Files starting with .
(dot) will be interpreted as relative paths:
./raynodes.exe ./MyRelativePathFile.rn
Else it will be interpreted as absolute path:
./raynodes.exe C:\Users\Me\Documents\MyAbsoluteFile.rn
On Windows you can also set it as the default executable for ".rn" files, then double click to open any such file!
raynodes
has supports all common shortcuts. For a comprehensive list check out
the shortcuts page in the wiki!.
The user interface takes inspiration from other editors like paint.net. For a comprehensive list checkout the user-interface page of the wiki!
raynodes
gives you the ability to create your own nodes without even interacting with the source code!
Open the Node Creator
window via the button on the left and add a new node by clicking +Add
. For more in-depth
information check out the User-Created-Nodes page of the
wiki!
Components are the second level of customization you can achieve. For that you can implement the Component interface inside your own plugin. For the most part a component is a simple struct that is drawn and updated inside the node. Inside the update function you have full write and read access to the context and design and draw complex components.
This is a glimpse at the interface but the Component.h header has a lot more and is self documenting! Check it out!
//-----------CORE-----------//
// Necessary to copy the component
virtual Component* clone() = 0;
// IMPORTANT: Only called when its bounds are visible on the screen!
virtual void draw(EditorContext& ec, Node& parent) = 0;
// Guaranteed to be called once per tick (on the main thread) (not just when focused)
virtual void update(EditorContext& ec, Node& parent) = 0;
// Use the symmetric helpers : io_save(file,myFloat)...
virtual void save(FILE* file) {}
//Use the symmetric helpers : io_load(file,myFloat)...
virtual void load(FILE* file) {}
//-----------EVENTS-----------//
// All called once, guaranteed before update() is called
virtual void onMouseEnter(EditorContext& ec) {}
virtual void onMouseLeave(EditorContext& ec) {}
virtual void onFocusGain(EditorContext& ec) {}
virtual void onFocusLoss(EditorContext& ec) {}
//-----------LIFE CYCLE-----------//
// Called once at creation time after the constructor
virtual void onCreate(EditorContext& ec, Node& parent) {}
// IMPORTANT: Only called once when the node is finally destroyed (only happens after its delete action is destroyed)
// This may happen very delayed or even never!
virtual void onDestruction(Node& parent) {}
// Called whenever component is removed from the screen (delete/cut)
virtual void onRemovedFromScreen(EditorContext& ec, Node& parent) {}
// Called whenever component is added to the screen (paste)
virtual void onAddedToScreen(EditorContext& ec, Node& parent) {}
//-----------CONNECTIONS-----------//
// Called once when a new connection is added
virtual void onConnectionAdded(EditorContext& ec, const Connection& con) {}
// Called once when an existing connection is removed
virtual void onConnectionRemoved(EditorContext& ec, const Connection& con) {}
The interface is quite expansive and abstract allowing an implementation to react to a multitude of events.
Loading and saving its state is made easy through a supplied symmetric save format.
Just call the io_save
which supports int, float, string and raw data(future).
This is how you save and retrieve a string:
void TextInputField::save(FILE* file) {
cxstructs::io_save(file, buffer.c_str());
//More possible member variables:
//cxstructs::io_save(file, myFloat);
//cxstructs::io_save(file, myInt);
//cxstructs::io_save(file, myAllocatedDataPtr, mySize);
}
void TextInputField::load(FILE* file) {
cxstructs::io_load(file, buffer);
//More possible member variables
//cxstructs::io_load(file, myFloat);
//cxstructs::io_load(file, myInt);
//cxstructs::io_load(file, myAllocatedDataPtr, mySize);
}
Whenever you want to make custom components or nodes you will need to make your own plugin. raynodes
provides an
easy-to-use plugin interface that allows defining custom components and building nodes out of them.
When creating your plugin you have to link against the base editor (statically) and against raylib (shared).
struct RaynodesPluginI {
virtual ~RaynodesPluginI() = default;
// Called once after its been loaded
virtual void onLoad(EditorContext& ec) {}
// Called once at the correct time to register its components
virtual void registerComponents(ComponentRegister& cr) {}
// Called once at the correct time to register its nodes
virtual void registerNodes(NodeRegister& nr) {}
};
extern "C" EXPORT RaynodesPluginI* CreatePlugin() {
return new MyPlugin();
}
Implement this interface and add the exported function to create an instance.
Components are easily registered by providing a name and its type. This works cause all Components have a similar constructor. After you implemented a new Component by creating a derived class in your plugin files just include its header and register it in your plugin.
NOTE: Components are uniquely identified by this name (naming collision will be shown when loading). This means it's advised to pick a namespace for your plugin (small prefix).
Syntax: registerComponent<ComponentType>("<Component-Identifier>")
void BuiltIns::registerComponents(ComponentRegister& cr) {
cr.registerComponent<MathC>("BI_MathOp");
cr.registerComponent<DisplayC>("BI_Display");
cr.registerComponent<SeparateXYZC>("BI_SeparateXYZ");
cr.registerComponent<Vec3OutC>("BI_Vec3Out");
}
A node is then just a container for components. The register process allows to set custom labels for each component which helps to reference a specific component later on:
Syntax: registerNode("<NodeName>", { {<CustomLabel>,<Component-Identifier>}, ... }
void BuiltIns::registerNodes(NodeRegister& nr) {
nr.registerNode("MathOp", {{"Operation", "BI_MathOp"}});
nr.registerNode("TextField", {{"TextField", "BI_TextOut"}});
nr.registerNode("NumberField", {{"Number", "BI_NumberOut"}});
nr.registerNode("SeparateXYZ", {{"Vector", "BI_SeparateXYZ"}});
nr.registerNode("Vector 3", {{"Vector3", "BI_Vec3Out"}});
}
Plugins will be loaded from the ./plugins
folder relative to the executable
The node interface is entirely optional but can be used to gain another level of control to implement functionality. It's mostly there to orchestrate existing components by allowing to specify an update and render tasks that runs after all components.
//-----------CORE-----------//
// Guaranteed to be called each tick and AFTER all components have been updated
virtual void update(EditorContext& ec) {}
// Called only when the node bounds are (partially) within the screen
virtual void draw(EditorContext& ec) {}
// Called at save time - allows you to save arbitrary, unrelated additional state data
virtual void saveState(FILE* file) const {}
// Called at load time - should be the exact same statements and order as in saveState();
virtual void loadState(FILE* file) {}
//-----------LIFE CYCLE-----------//
// Called once when the node is created and AFTER all components
virtual void onCreation(EditorContext& ec) {}
This interface is still quite young and will be expanded.
- Prefer fixed size containers for lower level structs for better cache efficiency
- Also helps to reduce allocations
- No global state to allow for modularity if needed (and testability...)
- Move away from providing source code extensibility in favour of in-editor extensibility
- Project isn't centered around code interaction but in-editor interaction
2 thread model - Logic and draw thread (remove logic time overhead and memory management from draw thread)Only minimal synchronization needed (with spin lock and only in EditorContext getters and setters)Through clean code structure critical sections can easily be defined
- 1 Thread is enough - 2 Threads introduce code complexity
- Logic overhead is very small
- Structure code by exporting big functions into headers
EditorContext - DataBackend
- Should just hold data and perform up to clever getter and setter tasks
- Exceptions to this are backend tasks which have a limited scope inside the EditorContext
NodeEditor - Logic
- Will be passed a EditorContext& to operate on
- Defines logic that works on the data
- General style guide cxutil/cxtips.h
- .clang-format at root for format
The main inspiration was to create an editor that just provides a basic interface that can be used to create anything node based like:
- Blender Node Editor
- Quest Tree (for games, DialogueTree and choices)
- Basic Logic Circuit
raynodes
is a component-centric editor meaning that most things happen in and around components. However, with the
addition custom nodes which allows for even more control you could almost call it a hybrid.
This choice of having components was made due to a number of reasons:
- Having reusable building blocks (components) throughout nodes is good
- With only a node it's not clear who handles the input and outputs
- Now each node has its dedicated input and outputs -> might add node level connections as well
- Components can be tied to some identifier (a string here) which allows nodes to be built at runtime
- This allows for instructions in string form on how to build a specific node -> plugins
However, this also has some drawbacks:
- A node doesn't have the functionality of all its components combined
- It's just a container... -> this could be improved with inter-component data transfers
- Having a nested structure adds extra complexity to reading and saving
Pretty much all the drawbacks of some earlier approaches are solved by introducing custom nodes, although this adds more complexity and needs proper implementation of inter-component data transfer.