How To Make A Custom Group


Odin comes with many built-in group attributes. You can check them out in the attribute overview.

The goal of this guide is to create a ColoredFoldoutGroupAttribute that can be used on any field, property or method and can be combined with Odin's other group attributes using group paths.

The first thing we need is to define the group attribute itself. We'll create a ColoredFoldoutGroupAttribute and inherit from the PropertyGroupAttribute class. The PropertyGroupAttribute is a special attribute that must be derived from to create proper group attributes.

public class ColoredFoldoutGroupAttribute : PropertyGroupAttribute

Unfortunately, C# does not allow passing custom structs directly to attribute constructors, so we cannot use Unity's Color struct. Instead, we define 4 fields for the red, green, blue and alpha channels.

We will also define two constructors. One of the constructors will only take a path. This lets the user specify the colour just once on a single attribute, and then all other ColoredFoldoutGroupAttributes with the same path will be merged together to take that colour value.

public float R, G, B, A;

public ColoredFoldoutGroupAttribute(string path)
    : base(path)
{
}

public ColoredFoldoutGroupAttribute(string path, float r, float g, float b, float a = 1f)
    : base(path)
{
    this.R = r;
    this.G = g;
    this.B = b;
    this.A = a;
}

Finally, we need to implement the PropertyGroupAttribute's CombineValuesWith method. This method is called for all attributes with the same path, and this is where we will handle merging the colour values as mentioned earlier.

protected override void CombineValuesWith(PropertyGroupAttribute other)
{
    var otherAttr = (ColoredFoldoutGroupAttribute)other;

    this.R = Math.Max(otherAttr.R, this.R);
    this.G = Math.Max(otherAttr.G, this.G);
    this.B = Math.Max(otherAttr.B, this.B);
    this.A = Math.Max(otherAttr.A, this.A);
}

With that our new group attribute is done. Now let's make the group drawer. Create a new class and name it ColoredFoldoutGroupAttributeDrawer. This class will be utilizing editor code so remember to make sure it will only be included in editor compilations. For example, do this by placing the file in an Editor folder or wrapping its contents in an #if UNITY_EDITOR preprocessor directive.

The ColoredFoldoutGroupAttributeDrawer should inherit the OdinGroupDrawer<TGroupAttribute> class.

public class ColoredFoldoutGroupAttributeDrawer : OdinGroupDrawer<ColoredFoldoutGroupAttribute>

Since this is supposed to have a foldout, we'll need a foldout state. We could just add a bool field and be done with it, but then that would be reset everytime the drawer is instantiated. Wouldn't it be nicer to have a persistent foldout state? We can achieve this by getting a LocalPersistentContext<T> from Odin's persistence system. If enabled, that will save the state of the drawer between Unity reloads and even opening and closing the editor.

private LocalPersistentContext<bool> isExpanded;

protected override void Initialize()
{
    this.isExpanded = this.GetPersistentValue<bool>(
		"ColoredFoldoutGroupAttributeDrawer.isExpanded",
		GeneralDrawerConfig.Instance.ExpandFoldoutByDefault);
}

Great! Let's move on to implementing the actual group drawer. We need to override the DrawPropertyLayout method for this.

protected override void DrawPropertyLayout(GUIContent label)
{
}

We will get the colour from the attribute attached to the drawer, and we can use the methods from SirenixEditorGUI to draw the foldout box. We will also use GUIHelper to push our colour to GUI.color. And as soon as we have made the draw call to both BeginBox and BeginBoxHeader we will pop the colour again. This way, only our foldout box will be coloured.

GUIHelper.PushColor(new Color(this.Attribute.R, this.Attribute.G, this.Attribute.B, this.Attribute.A));
SirenixEditorGUI.BeginBox();
SirenixEditorGUI.BeginBoxHeader();
GUIHelper.PopColor(); 

Now we will draw the foldout control inside the box header and also end the box header.

this.isExpanded.Value = SirenixEditorGUI.Foldout(this.isExpanded.Value, label);
SirenixEditorGUI.EndBoxHeader();

Finally, we will draw all child properties of this group in a special fade group that will animate nicely, and then end our box.

if (SirenixEditorGUI.BeginFadeGroup(this, this.isExpanded.Value))
{
    for (int i = 0; i < this.Property.Children.Count; i++)
    {
        this.Property.Children[i].Draw();
    }
}
SirenixEditorGUI.EndFadeGroup();
SirenixEditorGUI.EndBox();

And that should do it. That should cover how you can create custom group attributes yourself and implement them in Odin's drawer system. You can try and play around with this, and see what you can make.


Attribute:

using System;
using Sirenix.OdinInspector;

public class ColoredFoldoutGroupAttribute : PropertyGroupAttribute
{
	public float R, G, B, A;

	public ColoredFoldoutGroupAttribute(string path)
		: base(path)
	{
	}

	public ColoredFoldoutGroupAttribute(string path, float r, float g, float b, float a = 1f)
		: base(path)
	{
		this.R = r;
		this.G = g;
		this.B = b;
		this.A = a;
	}

	protected override void CombineValuesWith(PropertyGroupAttribute other)
	{
		var otherAttr = (ColoredFoldoutGroupAttribute)other;

		this.R = Math.Max(otherAttr.R, this.R);
		this.G = Math.Max(otherAttr.G, this.G);
		this.B = Math.Max(otherAttr.B, this.B);
		this.A = Math.Max(otherAttr.A, this.A);
	}
}

Drawer:

using Sirenix.OdinInspector.Editor;
using Sirenix.Utilities.Editor;
using UnityEngine;

public class ColoredFoldoutGroupAttributeDrawer : OdinGroupDrawer<ColoredFoldoutGroupAttribute>
{
	private LocalPersistentContext<bool> isExpanded;

	protected override void Initialize()
	{
		this.isExpanded = this.GetPersistentValue<bool>(
			"ColoredFoldoutGroupAttributeDrawer.isExpanded",
			GeneralDrawerConfig.Instance.ExpandFoldoutByDefault);
	}

	protected override void DrawPropertyLayout(GUIContent label)
	{
		GUIHelper.PushColor(new Color(this.Attribute.R, this.Attribute.G, this.Attribute.B, this.Attribute.A));
		SirenixEditorGUI.BeginBox();
		SirenixEditorGUI.BeginBoxHeader();
		GUIHelper.PopColor(); 
		this.isExpanded.Value = SirenixEditorGUI.Foldout(this.isExpanded.Value, label);
		SirenixEditorGUI.EndBoxHeader();

		if (SirenixEditorGUI.BeginFadeGroup(this, this.isExpanded.Value))
		{
			for (int i = 0; i < this.Property.Children.Count; i++)
			{
				this.Property.Children[i].Draw();
			}
		}

		SirenixEditorGUI.EndFadeGroup();
		SirenixEditorGUI.EndBox();
	}
}