제 5장 SerializedObject에 대해서

2019. 7. 28. 15:56유니티/(Old)에디터 확장

 

Unity エディター拡張入門

Web版無償公開中!Unityエディター拡張の入門書!

anchan828.github.io

Unity에서는 파일(Unity에서는 에셋)을 조금 특수한 형식으로 변환해서 사용합니다. 본 장에서는 Unity로 객체를 사용할 때 기반이 되는 SerializedObject에 대해 설명합니다. 그리고 「Serialize에 대해 모든 것에 대한 정보」는 Unity 공식 메뉴얼에서 설명되어있습니다. 본 장에서는 입문으로써 알아둬야하는 정보를 발췌해서 설명합니다.

 

Unity - Manual: Script Serialization

Script Serialization Serialization is the automatic process of transforming data structures or object states into a format that Unity can store and reconstruct later. Some of Unity’s built-in features use serialization; features such as saving and loading,

docs.unity3d.com

5.1 SerializedObject란

SerializedObject는 Serialize된 데이터를 Unity가 사용하도록 가공한 객체입니다. 이 객체를 사용하여 여러가지 데이터에 접근이 가능하며, Undo 처리, 게임 객체로부터 프리팹을 용이하게 작성할 수 있습니다.

SerializedObject는 Unity에서 사용하는 모든 객체에 관계가 있습니다. 주로 사용하는 에셋인 재질, 텍스처, 애니메이션 등도 SerializedObject를 사용합니다.

UnityEngine.Object 와 SerializedObject의 관계

Unity 에디터에서 모든 객체(UnityEngine.Object)는 SerializedObject로 변환해서 사용합니다. 인스펙터로 컴포넌트의 값을 편집할 때, Component의 인스턴스를 편집하는 것이 아니라, SerializedObject의 인스턴스를 편집합니다.

그림 5.1 : 유니티 에디터에서는 반드시 SerializedObject를 통해서 편집됩니다. 단 CustomEditor를 사용하는 경우 별도로 처리됩니다.

Unity 에디터, 즉 에디터 확장에서는 가능한 모든 객체의 조작을 SerializedObject로 접근할 필요가 있습니다. 에디터에서는 Serialize된 데이터를 사용할 뿐만 아니라 Undo, Selection의 핸들링도 가지기 때문에 이와 같은 처리를 하려면 SerializedObject가 적합합니다.

Undo의 핸들링

SerializedObject로 값을 편집할 때, Undo처리는 사용자가 별도로 처리할 필요없이 자동적으로 제공됩니다.

UnityEngine.Object의 인스턴스를 직접 편집할 경우, Undo처리를 별도로 처리할 필요가 있습니다. Undo에 대한 자세한 부분은 제 12장 「Undo 에 대해서」를 참고해주세요.

Selection의 핸들링

프로젝트 윈도우로 에셋을 선택할 때, 즉시 Deserialize하여 UnityEngine.Object의 인스턴스를 취득하여, 인스팩터에 값을 표시할 수 있습니다. 이 핸들링의 주된 용도는 복수의 객체를 선택했을 때, Serialize된 프로퍼티의 동시편집을 할 수 있다는 것입니다.

프로젝트 윈도우

이처럼 Unity에서는 객체를 사용함에 있어서 편리한 기능들을 제공하고 있습니다. 혹시 SerializedObject를 통해서 객체를 사용하지 않을 경우, Undo, Selection의 핸들링을 직접 구현해야합니다. 이 2개의 핸들링에 대해서는 제 9장 「CustomEditor」에서 설명하며 이 장에서도 가볍게 언급을 합니다.

에셋과 SerializedObject의 관계

UnityEngine.Object를 에셋으로 보존할 경우 이진 형식, 혹은 YAML 형식의 텍스트 데이터로써 보존됩니다. 이들에 대한 직렬화를 처리하는 것이 SerializedObject입니다.

위의 관계를 표현하면 그림 5.2와 같습니다. UnityEngine.Object를 에셋으로 보존하려면 SerializedObject로 한번 변환을 거칩니다. 변환한 SerializedObject는 에셋과 .meta파일로 작성돱니다.

그림 5.2 : 데이터의 흐름

에셋과 .meta 파일

SerializedObject에서 에셋과 .meta파일 2개를 작성합니다. 에셋은 실제 객체가 직렬화된 것입니다. .meta파일은 Import 설정과 같은 정보를 보존합니다. Import에 대해서는 밑의 사이트를 참고해주세요.

 

