Repository navigation
Visual Scripting
SparkEngine now includes a unified visual scripting system — a single, properly wired implementation that compiles node graphs to AngelScript source code.
| Component | File | Purpose |
|---|---|---|
VisualScriptCompiler |
SparkEngine/Source/Engine/Scripting/VisualScriptCompiler.h |
Node graph → AngelScript compiler |
VisualScriptGraphIO |
SparkEngine/Source/Engine/Scripting/VisualScriptGraphIO.h |
.vscript graph files: fail-closed parse, canonical save |
VisualScriptPanel |
SparkEditor/Source/Panels/VisualScriptPanel.h |
ImGui node graph editor |
- Non-coders build gameplay logic by connecting nodes in the Visual Script panel
-
Save/Load read and write the graph as a
.vscriptfile through the engine'sVisualScriptGraphIO - Clicking Compile runs
VisualScriptCompiler::Compile()which generates a.asfile - The generated file is written to
Assets/Scripts/Generated/ - The existing
ScriptHotReloadsystem detects the change and recompiles automatically - No new runtime — visual scripts use the same AngelScript pipeline as hand-written scripts
| Category | Examples |
|---|---|
| Events | OnStart, OnUpdate, OnTriggerEnter, OnDamaged, OnKeyPress, OnCollision |
| Flow Control | Branch (If), ForLoop, Sequence |
| Actions | SetPosition, PlaySound, PlayAnimation, SpawnEntity, PrintMessage |
| Math | Add, Subtract, Multiply, Divide, Lerp, Clamp, Random, Break/Make Vector3, To Int |
| Logic | AND, OR, NOT, Equal, Greater, Less, Select |
| Text | Append (string + any value) |
| Getters | GetKeyDown, GetDeltaTime, GetSelf |
| Constants | Float, Int, Bool, String, Vector3 |
| Variables | GetVariable, SetVariable |
- Left sidebar: Node palette organized by category
- Center: Canvas with pan (middle-mouse), zoom (scroll), node dragging, bezier connections
- Right sidebar: Variable definitions + selected node properties
- Bottom bar: Compile button, error display
A visual script with:
-
OnStart→PrintMessage("Hello World") -
OnUpdate→GetKeyDown("Space")→Branch→ApplyForce(0, 10, 0)
Generates:
class MyScript
{
void Start()
{
print("Hello World");
}
void Update(float dt)
{
if (getKeyDown("Space"))
{
// ApplyForce...
}
}
}The original visual scripting system was removed in March 2026 as dead code — two parallel implementations that were never wired in. The current system is a clean reimplementation following the ShaderGraphCompiler pattern (node graph → code generation) with a single unified engine + editor implementation.
- Event Response System -- Data-driven "When/If/Then" rules (simpler alternative for common gameplay)
- Scripting with AngelScript -- Text-based scripting (the compilation target)
- Entity-Component-System -- How scripts attach to entities
The Event Response System provides a no-code "When/If/Then" rule engine for common gameplay patterns. Rules are defined as data (no scripting required) and evaluated at runtime.
| Component | File | Purpose |
|---|---|---|
EventResponseSystem |
SparkEngine/Source/Engine/Gameplay/EventResponseSystem.h |
Rule engine (singleton) |
EventResponsePanel |
SparkEditor/Source/Panels/EventResponsePanel.h |
Visual rule builder |
WHEN [Event Trigger]
IF [Conditions] (optional, AND/OR/NOT groups)
THEN [Action 1] → [Action 2] → ...
OnTriggerEnter, OnTriggerExit, OnDamaged, OnKilled, OnItemPickup, OnKeyPress, OnKeyRelease, OnCollision, OnQuestComplete, OnTimer, OnStart, OnWeatherChange, OnTimeOfDay, OnEntityCreated, OnEntityDestroyed, OnCustom
SpawnEntity, DestroyEntity, EnableEntity, DisableEntity, SetPosition, MoveToward, TeleportEntity, RotateEntity, SetHealth, DealDamage, HealEntity, PlaySound, StopSound, PlayAnimation, ApplyForce, ApplyImpulse, ShowDialogue, ShowMessage, SetWeather, SetTimeOfDay, SetWorldVariable, SetWorldFlag, Delay, FireCustomEvent
- Subscribes to existing
EventBusevents via RAII handles - Conditions reuse
ConditionSystem(60+ condition types) - Rules can be serialized to/from JSON
- Timer-based rules and delayed actions processed each frame via
Update(dt) - One-shot rules auto-disable after firing once
Pre-configured entity templates for rapid no-code game creation.
| Category | Presets |
|---|---|
| Character | Player, Enemy, NPC |
| Prop | Door, Chest, Destructible |
| Pickup | Health Pickup, Ammo Pickup, Key Item |
| Effect | Sound Source, Particle Effect, Spawn Point |
Right-click in the Hierarchy panel → Create From Preset → select category → select preset.
| Component | File | Purpose |
|---|---|---|
EntityPresetManager |
SparkEngine/Source/Engine/ECS/EntityPresetManager.h |
Preset registry (singleton) |
| HierarchyPanel integration | SparkEditor/Source/Panels/HierarchyPanel.cpp |
Context menu preset picker |
Open the Visual Script panel from the editor menu. The panel is divided into four areas:
- Left sidebar -- Node Palette organized by category (Events, Flow Control, Actions, Math, Logic, Getters, Constants, Variables)
- Center -- Canvas with pan (middle-mouse), zoom (scroll wheel), node dragging, and bezier connection drawing
- Right sidebar -- Variable definitions and selected node properties
- Bottom bar -- Compile button, error display, generated script path
Nodes can be added in two ways:
- Click a node type in the left sidebar palette to place it on the canvas
- Right-click the canvas to open the context menu and select a node type
// Programmatic node creation (used by undo/redo system)
panel.AddNodeAtPositionDirect(Spark::Scripting::ScriptNodeType::OnUpdate, 100.0f, 200.0f);
panel.AddNodeAtPositionDirect(Spark::Scripting::ScriptNodeType::GetKeyDown, 300.0f, 200.0f);
panel.AddNodeAtPositionDirect(Spark::Scripting::ScriptNodeType::Branch, 500.0f, 200.0f);
panel.AddNodeAtPositionDirect(Spark::Scripting::ScriptNodeType::ApplyForce, 700.0f, 150.0f);Drag from an output pin to an input pin to create a connection. The system enforces type safety:
The panel and the .vscript loader share one rule, VisualScriptGraphIO::ArePinKindsCompatible():
Execution pins connect only to Execution pins; data pins connect when the kinds match, when either is Any,
or between Int and Float. An input takes one wire, and an execution output feeds one input: a new wire into
an input, or out of an execution output, replaces the old one. The panel also names new variables uniquely, clamps
Int defaults to the exact float range (±2^24) and stops adding nodes before ids pass kMaxNodeId, so every graph
it builds can be saved.
Pin types and their wire colors:
| PinKind | Purpose | Typical Color |
|---|---|---|
Execution |
Flow control (white wire) | White |
Bool |
Boolean values | Red |
Int |
Integer values | Cyan |
Float |
Floating-point values | Green |
String |
Text values | Magenta |
Vector3 |
3D position/direction | Yellow |
Entity |
Entity ID reference | Orange |
Any |
Untyped (resolved at compile) | Gray |
In the right sidebar, add variables that become class members in the generated script:
// Variable definitions compile to class member declarations
struct VariableDecl
{
std::string name; // Variable name (e.g., "speed")
PinKind type = PinKind::Float; // Data type
std::string defaultValue; // Initial value (e.g., "10.0")
};
// In the generated AngelScript:
// class MyScript {
// float speed = 10.0;
// ...
// }Click the Compile button. The compiler performs:
-
Find event entry points -- Scans for
OnStart,OnUpdate,OnTriggerEnter, etc. -
Walk execution wires -- Each event's chain is emitted in order. Branch, Sequence and ForLoop emit their
output chains inside their own blocks, so control flow nests (an
else ifis a Branch on a False output). Branch and Sequence end a chain; ForLoop continues from its Completed pin. - Evaluate data at the point of use -- Pure data nodes (no execution pins) are re-evaluated in a scoped block in front of every statement that reads them, so a Get Variable after a Set Variable in the same chain sees the new value. Execution or data cycles, and variable defaults that are not a literal of the variable's type, fail the compile with an error.
- Code emission -- Generates AngelScript class with member variables and lifecycle methods
-
Write to disk -- Saves
.asfile toAssets/Scripts/Generated/(binary, so the bytes match on every platform) -
Hot reload --
ScriptHotReloaddetects the file change and recompiles automatically
// Programmatic compilation
Spark::Scripting::VisualScriptGraph graph;
graph.className = "PlayerController";
// ... add nodes, connections, variables ...
auto result = Spark::Scripting::VisualScriptCompiler::Compile(graph);
if (result.success)
{
// result.angelScriptSource contains the generated code
// Write it to Assets/Scripts/Generated/PlayerController.as
}
else
{
for (const auto& error : result.errors)
{
LOG_ERROR("Compile error: {}", error);
}
}Event nodes are the starting points of execution chains. Each generates a method in the AngelScript class.
| Node Type | ScriptNodeType Value | Generated Method | Parameters |
|---|---|---|---|
| OnStart | 0 | void Start() |
None |
| OnUpdate | 1 | void Update(float dt) |
deltaTime |
| OnTriggerEnter | 2 | void OnTriggerEnter(uint entity) |
triggering entity |
| OnTriggerExit | 3 | void OnTriggerExit(uint entity) |
exiting entity |
| OnDamaged | 4 | void OnDamaged(float amount) |
damage amount |
| OnKeyPress | 5 | void OnKeyPress(string key) |
key name |
| OnCollision | 6 | void OnCollision(uint entity) |
colliding entity |
| OnCustomEvent | 7 |
void On<Name>(...) of the declared custom event its event property names (raised in-graph with CallFunction; an undeclared name fails the compile) |
the event's declared parameters |
| Node | Behavior |
|---|---|
| Branch | If/else -- routes execution based on a bool input |
| ForLoop | Repeats execution N times with an index output |
| Sequence | Executes multiple output chains in order |
| DoNothing | Pass-through (useful for graph organization) |
| Node | Effect |
|---|---|
| SetPosition | Move entity to a Vector3 position |
| SetRotation | Set entity rotation |
| SetHealth | Set entity health value |
| ApplyForce | Apply physics force to entity |
| PlaySound | Play an audio clip |
| PlayAnimation | Trigger animation state |
| SpawnEntity | Create a new entity at position |
| DestroyEntity | Remove entity from world |
| PrintMessage | Output debug text |
| FireEvent | Fire a custom event |
| Node | Operation | Inputs | Output |
|---|---|---|---|
| Add | a + b | Float/Vector3 | Float/Vector3 |
| Subtract | a - b | Float/Vector3 | Float/Vector3 |
| Multiply | a * b | Float | Float |
| Divide | a / b | Float | Float |
| Lerp | Linear interpolate | a, b, t | Float/Vector3 |
| Clamp | Constrain to range | value, min, max | Float |
| Random | Random [0, 1] | None | Float |
| RandomRange | Random [min, max] | min, max | Float |
| Distance | Vector distance | a, b | Float |
| Normalize | Unit vector | Vector3 | Vector3 |
| DotProduct | Dot product | a, b | Float |
| Abs | Absolute value | Float | Float |
| Negate | -value | Float | Float |
Visual scripts support reusable function sub-graphs that compile to separate methods:
// Define a function sub-graph
Spark::Scripting::FunctionGraph healFunction;
healFunction.name = "HealEntity";
healFunction.returnType = Spark::Scripting::PinKind::Execution; // void return
// Add parameters
Spark::Scripting::VariableDecl targetParam;
targetParam.name = "target";
targetParam.type = Spark::Scripting::PinKind::Entity;
healFunction.parameters.push_back(targetParam);
Spark::Scripting::VariableDecl amountParam;
amountParam.name = "amount";
amountParam.type = Spark::Scripting::PinKind::Float;
amountParam.defaultValue = "25.0";
healFunction.parameters.push_back(amountParam);
// Add nodes inside the function (SetHealth, etc.)
// ... add nodes and connections ...
// Register with the graph
graph.functions.push_back(healFunction);A function body starts at its entry statements: statement nodes with no incoming execution wire, taken in the order the nodes are listed. Each entry is emitted with its downstream chain, so Branch, Sequence and ForLoop nest exactly as they do in event methods and their targets are not emitted a second time. A body whose every statement sits on an execution cycle fails the compile.
This compiles to:
class MyScript
{
void HealEntity(uint target, float amount)
{
setHealth(target, getHealth(target) + amount);
}
}Spark::Scripting::CustomEventDef onLevelUp;
onLevelUp.name = "OnLevelUp";
Spark::Scripting::VariableDecl levelParam;
levelParam.name = "newLevel";
levelParam.type = Spark::Scripting::PinKind::Int;
onLevelUp.parameters.push_back(levelParam);
graph.customEvents.push_back(onLevelUp);Enable debug mode to insert debugTrace() calls at every node, allowing step-by-step execution tracing:
auto result = Spark::Scripting::VisualScriptCompiler::Compile(graph, true); // debugMode = trueThis inserts trace calls into the generated AngelScript:
void Update(float dt)
{
debugTrace("Node_1: GetKeyDown"); // Trace each node execution
bool node_1_0 = getKeyDown("Space");
debugTrace("Node_2: Branch");
if (node_1_0)
{
debugTrace("Node_3: ApplyForce");
applyForce(0, 10, 0);
}
}The VisualScriptPanel has a m_debugCompile flag that can be toggled in the UI. When enabled, compiled scripts include trace output.
All graph operations support undo/redo through the editor command system:
// AddNodeCommand -- undoable node creation
class AddNodeCommand : public EditorCommand
{
void Execute() override; // Creates the node
void Undo() override; // Removes the node
};
// RemoveNodeCommand -- undoable node deletion (saves full node state)
class RemoveNodeCommand : public EditorCommand
{
void Execute() override; // Removes node + saves connections
void Undo() override; // Restores node + reconnects
};
// AddConnectionCommand -- undoable connection creation
class AddConnectionCommand : public EditorCommand
{
void Execute() override; // Creates the connection
void Undo() override; // Removes the connection
};VisualScriptGraphIO (engine side, used by the editor panel and by tests) owns the graph file format:
auto graph = Spark::Scripting::VisualScriptGraphIO::LoadFile("Assets/Graphs/PlayerController.vscript");
if (!graph)
SPARK_LOG_ERROR(Spark::LogCategory::Scripting, "%s", graph.error().c_str()); // names file and element
auto saved = Spark::Scripting::VisualScriptGraphIO::SaveFile(path, *graph); // atomic: temp file + renameA .vscript file is JSON with "format": "spark.vscript" and "version": 1, the class name, an optional
description (emitted as the generated file's header comment), variables, nodes, connections, functions and custom
events. Node types and pin kinds are stored by name ("type": "Branch", "kind": "Float"), never by enum value.
Each node records its pins with their defaults and its editor position.
Loading fails closed. An unknown or repeated key, format, version, node type or pin kind, a duplicate, zero or
out-of-range (above 2^24 - 1) node id, a wire to a missing node or pin, an execution wire joined to a data pin,
incompatible data kinds, a second wire into one input, or a second wire out of one execution output rejects the
whole file, and the message names the element (graph.connections[3].to: ...). The editor keeps
its current graph when a load fails. SaveFile parses its own output before writing, so it never writes a file that
would not load. The editor panel has no canvas for function sub-graphs or custom events yet; it keeps a loaded
graph's definitions and writes them back on save, and its variable name, type and default fields hold every value
the loader accepts, so a Load then Save in the panel loses nothing. Output is canonical, with one node or wire per line, a fixed key order and floats in shortest
round-trip form. Re-saving an unchanged graph therefore reproduces the file byte for byte, and graph diffs stay
reviewable.
GameModules/SparkGameVisualScript/Assets/Graphs/*.vscript are the sources of that module's five shipped scripts
(Assets/Scripts/Generated/*.as). VisualScriptGraphs_CheckedInGraphsRegenerateShippedScripts
(Tests/TestMOD390VisualScriptGraphsReal.cpp) compiles each graph and requires the shipped .as to match byte for
byte. VisualScriptGameplay_* plays those scripts to the five-pickup win, so the demo's gameplay is authored as
graphs. After editing a graph, regenerate:
SPARK_VSCRIPT_OUTPUT_DIR=/tmp/vscript SPARK_TEST_NAME=VisualScriptGraphs_CheckedIn build/linux-gcc-release/bin/SparkTests
cp /tmp/vscript/*.as GameModules/SparkGameVisualScript/Assets/Scripts/Generated/
cp /tmp/vscript/*.vscript GameModules/SparkGameVisualScript/Assets/Graphs/ # canonical formThen update the SHA-256 values in GameModules/SparkGameVisualScript/Assets/manifest.json.
To try an edit in a running demo without a restart, copy the regenerated .as into the script root the module
loaded from and run vs_reload. The demo compiles one module per entity from in-memory source (its selfEntity
placeholder bound to the entity id), so it reloads through AngelScriptEngine::HotReloadModuleFromSource(), the
in-memory counterpart of HotReloadModule() with the same state rules R1-R8
(Scripting with AngelScript, "Hot-reload state rules"). All five files are
validated first; a compile error, a missing class or a lost placeholder rejects the reload with a <file>:<line>
diagnostic and leaves every script as it was. Score, health and positions live in ECS components and are untouched;
script fields of the same name and type carry over. VisualScriptHotReload_*
(Tests/TestMOD390VisualScriptHotReloadReal.cpp) covers it.
VisualScriptRuntime_* (Tests/TestENG200VisualScriptRuntimeReal.cpp, CTest VisualScriptRuntimePackagedLoop)
covers the path from graph to packaged game in-process. It compiles the five graphs, has GamePackager lay the scripts
out as a package next to the manifest.json and spark.modules.json markers, and launches from an unrelated working
directory. RuntimePackage::AnchorWorkingDirectory anchors to the package and VisualScriptDemo::ScriptSearchPaths
(the module's OnLoad search order) resolves the package's scripts, which are then played to the win. A truncated
graph stops the build with a diagnostic that names the graph. The package's executable is a placeholder file, so a
launch of the real packaged engine binary is not covered here.
The demo's graphs request sounds and animation clips by name (PlaySound/PlayAnimation nodes). Its cue audio ships
in Assets/Audio/VisualScript, and DemoWorld spawns the animated entities with their AnimationController clip
lists. The module README covers both.
- No runtime overhead -- Visual scripts compile to the same AngelScript as hand-written code. There is no visual script interpreter or VM.
- Compile-time cost -- Code generation walks each execution chain once and each statement's data inputs once. Measure compilation time on representative graphs before assigning a latency budget.
-
Hot reload latency -- After compilation,
ScriptHotReloaddetects the file change within one frame and triggers AngelScript recompilation. Total turnaround is typically under 100ms. - Graph size -- Large graphs may slow canvas rendering in the editor. Consider splitting logic across multiple scripts attached to different entities.
The generated code uses the standard AngelScript API bindings:
// All visual script outputs use the same engine bindings as hand-written scripts
class PlayerController
{
float speed = 5.0;
float jumpForce = 10.0;
void Start()
{
print("PlayerController initialized");
}
void Update(float dt)
{
if (getKeyDown("W"))
{
vec3 pos = getPosition(getSelf());
pos.y += speed * dt;
setPosition(getSelf(), pos);
}
if (getKeyDown("Space"))
{
applyForce(getSelf(), 0, jumpForce, 0);
}
}
}The compiler's PinTypeString() maps each PinKind to its AngelScript equivalent:
| PinKind | AngelScript Type |
|---|---|
Bool |
bool |
Int |
int |
Float |
float |
String |
string |
Vector3 |
vec3 |
Entity |
uint |
Execution |
(flow control, no type) |
The separate advisory visual-script-package workflow job builds a real
AngelScript-enabled host and module, regenerates the module scripts from its
graphs, and packages through spark-cli. Tests/run_visual_script_package.py
launches that package from a foreign directory and checks a deterministic
waypoint playthrough for one win, clean module teardown and exit zero. The
source change adds this executable lane; its hosted result remains unverified.
Engine-owned ECS lifecycle dispatch replaces the module's direct Start/Update
calls, and the in-process gameplay fixture uses the same script system.
Published from 614bca17b41e. Edit the canonical source in wiki/.
- Documentation
- Docs route
- Wiki index
- Guides
- Tutorials
- Samples
- Examples
- API Reference
- API route
- Reference
- Build Guide
- Dependencies
- FAQ
- Changelog
- Roadmap
- Contributing
- Code of Conduct
- Home
- FAQ
- Getting Started
- Quick-Start Tutorial
- Making Your First Game
- Making Your First Multiplayer Game
- Artist Workflow Guide
- Editor Walkthrough
- Migration Guide
- How SparkEngine Works
- Architecture Overview
- Engine Architecture Flowchart
- Creating a Game Module
- Game Modules (catalog)
- Entity Component System
- Rendering and Graphics
- Physics
- Cloth Simulation
- Audio
- Input System
- Camera System
- Scripting with AngelScript
- Visual Scripting
- AI and Navigation
- Animation
- 2D Systems
- Networking
- Dedicated Server
- Multiplayer Quick Start
- Area Server Architecture
- Scene Management
- Large World Support
- Collaborative Editing
- Coroutine System
- Event System
- Event Response System
- Job System
- UI System
- UI Layout Extensions
- Localization
- Dialogue System
- Destruction System
- Replay System
- Achievement System
- Loading System
- Mod System
- Content Delivery
- Tween System
- Memory Integrity
- Gameplay Systems
- Terrain and Procedural Generation
- Save System
- Persistence System
- Day Night Cycle and Weather
- Cinematic Sequencer
- Runtime Prefabs
- SparkEditor
- Editor Tutorials
- SparkConsole
- SparkDaemon
- Shader Pipeline
- Asset Pipeline
- Asset Validation
- Asset Migration
- Game Packaging
- Online Services
- DataTable System
- Loot and Crafting System
- CSG System
- Font System
- Timer Manager
- Movie Render Pipeline
- HLOD and World Partition
- Remote Debug System
- Selection Manager
- Asset Dependency Graph
- Editor Automation
- File Watcher
- Project Templates
- System Requirements
- VR Support
- Mobile Platform
- Accessibility
- Platform Input
- Platform Certification
- Cross-Compilation: Wine Testing
- RHI Abstraction Layer
- D3D11 Backend
- D3D12 Backend
- Vulkan Backend
- OpenGL Backend
- Metal Backend
- DXR Raytracing
- Hybrid Ray Tracing
- Upscaling (DLSS/FSR)
- Render Graph
- Shader Graph
- GPU Particles
- GPU-Driven Rendering
- Volumetric Fog
- Volumetric Clouds
- Global Illumination
- Virtual Texturing
- Water Rendering
- Clustered Lighting
- Material System
- Post-Processing
- Shadow System
- Particle System
- Decal System
- Sky and Atmosphere
- Foliage System
- Mesh Shaders
- Neural Rendering
- Configuration Reference
- Performance Tips
- Benchmark Framework
- Threading Model
- Fuzz Policy and Parser Security
- Memory Safety
- Memory Management Patterns
- Build System and CMake Modules
- Profiler and Debugging
- Performance Profiling Guide
- Telemetry System
- Crash Reporting
- Golden Image Testing
- Utilities
- Testing
- Fuzz Policy and Parser Security
- Codebase Statistics
- Codebase Health
- Error Handling Patterns
- Hot Reload Overview
- Troubleshooting
- Contributing
- Workflow Patterns
- Build Optimizations
- CI Reproducible Builds
- CI Controlled-Failure Rehearsal
- Release Publication Stages
- Session Handoff 2026-09-27
- GitHub API and PR Checks
- Git Rebase Conflicts
- Clang-Format
- Code Quality Violations
- AI Bloat Pattern
- MinGW + Wine Cross-Compilation
- Live Editor Testing
- Engine & Renderer Landscape
- DuetOS Portability Catalog
- Five-Engine Analysis
- Eleven-Engine Analysis
- ThorVG / Unity Graphics Analysis
- Advanced Techniques Catalog
- Third-Party Library Evaluation
- Engine Viability Evaluation
- Engine Feature Recommendations
- Project Recommendations
- Mac Compatibility Analysis
- Codebase Observations
- Codebase Bloat Audit
- Test Suite Audit
- Documentation Coverage Audit
- ThirdParty Dependencies Audit
- Load Test Baseline
- Gameplay Systems Status
- SparkGame Module Status
- Stub and Abandoned Features
- Memory Integrity System
- Memory Safety Evaluation
- Hardware Acceleration Systems
- Jolt Physics Integration
- GPU/CPU Separation Plan
- Daemon Services Architecture
- Online Service Boundary
- Server Operations Runbook
- Reflection & Polymorphism Refactoring Plan
- SparkBuild In-Tree
- Wine No-JobSystem Breakthrough
- Wine Role and Fallback Tiers