Creating Custom CG Modules
This page explains how to write your own CG modules. Before reading, make sure you understand CG's Advanced Concepts.
Creating Module Files
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.
| 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 }
CGModuleEditor<T>. Without it, the module will not display correctly in the Curvy Generator Editor.
Defining Slots
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. |
Defining Module Settings
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. |
Defining Processing Types
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 { }
Reading Input Data
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.
Writing Output Data
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.
Using Custom Data Types
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.
Customizing Module UI
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}"); } }
Tips
- Read existing module implementations.
ModifierTRSMeshis a simple normal module.ConformPathis 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 inDispose(bool)of custom CGData subclasses. - Ask on the forum or read the API reference if you get stuck.