Skip to content

Visual Scripting

github-actions[bot] edited this page Oct 18, 2026 · 2 revisions

Visual Scripting

Status: Active (Reimplemented April 2026)

SparkEngine now includes a unified visual scripting system — a single, properly wired implementation that compiles node graphs to AngelScript source code.

Architecture

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

How It Works

  1. Non-coders build gameplay logic by connecting nodes in the Visual Script panel
  2. Save/Load read and write the graph as a .vscript file through the engine's VisualScriptGraphIO
  3. Clicking Compile runs VisualScriptCompiler::Compile() which generates a .as file
  4. The generated file is written to Assets/Scripts/Generated/
  5. The existing ScriptHotReload system detects the change and recompiles automatically
  6. No new runtime — visual scripts use the same AngelScript pipeline as hand-written scripts

Node Types (~60 types across 8 categories)

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

Editor UI

  • 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

Example

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...
        }
    }
}

History

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.

See Also

Event Response System

Status: Active (April 2026)

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.

Architecture

Component File Purpose
EventResponseSystem SparkEngine/Source/Engine/Gameplay/EventResponseSystem.h Rule engine (singleton)
EventResponsePanel SparkEditor/Source/Panels/EventResponsePanel.h Visual rule builder

Rule Structure

WHEN [Event Trigger]
  IF [Conditions] (optional, AND/OR/NOT groups)
THEN [Action 1] → [Action 2] → ...

Trigger Types (16)

OnTriggerEnter, OnTriggerExit, OnDamaged, OnKilled, OnItemPickup, OnKeyPress, OnKeyRelease, OnCollision, OnQuestComplete, OnTimer, OnStart, OnWeatherChange, OnTimeOfDay, OnEntityCreated, OnEntityDestroyed, OnCustom

Action Types (24)

SpawnEntity, DestroyEntity, EnableEntity, DisableEntity, SetPosition, MoveToward, TeleportEntity, RotateEntity, SetHealth, DealDamage, HealEntity, PlaySound, StopSound, PlayAnimation, ApplyForce, ApplyImpulse, ShowDialogue, ShowMessage, SetWeather, SetTimeOfDay, SetWorldVariable, SetWorldFlag, Delay, FireCustomEvent

Integration

  • Subscribes to existing EventBus events 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

Entity Presets

Status: Active (April 2026)

Pre-configured entity templates for rapid no-code game creation.

Built-in Presets (12)

Category Presets
Character Player, Enemy, NPC
Prop Door, Chest, Destructible
Pickup Health Pickup, Ammo Pickup, Key Item
Effect Sound Source, Particle Effect, Spawn Point

Usage

Right-click in the Hierarchy panel → Create From Preset → select category → select preset.

Architecture

Component File Purpose
EntityPresetManager SparkEngine/Source/Engine/ECS/EntityPresetManager.h Preset registry (singleton)
HierarchyPanel integration SparkEditor/Source/Panels/HierarchyPanel.cpp Context menu preset picker

Visual Scripting Deep Dive

Complete Workflow: Creating a Visual Script

Step 1: Open the Visual Script Panel

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

Step 2: Add Nodes

Nodes can be added in two ways:

  1. Click a node type in the left sidebar palette to place it on the canvas
  2. 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);

Step 3: Connect Pins

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

Step 4: Define Variables

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;
//     ...
// }

Step 5: Compile

Click the Compile button. The compiler performs:

  1. Find event entry points -- Scans for OnStart, OnUpdate, OnTriggerEnter, etc.
  2. 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 if is a Branch on a False output). Branch and Sequence end a chain; ForLoop continues from its Completed pin.
  3. 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.
  4. Code emission -- Generates AngelScript class with member variables and lifecycle methods
  5. Write to disk -- Saves .as file to Assets/Scripts/Generated/ (binary, so the bytes match on every platform)
  6. Hot reload -- ScriptHotReload detects 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);
    }
}

Complete Node Type Reference

Event Nodes (Entry Points)

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

Flow Control Nodes

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)

Action Nodes

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

Math Nodes

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

Custom Node Creation Guide

Defining a Custom Function Sub-Graph

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);
    }
}

Defining Custom Events

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);

Debugging Visual Scripts

Debug Mode Compilation

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 = true

This 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);
    }
}

Editor Debug Toggle

The VisualScriptPanel has a m_debugCompile flag that can be toggled in the UI. When enabled, compiled scripts include trace output.

Undo/Redo Support

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
};

The .vscript Graph File

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 + rename

A .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.

Checked-in module graphs

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 form

Then 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.

Performance Considerations

  1. No runtime overhead -- Visual scripts compile to the same AngelScript as hand-written code. There is no visual script interpreter or VM.
  2. 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.
  3. Hot reload latency -- After compilation, ScriptHotReload detects the file change within one frame and triggers AngelScript recompilation. Total turnaround is typically under 100ms.
  4. Graph size -- Large graphs may slow canvas rendering in the editor. Consider splitting logic across multiple scripts attached to different entities.

Integration with AngelScript

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.

SparkEngine Wiki

Website Entry Points

Getting Started

Engine Subsystems

Gameplay & Tools

Platform Support

Graphics

Advanced

Development & Process

Research & Analysis

Engineering Notes & Audits

Specifications

Reference

Clone this wiki locally