우당탕탕 개발일지

[Unity] CustomEditor - Dictionary 사용하기 본문

Unity

[Unity] CustomEditor - Dictionary 사용하기

devchop 2025. 3. 17. 17:12

enum 인 SkillElement 와, Class인 ElementBonus에 대해 다음과같이 dictionary로 선언된 변수를 커스텀에디터에서 설정하고싶었다. 그러나 딕셔너리는 커스텀에디터에서 자동으로 직렬화를 해주지 않으므로, 직접 직렬화를 해줘야 한다.

 

private Dictionary<SkillElement, List<ElementBonus>> skillBonuses = new Dictionary<SkillElement, List<ElementBonus>>();

 

[System.Serializable]
public class ElementBonus
{
    public int skillCount;
    public float bonus;
}


오늘은 이 dictionary를 커스텀에디터에서 수정하고, 직렬화/역직렬화 함수를 통해 데이터가 손실되지 않는 방법을 알아보겠다.

 

 

1. 두 개의 List 생성하기

 

에디터는 dictionary를 직렬화하지 못하므로, key와 value를 나누어서 List로 변환한다.

[SerializeField] private List<SkillElement> keys = new List<SkillElement>();
[SerializeField] private List<ElementBonusList> values = new List<ElementBonusList>();

 

Value부분이 그냥 값이면 상관없지만, 나의 경우 Value부분이 List이다. 그러면 values부분이 List<List<ElementBonus>>인데, 이러면 또 직렬화가 안된다고 한다. 따라서 아래처럼 List를 감싸는 새로운 WrappingClass를 하나 만들어준다음, values에 넣어주었다. 만약 Value부분이 List로 이루어져있지 않은 경우는 그냥 원래Class로 진행하면된다.

 

// ✅ 리스트를 감싸는 직렬화 가능한 클래스 추가 (List<List<T>> 직렬화 문제 해결)
[System.Serializable]
public class ElementBonusList
{
    public List<ElementBonus> list = new List<ElementBonus>();

    public ElementBonusList(List<ElementBonus> elements)
    {
        list = new List<ElementBonus>(elements);
    }
}

 

 

 

2. ISerializationCallbackReceiver상속받고 함수 구현하기

 

우선 ISerializationCallbackReceiver 를 상속받아서 OnBeforeSerialize() 와 OnDeserialize() 함수를 구현해준다. 

using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName = "NewSkillDamageBonusPreset", menuName = "Skill/SkillDamageBonusPreset")]
public class SkillDamageBonusPresetData : ScriptableObject, ISerializationCallbackReceiver
{
    // ✅ Dictionary 대신 List 사용 (Unity에서 직렬화 가능하도록 변경)
    [SerializeField] private List<SkillElement> keys = new List<SkillElement>();
    [SerializeField] private List<ElementBonusList> values = new List<ElementBonusList>();

    private Dictionary<SkillElement, List<ElementBonus>> skillBonuses = new Dictionary<SkillElement, List<ElementBonus>>();

    public Dictionary<SkillElement, List<ElementBonus>> GetSkillBonuses()
    {
        return skillBonuses;
    }

    public void OnBeforeSerialize()
    {
        keys.Clear();
        values.Clear();

        foreach (var pair in skillBonuses)
        {
            keys.Add(pair.Key);
            values.Add(new ElementBonusList(pair.Value)); // ✅ 리스트를 감싸는 클래스 사용
        }
    }

    public void OnAfterDeserialize()
    {
        skillBonuses = new Dictionary<SkillElement, List<ElementBonus>>();

        if (keys.Count != values.Count)
        {
            //Debug.LogError($"❌ 데이터 불일치 발생! keys({keys.Count}) / values({values.Count}) → 자동 복구");
            return;
        }

        for (int i = 0; i < keys.Count; i++)
        {
            skillBonuses[keys[i]] = new List<ElementBonus>(values[i].list);
        }
    }

    public float GetBonusDamage(SkillElement type, int count)
    {
        if (!skillBonuses.TryGetValue(type, out var list) || list == null)
        {
            return 0;
        }

        float value = 0f;
        foreach (var item in list)
        {
            if (item.skillCount <= count && value < item.bonus) value = item.bonus;
            if (item.skillCount == count) break;
        }
        return value;
    }
}

 

 

 

 

3. 커스텀 에디터 구현하기

커스텀에디터에서 값을 수정/삭제/추가할때는 원래의 Dictionary에 값을 수정한다.

    public override void OnInspectorGUI()
    {
        serializedObject.Update();

        EditorGUILayout.LabelField("🔥 스킬 속성별 데미지 보너스 설정 🔥", EditorStyles.boldLabel);
        EditorGUILayout.Space();

        var skillBonuses = preset.GetSkillBonuses();

        foreach (SkillElement element in skillBonuses.Keys)
        {
            foldouts[element] = EditorGUILayout.Foldout(foldouts[element], GetElementIcon(element) + $" {element}", true, EditorStyles.boldLabel);
            if (foldouts[element])
            {
                DrawElementSection(element, skillBonuses);
            }
        }

        if (GUI.changed)
        {
            EditorUtility.SetDirty(preset);
        }

        serializedObject.ApplyModifiedProperties();
    }

    private void DrawElementSection(SkillElement element, Dictionary<SkillElement, List<ElementBonus>> skillBonuses)
    {
        EditorGUILayout.BeginVertical(GUI.skin.box);
        List<ElementBonus> bonuses = skillBonuses[element];

        EditorGUILayout.BeginHorizontal();
        EditorGUILayout.LabelField("보유 개수", GUILayout.Width(80));
        EditorGUILayout.LabelField("데미지 증가 (%)", GUILayout.ExpandWidth(true));
        EditorGUILayout.LabelField("", GUILayout.Width(50));
        EditorGUILayout.EndHorizontal();

        for (int i = 0; i < bonuses.Count; i++)
        {
            EditorGUILayout.BeginHorizontal();
            bonuses[i].skillCount = EditorGUILayout.IntField(bonuses[i].skillCount, GUILayout.Width(80));
            bonuses[i].bonus = EditorGUILayout.FloatField(bonuses[i].bonus, GUILayout.ExpandWidth(true));

            if (GUILayout.Button("❌", GUILayout.Width(50)))
            {
                bonuses.RemoveAt(i);
                EditorUtility.SetDirty(preset);
                break;
            }

            EditorGUILayout.EndHorizontal();
        }

        EditorGUILayout.Space(5);

        if (GUILayout.Button("➕ 세트 추가", GUILayout.Height(25)))
        {
            int nextCount = bonuses.Count > 0 ? bonuses[bonuses.Count - 1].skillCount + 1 : 1;
            bonuses.Add(new ElementBonus() { skillCount = nextCount, bonus = 0f });
            EditorUtility.SetDirty(preset);
        }

        EditorGUILayout.Space(5);
        EditorGUILayout.EndVertical();
    }