Skip to content

Instantly share code, notes, and snippets.

@JonahGrimm
Last active June 14, 2023 20:35
Show Gist options
  • Save JonahGrimm/accc0f3c8ca60456bde745a99b950d78 to your computer and use it in GitHub Desktop.
Save JonahGrimm/accc0f3c8ca60456bde745a99b950d78 to your computer and use it in GitHub Desktop.
The following is a basic example of Polymorphic Serialization in Unity. It uses Food as the concept for a base class and the derived types are different types of foods. It is complete with a property drawer so that it is easy to work with. Be sure to remove the $ from the first file's name. That's just for sorting.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PolymorphicSerializationExample : MonoBehaviour
{
[SerializeReference] public List<Food> classes = new List<Food>();
#if UNITY_EDITOR
private void OnValidate()
{
for (int i = 0; i < classes.Count; i++)
{
if (classes[i] == null)
{
classes[i] = new AppleFood();
UnityEditor.EditorUtility.SetDirty(this);
}
}
}
#endif
}
using System;
using UnityEngine;
[Serializable]
public abstract class Food
{
public enum FoodType
{
Apple,
Orange,
Grape
}
public FoodType Type;
}
public class AppleFood : Food
{
public int AppleCount;
public AppleFood()
{
Type = FoodType.Apple;
}
}
public class OrangeFood : Food
{
public float OrangeSize;
public OrangeFood()
{
Type = FoodType.Orange;
}
}
public class GrapeFood : Food
{
public Color GrapeColor;
public GrapeFood()
{
Type = FoodType.Grape;
}
}
using System.Collections;
using System.Collections.Generic;
using System;
using UnityEngine;
using UnityEditor;
using System.Linq;
using System.Text.RegularExpressions;
[CustomPropertyDrawer(typeof(Food))]
public class FoodPropertyDrawer : PropertyDrawer
{
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginProperty(position, label, property);
Rect systemRect = new Rect(position.x, position.y, position.width - 25, EditorGUIUtility.singleLineHeight);
PolymorphicSerializationExample script = (PolymorphicSerializationExample)property.serializedObject.targetObject;
int elementIndex = ExtractIndexFromLabel(label.text);
// Create drop down options for later
string[] names = System.Enum.GetNames(typeof(Food.FoodType));
string[] options = new string[names.Length+1];
names.CopyTo(options, 0);
options[options.Length-1] = "(X) DELETE";
// Draw fields
int dropDownValue = -1;
int count = 0;
var enumerator = property.GetEnumerator();
position.height = EditorGUIUtility.singleLineHeight - 1f;
while (enumerator.MoveNext())
{
var prop = enumerator.Current as SerializedProperty;
if (prop == null) continue;
// Skip the first item, since that's going to be the type. Ignore it.
if (count == 0)
{
count++;
continue;
}
// If this is our first item, we want to draw our down arrow.
if (count == 1)
{
Rect editRect = new Rect(position.x + position.width - 20, position.y, 20, position.height);
dropDownValue = EditorGUI.Popup(editRect, -1, options);
// Reduce width of our main field
position.width -= 25;
}
// Render property and prepare for next line
EditorGUI.PropertyField(position, prop);
position.y += EditorGUIUtility.singleLineHeight;
// If this is our first item, we need to readjust our width back to normal
if (count == 1) position.width += 25;
count++;
}
EditorGUI.EndProperty();
// Respond to drop down change
if (dropDownValue != -1)
{
if (dropDownValue == options.Length-1)
{
// Delete option
script.classes.RemoveAt(elementIndex);
EditorUtility.SetDirty(script);
}
else
{
string typeName = $"{(Food.FoodType)dropDownValue}Food, Assembly-CSharp";
var type = Type.GetType(typeName);
script.classes[elementIndex] = (Food)Activator.CreateInstance(type);
}
EditorUtility.SetDirty(script);
}
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
int count = 0;
var enumerator = property.GetEnumerator();
while (enumerator.MoveNext())
{
var prop = enumerator.Current as SerializedProperty;
if (prop == null) continue;
count++;
}
return Mathf.Max(count-1, 0) * EditorGUIUtility.singleLineHeight;
}
private int ExtractIndexFromLabel(string label)
{
// Define the regular expression pattern
string pattern = @"Element (\d+)";
// Create a Regex object with the pattern
Regex regex = new Regex(pattern);
// Match the pattern against the input string
Match match = regex.Match(label);
// Extract the captured number
if (match.Success)
{
string numberString = match.Groups[1].Value;
// Parse the number string to an integer
int number = int.Parse(numberString);
return number;
}
// Return a default value if no match found
return 0;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment