게임엔진/Unity

[Unity] Editor SerializedObject에 대해서

ShovelingLife 2025. 6. 12. 18:56

SerializedObject란

SerializedObject는, Serialize된 데이터를 Unity에서 다루기 쉬운 쪽으로 가공한 것이다. 이로 인해, 다양한 데이터에 접근할 수 있다. 또한 Undo 처리와 게임 오브젝트에서 Prefab를 쉽게 작성할 수 있도록 한다.

SerializedObject는, Unity 상에서 다루는 모든 오브젝트와 관계되어 있다. 보통 다루고 있는 Asset(재질이나 텍스쳐, 애니메이션 클립 등)도 SerializedObject가 없으면 만들 수 없다.

 

[UnityEngine.Object와 SerializedObject의 관계]

유니티 에디터 상에서는, 모든 오브젝트(UnityEngine.Object)는 SerializedObject로 변환되어 다루어다. 인스펙터에서 컴포넌트의 수치를 편집할 때도, Component의 인스턴스를 편집하고 있는게 아니라, SerializedObject의 인스턴스를 편집하고 있는 것이다.

유니티 에디터 상에서는 반드시 SerializedObject 경유로 수치를 편집한다. 단, CustomEditor를 껴넣은 경우에는 아니다.

유니티 에디터 상에서, 즉 에디터 확장에서는 가능한 한 모든 오브젝트 조작을 SerializedObject에서 실행할 필요가 있기 때문에 SerializedObject에서는 Serialize된 데이터를 다루는 것 뿐만 아니라 Undo 나 Selection의 조작도 실행한다.

Undo의 조작

SerializedObject에서 수치를 편집할 때, Undo 처리는 의식하지 않고도 등록되어 있다. UnityEngine.Object의 인스턴스를 직접 편집한 경우에는, Undo 처리를 독자적으로 만들어야 한다. Undo에 대해서 자세한 것은 12장에서 다룬다.

Selection의 조작

프로젝트 윈도우에서 Asset을 선택할때, 즉석에서 Deserialize해서 UnityEngine.Object의 인스턴스를 얻어와서, 인스펙터에 수치를 표시한다. 이 조작은 다수의 오브젝트를 선택했을때에 Serialize된 프로퍼티의 동시 편집을 가능하게 하는 구조에서 도움이 된다.

이렇듯 유니티에서 오브젝트를 다룰 때 편리한 기능을 포함하고 있다. 만약, SerializedObject를 경유해서 오브젝트를 다루지 않을경우, Undo나 Selection같은 조작을 직접 만들어야 한다. 이 두개의 조작에 대해서는 9장 CustomEditor에서 설명하고 있다. 또한, 이 장의 후반에서도 가볍게 설명한다.

[Asset과 SerializedObject의 관계]

UnityEngine.Object를 Asset으로 저장할 때, 바이너리 형식, 혹은 YAML 형식의 텍스트데이터로써 보존한다. 이들의 Serial화를 담당하는게 SerializedObject다.

단순하게 이미지화 해보면 아래 그림과 같이 다. UnityEngine.Object를 Asset으로 보존하는것은 SerializedObject로 한번 변환시키고, 다음에 변환시킨 SerializedObject는 Asset과 .meta 파일의 작성을 시도한다.

[Asset과 .meta 파일]

SerializedObjec에서는 Asset과 .meta 파일의 2가지를 작성한다. Asset은 실제 오브젝트가 Serial화 된 것이다. .meta 파일은 importer의 설정 등을 저장한다.

[InitializeOnLoadMethod]
static void CheckPropertyPaths ()
{
        var so = new SerializedObject (Texture2D.whiteTexture);

        var pop = so.GetIterator ();

        while (pop.NextVisible (true))
                Debug.Log (pop.propertyPath);

}

 

로그에 표시되는 것은 아래와 같다

m_ImageContentsHash.bytes[0]
m_ImageContentsHash.bytes[1]
.
.
.
m_IsReadable
m_TextureSettings
m_ColorSpace

 

이와 같이 Texture2D 오브젝트는 SerializedObject로 변환한 때에는 importer의 설정도 가지고 있다. Texture2D를 Asset으로써 저장할 경우에는, 이들 설정을 디스크 상에 있는 텍스쳐(jpg, png) 에 덮어쓸 수는 없기 때문에 .meta 파일에 쓰는 형태로 되어 있다.

또한 반대로, Asset을 import할 때에는 Asset과 .meta파일(.meta 파일이 없으면 기본 설정으로 자동생성)로부터 SerializedObject가 생성되어, UnityEngine.Object로 변환된다.

[Serialize 대상의 클래스 변수]

