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

Scriptable Object - 응용: ReactionCollection, 팝업과 드래그박스, 폴드아웃 (13)

by PlaneK 2020. 6. 11.

ReactionCollection

ReactionCollection은 Reaction들을 저장한다.

ReactionCollection의 React()함수는 Interactable에 의해 호출된다.

React()함수가 호출되면 저장된 Reaction들을 모두 실행한다.

위 Reaction들은 ScriptableObject로 만들어진 것이며 추상클래스로부터 구체화된 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using UnityEngine;
 
public abstract class Reaction : ScriptableObject
{
    public void Init ()
    {
        SpecificInit ();
    }
 
    protected virtual void SpecificInit()
    {}
 
    public void React (MonoBehaviour monoBehaviour)
    {
        ImmediateReaction ();
    }
 
    protected abstract void ImmediateReaction ();
}
 

Reaction.cs. 리액션 클래스들의 최상위 클래스다.

시간 지연(delay)없이 곧바로 실행되는 리액션을 정의한다.

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
using UnityEngine;
using System.Collections;
 
public abstract class DelayedReaction : Reaction
{
    public float delay;
 
    protected WaitForSeconds wait;
 
    public new void Init ()
    {
        wait = new WaitForSeconds (delay);
 
        SpecificInit ();
    }
 
    public new void React (MonoBehaviour monoBehaviour)
    {
        monoBehaviour.StartCoroutine (ReactCoroutine ());
    }
 
    protected IEnumerator ReactCoroutine ()
    {
        yield return wait;
 
        ImmediateReaction ();
    }
}
 

DelayedReaction.cs. Reaction의 파생클래스이며 시간 지연(delay) 후 실행된다.

팝업과 드래그박스, 폴드아웃

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
private void OnEnable ()
{
    reactionCollection = (ReactionCollection)target;
 
    reactionsProperty = serializedObject.FindProperty(reactionsPropName);
 
    CheckAndCreateSubEditors (reactionCollection.reactions);
 
    SetReactionNamesArray ();
}
 
private void SetReactionNamesArray ()
{
    Type reactionType = typeof(Reaction);
 
    // Reaction이 속하는 어셈블리의 모든 클래스 추출
    Type[] allTypes = reactionType.Assembly.GetTypes();
 
    List<Type> reactionSubTypeList = new List<Type>();
 
    for (int i = 0; i < allTypes.Length; i++)
    {
        // Reaction클래스의 파생클래스와 추상 클래스 필터링
        if (allTypes[i].IsSubclassOf(reactionType) && !allTypes[i].IsAbstract)
        {
            // 추상클래스가 아닌 클래스 저장.
            reactionSubTypeList.Add(allTypes[i]);
        }
    }
    
    // 리스트의 배열 반환.
    reactionTypes = reactionSubTypeList.ToArray();
 
    List<string> reactionTypeNameList = new List<string>();
 
    for (int i = 0; i < reactionTypes.Length; i++)
    {
        // Type의 Name을 저장.
        reactionTypeNameList.Add(reactionTypes[i].Name);
    }
    // 팝업 리스트를 위한 string[] 초기화.
    reactionTypeNames = reactionTypeNameList.ToArray();
}
 

ReactionCollectionEditor.cs.

활성화 시 Reaction들의 에디터와 팝업을 구성하기 위한 팝업 리스트를 매번 생성한다.

Reflection과 Type API

System.Reflection.Assembly.GetTypes() : 해당 어셈블리의 모든 클래스 타입을 반환한다.

Type.IsAbstract : 추상클래스를 판단하는 플래그 프로퍼티.

Type.IsSubclassOf(Type) : 파생클래스를 판단하고 플래그를 반환한다.

팝업

팝업의 리스트는 string[]로 구성된다.

Popup은 선택된 항목의 index를 반환한다.

index를 사용하려면 팝업 리스트와 매핑된 컨테이너가 필요하다

