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).
Quick Start
The fastest way to start is the module wizard:
- In Unity's Project window, right-click → Create → Curvy → CG Module.
- Fill in the four fields and click Create.
- 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.
Module Anatomy
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 }
CGModuleEditor<T>. Without it, the module will not display correctly in the graph UI.
The [ModuleInfo] Attribute
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.
Defining Slots
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).
Module Processing Types
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 { }
Reading Input Data
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.
Writing Output Data
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.
Data & Disposal
Modules exchange large arrays (positions, vertices, triangles) through CGData subclasses. To avoid garbage collection:
- Use pooled
SubArray<T>for large arrays (allocate viaArrayPools.Vector3.Allocate(count), free inDispose(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.
Serialized Fields & Inspector
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.