Creating Custom CG Modules

This page explains how to write your own CG modules. Before reading, make sure you understand CG's Advanced Concepts.

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.
Field Description
Class Name C# class name (e.g. MyModule)
Module Name Module name in UI (e.g. My Module)
Menu Name Path in the CG Editor's Add menu, using / as separator (e.g. Custom/My Module)
Description A description of the module

The wizard will generate two files under your customization folder. These files are auto-discovered via reflection. No manual registration is needed.

Runtime Script

The runtime script (e.g. MyModule.cs) defines the behaviour of the module.

[ModuleInfo("Custom/My Module", ModuleName = "My Module", Description = "Does something")]
public class MyModule : CGModule
{
 
    // Input and/or output slots, serialized fields, Refresh(), etc.
}

Editor Script

The editor script (e.g. Editor/MyModuleEditor.cs) defines the UI of the module.

[CustomEditor(typeof(MyModule))]
public class MyModuleEditor : CGModuleEditor<MyModule>
{
    // 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 Curvy Generator Editor.

Once your module is created, you will most probably need to define its slots.

Slots are defined by public fields of type CGModuleInputSlot or CGModuleOutputSlot, annotated with slot info attributes. You will need to associate each slot with a data type.

Examples of input and output slots:

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

Slot Info Properties

Property Default Description
DataType - The CGData subclass this slot accepts/produces. Required.
Name Field name Internal name used for serialization and linking.
DisplayName Name Name shown in the 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. When set to false, the module will clone the data before passing it, so the original stays intact.

Similar to Inspectors, module UIs show serialized fields as Settings.

When a module setting changes, set Dirty = true so the generator knows to reprocess the module:

[SerializeField]
private int m_Resolution = DefaultResolution;
 
public int Resolution
{
    get => m_Resolution;
    set
    {
        if (m_Resolution != value)
        {
            m_Resolution = value;
            Dirty = true;
        }
    }
}

You can add one of many attributes on your serialized fields to control their display. This allows for module UI customization without having to write complex code in the module's editor script. Such attributes are:

Attribute Purpose
[Tab] Groups fields under a tab.
[Section] Groups fields under a collapsible section.
[RangeEx] Show a float/int slider.
[FieldCondition] Shows/hides the field based on some condition.
[Label] Sets a label and optionally a tooltip.

Now is the time to define the module's behaviour.

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);
 
    // data processing
 
    // writing output data if any (see section below)
 
    if (isDisposable)
        path.Dispose();
}

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

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

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

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.

To set your output slot's data, use one of these methods:

Method Usage
SetDataToElement(data) Single-element output (most common).
SetDataToCollection(array) Multi-element output (for array slots).
ClearData() Empty 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.

If you need a custom data type:

  • Inherit from the most appropriate base (CGData, CGShape, CGPath, etc.).
  • Decorate with [CGDataInfo(r, g, b)] to define the associated color in the CG Editor.
  • If allocating pooled arrays, override Dispose(bool) to free them.
  • Add the type to link.xml (see Managed code stripping) to avoid it being stripped from builds.

The editor script controls the module's visual feedback. Override these methods as needed:

Method When Called
OnModuleSceneGUI() Every Scene repaint. Use for custom scene handles.
OnModuleSceneDebugGUI() Scene repaint when Show Debug Visuals is active. Set HasDebugVisuals = true in OnEnable() to enable.
OnModuleDebugGUI() Inspector repaint. Use for displaying data stats.
OnCustomInspectorGUI() After the default inspector draws. Use for extra inspector UI.
OnReadNodes() When the inspector node tree is built. Use to add/remove tabs or sections dynamically.

Example - showing point count in the inspector debug panel:

[CustomEditor(typeof(MyModule))]
public class MyModuleEditor : CGModuleEditor<MyModule>
{
    public override void OnModuleDebugGUI()
    {
        if (Target.OutPath.Data.Length == 0)
            return;
        EditorGUILayout.LabelField($"Points: {Target.OutPath.Data[0].Count}");
    }
}
  • Read existing module implementations. ModifierTRSMesh is a simple normal module. ConformPath is a simple on-request module. Use them as reference.
  • Pool large arrays. For performance reasons, use ArrayPools.Vector3.Allocate(count) etc. for pooling and free them in Dispose(bool) of custom CGData subclasses.
  • Ask on the forum or read the API reference if you get stuck.