ConditionEditor는 index로부터 얻은 값을 인스턴스 초기화에 사용한다.

ReactionCollectionEditor는 index로부터 Type 값을 얻어 해당 Reaction 타입의 인스턴스 생성에 사용한다.

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
public override void OnInspectorGUI ()
{
    serializedObject.Update ();
 
    CheckAndCreateSubEditors(reactionCollection.reactions);
 
    for (int i = 0; i < subEditors.Length; i++)
    {
        subEditors[i].OnInspectorGUI ();
    }
 
    if (reactionCollection.reactions.Length > 0)
    {
        EditorGUILayout.Space();
        EditorGUILayout.Space ();
    }
 
    Rect fullWidthRect = GUILayoutUtility.GetRect(GUIContent.none, GUIStyle.none, GUILayout.Height(dropAreaHeight + verticalSpacing));
 
    Rect leftAreaRect = fullWidthRect;
    leftAreaRect.y += verticalSpacing * 0.5f;
    leftAreaRect.width *= 0.5f;
    leftAreaRect.width -= controlSpacing * 0.5f;
    leftAreaRect.height = dropAreaHeight;
 
    Rect rightAreaRect = leftAreaRect;
    rightAreaRect.x += rightAreaRect.width + controlSpacing;
 
    TypeSelectionGUI (leftAreaRect);
    DragAndDropAreaGUI (rightAreaRect);
 
    DraggingAndDropping(rightAreaRect, this);
 
    serializedObject.ApplyModifiedProperties ();
}
 
private void TypeSelectionGUI (Rect containingRect)
{
    Rect topHalf = containingRect;
    topHalf.height *= 0.5f;
    
    Rect bottomHalf = topHalf;
    bottomHalf.y += bottomHalf.height;
 
    selectedIndex = EditorGUI.Popup(topHalf, selectedIndex, reactionTypeNames);
 
    if (GUI.Button (bottomHalf, "Add Selected Reaction"))
    {
        Type reactionType = reactionTypes[selectedIndex];
        Reaction newReaction = ReactionEditor.CreateReaction (reactionType);
        reactionsProperty.AddToObjectArray (newReaction);
    }
}
 

ReactionCollectionEditor.cs. TypeSlectionGUI 함수를 통해 팝업을 띄운다.

 

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
private static void DragAndDropAreaGUI (Rect containingRect)
{
    // GUIStyle 생성 및 초기화
    GUIStyle centredStyle = GUI.skin.box;
    // Text 중앙 정렬
    centredStyle.alignment = TextAnchor.MiddleCenter;
    centredStyle.normal.textColor = GUI.skin.button.normal.textColor;
 
    GUI.Box (containingRect, "Drop new Reactions here", centredStyle);
}
 
private static void DraggingAndDropping (Rect dropArea, ReactionCollectionEditor editor)
{
    // Drag 이벤트 추출
    Event currentEvent = Event.current;
 
    // Rect.Contains(vector2) 해당 벡터가 Rect 내에 있는지 체크
    // mousePosiiton은 현재 마우스 커서의 위치를 반환한다.
    if (!dropArea.Contains (currentEvent.mousePosition))
        return;
 
    switch (currentEvent.type)
    {
        // Drag 이벤트
        case EventType.DragUpdated:
            // DragAndDrop의 아이템 유효 체크
            // 유효하면 Link 아이콘, 그렇지 않으면 Rejected 아이콘
            DragAndDrop.visualMode = IsDragValid () ? DragAndDropVisualMode.Link : DragAndDropVisualMode.Rejected;
            // DragUpdated 이벤트 실행
            currentEvent.Use ();
 
            break;
        // Drop 이벤트
        case EventType.DragPerform:
            
            // 용도 불분명, 주석 후에도 정상 동작함.
            //DragAndDrop.AcceptDrag();
            
            for (int i = 0; i < DragAndDrop.objectReferences.Length; i++)
            {
                // MonoScript는 MonoBehaiour 또는 ScriptableObject을 가리킨다.
                // MonoScript 변수에 드래그 아이템을 저장한다.
                MonoScript script = DragAndDrop.objectReferences[i] as MonoScript;
 
                // MonoScript.GetClass()는 해당 클래스를 반환한다.
                Type reactionType = script.GetClass();
 
                // 해당 타입의 Reaction 생성
                Reaction newReaction = ReactionEditor.CreateReaction (reactionType);
                /* editor는 인자로 넘어온 ReactionCollectionEditor 본인의 인스턴스다.
                 이 함수는 static이기 때문에 직접 참조할 수 없어서 인자로부터 우회 참조한다.
                "static 함수 입장에서는 인스턴스를 분별할 수 없지만 인자로 받으면 분별 가능하므로" */
                // reactions에 추가 할당.
                editor.reactionsProperty.AddToObjectArray (newReaction);
            }
            // DragPerform 이벤트 실행
            currentEvent.Use();
 
            break;
    }
}
 
