Coding Memo

SerializedObject 본문

Unity

SerializedObject

minttea25 2023. 5. 12. 16:45

Unity에서 GameObject에 Component(MonoBehaviour)를 추가 하면 ‘Inspector’에 관련 내용이 표시가 된다.

위 이미지에서는 Transform과 SpriteRenderer가  Component로 붙어있고, 각 컴포넌트에 에디터에서 직접 편집하고 수정할 수 있는 값들이 나타나 있다.

 

이 값들은 흔히 우리가 MonoBehaviour에서 [SerializeField]태그가 붙거나 public으로 선언한 필드가 있으면 자동으로 Inspector에 나타나게 된다. 이 값들을 (태그에서 알 수 있듯이) SerializedProperty라고 하고 이것을 가지고 있는 객체를 SerializedObject라고 한다. 말 그대로 직렬화 되어서 우리가 유니티 에디터에서 다루기 쉽도록 바꾼 것이다. 이 값들은 에디터 내에서 해당되는 데이터에 접근 할수 있도록 해준다. 

 

예를 들어, SpriteRenderer에서 이미지를 스크립트 상에서 바꿀 수도 있겠지만, 이 대신에 에디터에서 직접 Sprite 객체를 할 당할 수 있도록 편하고 쉽게 해준다. (꼭 Inspector에 국한되는 것이 아니라 에디터 내의 여러가지 Panel(EidtorWindow)도 이에 해당된다.)

 


유니티는 모든 것이 오브젝트 기반으로 돌아간다. 어떤 컴포넌트들도 결국에 UnityEngine.Object를 기반으로 두는 객체들이다. 그러면 UnityEngine.Object와 SerializedObject는 어떤 관계일까?

 

간단히 말해서 UnityEngine.Object는 코드 상에서 존재는 실제 인스턴스를 가지고 있는 객체이고 SerializedObject는 UnityEngine.Object가 가지고 있는 인스턴스를 유니티 에디터 상에서 변경할 수 있도록 해주는 객체이다.

추가 정보: SerializedObject를 사용하지 않고 오브젝트를 다룰 경우, Undo나 Selection등을 직접 호출하여 다루어야 한다고 한다.

 

Object가 Asset 파일로 저장 될 때, Binary나 YAML 형식의 텍스트로 저장이 되는데 이 때 저장하기 위한 직렬화를 하는 것이 SerializedObject이다. Asset파일과 Meta파일의 직렬화된 데이터가 SerializedObject로 바뀌고 이는 다시 UnityEnginel.Object로 데이터를 로드하고 반대로, UnityEngine.Object에서는 데이터를 SerializedObject로 변환 후, SerializedObject는 Asset과 Meta파일에 직렬화된 데이터를 저장한다.

 

Project Settings - Editor - Asset Serialization 설정에서 Serialization이후 저장을 어떻게 할 것인지 (Binary, Force Text, Mixed) 지정할 수 있다.

에디터에서 값을 수정할 수 있는 SerializedObject가 되려면 그 객체는 몇 가지 조건을 충족 시켜야 한다. (https://docs.unity3d.com/Manual/script-Serialization.html 에 좀 더 구체적으로 나와 있다.)

  1. 이 객체를 포함하고 있는 클래스가 MonoBehaviour, ScriptableObject, Editor, EditorWindow 등의 유니티 에디터에서 활용할 수 있는 클래스여야 함
  2. public으로 선언된 수정 가능한 변수이거나 [SerializeField]로 선언된 변수
  3. Unity에 기본으로 지원되고 있는 직렬화 가능한 타입(Unity Built-in Types)이거나 열거형, 직렬화를 (명시적으로) 선언한 구조체나 클래스
  4. 정적(static) 변수가 아니어야 함
  5. 추상(abstract) 클래스가 아니어야 함

 

좀 더 자세히 살펴보자.

 

[이 객체를 포함하고 있는 클래스가 MonoBehaviour, ScriptableObject, Editor, EditorWindow 등의 유니티 에디터에서 활용할 수 있는 클래스여야 함]

이건 당연한 말이기도 하다. 물론, 위 클래스를 상속받은 클래스도 가능하다. MonoBehaviour은 Component로서 게임 오브젝트에 직접 추가가 가능하고 ScriptableObject는 생성 후 데이터를 입력할 수 있고, Editor는 Inspector에 표시될 내용을 직접 만들 수 있으며, EditorWindow는 유니티 에디터에서 커스텀 창을 만들 수 있도록 해준다.

 

 

[public으로 선언된 수정 가능한 변수이거나 [SerializeField]로 선언된 변수]

public으로 선언되어 전역에서 접근을 할 수 있으면 자동으로 Serialize 대상으로 본다. 추가적으로 접근 제한자가 public이 아닌 protected나 private, internal 같은 경우도 [SerializeField]를 붙여 주면 public과 마찬가지로 직렬화 대상으로 본다.

 

위에서 ‘수정 가능한’이라고 표현했는데, 변수가 public으로 선언되거나 [SerializeField]가 붙어도 readonly나 const로 선언되어 값 변경이 불가한 변수라면 SerializeObject로 보지 않는다.

NOTE: SerializeField는 UnityEngine에 있는 속성이다. [UnityEngine.SerializeField]

 

 

[Unity에 기본으로 지원되고 있는 직렬화 가능한 타입(Unity Built-in Types)이거나 열거형, 직렬화를 (명시적으로) 선언한 구조체나 클래스]

SerializedObject가 되려면 직렬화가 가능해야 한다.

유니티에서 기본으로 직렬화되어 있는 타입(Unity built-in Types)은 다음과 같다.

  • byte, short, int, long, uint, float 등의 몇몇 기본 자료형
  • Vector3, Vector2, Rect, Color 등 유니티가 지원하는 자료형
  • Colloections - List (Dictionary<>는 지원하지 않는다.)

또한 [System.Serializable]로 클래스나 구조체를 선언하면 해당 객체도 직렬화 대상으로 간주된다. 에디터에는 기본적으로 변수 이름 옆에 접었다 펼칠 수 있는 버튼과 레이아웃이 나타나고 들여쓰기로 구조체나 클래스 내에서 SerializedObject가 될 수 있는 속성을 가진 필드에 대한 목록이 표시 된다.

 

 

[정적(static) 변수가 아니어야 함]

생각해보면 정적변수는 에디터 상에서 편집되어서는 안되는 값이다.

 

 

[추상(abstract) 클래스가 아니어야 함]

추상 클래스면 이 클래스 자체로 인스턴스화 될 수 없기 때문에 에디터에 표시 할 수 없겠다.

 

 

몇 가지 직렬화 대상을 테스트 할 수 있는 코드이다. (오브젝트에 붙여보면 된다.)

더보기
using UnityEngine;
using UnityEditor;

[System.Serializable]
public struct StructA
{
    public int n;
    public string s;
}

public class Test : MonoBehaviour
{
    readonly public int ReadonlyPublicA = 1;
    public int PublicA = 2;
    [SerializeField] int SerializeFieldA = 3;
    public const int PublicConstA = 4;

    public StructA StructA;
}


[CustomEditor(typeof(Test), true)]
public class TestEditor : Editor
{
    const string a1Name = "ReadonlyPublicA";
    const string a2Name = "PublicA";
    const string a3Name = "SerializeFieldA";
    const string a4Name = "PublicConstA";
    const string a5Name = "StructA";

    SerializedProperty a1;
    SerializedProperty a2;
    SerializedProperty a3;
    SerializedProperty a4;
    SerializedProperty a5;

    private void OnEnable()
    {
        a1 = serializedObject.FindProperty(a1Name);
        a2 = serializedObject.FindProperty(a2Name);
        a3 = serializedObject.FindProperty(a3Name);
        a4 = serializedObject.FindProperty(a4Name);
        a5 = serializedObject.FindProperty(a5Name);

        if (a1 == null) Debug.LogWarning($"{a1Name} is not serializedObject");
        if (a2 == null) Debug.LogWarning($"{a2Name} is not serializedObject");
        if (a3 == null) Debug.LogWarning($"{a3Name} is not serializedObject");
        if (a4 == null) Debug.LogWarning($"{a4Name} is not serializedObject");
        if (a5 == null) Debug.LogWarning($"{a5Name} is not serializedObject");

    }

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

        if (a1 != null) EditorGUILayout.PropertyField(a1, true);
        if (a2 != null) EditorGUILayout.PropertyField(a2, true);
        if (a3 != null) EditorGUILayout.PropertyField(a3, true);
        if (a4 != null) EditorGUILayout.PropertyField(a4, true);
        if (a5 != null) EditorGUILayout.PropertyField(a5, true);

        serializedObject.ApplyModifiedProperties();
    }
}

