본문 바로가기
Unity/스크립터블오브젝트

Scriptable Object - 응용: Interactable, 커스텀 에디터 (10)

by PlaneK 2020. 6. 8.

Interactable

Interactable은 구성한 Condition들로 Reaction을 수행한다.

Condition과 해당 Reaction의 구성은 ConditionCollection에 저장된다.

저장된 Condition들을 현재 Player의 상태값인 'AllConditions'와 비교해서 Reaction을 수행할지 말지 결정한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using UnityEngine;
 
public class Interactable : MonoBehaviour
{
    public Transform interactionLocation;
    public ConditionCollection[] conditionCollections = new ConditionCollection[0];
    public ReactionCollection defaultReactionCollection;
 
 
    public void Interact ()
    {
        for (int i = 0; i < conditionCollections.Length; i++)
        {
            if (conditionCollections[i].CheckAndReact ())
                return;
        }
 
        defaultReactionCollection.React ();
    }
}
 

Interactable.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using UnityEngine;
 
public class ConditionCollection : ScriptableObject
{
    public string description;
    public Condition[] requiredConditions = new Condition[0];
    public ReactionCollection reactionCollection;
 
 
    public bool CheckAndReact()
    {
        for (int i = 0; i < requiredConditions.Length; i++)
        {
            if (!AllConditions.CheckCondition (requiredConditions[i]))
                return false;
        }
 
        if(reactionCollection)
            reactionCollection.React();
 
        return true;
    }
}
 

