Odin Inspector & Serializer
Odin Inspector (Sirenix) extends Unity's inspector with powerful serialization (dictionaries, interfaces, polymorphic types), validation attributes, layout control, and custom editor window tools. It dramatically reduces the need for custom editors.
Serialization — SerializedMonoBehaviour & SerializedScriptableObject
Unity's default serializer cannot handle dictionaries, interfaces, abstract classes, or deeply nested polymorphic types. Odin's serializer can.
using Sirenix.OdinInspector;
using Sirenix.Serialization;
// Inherit from SerializedMonoBehaviour instead of MonoBehaviour
public class EnemyData : SerializedMonoBehaviour
{
// Dictionary — just works in the inspector
public Dictionary<string, int> Stats = new Dictionary<string, int>
{
{ "Health", 100 },
{ "Attack", 15 },
{ "Defense", 8 },
};
// Interface field — shows a polymorphic dropdown
public IAbility PrimaryAbility;
public List<IAbility> AbilityPool;
// Nested complex types
public Dictionary<DamageType, List<StatusEffect>> DamageEffects;
}
// For ScriptableObjects
public class GameConfig : SerializedScriptableObject
{
public Dictionary<string, EnemyWaveConfig> Waves;
public Dictionary<ItemRarity, Color> RarityColors;
}
When to Use Odin Serialization vs Unity Serialization
| Use Odin Serialization | Use Unity Serialization | |------------------------|------------------------| | Dictionaries, interfaces, polymorphism | Simple fields (int, float, string, Vector3) | | Editor-time configuration data | Runtime hot-path data | | Complex nested structures | Arrays/Lists of concrete types | | Tool/editor windows | Prefabs and ScriptableObjects that must work without Odin |
IMPORTANT: Migration Caution
Projects using SerializedMonoBehaviour or SerializedScriptableObject are coupled to Odin. If Odin is removed from the project, all data stored via Odin's extended serialization is permanently lost. Unity only reads its own serialized data. Plan accordingly.
Validation Attributes
Catch configuration errors in the inspector before they become runtime bugs.
Required Fields
public class UIManager : MonoBehaviour
{
[Required]
[SerializeField] private Canvas _mainCanvas;
[Required("Assign the health bar prefab!")]
[SerializeField] private GameObject _healthBarPrefab;
}
Value Validation
public class EnemyConfig : ScriptableObject
{
[MinValue(1)]
public int Health = 100;
[MaxValue(100)]
public int SpawnChance = 50;
[MinValue(0), MaxValue(1)]
public float CriticalChance = 0.1f;
[PropertyRange(0, 10)]
public float MoveSpeed = 3f;
[ValidateInput("IsPositive", "Damage must be positive")]
public int Damage = 10;
private bool IsPositive(int value) => value > 0;
}
Asset Constraints
[AssetsOnly]
public GameObject EnemyPrefab; // Only accepts project assets, not scene objects
[SceneObjectsOnly]
public Transform SpawnPoint; // Only accepts objects in the scene
[ChildGameObjectsOnly]
public Transform WeaponMount; // Only accepts children of this object
Custom Validation
[ValidateInput("ValidateWaveConfig", "Wave must have at least one enemy type")]
public WaveConfig CurrentWave;
private bool ValidateWaveConfig(WaveConfig config)
{
return config != null && config.EnemyTypes.Count > 0;
}
Layout Attributes
Organize inspector fields into groups, tabs, and collapsible sections.
Box Group
[BoxGroup("Movement")]
public float MoveSpeed = 5f;
[BoxGroup("Movement")]
public float JumpHeight = 2f;
[BoxGroup("Combat")]
public int Damage = 10;
[BoxGroup("Combat")]
public float AttackRange = 1.5f;
Tab Group
[TabGroup("General")]
public string DisplayName;
[TabGroup("General")]
public Sprite Icon;
[TabGroup("Stats")]
public int Health = 100;
[TabGroup("Stats")]
public int Attack = 15;
[TabGroup("Audio")]
public AudioClip HitSound;
[TabGroup("Audio")]
public AudioClip DeathSound;
Foldout Group
[FoldoutGroup("Advanced Settings")]
public float GravityMultiplier = 1f;
[FoldoutGroup("Advanced Settings")]
public bool UseCustomPhysics;
[FoldoutGroup("Advanced Settings")]
public LayerMask CollisionLayers;
Horizontal and Vertical Groups
[HorizontalGroup("Row1")]
public int Health;
[HorizontalGroup("Row1")]
public int Mana;
[HorizontalGroup("Row1")]
public int Stamina;
// Nested groups
[VerticalGroup("Row2/Left")]
public float Speed;
[VerticalGroup("Row2/Left")]
public float JumpForce;
[VerticalGroup("Row2/Right")]
public int Armor;
[VerticalGroup("Row2/Right")]
public int MagicResist;
Title and Header
[Title("Player Configuration", "Adjust these values for game balance")]
public float MoveSpeed;
[Title("Visual Settings", bold: true, horizontalLine: true)]
public Color TrailColor;
Display Attributes — Conditional Visibility
Show/Hide Based on Conditions
public bool UseCustomGravity;
[ShowIf("UseCustomGravity")]
public float CustomGravity = 9.81f;
[HideIf("UseCustomGravity")]
public string DefaultGravityNote = "Using Physics.gravity";
// Method-based condition
public EnemyType Type;
[ShowIf("IsBoss")]
public float EnrageThreshold = 0.3f;
private bool IsBoss() => Type == EnemyType.Boss;
Enable/Disable
[EnableIf("IsDebugMode")]
public bool ShowHitboxes;
[DisableIf("IsReleaseMode")]
public string DebugCommand;
public bool IsDebugMode;
private bool IsReleaseMode() => !IsDebugMode;
Read Only
[ReadOnly]
public string GeneratedId;
[DisplayAsString]
public string InfoMessage = "This is display-only text";
// Show in inspector but cannot edit
[ShowInInspector, ReadOnly]
public int CalculatedDPS => Damage * AttacksPerSecond;
Preview and Inline Editor
[PreviewField(75)]
public Sprite CharacterIcon;
[PreviewField(100, ObjectFieldAlignment.Left)]
public Texture2D SplashArt;
// Show full inspector of referenced ScriptableObject inline
[InlineEditor]
public EnemyConfig EnemySettings;
[InlineEditor(InlineEditorModes.FullEditor)]
public Material EffectMaterial;
Button Attribute
Turn methods into inspector buttons for quick testing and tooling.
public class LevelBuilder : MonoBehaviour
{
public int Width = 10;
public int Height = 10;
[Button("Generate Level", ButtonSizes.Large)]
private void GenerateLevel()
{
// Level generation logic
Debug.Log($"Generating {Width}x{Height} level");
}
[Button]
private void ClearLevel()
{
// Clear all tiles
}
[ButtonGroup("Actions")]
private void Save() { /* ... */ }
[ButtonGroup("Actions")]
private void Load() { /* ... */ }
[ButtonGroup("Actions")]
private void Reset() { /* ... */ }
// Button with parameters
[Button]
private void SpawnEnemies(EnemyType type, int count)
{
for (int i = 0; i < count; i++)
SpawnEnemy(type);
}
}
Color and Enum Enhancements
// Color palette picker
[ColorPalette("My Palette")]
public Color TeamColor;
// Enum as toggle buttons instead of dropdown
[EnumToggleButtons]
public Direction Facing;
// Custom value dropdown from method
[ValueDropdown("GetAvailableItems")]
public string SelectedItem;
private IEnumerable<string> GetAvailableItems()
{
return new[] { "Sword", "Shield", "Potion", "Bow" };
}
// Dropdown with custom display names
[ValueDropdown("GetDifficultyOptions")]
public int DifficultyLevel;
private IEnumerable<ValueDropdownItem<int>> GetDifficultyOptions()
{
return new[]
{
new ValueDropdownItem<int>("Easy", 1),
new ValueDropdownItem<int>("Normal", 2),
new ValueDropdownItem<int>("Hard", 3),
new ValueDropdownItem<int>("Nightmare"