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:
| 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.
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. }
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.
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();
| 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. |
| 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.
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(); }
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 }; } }
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):
CGPath path = InPath.GetData<CGPath>(out bool isDisposable);
List<CGVMesh> meshes = InVMeshArray.GetAllData<CGVMesh>(out bool isDisposable);
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:
CGData, CGShape, CGPath, etc.).[CGDataInfo(r, g, b)] to define the associated color in the CG Editor.Dispose(bool) to free them.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}"); } }
ModifierTRSMesh is a simple normal module. ConformPath is a simple on-request module. Use them as reference.ArrayPools.Vector3.Allocate(count) etc. for pooling and free them in Dispose(bool) of custom CGData subclasses.