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

Scriptable Object - 응용: Enum 대체, 오해와 진실, 프로퍼티 드로워 (5)

by PlaneK 2020. 6. 1.

Enum 대체

Scriptable Object의 Asset으로 Enum을 대체할 수 있다.

Enum은 각 요소가 문자열과 int형으로 매핑된다. 그래서 Enum은 Dictionary의 키로 사용하거나 메뉴 인덱싱을 위해 사용한다. 

Enum은 변경에 취약하다. 중간 요소를 제거하거나 추가해서 인덱스가 변경되면 관련한 모든 코드를 재정렬해야 한다. Switch/case문의 요소 제거 및 추가, 인덱스로 활용 중인 코드의 인덱스 조정 등 활용 범위가 넓을 수록 코드 수정 범위도 넓어진다.

하지만 Enum을 Asset으로 대체하면 수정 범위가 Inspector뷰에 한정된다. 따라서 변경에 대한 단점을 최소화 할 수 있다. 특히 Scriptabl Object는 기능도 정의할 수 있으므로 Enum의 인덱싱과 동시에 Data나 기능 구현의 역할을 할 수 있다.

Paper, Rock, Scissors 이 3가지는 'AttackElement'의 인스턴스로 Enum의 요소에 해당한다. 각 요소는 Defeated Elements라는 컨테이너를 통해 Data역할을 하고있다.

1
2
3
4
5
6
7
8
9
using System.Collections.Generic;
using UnityEngine;
 
[CreateAssetMenu]
public class AttackElement : ScriptableObject
{
    [Tooltip("The elements that are defeated by this element.")]
    public List<AttackElement> DefeatedElements = new List<AttackElement>();
}
 

AttackElement 소스. Enum 구조체 정의에 해당한다. 

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
using UnityEngine;
using UnityEngine.UI;
 
// Enum을 사용하는 MonoBehaviour 스크립트
public class Elemental : MonoBehaviour
{
    // Enum 구조체 필드
    [Tooltip("Element represented by this elemental.")]
    public AttackElement Element;
 
    [Tooltip("Text to fill in with the element name.")]
    public Text Label;
 
    private void OnEnable()
    {
        // Enum 참조
        if(Label != null)
            Label.text = Element.name;
    }
 
    private void OnTriggerEnter(Collider other)
    {
        // 외부 개체의 Enum 추출
        Elemental e = other.gameObject.GetComponent<Elemental>();
        if (e != null)
        {
            // 외부 개체의 Enum 참조
            if (e.Element.DefeatedElements.Contains(Element))
                Destroy(gameObject);
        }
    }
}
 

Elemental 소스. AttackElement를 실제 사용하는 소스다. 

 

오해와 진실

위에서 봤듯이 Scripatabl Object는 Data역할만 수행하지 않는다. Data의 역할과 더불어 Enum의 인덱싱 역할에 특정 기능도 정의할 수 있다.

처음에 봤던 FloatVariable은 float가 다를 바 없지만 기능이 정의된 상태다.

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
using UnityEngine;
 
[CreateAssetMenu]
public class FloatVariable : ScriptableObject
{
#if UNITY_EDITOR
    // 텍스트 상자
    [Multiline]
    public string DeveloperDescription = "";
#endif
    public float Value;
 
    public void SetValue(float value)
    {
        Value = value;
    }
 
    public void SetValue(FloatVariable value)
    {
        Value = value.Value;
    }
 
    public void ApplyChange(float amount)
    {
        Value += amount;
    }
 
    public void ApplyChange(FloatVariable amount)
    {
        Value += amount.Value;
    }
}
 

FloatVariable을 float처럼 산술연산하려면 기능 구현이 필요하다. ApplyChange를 통해 산술연산을 수행한다.

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
using UnityEngine;
using UnityEngine.Events;
 
public class UnitHealth : MonoBehaviour
{
    public FloatVariable HP;
 
    public bool ResetHP;
    public FloatReference StartingHP;
    public UnityEvent DamageEvent;
    public UnityEvent DeathEvent;
 
    private void Start()
    {
        if (ResetHP)
            HP.SetValue(StartingHP);
    }
 
    private void OnTriggerEnter(Collider other)
    {
        DamageDealer damage = other.gameObject.GetComponent<DamageDealer>();
        if (damage != null)
        {
            HP.ApplyChange(-damage.DamageAmount);
            DamageEvent.Invoke();
        }
 
        if (HP.Value <= 0.0f)
        {
            DeathEvent.Invoke();
        }
    }
}
 

UnitHealth 소스.

 

