NUI Editor Framework
NWH User Interface (NUI) is a custom editor framework that gives inspectors consistent styling across all NWH assets.
Overview
NUI provides:
- Titled, indented subsections for grouping related fields
- Tab-based organization, with the selected tab remembered per component type
- Consistent styling across all NWH packages
- Tooltips for all fields
- Custom property drawers
- Layout that adapts to inspector width
All NWH component inspectors use NUI.
NUI draws only the fields an editor explicitly asks for. A serialized field that no
drawer.Field() call names does not appear in the inspector at all, which is the usual reason a
newly added field seems to be missing.
NUI Components
NUIEditor
Base class for custom editors. Override OnInspectorNUI(), call the base implementation first, draw
the fields, then call drawer.EndEditor(this).
OnInspectorNUI() returns bool and the base implementation always returns true. The
if (!base.OnInspectorNUI()) return false; line is the house pattern and is kept for consistency,
not because the header can collapse the body - Unity's own component foldout does that.
Partial editor example (the inspected component is omitted):
using NWH.NUI;
using UnityEditor;
[CustomEditor(typeof(MyComponent))]
public class MyComponentEditor : NUIEditor
{
public override bool OnInspectorNUI()
{
if (!base.OnInspectorNUI())
return false;
drawer.BeginSubsection("Main Settings");
drawer.Field("fieldName");
drawer.EndSubsection();
drawer.BeginSubsection("Advanced");
drawer.Field("advancedField");
drawer.EndSubsection();
drawer.EndEditor(this);
return true;
}
}
NUIPropertyDrawer
Base class for custom property drawers.
Features:
- Consistent styling with NUIEditor
- Tooltip support
- Flexible layout
Partial property-drawer example (the MyStruct declaration is omitted):
using NWH.NUI;
using UnityEditor;
using UnityEngine;
[CustomPropertyDrawer(typeof(MyStruct))]
public class MyStructDrawer : NUIPropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
drawer.BeginProperty(position, property, label);
drawer.Field("field1");
drawer.Field("field2");
drawer.EndProperty();
}
}
Creating Custom Editors
Basic Editor
Partial editor example (the inspected VehicleComponent and its fields are omitted):
using UnityEditor;
using NWH.NUI;
[CustomEditor(typeof(VehicleComponent))]
public class VehicleComponentEditor : NUIEditor
{
public override bool OnInspectorNUI()
{
if (!base.OnInspectorNUI())
return false;
// Start drawing
drawer.BeginSubsection("Physics");
drawer.Field("mass");
drawer.Field("drag");
drawer.EndSubsection();
drawer.BeginSubsection("Visuals");
drawer.Field("meshRenderer");
drawer.Field("material");
drawer.EndSubsection();
drawer.EndEditor(this);
return true;
}
}
Editor with Tabs
drawer.HorizontalToolbar(name, texts) draws the tab row and returns the selected index. The
selection is stored in EditorPrefs under a key scoped to the inspected type, so two components
that use the same tab name keep separate selections. NUIDrawer.SetTabIndex(name, value) is static
and has no type to scope with, so it writes the unscoped key; that value is read only while the
drawer has no scoped entry of its own, which means it applies until the user clicks a tab.
Partial OnInspectorNUI() implementation (the containing editor class is omitted):
public override bool OnInspectorNUI()
{
if (!base.OnInspectorNUI())
return false;
// Create tabs
drawer.BeginSubsection("Settings");
int tabIndex = drawer.HorizontalToolbar("settingsTab",
new string[] { "General", "Physics", "Audio" });
switch (tabIndex)
{
case 0: // General
drawer.Field("componentName");
drawer.Field("enabled");
break;
case 1: // Physics
drawer.Field("mass");
drawer.Field("drag");
break;
case 2: // Audio
drawer.Field("audioSource");
drawer.Field("volume");
break;
}
drawer.EndSubsection();
drawer.EndEditor(this);
return true;
}
Conditional Fields
Partial inspector-body example (place this after a successful base.OnInspectorNUI() call):
drawer.BeginSubsection("Engine");
drawer.Field("engineEnabled");
// Only show engine settings if enabled
if (drawer.FindProperty("engineEnabled").boolValue)
{
drawer.Field("horsePower");
drawer.Field("maxRPM");
}
drawer.EndSubsection();
Drawer Methods
Fields
Partial inspector-body examples:
// Draw property field
drawer.Field("propertyName");
// Field with custom label
drawer.Field("propertyName", alternateLabel: "Custom Label");
// Disabled field (the second parameter controls whether editing is enabled)
drawer.Field("propertyName", enabled: false);
// Nested property
drawer.Field("parent.child.propertyName");
Buttons
Partial inspector-body examples (DoSomething() and PerformAction() are application methods):
// Button
if (drawer.Button("Click Me"))
{
// Button clicked
DoSomething();
}
// Button with a custom GUIStyle
GUIStyle actionStyle = new(EditorStyles.miniButton)
{
fontStyle = FontStyle.Bold
};
if (drawer.Button("Action", actionStyle))
{
PerformAction();
}
Labels and Info
Partial inspector-body examples:
// Info box
drawer.Info("This is an information message");
// Warning box
drawer.Info("This is a warning", MessageType.Warning);
// Error box
drawer.Info("This is an error", MessageType.Error);
// Label
drawer.Label("Status: Active");
// Bold label on a tinted strip
drawer.Title("Configuration");
drawer.Header() is the component header that NUIEditor draws for you. Do not call it from an
editor body; use Title or BeginSubsection there.
Layout
Partial inspector-body examples:
// Space
drawer.Space(10f);
// Horizontal separator line
drawer.HorizontalRuler();
Subsections
Partial inspector-body example:
// Titled subsection
drawer.BeginSubsection("Section Name");
drawer.Field("field1");
drawer.Field("field2");
drawer.EndSubsection();
Property Attributes
NUI supports custom attributes for additional control:
Tooltip Attribute
Partial component field declaration:
[Tooltip("Maximum speed in m/s")]
public float maxSpeed = 50f;
Automatically displayed in NUI editors.
Range Attribute
Partial component field declaration:
[Range(0, 100)]
public float volume = 50f;
Creates slider in inspector.
Header Attribute
Do not use Unity's [Header]. NUI positions each field in a rect sized from
EditorGUI.GetPropertyHeight, which does not account for decorator attributes, so a header
attribute has no space reserved for it. Group fields with drawer.BeginSubsection() /
drawer.EndSubsection() instead. No NWH component uses [Header].
Styling
Layout constants and the header colors live in the static NUISettings class. The values that
matter when writing a custom editor:
| Member | Value | Meaning |
|---|---|---|
fieldHeight |
23 | Default height of one inspector row, in pixels |
fieldSpacing |
3 | Vertical gap between rows, in pixels |
textMargin |
2 | Margin around text elements, in pixels |
editorHeaderColor |
RGB 20, 125, 211 | Header bar of a MonoBehaviour editor |
scriptableObjectHeaderColor |
RGB 220, 122, 32 | Header bar of a ScriptableObject editor |
propertyHeaderColor |
RGB 78, 152, 213 | Header bar of a property drawer |
Info, warning and error boxes are Unity's EditorGUI.HelpBox and follow the editor theme.
Best Practices
- Group related properties in subsections.
- Give every public field a tooltip.
- Consider tabs if the editor has >3 sections.
- Hide fields that are irrelevant to the current settings.
- Use descriptive field names and custom labels.
- Follow NWH naming conventions.
- Add a
drawer.Field()call for each new serialized field. NUI shows only what the editor names.
Example: Complete Component Editor
This complete editor script targets NWH.WheelController3D.WheelController:
using NWH.NUI;
using NWH.WheelController3D;
using UnityEditor;
[CustomEditor(typeof(WheelController))]
public class CustomWheelControllerEditor : NUIEditor
{
public override bool OnInspectorNUI()
{
if (!base.OnInspectorNUI())
return false;
// Tabs for major categories
int tabIndex = drawer.HorizontalToolbar("mainTab",
new string[] { "Wheel", "Suspension", "Friction", "Debug" });
switch (tabIndex)
{
case 0: // Wheel
DrawWheelTab();
break;
case 1: // Suspension
DrawSuspensionTab();
break;
case 2: // Friction
DrawFrictionTab();
break;
case 3: // Debug
DrawDebugTab();
break;
}
drawer.EndEditor(this);
return true;
}
private void DrawWheelTab()
{
drawer.BeginSubsection("Wheel Properties");
drawer.Field("wheel.radius");
drawer.Field("wheel.width");
drawer.Field("wheel.mass");
drawer.EndSubsection();
drawer.BeginSubsection("Visuals");
drawer.Field("wheel.rotatingContainer");
drawer.Field("wheel.nonRotatingContainer");
drawer.EndSubsection();
}
private void DrawSuspensionTab()
{
drawer.BeginSubsection("Spring");
drawer.Field("spring.maxForce");
drawer.Field("spring.maxLength");
drawer.Field("spring.progressiveness");
drawer.EndSubsection();
drawer.BeginSubsection("Damper");
drawer.Field("damper.bumpRate");
drawer.Field("damper.reboundRate");
drawer.EndSubsection();
}
private void DrawFrictionTab()
{
drawer.BeginSubsection("Friction");
drawer.Field("activeFrictionPreset");
if (drawer.Button("Create New Preset"))
{
// Create friction preset logic
}
drawer.EndSubsection();
}
private void DrawDebugTab()
{
WheelController wheelController = (WheelController)target;
drawer.BeginSubsection("Debug Info");
drawer.Info($"Is Grounded: {wheelController.isGrounded}");
drawer.Label($"Spring Force: {wheelController.spring.forceMagnitude:F1} N");
drawer.Label(
$"Slip: {wheelController.frictionSlip.x:F2}, {wheelController.frictionSlip.y:F2}");
drawer.EndSubsection();
}
}
Extending NUI
Custom Drawer Helper
Create reusable drawer methods:
Partial extension example (the surrounding editor script imports UnityEditor):
public static class NUIExtensions
{
public static SerializedProperty CurveField(this NUIDrawer drawer,
string propertyPath, string label = null)
{
return drawer.Field(propertyPath, alternateLabel: label);
}
}
// Usage
drawer.CurveField("torqueCurve");
Custom Property Drawer
Partial property-drawer example (the Wheel type and PreviewWheel() helper are omitted):
[CustomPropertyDrawer(typeof(Wheel))]
public class WheelDrawer : NUIPropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
drawer.BeginProperty(position, property, label);
drawer.Field("radius");
drawer.Field("width");
// Custom visualization
if (drawer.Button("Preview Wheel"))
{
PreviewWheel(property);
}
drawer.EndProperty();
}
}
Troubleshooting
Editor not displaying:
- Ensure script compiles without errors
- Check
[CustomEditor(typeof(YourClass))]attribute - Verify class inherits from NUIEditor
Fields not showing:
- Check property name matches exactly (case-sensitive)
- Ensure property is serializable
- Verify property is public or has
[SerializeField]
Sections not collapsing:
BeginSubsection()creates a titled, indented group; it does not create a foldout.- The component header created by
NUIEditoris the inspector's collapsible section.
A tab selection made from script does nothing:
SetTabIndexwrites the unscoped preference key. Once the user has clicked a tab on that component type, the scoped key exists and wins.
Tooltips not working:
- Add
[Tooltip("...")]attribute to field - Draw the serialized field with
drawer.Field("field"); Unity uses the property's tooltip.
API Reference
Key classes in the NWH.NUI namespace (editor-only):
NUIEditor- Base custom editor classNUIDrawer- Drawing utilities for inspector elementsNUIPropertyDrawer- Base property drawer class