Creating Custom Generator Modules

This page explains how to write your own CG modules. Before reading, make sure you understand the Curvy Generator basics and the Advanced Concepts (dirtying, on-request processing, data disposal).

The fastest way to start is the module wizard:

  1. In Unity's Project window, right-click → Create → Curvy → CG Module.
  2. Fill in the four fields and click Create.
  3. Two C# files are generated: a runtime module and an editor script.
Field Purpose
Class Name C# class name (e.g. MyCustomModule)
Menu Name Path in the graph's Add menu, using / as separator (e.g. Build/My Module)
Module Name Display name shown on the module in the graph (e.g. My Module)
Description Tooltip text shown when hovering the module in the Add menu

By default the wizard saves scripts under your project's customization folder. You can move them anywhere outside Curvy's main folders.

The wizard creates two files:

Runtime Script

[ModuleInfo("Build/My Module", ModuleName = "My Module", Description = "Does something")]
public class MyCustomModule : CGModule
{
    [HideInInspector]
    [InputSlotInfo(typeof(CGPath))]
    public CGModuleInputSlot InPath = new CGModuleInputSlot();
 
    [HideInInspector]
    [OutputSlotInfo(typeof(CGPath))]
    public CGModuleOutputSlot OutPath = new CGModuleOutputSlot();
 
    // ... serialized fields, properties, Refresh() ...
}

Editor Script

[CustomEditor(typeof(MyCustomModule))]
public class MyCustomModuleEditor : CGModuleEditor<MyCustomModule>
{
    // Optional overrides for scene GUI and debug display
}
Every CG module must have a matching editor script inheriting CGModuleEditor<T>. Without it, the module will not display correctly in the graph UI.

The [ModuleInfo] attribute on the module class defines how it appears in the generator:

[ModuleInfo("Build/My Module", ModuleName = "My Module", Description = "Does something")]
Property Description
MenuName (constructor param) Menu path in the Add menu. Use / for submenus (e.g. “Build/My Module”).
ModuleName Display name on the module node. If omitted, derived from MenuName.
Description Tooltip shown in the Add menu.
UsesRandom Set to true if the module uses Unity's Random — shows Seed options in the inspector.

Modules are auto-discovered via reflection. No manual registration is needed.

Slots are public fields of type CGModuleInputSlot or CGModuleOutputSlot, annotated with slot info attributes.

Input Slots

[HideInInspector]
[InputSlotInfo(typeof(CGPath), Name = "Path", RequestDataOnly = true)]
public CGModuleInputSlot InPath = new CGModuleInputSlot();

Output Slots

[HideInInspector]
[OutputSlotInfo(typeof(CGPath), Name = "Path", DisplayName = "Rasterized Path")]
public CGModuleOutputSlot OutPath = new CGModuleOutputSlot();

Slot Info Properties

Property Default Description
DataType (constructor) The CGData subclass this slot accepts/produces. Required.
Name Field name Internal name used for serialization and linking.
DisplayName Name Name shown in the graph UI.
Tooltip null Hover tooltip on the slot.
Array false Whether the slot accepts/produces an array of data.
ArrayType Normal Normal = multi-link array. Hidden = array but single-link in UI.

InputSlotInfo-specific

Property Default Description
RequestDataOnly false Slot requests data from on-request modules. See Advanced Concepts.
Optional false Slot does not need to be linked for the module to be configured.
ModifiesData false Module alters the input data. The framework will clone data before passing it, so the original stays intact.

Slot Compatibility

An output slot can connect to an input slot only if:

  • The output's DataType matches or is a subtype of the input's DataType.
  • The on-request compatibility is satisfied (see Advanced Concepts).

Choose one of three processing strategies. See Advanced Concepts for full details.

Normal Module (default)

Override Refresh(). Called each generator pass for dirty modules.

public override void Refresh()
{
    base.Refresh();
    CGPath path = InPath.GetData<CGPath>(out bool isDisposable);
    if (path == null)
    {
        OutPath.ClearData();
        return;
    }
    // ... process path ...
    OutPath.SetDataToElement(result);
}

On-Request Module

Implement IOnRequestProcessing. Replace Refresh() with OnSlotDataRequest().

public class MyModule : CGModule, IOnRequestProcessing
{
    public CGData[] OnSlotDataRequest(
        CGModuleInputSlot requestedBy,
        CGModuleOutputSlot requestedSlot,
        params CGDataRequestParameter[] requests)
    {
        CGDataRequestRasterization raster =
            GetRequestParameter<CGDataRequestRasterization>(ref requests);
        // ... compute data based on requests ...
        return new CGData[] { result };
    }
}

No-Processing Module

Implement INoProcessing. No data processing — used for utility modules like the Note module.

public class MyModule : CGModule, INoProcessing { }

Inside Refresh() (or OnSlotDataRequest() for on-request modules), read data from input slots:

Single data

CGPath path = InPath.GetData<CGPath>(out bool isDisposable);

Array data

List<CGVMesh> meshes = InVMeshArray.GetAllData<CGVMesh>(out bool isDisposable);

With request parameters (for on-request upstreams)

CGPath path = InPath.GetData<CGPath>(
    out bool isDisposable,
    new CGDataRequestRasterization(from, length, resolution, angle, mode)
);

The isDisposable output tells you whether you own the returned data and should dispose it when done. See Advanced Concepts.

Set your output slot's data using one of these methods:

Method Usage
SetDataToElement(data) Single-element output (most common).
SetDataToCollection(array) Multi-element output (for array slots).
ClearData() No output (module is not configured or has nothing to produce).

When you call any of these, the previous data on the slot is automatically disposed.

Modules exchange large arrays (positions, vertices, triangles) through CGData subclasses. To avoid garbage collection:

  • Use pooled SubArray<T> for large arrays (allocate via ArrayPools.Vector3.Allocate(count), free in Dispose(bool)).
  • Dispose data that was returned as disposable (isDisposable == true).
  • Don't dispose data that is a direct reference to another module's internal output.

See Advanced Concepts for full details.

Custom Data Types

If you need a new data type:

  • Inherit from the most appropriate base (CGData, CGShape, CGPath, etc.).
  • Decorate with [CGDataInfo(r, g, b)] for a color in the graph UI.
  • Override Dispose(bool) to free pooled arrays.
  • If using IL2CPP, add the type to link.xml.

See Data Types for the full type hierarchy.

Use DevTools attributes on your serialized fields to control the inspector layout:

Attribute Purpose
[Tab(“Name”)] Groups fields under a tab.
[Section(“Name”)] Collapsible section.
[FloatRegion] Min/max float slider.
[RangeEx] Float/int slider with label and tooltip.
[FieldCondition] Show/hide field based on another field's value.

* Ask on the forum if you get stuck.