임포트 - Unity 매뉴얼

프로젝트의 Assets 폴더로 파일을 직접 익스포트하거나 해당 폴더로 복사하면 Unity로 생성하지 않은 에셋을 Unity 프로젝트로 가져올 수 있습니다. 여러 일반적인 포맷의 소스 파일을 프로젝트의 Assets 폴더에 직접 저장할 수 있고 Unity에서 해당 파일을 읽을 수 있습니다. 또한 파일에 새로운 변경 사항을 저장하면 Unity가 인식하여 필요한 경우 파일을 다시 임포트합니다.

docs.unity3d.com

시험삼아 밑의 코드를 테스트해봅니다.

[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
public sealed class Texture2D : Texture
> public class Texture : Object

이처럼 Texture2D(UnityEngine.Object) 객체는 SerializedObject로 변환될 때 Import 설정을 가진다는 것을 알 수 있습니다. Texture2D를 에셋으로 변환할 경우 이 에셋에 관한 설정을 디스크의 어딘가에 저장되어있는 텍스처(jpg나 png)에 쓰면 안 되므로 .meta 파일에 따로 쓰는 방식으로 되어있습니다.

역으로, 에셋을 Import할 떄는 에셋과 .meta파일(.meta 파일이 없으면 디폴트 설정으로 자동 생성)로부터 SerializedObject가 생성되어, UnityEngine.Object로 변환됩니다.

직렬화 대상이 되는 클래스 변수

UnityEngine.Object의 파생 클래스(사용자가 자주 접하는 Monobehaviour, ScriptableObject, Editor, EditorWindow와 같은)에서 직렬화 대상이 되는 필드인지 아닌지 판단하는 조건이 있습니다.

접근제한자가 public 변수이거나 SerializeField 속성을 가진 필드이어야합니다.

Unity에서 직렬화 가능한 타입이어야합니다.(sbyte, short, int, long, byte, ushort, uint, ulong, float, double, bool, char, string, UnityEngine.Object, Serializable 속성을 부가한 클래스와 구조체 등)

변수가 static, const, readonly가 아니어야합니다.

abstract 클래스가 아니어야합니다.

에디터 확장을 진행하는 사용자는 private 필드에 SerializeField 속성을 선언하는 것으로 직렬화를 할 수 있습니다.

[SerializeField]
private string m_str;
 
public string str {
        get {
                return m_str;
        }
        set { m_str = value;
        }
}

외부에서 SerializeField 속성을 지정한 필드에 접근할 때 SerializedObject를 통해서 접근합니다.

5.2 SerializedObject의 사용하는 방법

본격적으로 다루는 방법은 제 9장 「CustomEditor」에 소개합니다. SerializedObject를 잘 다루기 위해서 API를 소개합니다.

SerializedObject로부터 파라매터를 얻어오기

직렬화된 데이터는 SerializedProperty로 얻어오는 것이 가능하며 반복자로 사용할 수 있습니다.

전반으로 소개한 프로퍼티의 일람을 로그로 표시한 코드는 반복자를 사용하여 조작가능한 모든 프로퍼티를 얻어왔습니다.

[InitializeOnLoadMethod]
static void CheckPropertyPaths()
{
    var so = new SerializedObject(Texture2D.whiteTexture);
    var pop = so.GetIterator();
 
    while (pop.NextVisible(true))
        Debug.Log(pop.propertyPath);
}

혹은 경로를 지정해서 특정 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 배열에서 2번 째」의 값을 취득할 때

public class Hoge : MonoBehaviour
{
    [SerializeField] Fuga fuga;
}
var hoge = /* 여러가지 방법으로 Hoge 컴포넌트를 취득 */;
 
var serializedObject = new SerializedObject(hoge);
serializedObject.FindProperty("names").GetArrayElementAtIndex(1);

최신 데이터를 취득, 갱신

SerializedObject는 내부로 캐시가 되어, 인스턴스화 되었을 때, 이미 캐시되어 있으면 캐시되어있는 것을 가져옵니다.

예를 들면 에디터 윈도우와 인스팩터 내부에 각 각 하나의 객체에 대한 SerializedObject를 생성한 경우 이 2개의 SerializedObject를 동기화해야합니다. 동기화를 하지 않으면 어느 한 쪽이 이전의 정보를 가지고 작업을 하게 되며 서로간 다른 정보를 가지게 됩니다.

그림 5.3 : 같은 UnityEngine.Object를 SerializedObject로 변환한 객체

이처럼 객체에 대한 SerializedObject가 2개 존재하는 경우, 어느 한 쪽에 이전 정보로 갱신하지 않도록 해야합니다. 2개의 SerializedObject는 항상 최신 정보 상태로 가지고 있어야합니다.

이를 해결하기 위하여 2개의 API를 제공합니다.

Update

내부 캐시에서 최신 데이터를 취득합니다. 항상 최신 정보로 두기 위해서는 SerializedObject에 접근하기 전에 Update함수를 호출합니다.

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

ApplyModifiedProperties

내부 캐시에 변경된 부분을 적용합니다. 앞서 Update로 항상 최신 정보로 두고, 함수 내에서 변경된 부분을 적용하는 것은 ApplyModifiedProperties를 사용합니다. Update와 ApplyModifiedProperties는 한 셋트로 생각해야합니다.

변경되는 부분을 적용하기 위한 조건이 없을 경우 Update를 함수의 최초 행에, ApplyModifiedProperties를 함수의 최하단 행에 기술합니다.

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

Update를 호출하지 않으면 외부로부터 변경된 프로퍼티를 반영할 수 없으며, ApplyModifiedProperties를 호출하지 않으면 외부에 적용할 수 없습니다.

(tortoisegit으로 비유하자면 Pull은 Update, ApplyModifiedProperties은 Commit->Push로 볼 수 있습니다.)

5.3 복수 UnityEngine.Object를 단일 SerializedObject로 사용

SerializedObject의 생성자로 배열을 전달하는 것으로 복수의 UnityEngine.Object를 사용하는 것이 가능합니다. 단, 인수를 전달하는 것은 같은 타입에 한해서 입니다. 혹시 다른 타입의 객체를 인수로 전달한 경우 키 맵에서 일치하지 않는다는 에러를 발생합니다.

//복수의 강체
Rigidbody[] rigidbodies = /* 여러가지 방법으로 Rigidbody 컴포넌트를 취득 */;
 
var serializedObject = new SerializedObject(rigidbodies);
 
serializedObject.FindProperty("m_UseGravity").boolValue = true;

5.4 프로퍼티 명을 알아내기

SerializedProperty에 접근하려면 프로퍼티의 경로를 알아야합니다. 자신이 작성한 MonoBehaviour 컴포넌트에 접근할 경우 프로퍼티의 경오는 스크립트 파일을 보면 알 수 있습니다.

Unity 측에서 사용되는 컴포넌트와 UnityEngine.Object 관련된 프로퍼티는 프로퍼티 명에 m_로 시작되는 경우가 있습니다. m_는 인스펙터에서 표시될 때 생략되어 표시되기에 실제 프로퍼티를 파악하는 것이 어려울 수 있습니다. 혹은 인스펙터에 표시된 프로퍼티 명과 실제 프로퍼티 명이 일치하지 않는 경우도 있습니다. 프로퍼티를 알기 위한 방법은 크게 2 패턴이 있습니다.

SerializedObject.GetIterator

반복자를 사용하여 프로퍼티 명을 모두 가져오는 방법입니다. 이에 대한 방법은 본 장의 최초에서 소개했습니다.

에셋을 텍스트 파일로 만들어서 살펴보기

대상이 컴포넌트라면 Asset SerializationForce Text로 설정하여 프리팹화하여 텍스트 에디터로 프리팹을 열어봅니다.

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 도작적인 에셋도 텍스트 에디터로 보는 것이 가능합니다.

간단하게 언급하며 UnityEditorInternal 이름 공간에서 InternalEditorUtility.SaveToSerializedFileAndForget에서 UnityEngine.Object를 에셋으로 보존하는 것이 가능합니다.

using UnityEngine;
using UnityEditorInternal;
using UnityEditor;
 
public class NewBehaviourScript : MonoBehaviour
{
    void Start()
    {
        var rigidbody = GetComponent<Rigidbody>();
 
        InternalEditorUtility.SaveToSerializedFileAndForget(
            new Object[] { rigidbody },
            "Rigidbody.yml",
            true);
    }
}

 

 

'유니티 > (Old)에디터 확장' 카테고리의 다른 글

제7장 EditorWindow  (0) 2019.08.11
제 6장 EditorGUI (EditorGUILayout)  (0) 2019.07.28
제4장 ScriptableObject  (0) 2019.07.28
제3장 데이터 보존  (0) 2019.07.28
제2장 표준에서 사용하는 에디터 확장기능  (0) 2019.07.27