Advanced Scripting Techniques for Custom Node Graph Logic in Pyrite64
You can extend Pyrite64's visual editor by subclassing Node::Base, implementing the generateCode() method to emit C++ fragments, and registering new types in the NODE_TABLE registry to create runtime-editable gameplay logic that compiles directly into N64 binaries.
Pyrite64 ships with a flexible Node Graph system that enables visual scripting for gameplay logic, shader pipelines, and scene flow without writing C++ directly. By leveraging the architecture in src/project/graph and the script-generation pipeline in data/scripts/scriptTable.cpp, you can author custom node types that persist as JSON and compile into production-ready Nintendo 64 code. This guide covers the advanced techniques for extending the node graph, from variable resolution to conditional code generation.
Architecture of the Node Graph System
The editor’s graph system is built on a layered architecture that separates UI, serialization, and code generation. Understanding these layers is essential before implementing custom logic.
The base node class in src/project/graph/nodes/baseNode.h provides the common API for all nodes, including methods like addInPin(), addOutPin(), serialize(), and deserialize(). Concrete implementations such as Node::Func and Node::Compare in src/project/graph/nodes/nodeFunc.h handle specific behaviors and code emission. The graph manager (src/project/graph/graph.h) stores the node collection and connections, while the node-type registry in src/project/graph/graph.cpp maintains a static NODE_TABLE that maps type IDs to factory lambdas for dynamic instantiation. Finally, the script-builder (data/scripts/scriptTable.cpp) walks the saved graph at build time, invoking each node’s generateCode() method to produce the final C++ source.
Creating a Custom Node from Scratch
To add a new node type, you must subclass the base class, implement the UI and generation logic, and register the type in the factory table.
Step 1: Declare the Node Class
Create a new header file under src/project/graph/nodes/ that inherits from Node::Base. Define a unique TYPE_ID and implement the required overrides.
// src/project/graph/nodes/nodeMyLogic.h
#pragma once
#include "baseNode.h"
namespace Project::Graph::Node {
class MyLogic : public Base {
public:
static constexpr const char* NAME = "MyLogic";
static constexpr uint32_t TYPE_ID = 0x0100; // Use unique ID < 0x8000
MyLogic(ImFlow::ImNodeFlow& flow, const ImVec2& pos);
void drawNode() override;
void generateCode(std::ostream& out) const override;
private:
int multiplier = 1;
};
} // namespace Project::Graph::Node
Step 2: Implement UI and Code Generation
Implement the constructor to set up pins and styling, drawNode() for the ImGui interface, and generateCode() to emit C++.
// src/project/graph/nodes/nodeMyLogic.cpp
#include "nodeMyLogic.h"
#include "ImNodeFlow.h"
#include <sstream>
namespace Project::Graph::Node {
MyLogic::MyLogic(ImFlow::ImNodeFlow& flow, const ImVec2& pos) {
setPos(pos);
setName(NAME);
setStyle(std::make_shared<ImFlow::NodeStyle>(
IM_COL32(0x88, 0x00, 0xCC, 0xFF),
ImColor(255, 255, 255, 255),
4.0f
));
addInPin("in", ImFlow::Pin::Type::Int);
addOutPin("out", ImFlow::Pin::Type::Int);
}
void MyLogic::drawNode() {
ImGui::Text("Multiplier:");
ImGui::InputInt("##mult", &multiplier);
Base::drawNode(); // Renders pins
}
void MyLogic::generateCode(std::ostream& out) const {
std::string srcVar = resolvePinVariable(0); // Input pin index 0
out << "int " << varName() << " = " << srcVar << " * " << multiplier << ";\n";
}
} // namespace Project::Graph::Node
Key API methods:
resolvePinVariable(pinIndex): Returns the C++ variable name feeding the specified input pin at generation time.varName(): Provides a unique identifier derived from the node’s UUID for naming generated variables.
Step 3: Register in NODE_TABLE
Add your node to the factory registry in src/project/graph/graph.cpp to enable instantiation from the editor palette and deserialization.
// Inside NODE_TABLE definition in src/project/graph/graph.cpp
{ Node::MyLogic::TYPE_ID,
{ Node::MyLogic::NAME,
[](ImFlow::ImNodeFlow& m, const ImVec2& pos) {
return std::make_shared<Node::MyLogic>(m, pos);
}
}
},
After recompiling the editor, the node appears in the Create menu and participates in the build pipeline.
Advanced Code Generation Patterns
Once you have basic nodes working, you can implement sophisticated scripting techniques using the following patterns.
Reusing Computed Variables Across Outputs
When a node performs expensive calculations that multiple outputs need, emit a temporary variable and reference it for each pin.
void MyLogic::generateCode(std::ostream& out) const override {
std::string src = resolvePinVariable(0);
std::string tmp = varName() + "_tmp";
out << "int " << tmp << " = heavyComputation(" << src << ");\n";
out << "int " << pinVarName(0) << " = " << tmp << ";\n"; // First output
out << "int " << pinVarName(1) << " = " << tmp << " * 2;\n"; // Second output
}
pinVarName(pinIdx) generates the specific variable name for an output pin, allowing other nodes to reference it via resolvePinVariable().
Conditional Branch Generation
Implement comparison or branching logic by emitting if statements based on node state. The Node::Compare type demonstrates this pattern:
void Compare::generateCode(std::ostream& out) const override {
std::string lhs = resolvePinVariable(0);
std::string rhs = resolvePinVariable(1);
out << "bool " << varName() << " = (" << lhs << " " << opString << " " << rhs << ");\n";
}
Store the operator string (e.g., "==", "<=") as a member variable set through a combo box in drawNode().
Accessing Global Script Variables
Query the global variable table during code generation to interact with shared state like player scores or game flags.
void GlobalRead::generateCode(std::ostream& out) const override {
std::string globalVar = ctx.globalVar("PlayerHealth", "int");
out << "int " << varName() << " = " << globalVar << ";\n";
}
Add UI controls in drawNode() using ImGui::InputText to let users select which global variable to access.
Custom Serialization for Complex State
If your node stores data structures like string tables or enums, override the serialization methods to persist them in the project JSON.
void MyLogic::serialize(nlohmann::json& j) const {
Base::serialize(j);
j["operator"] = opString;
j["values"] = valueArray;
}
void MyLogic::deserialize(const nlohmann::json& j) {
Base::deserialize(j);
opString = j.value("operator", "==");
valueArray = j["values"].get<std::vector<int>>();
}
The graph loader in Graph::load() automatically invokes these methods when opening projects.
Nesting Graphs as Sub-Functions
Pyrite64 supports modular logic through the Func node defined in src/project/graph/nodes/nodeFunc.h. To create reusable sub-routines:
- Create a separate NodeGraph asset via Assets → New → NodeGraph.
- In your main graph, place a Func node and assign the sub-graph asset.
- The
Funcnode’sgenerateCode()automatically emits a function call to the generated C++ function, mapping input pins to arguments and output pins to return values.
This technique is ideal for behavior trees, audio event pipelines, and reusable utility functions that appear across multiple levels.
Build Integration and Debugging
When you press Build, the script-builder iterates over the serialized graph stored in the project JSON. It calls generateCode() on each node and writes the output to data/scripts/scriptTable.cpp. The build system then compiles this generated source into the final N64 binary.
To debug your generated logic:
- Inspect
data/scripts/generatedNodeGraph.cppafter a build to view the exact C++ output. - Ensure your graph remains acyclic; the generator performs no topological sorting, so circular dependencies create forward-reference compiler errors.
- Keep
TYPE_IDvalues below0x8000to avoid collisions with built-in engine nodes.
Summary
- Subclass
Node::Baseinsrc/project/graph/nodes/to define custom node types with uniqueTYPE_IDvalues. - Implement
generateCode()to emit C++ fragments usingresolvePinVariable()for inputs andvarName()for local identifiers. - Register nodes in the
NODE_TABLEinsidesrc/project/graph/graph.cppto enable editor integration. - Use advanced patterns like
pinVarName()for multi-output nodes, conditional emission for branching logic, and global variable access for shared state. - Leverage
Funcnodes to nest graphs as reusable sub-functions, enabling complex modular architectures.
Frequently Asked Questions
How do I debug code generation errors in my custom nodes?
After triggering a build, open data/scripts/generatedNodeGraph.cpp to inspect the exact C++ code emitted by your nodes. If the N64 compiler reports errors, check that resolvePinVariable() indices match the order of addInPin() calls in your constructor, and verify that no circular connections exist in the graph topology.
Can I create nodes that output multiple data types?
Yes. Call addOutPin() multiple times with different ImFlow::Pin::Type values (e.g., Int, Float, Bool). In generateCode(), use pinVarName(0), pinVarName(1), etc., to declare separate variables for each output pin, allowing downstream nodes to reference them by type.
What is the maximum number of custom nodes I can register?
The TYPE_ID field is a uint32_t, but you should reserve values below 0x8000 for custom types to avoid conflicts with engine-internal nodes. This provides approximately 32,767 unique identifiers for project-specific logic, which is sufficient for even large-scale N64 projects.
How do I share custom nodes between different Pyrite64 projects?
Since nodes are standard C++ classes in src/project/graph/nodes/, you can copy the header and implementation files into another project’s source tree. Ensure you also copy the corresponding entry from NODE_TABLE in src/project/graph/graph.cpp. For distribution, consider creating a static library or contributing the nodes to the upstream HailToDodongo/pyrite64 repository.
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 →