SerializedObject 사용

 

아래 클래스가 있다고 하자.

public class A : UnityEngine.MonoBehaviour
{
    public B BClass = new();
    public string Name = "AClass";
    public int[] Arr = { 1, 2, 3 };
}

[System.Serializable]
public class B
{
    public string Name = "BClass";
}

 

[SerializedObject 가져오기]

생성자로 SerializedObject를 가져올 수 있다. 이 때 인자는 UnityEngine.Object인데, Component라고 생각하면 편할 것 같다.

A a = GetComponent<A>(); // 이 외에도 UnityEngine.Object인 값을 여러가지 방법으로 가져올 수 있음
SerializedObject serializedObject = new(a);

다음과 같이 생성자에 배열을 넘겨주어 한번에 여러개를 가져올 수도 있다.

Transform[] transforms = ~~~ // 컴포넌트를 얻었다고 가정
SerializedObject transformArraySerializeObject = new(transforms); // 배열이 아님에 주의!

 

[FindProperty(string propertyPath) 사용]

SerializedObject에 포함되어 있는 함수인 FindProperty() 함수로 직렬화 가능한 프로퍼티를 변수 이름으로 가져올 수 있고 타입은 SerializedProperty이다. 해당 이름의 변수를 찾지 못하면 null 값을 반환한다.

추가적으로, ‘.’을 이용하면 프로퍼티의 맴버에도 접근할 수 있다. (아래에는 BClass라는 이름의 변수에 있는 Name이란 이름의 변수의 프로퍼티를 가져온다.)

SerializedProperty nameProperty = serializedObject.FindProperty("Name");
SerializedProperty bClassProperty = serializedObject.FindProperty("BClass");
SerializedProperty arrProperty = serializedObject.FindProperty("Arr");
// 아래 두 코드는 같은 값을 반환한다.
SerializedProperty bClassNameProperty = serializedObject.FindProperty("BClass.Name");
SerializedProperty bClassNameRelativeProperty = serializedObject.FindProperty("BClass").FindPropertyReleative("Name");

NOTE: 변수 이름을 인자로 넣는데 정확히 일치해야 한다! (대소문자 주의!)

NOTE: 테스트를 해봤는데 serializedObject가 UnityEngine.Object를 상속하고 있을 경우 ‘.’을 이용해 맴버 프로퍼티에 접근하거나 FindPropertyRelative()로 상대 경로를 통해 접근할 수 없다. (null 값 반환)

 

[value 가져오기]

SerializedProperty에는 프로퍼티에 대한 value를 가져올 수 있는 여러 함수가 있다.

가져오려는 타입에 맞는 함수를 이용해 value 값을 가져오면 되는데, 주의해야 할 점은 반드시 타입이 일치해야 한다는 것이다. 만약 일치하지 않으면 Error가 발생할 것이고 할당하려던 값은 default 값이 들어가 있을 것이다.

배열 데이터는 GetArrayElementAtIndex()를 사용하면 된다.

 

몇몇 기본으로 가져올 수 있는 직렬화된 타입이 있고 scene 상에 참조하고 있는 오브젝트가 있을 경우, object로도 가져올 수가 있다.

 

stringValue, intValue, floatValue, boolValue, animationCurveValue, vector3Value …

objectReferenceValue (return UnityEngine.Object)

string name = serializedObject.FindProperty("Name").stringValue;
int index0 = serializedObject.FindProperty("Arr").GetArrayElementAtIndex(0).intValue;
int intName = serializedObject.FindProperty("Name").intValue; // Error: intName would be 0 (default)
public class C : UnityEngine.MonoBehaviour { ... }

public class Monster : MonoBehaviour 
{
	public C CClass;
	void Start() { CClass = GetComponent<C>(); }
}

////////

// Monster에 대한 SerializedObject가 있다고 가정: serializedObjectC

C obj = serializedObjectC.FindProperty("CCLass").objectReferenceValue as C;
Debug.Log(obj); // scene의 CClass가 참조하고 있는 게임 오브젝트 name 출력
// 마찬가지로 obj.~~를 통해 C의 맴버 접근 가능