UnityEngine.Objectr의 자식 클래스(유저가 자주 손대는 MonoBehaviour, ScriptableObject, Editor, EditorWindow 등)에서, Serialize 대상의 필드라고 판단시키기 위해서는 조건을 충족해야 한다.

- public 변수일것. 혹은 SerializedField 속성이 붙은 필드일것.

- Serialize 가능한 유니티가 지원하고 있는 자료형일것.(byte、short、int、long、byte、ushort、uint、ulong、float、double、bool、char、string、UnityEngine.Object、Serializable 속성을 추가한 클래스와 구조체 등)

더해서 자세하게 들어가면 두개가 더 붙는다.

- 변수가 static, const, readonly가 아닐것.

- abstract 클래스가 아닐것.

입문서 등에서는 곧잘 [인스펙터에 변수의 수치를 표시하기 위해서는 public으로 둔다라고 하고 있지만 이것은 프로그래머 이외의 사람들도 이해하기 쉽게 말한 것이고, public 변수로 두는 것은 serialize 대상의 조건 중 하나일 뿐이다. 에디터 확장을 하는 유저는 private 필드에 serializeField 속성을 붙이는 것을 추천한다.

[SerializeField]
private string m_str;

public string str {
        get {
                return m_str;
        }
        set {
                m_str = value;
        }
}

외부에서 SerializeField 속성을 붙인 필드에 접근할 때 SerializedObject를 경유해서 접근한다.

​사용법

[SerializedObject에서 파라미터를 얻기]

Serialize된 데이터는 SerializedProperty로써 얻을수 있다.

iterator로써 얻을수 있고, 본 장의 전반부에 소개했던, 프로퍼티의 목록을 로그로 표시하는 코드는 iterator를 써서 조작가능한 모든 프로퍼티를 얻어온다.

[InitializeOnLoadMethod]
static void CheckPropertyPaths ()
{
        var so = new SerializedObject (Texture2D.whiteTexture);

        var pop = so.GetIterator ();

        while (pop.NextVisible (true))
                Debug.Log (pop.propertyPath);

}

 

또한, Path를 지정해서 특정 SerializedProperty를 얻을 수 있다.

예를들어 Vector3 형 position변수의 수치를 얻을 때,

public class Hoge : MonoBehaviour
{
    [SerializeField] Vector3 position;
}
var hoge = /* 다양한 방법으로 Hoge 컴포넌트를 얻을 수 있음 */;

var serializedObject = new SerializedObject(hoge);
serializedObject.FindProperty ("position").vector3Value;

 

Fuga형의 fuga 변수 안에 있는 string형 bar 변수의 수치를 얻고싶을때

[System.Serializable]
public class Fuga
{
        [SerializeField] string bar;
}
public class Hoge : MonoBehaviour
{
    [SerializeField] Fuga fuga;
}
var hoge = /* 다양한 방법으로 Hoge 컴포넌트를 얻을 수 있음 */;

var serializedObject = new SerializedObject(hoge);
serializedObject.FindProperty ("fuga.bar").stringValue;

 

[string형 배열에서 두번째] 수치를 얻고 싶을때

public class Hoge : MonoBehaviour
{
    [SerializeField] string[] names;
}
var hoge = /* 다양한 방법으로 Hoge 컴포넌트를 얻을 수 있음 */;

var serializedObject = new SerializedObject(hoge);
serializedObject.FindProperty ("names").GetArrayElementAtIndex(1);

 

[최신 데이터를 얻고 갱신하기]

SerializedObject는 내부에서 캐싱되고 인스턴스화될 때, 이미 캐싱되어 있으면 캐쉬로부터 끌어온다. 예를들어, 에디터 윈도우와 인스펙터 내부에서 각자 1개의 오브젝트에 대한 SerializedObject를 생성하는 경우, 2개의 SerializedObject를 동기화하지 않으면, 둘중 하나는 낡은 정보로 갱신되어 버릴지도 모른다.

이렇듯 오브젝트에 대한 SerializedObject가 2개 존재하는 경우, 한쪽이 낡은 정보로 갱신되면 안되듯이, 2개의 SerializedObject는 항상 최신 상태를 유지해야 한다.

이 현상을 해결하기 위해서 2개의 API가 제공된다.

[Update]

내부 캐쉬에서 최신 데이터를 얻는다. 항상 최신 정보를 다루기 위해서 SerializedObject에 접근하기 전에 호출하자

using UnityEngine;
using UnityEditor;

public class NewBehaviourScript : Editor
{
        public override void OnInspectorGUI ()
        {
                serializedObject.Update ();

                EditorGUILayout.PropertyField (serializedObject.FindProperty ("name"));
        }
}

 

[ApplyModifiedProperties]

내부 캐쉬에 변경점을 적용한다. 앞서 본 Update로 항상 최신 정보를 받으면, ApplyModifiedProperties로 변경점을 적용한다. 이것들을 한 세트로 써야한다

딱히 변경점을 적용하기 위한 조건이 없는 경우, Update를 함수의 처음 행에, ApplyModifiedProperties를 함수의 마지막 행에 넣는다.

using UnityEngine;
using UnityEditor;

public class NewBehaviourScript : Editor
{
        public override void OnInspectorGUI ()
        {
                serializedObject.Update ();
                EditorGUILayout.PropertyField (serializedObject.FindProperty ("name"));

                // 그외, 여러가지 처리
                serializedObject.ApplyModifiedProperties ();
        }
}

 

반대로 말하면, Update를 하지 않는 한 외부에서 변경된 프로퍼티를 반영하지 않고, 또한 ApplyModifiedProperties를 하지 않는 한 외부에 적용하지 않는다는 것이다.

​여러개의 UnityEngine.Object를 1개의 SerializedObject로 다루기

SerializedObject의 생성자(Constructor)에서 배열을 건네주는 것만으로 여러 개의 UnityEngine.Object를 다룰 수 있다. 단, 인자로써 넘겨주는 것은 같은 자료형만 가능하다. 만약 다른 자료형의 오브젝트를 인자로 넘겨주면 키맵(?)이 일치하지 않고 에러가 발생한다.

// 여러개의 Rigidbody
Rigidbody[] rigidbodies = /* 다양한 방법으로 Rigidbody 컴포넌트를 얻기 */;

var serializedObject = new SerializedObject(rigidbodies);

serializedObject.FindProperty ("m_UseGravity").boolValue = true;

프로퍼티 이름을 알기 위해서는

SerializedProperty에 접근하기 위해서는 프로퍼티의 경로를 알아야 한다. 직접 만든 MonoBehaviour 컴포넌트에 접근하는 경우에는 프로퍼티의 경로는 스크립트 파일을 보면 바로 알 수 있다. 유니티 측에서 설치한 컴포넌트와 UnityEngine.Object 관련 프로퍼티는 프로퍼티 이름이 m_이 붙어 있는 경우가 있다. m_은 인스펙터 상에서는 생략되어 프로퍼티 이름으로써 표시되기 때문에, 실제 프로퍼티를 다루기 힘들게 되어 있다. 또한, 인스펙터에 표시되는 프로퍼티 이름과 실제 프로퍼티 이름이 일치하지 않는 경우가 있다.

프로퍼티를 아는 방법은 크게 2가지 패턴이 있다.

[SerializedObject.GetIterator]

iterator를 사용해 프로퍼티 이름을 아는 방법이다.

[Asset을 텍스트에디터로 보기]

대상이 컴포넌트이면, Asset Serialization을 Force Text로 설정한 다음에 Prefab으로 만들어, 텍스트 에디터로 Prefab을 연다.

YAML 형식의 데이터를 볼 수 있고, 거기에 프로퍼티 이름이 들어있다.

%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &113998
GameObject:
  m_ObjectHideFlags: 0
  m_PrefabParentObject: {fileID: 0}
  m_PrefabInternal: {fileID: 100100000}
  serializedVersion: 4
  m_Component:
  - 4: {fileID: 442410}
  - 54: {fileID: 5488994}

... 略 ...

--- !u!54 &5488994
Rigidbody:
  m_ObjectHideFlags: 1
  m_PrefabParentObject: {fileID: 0}
  m_PrefabInternal: {fileID: 100100000}
  m_GameObject: {fileID: 113998}
  serializedVersion: 2
  m_Mass: 1
  m_Drag: 0
  m_AngularDrag: .0500000007
  m_UseGravity: 1
  m_IsKinematic: 0
  m_Interpolate: 0
  m_Constraints: 0
  m_CollisionDetection: 0

 

또한, 재질 등의 Unity 독자적인 Asset도 텍스트에디터로 볼 수 있다.

추가적인 지식으로써, UnityEditorInternal 네임 스페이스에 있는 InternalEditorUnity.SaveToSerializedFileAndForget 으로 UnityEngine.Object를 Asset으로써 저장할 수 있다.

using UnityEngine;
using UnityEditorInternal;
using UnityEditor;

public class NewBehaviourScript : MonoBehaviour
{
        void Start ()
        {
                var rigidbody = GetComponent<Rigidbody> ();

                InternalEditorUtility.SaveToSerializedFileAndForget (
                    new Object[]{ rigidbody },
                    "Rigidbody.yml",
                    true);
        }
}

 

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