프로퍼티 드로워

프로퍼티 드로워로 공유데이터를 사용할 지, 따로 설정한 값을 사용할 지 정할 수 있다. 사실 'damageAmount'라는 필드의 자료형은 FloatRefernce다.

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
using System;
 
[Serializable]
public class FloatReference
{
    /* Inspector뷰에 노출되는 필드들 */
    // UseConstant와 ConstantValue는 '...' 버튼으로 구성된다.
    public bool UseConstant = true;
    public float ConstantValue;
    public FloatVariable Variable;
 
    // 생성자
    public FloatReference()
    { }
 
    // 생성자
    public FloatReference(float value)
    {
        UseConstant = true;
        ConstantValue = value;
    }
    
    // 최종 값 반환 프로퍼티
    public float Value
    {
        get { return UseConstant ? ConstantValue : Variable.Value; }
    }
 
    // 암묵적 형변환 설정. float f = floatReference;
    public static implicit operator float(FloatReference reference)
    {
        return reference.Value;
    }
}
 

FloatReference 소스. 위와 같이 인스펙터뷰를 통해 조정 가능하려면 프로퍼티의 커스텀 작업이 필요하다.

암묵적 형변환은 float 인자를 요구하는 각종 Unity API를 사용하기 위해 반드시 필요하다.

 

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
using UnityEditor;
 
[CustomPropertyDrawer(typeof(MyProperty))]
public class MyPropertyDrawer : PropertyDrawer
{
    //which index of the popupOptions you want selected
    private int selectedIndex = 0;
 
    //the different options you want to display when the Popup is selected
    private readonly string[] popupOptions =
        { "Option One""Option Two" };  
 
    private GUIStyle popupStyle;
 
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        //stuff above here
 
        if (popupStyle == null)
        {
            popupStyle = new GUIStyle(GUI.skin.GetStyle("PaneOptions"));
            popupStyle.imagePosition = ImagePosition.ImageOnly;
        }
        EditorGUI.Popup(position /*or whatever position you want*/,
                         selectedIndex, popupOptions, popupStyle);
 
        //stuff below here
    }
}
 

프로퍼티를 팝업("PaneOptions")으로 구성하는 기본적인 프레임은 위와 같다. 

 

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
using UnityEditor;
using UnityEngine;
 
[CustomPropertyDrawer(typeof(FloatReference))]
public class FloatReferenceDrawer : PropertyDrawer
{
    /// <summary>
    /// Options to display in the popup to select constant or variable.
    /// </summary>
    private readonly string[] popupOptions = 
        { "Use Constant""Use Variable" };
 
    /// <summary> Cached style to use to draw the popup button. </summary>
    private GUIStyle popupStyle;
 
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        if (popupStyle == null)
        {    
            // '...' 모양의 팝업 버튼 스타일 생성
            popupStyle = new GUIStyle(GUI.skin.GetStyle("PaneOptions"));
            popupStyle.imagePosition = ImagePosition.ImageOnly;
        }
 
        label = EditorGUI.BeginProperty(position, label, property);
        position = EditorGUI.PrefixLabel(position, label);
        
        EditorGUI.BeginChangeCheck();
 
        // Get properties
        SerializedProperty useConstant = property.FindPropertyRelative("UseConstant");
        SerializedProperty constantValue = property.FindPropertyRelative("ConstantValue");
        SerializedProperty variable = property.FindPropertyRelative("Variable");
 
        // Calculate rect for configuration button
        Rect buttonRect = new Rect(position);
        buttonRect.yMin += popupStyle.margin.top;
        buttonRect.width = popupStyle.fixedWidth + popupStyle.margin.right;
        position.xMin = buttonRect.xMax;
 
        // Store old indent level and set it to 0, the PrefixLabel takes care of it
        int indent = EditorGUI.indentLevel;
        EditorGUI.indentLevel = 0;
 
        int result = EditorGUI.Popup(buttonRect, useConstant.boolValue ? 0 : 1, popupOptions, popupStyle);
 
        useConstant.boolValue = result == 0;
 
        EditorGUI.PropertyField(position, 
            useConstant.boolValue ? constantValue : variable, 
            GUIContent.none);
 
        if (EditorGUI.EndChangeCheck())
            property.serializedObject.ApplyModifiedProperties();
 
        EditorGUI.indentLevel = indent;
        EditorGUI.EndProperty();
    }
}
 

FloatReferenceDrawer.cs

FloatReference의 프로퍼티 필드를 팝업을 통해 선택할 수 있다.

 

댓글