NOTE: GetArrayElementAtIndex(int index)는 인덱스에 해당하는 값을 반환하는 것이 아닌 해당하는 값을 SerializedProperty값으로 반환하기 때문에 타입에 맞는 필드를 다시 가져와야 한다.

NOTE: objectReferenceValue는 UnityEngine.Object를 반환한다는 것에 유의하자.

 


Property 이름 얻기

GetIterator()로 SerializedObject가 가지고 있는 프로퍼티의 propertyPath(변수 이름)를 모두 string으로 읽고 가져올 수 있다.

var it = serializedObject.GetIterator();
while(it.NextVisible(true))
{
	Debug.Log(it.propertyPath);
}

// result
m_Scirpt
BClass
BClass.Name // UnityEngine.Object 상속 시 포함 안됨
Name
Arr
Arr.Array.Size // 배열에 대한 사이즈도 따로 가지고 있는 것을 알 수 있다.
Arr.Array.data[0]
Arr.Array.data[1]
Arr.Array.data[2]

참고: 위에서 B를 UnityEngine.Object를 상속할 시에 내부 맴버를 ‘.’으로 접근하거 나 FindPropertyRelativePath로 접근이 안되다고 했었는데, 이 이유를 여기서 알 수 있다. GetIterator()로 프로퍼티를 모두 확인해보면 일반 클래스는 직렬화 가능한 맴버가 [이름].[맴버이름]이렇게 경로로 다 지정되어 있는데, UnityEngine.Object를 상속받은 클래스는 맴버에 대한 path가 없는 것을 확인할 수 있다. (오직 그 클래스 자체만 프로퍼티로 사용)

 

 

이 내용을 파일에서 텍스트로 직접 확인해 볼 수 있다. 위에서 SerializedObject가 필드 값을 직렬화하여 YAML의 텍스트로 저장한다고 했었는데, 실제로 위의 코드가 포함된 Scene이나 Prefab의 파일을 직접 텍스트 파일로 열면 알 수 있다.

 

아래는 위의 A 클래스를 포함하는 오브젝트를 프리팹으로 만들고 (Asset 형태로 만들고) 프리팹을 파일 탐색기에서 직접 텍스트 파일로 연내용이다. (Asset Serialization - Force Text 일때)

%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &468803989373382404
GameObject:
  ...(생략)
--- !u!114 &468803989373382405
MonoBehaviour:
  ... (생략)
  PublicA: 2
  SerializeFieldA: 3
  StructA:
    n: 0
    s: 
--- !u!114 &468803989373382400
MonoBehaviour:
  ... (생략)
  m_Script: {fileID: 11500000, guid: bb82660eb55292c45ab29ce90ea42a8f, type: 3}
  m_Name: 
  m_EditorClassIdentifier: 
  BClass:
    Name: BClass
  Name: AClass
  Arr: 010000000200000003000000

프리팹화 된 오브젝트에 붙어있던 Component 속성들이 다 나타나있고 Component 속성에 대한 필드 값이 직접적으로 나와 있다. 즉, 직렬화 되어 파일에 쓰여진 것이다.

 

GameObject는 말 그대로 기본이 되는 GameObject에 대한 내용이고, 다음은 프리팹에 붙여져 있던 첫번 째 MonoBehaviour를 상속한 클래스로 앞에 SerializedObject 예시에서 사용했던 스크립트에 대한 내용이다. (값이 직접적으로(직렬화되어) 나타나있다.)

 

마지막으로 위에서 출력했던 내용을 담은 스크립트인데 맴버를 보면 GetIterator()로 가져왔던 내용이 나와 있는 것을 알 수 있다! (신기신기!!!!!!!!!!!!)

(추가적으로 Arr가 왜 저렇게 저장되어 있는지 좀 더 연구해보면 재미있겠지?)


다음은 SerializedObject를 이용해 CustomEditor를 간단하게 만들고 사용하는 방법을 작성해보겠다.


References:

https://m.blog.naver.com/hammerimpact/220770624015

https://docs.unity3d.com/Manual/script-Serialization.html

 

Unity - Manual: Script serialization

Running Editor Script Code on Launch Script serialization Serialization is the automatic process of transforming data structures or GameObject states into a format that Unity can store and reconstruct later. How you organize data in your Unity project affe

docs.unity3d.com

 

참고할 만한 추가 자료 (Serialization Rules)