private static bool IsDragValid ()
{
    // 필터링으로 거른다.
    for (int i = 0; i < DragAndDrop.objectReferences.Length; i++)
    {
        if (DragAndDrop.objectReferences[i].GetType () != typeof (MonoScript))
            return false;
        
        MonoScript script = DragAndDrop.objectReferences[i] as MonoScript;
        Type scriptType = script.GetClass ();
 
        if (!scriptType.IsSubclassOf (typeof(Reaction)))
            return false;
 
        if (scriptType.IsAbstract)
            return false;
    }
    // 필터링을 통과하면 true 반환.
    return true;
}

ReactionCollectionEditor.cs. 드래그박스의 구현부다.

팝업 vs 드래그박스

팝업은 팝업 리스트와 index를 사용할 매핑된 컨테이너를 요구한다.
드래그박스는 팝업이 요구하는 작업은 필요없지만 드래그 아이템의 판별 작업이 필요하다.

 

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
using System;
using UnityEngine;
using UnityEditor;
 
public abstract class ReactionEditor : Editor
{
    public bool showReaction;
    public SerializedProperty reactionsProperty;
 
    private Reaction reaction;
 
    private const float buttonWidth = 30f;
 
    private void OnEnable ()
    {
        reaction = (Reaction)target;
        Init ();
    }
 
    protected virtual void Init () {}
 
    public override void OnInspectorGUI ()
    {
        serializedObject.Update ();
 
        EditorGUILayout.BeginVertical (GUI.skin.box);
        EditorGUI.indentLevel++;
 
        EditorGUILayout.BeginHorizontal ();
        
        // FoldOut(펼침 플래그, "폴드의 라벨")
        showReaction = EditorGUILayout.Foldout (showReaction, GetFoldoutLabel ());
        
        // 폴드 옆 '-' 버튼 생성.
        if (GUILayout.Button ("-", GUILayout.Width (buttonWidth)))
        {
            reactionsProperty.RemoveFromObjectArray (reaction);
        }
        EditorGUILayout.EndHorizontal ();
        
        // 펼쳐지면
        if (showReaction)
        {
            // 프로퍼티 GUI 호출
            DrawReaction ();
        }
 
        EditorGUI.indentLevel--;
        EditorGUILayout.EndVertical ();
 
        serializedObject.ApplyModifiedProperties ();
    }
 
    public static Reaction CreateReaction (Type reactionType)
    {
        return (Reaction)CreateInstance (reactionType);
    }
 
    protected virtual void DrawReaction ()
    {
        DrawDefaultInspector ();
    }
 
    // 초기화구문은 따로 없이 파생 클래스마다 하드코딩 되어있다.
    protected abstract string GetFoldoutLabel ();
}
 

ReactionEditor.cs. 

추상클래스이며 GetFoldoutLabel()을 위해 파생 Reaction클래스의 Editor클래스를 추가작성해야한다.

댓글