ConditionCollection.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// AllConditions.cs
public static bool CheckCondition (Condition requiredCondition)
{
    Condition[] allConditions = Instance.conditions;
    Condition globalCondition = null;
    
    if (allConditions != null && allConditions[0!= null)
    {
        for (int i = 0; i < allConditions.Length; i++)
        {
            if (allConditions[i].hash == requiredCondition.hash)
                globalCondition = allConditions[i];
        }
    }
 
    if (!globalCondition)
        return false;
 
    return globalCondition.satisfied == requiredCondition.satisfied;
}
 

AllConditions.cs

 

커스텀 에디터

인스턴스 생성 버튼은 Editor클래스로 만든다. 실제 인스턴스 생성 및 초기화 작업도 Editor클래스로 처리한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
using UnityEngine;
using UnityEditor;
 
[CustomEditor(typeof(Interactable))]
public class InteractableEditor : EditorWithSubEditors<ConditionCollectionEditor, ConditionCollection>
{
    private Interactable interactable;
    private SerializedProperty interactionLocationProperty;
    // isArray 플래그를 통해 해당 프로퍼티가 배열이나 콜렉션인지 판단한다.
    private SerializedProperty collectionsProperty;
    private SerializedProperty defaultReactionCollectionProperty;
 
    private const float collectionButtonWidth = 125f;
    private const string interactablePropInteractionLocationName = "interactionLocation";
    private const string interactablePropConditionCollectionsName = "conditionCollections";
    private const string interactablePropDefaultReactionCollectionName = "defaultReactionCollection";
 
 
    private void OnEnable ()
    {
        // target은 Editor클래스의 멤버변수(필드)다.
        // [CustomEditor(T))] 속성의 생성자를 통해 초기화된다.
        interactable = (Interactable)target;
 
        // 프로퍼티 초기화. target의 프로퍼티를 각 '필드명'으로 추출한다.
        collectionsProperty = serializedObject.FindProperty(interactablePropConditionCollectionsName);
        interactionLocationProperty = serializedObject.FindProperty(interactablePropInteractionLocationName);
        defaultReactionCollectionProperty = serializedObject.FindProperty(interactablePropDefaultReactionCollectionName);
        
        CheckAndCreateSubEditors(interactable.conditionCollections);
    }
 
 
    private void OnDisable ()
    {
        CleanupEditors ();
    }
 
 
    // CheckAndCreateSubEditors()의 콜백 함수
    protected override void SubEditorSetup(ConditionCollectionEditor editor)
    {
        // subEditor의 프로퍼티에 collectionProperty를 초기화 해준다.
        // collectionsProperty의 엘리먼트 제거는 CondtionCollectionEditor에서 처리하기 때문.
        editor.collectionsProperty = collectionsProperty;
    }
 
 
    public override void OnInspectorGUI ()
    {
        serializedObject.Update ();
        
        // subEditors를 생성하고 초기화하기 위해 호출
        CheckAndCreateSubEditors(interactable.conditionCollections);
        
        EditorGUILayout.PropertyField (interactionLocationProperty);
 
        for (int i = 0; i < subEditors.Length; i++)
        {
            // subEditors의 OnInspectorGUI를 호출한다.
            // 버튼 생성을 위해 Property Drawer 대신 Editor를 사용한다.
            // Property Drawer였으면 DrawDefaultInspector()만 호출하면 된다.
            subEditors[i].OnInspectorGUI ();
            EditorGUILayout.Space ();
        }
 
        EditorGUILayout.BeginHorizontal();
        GUILayout.FlexibleSpace ();
        if (GUILayout.Button("Add Collection", GUILayout.Width(collectionButtonWidth)))
        {
            // ConditionCollection의 인스턴스를 생성하고
            ConditionCollection newCollection = ConditionCollectionEditor.CreateConditionCollection ();
            // 프로퍼티(array)에 할당한다.
            collectionsProperty.AddToObjectArray (newCollection);
        }
        EditorGUILayout.EndHorizontal ();
 
        EditorGUILayout.Space ();
 
        EditorGUILayout.PropertyField (defaultReactionCollectionProperty);
 
        serializedObject.ApplyModifiedProperties ();
    }
}
 

InteractableEditor.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
using UnityEngine;
using UnityEditor;
 
[CustomEditor(typeof(ConditionCollection))]
public class ConditionCollectionEditor : EditorWithSubEditors<ConditionEditor, Condition>
{
    public SerializedProperty collectionsProperty;
 
    private ConditionCollection conditionCollection;
    private SerializedProperty descriptionProperty;
    private SerializedProperty conditionsProperty;
    private SerializedProperty reactionCollectionProperty;
 
    private const float conditionButtonWidth = 30f;
    private const float collectionButtonWidth = 125f;
    private const string conditionCollectionPropDescriptionName = "description";
    private const string conditionCollectionPropRequiredConditionsName = "requiredConditions";
    private const string conditionCollectionPropReactionCollectionName = "reactionCollection";
 
    private void OnEnable ()
    {
        conditionCollection = (ConditionCollection)target;
 
        if (target == null)
        {
            DestroyImmediate (this);
            return;
        }
 
        descriptionProperty = serializedObject.FindProperty(conditionCollectionPropDescriptionName);
        conditionsProperty = serializedObject.FindProperty(conditionCollectionPropRequiredConditionsName);
        reactionCollectionProperty = serializedObject.FindProperty(conditionCollectionPropReactionCollectionName);
 
        CheckAndCreateSubEditors (conditionCollection.requiredConditions);
    }
 
 
    private void OnDisable ()
    {
        CleanupEditors ();
    }
 
 
    protected override void SubEditorSetup (ConditionEditor editor)
    {
        editor.editorType = ConditionEditor.EditorType.ConditionCollection;
        editor.conditionsProperty = conditionsProperty;
    }
 
 
    public override void OnInspectorGUI ()
    {
        serializedObject.Update ();
 
        CheckAndCreateSubEditors(conditionCollection.requiredConditions);
        
        EditorGUILayout.BeginVertical(GUI.skin.box);
        EditorGUI.indentLevel++;
 
        EditorGUILayout.BeginHorizontal();
 
        descriptionProperty.isExpanded = EditorGUILayout.Foldout(descriptionProperty.isExpanded, descriptionProperty.stringValue);
 
        if (GUILayout.Button("Remove Collection", GUILayout.Width(collectionButtonWidth)))
        {
            // InteractableEditor의 프로퍼티를 직접 참조해서 요소를 제거한다.
            collectionsProperty.RemoveFromObjectArray (conditionCollection);
        }
 
        EditorGUILayout.EndHorizontal();
        
        if (descriptionProperty.isExpanded)
        {
            ExpandedGUI ();
        }
        
        EditorGUI.indentLevel--;
        EditorGUILayout.EndVertical();
 
        serializedObject.ApplyModifiedProperties();
    }
 
 
    private void ExpandedGUI ()
    {
        EditorGUILayout.Space();
 
        EditorGUILayout.PropertyField(descriptionProperty);
 
        EditorGUILayout.Space();
 
        float space = EditorGUIUtility.currentViewWidth / 3f;
 
        EditorGUILayout.BeginHorizontal();
        EditorGUILayout.LabelField("Condition", GUILayout.Width(space));
        EditorGUILayout.LabelField("Satisfied?", GUILayout.Width(space));
        EditorGUILayout.LabelField("Add/Remove", GUILayout.Width(space));
        EditorGUILayout.EndHorizontal();
 
        EditorGUILayout.BeginVertical(GUI.skin.box);
        for (int i = 0; i < subEditors.Length; i++)
        {
            subEditors[i].OnInspectorGUI();
        }
        EditorGUILayout.EndVertical();
 
        EditorGUILayout.BeginHorizontal();
        GUILayout.FlexibleSpace ();
        if (GUILayout.Button("+", GUILayout.Width(conditionButtonWidth)))
        {
            Condition newCondition = ConditionEditor.CreateCondition();
            conditionsProperty.AddToObjectArray(newCondition);
        }
        EditorGUILayout.EndHorizontal();
 
        EditorGUILayout.Space();
 
        EditorGUILayout.PropertyField(reactionCollectionProperty);
    }
 
    // 정적 함수. SO의 인스턴스 생성.
    public static ConditionCollection CreateConditionCollection()
    {
        ConditionCollection newConditionCollection = CreateInstance<ConditionCollection>();
        newConditionCollection.description = "New condition collection";
        newConditionCollection.requiredConditions = new Condition[1];
        newConditionCollection.requiredConditions[0= ConditionEditor.CreateCondition();
        return newConditionCollection;
    }
}
cs

ConditionCollectionEditor.cs

댓글