제3장 데이터 보존

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

 

Unity エディター拡張入門

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

anchan828.github.io

에디터 확장에서 기능을 구현할 때, 값을 보존해서 다음에도 계속해서 사용할 경우가 있습니다. 이 값은 에디터 확장의 설정에 대한 파라메터이거나, 게임의 대한 파라메터일 수도 있습니다. Unity는 데이터를 보존할 수단이 크게 구분해서 3개가 있습니다. 이 장에서는 이 3개를 소개하면서 목적에 맞게 사용하는 방법을 설명합니다.

3.1 EditorPrefs

PC 내에서 공유할 수 있는 데이터를 보존하는 방법입니다. 프로젝트에 관계없이 유니티 에디터를 통해 값을 공유할 때 적합합니다.

사용 범위

보존한 값은 메이저 버전끼리 공유가 가능합니다.

Unity 4.x로 보존한 값은 Unity 4.x에서 사용할 수 있으며, Unty 5.x도 Unity5.x에서 사용할 수 있습니다. 하지만 Unity 4.x로 보존한 값을 Unity 5.x에서는 사용할 수 없습니다.

그림 3.1: Unity4.x끼리 공유, Unity5.x끼리 공유

무엇을 보존하나요?

윈도우의 위치, 크기, Unity 에디터의 환경 설정(Edit - Preferences에 있는 설정)의 값입니다. 독자적인 에셋이라도 환경에 대한 설정이면 EditorPrefs를 사용해야합니다. 주의할 것은 EditorPrefs를 통해서 보존된 값은 전부 평문으로 저장되므로 절대 패스워드와 같은 중요한 정보를 보존하지 마세요.

[평문 : 암호화가 되지 않는 내용을 의미합니다.]

EditorPrefs가 보존되어있는 장소

표 3.1

Windows(Unity4.x)

HKEY_CURRENT_USER\Software\Unity Technologies\UnityEditor 4.x

Windows(Unity5.x)

HKEY_CURRENT_USER\Software\Unity Technologies\UnityEditor 5.x

Mac OS X(Unity4.x)

~/Library/Preferences/com.unity3d.UnityEditor4.x.plist

Mac OS X(Unity5.x)

~/Library/Preferences/com.unity3d.UnityEditor5.x.plist

EditorPrefs는 메이저 버전마다 분할되서 보존됩니다. 특히 Window는 레지스트리의 값을 보존합니다.

EditorPrefs 경유만을 위해서라면 별 문제가 없지만 직접 레지스트리를 조작할 수도 있으므로 이 과정에서 잘못된 설정을 할 수 있습니다. 최악의 경우 Window가 기동하지 않을 수도 있습니다. 충분히 주의해주세요.

그림3.2: com.unity3d.UnityEditor5.x.plist 을 Xcode 로 연 상태

사용 방법

OnEnable와 같은 한 번만 호출되는 함수 안에서 값을 얻습니다. 값의 변경은 EditorPrefs에 보존합니다.

using UnityEditor;
 
public class ExampleWindow : EditorWindow
{
    int intervalTime = 60;
    const string AUTO_SAVE_INTERVAL_TIME = "AutoSave interval time (sec)";
 
    [MenuItem("Window/Example")]
    static void Open()
    {
        GetWindow<ExampleWindow>();
    }
 
    void OnEnable()
    {
        intervalTime = EditorPrefs.GetInt(AUTO_SAVE_INTERVAL_TIME, 60);
    }
 
    void OnGUI()
    {
        EditorGUI.BeginChangeCheck();
 
        intervalTime = EditorGUILayout.IntSlider("간격(초)", intervalTime, 13600);
 
        if (EditorGUI.EndChangeCheck())
            EditorPrefs.SetInt(AUTO_SAVE_INTERVAL_TIME, intervalTime);
    }
}

윈도우의 크기를 보존할 경우 중요성이 높지 않기에 OnDisable로 값을 보존하는 것이 적합합니다. 절대로 OnGUI로 매번 보존하지 않도록 해주세요. OnGUI는 지속적으로 갱신하는 함수로 읽고 쓰는 작업을 하게 되면 과부하가 걸립니다.

using UnityEngine;
using UnityEditor;
 
public class ExampleWindow : EditorWindow
{
    const string SIZE_WIDTH_KEY = "ExampleWindow size width";
    const string SIZE_HEIGHT_KEY = "ExampleWindow size height";
 
    [MenuItem("Window/Example")]
    static void Open()
    {
        GetWindow<ExampleWindow>();
    }
 
    void OnEnable()
    {
        var width = EditorPrefs.GetFloat(SIZE_WIDTH_KEY, 600);
        var height = EditorPrefs.GetFloat(SIZE_HEIGHT_KEY, 400);
        position = new Rect(position.x, position.y, width, height);
    }
 
    void OnDisable()
    {
        EditorPrefs.SetFloat(SIZE_WIDTH_KEY, position.width);
        EditorPrefs.SetFloat(SIZE_HEIGHT_KEY, position.height);
    }
}

3.2 EditorUserSettings.Set/GetConfigValue

프로젝트 내에서 공유할 수 있는 데이터의 보존 방법입니다. 여기서 보존되는 값은 암호화가 되어있습니다. 개인 정보인 패스워드와 같은 중요한 정보를 저장하는데 적합합니다.

사용 범위와 보존 장소

이 API로 보존한 데이터는 프로젝트 내에서만 사용할 수 있습니다.

데이터의 보존 장소는 Library/EditorUserSettings.asset에 있으므로 Library 폴더를 다른 사람과 공유하지 않는 이상 정보가 유출되지 않습니다. 공유, 버전 관리는 Library 폴더로 공유하면 안됩니다. .meta 파일을 공유하도록 해주세요.

무엇을 보존하나요?

여러가지 툴을 사용하고 있다면, 로그인을 위해 메일의 주소, 패스워드가 필요합니다. Oauth의 접근에 대한 것도 하나의 방법입니다. EditorUserSettings.asset은 바이너리 형식으로 보존되서 일반적으로 해석할 수 없습니다. 하지만 Unity에서 제공하는 binary2text를 사용하는 것으로 바이너리를 텍스트 형식으로 변환해서 볼 수 있으므로 주의해주세요.

사용 방법

시험삼아 데이터를 보존해봅니다.

using UnityEditor;
 
public class NewBehaviourScript
{
    [InitializeOnLoadMethod]
    static void SaveConfig()
    {
        EditorUserSettings.SetConfigValue("Data 1""text");
    }
}

보존된 데이터를 확인해봅니다. EditorUserSettings.asset은 바이너리 형식이므로 텍스트 형식으로 변환해봅니다.

cd /Applications/Unity/Unity.app/Contents/Tools
./binary2text /path/to/unityproject/Library/EditorUserSettings.asset

값이 암호화되어 보존됩니다.

External References
 
ID: 1 (ClassID: 162) EditorUserSettings
    m_ObjectHideFlags 0 (unsigned int)
    m_ConfigValues  (map)
        size 2 (int)
        data  (pair)
            first "Data 1" (string)
            second "17544c12" (string)
        data  (pair)
            first "vcSharedLogLevel" (string)
            second "0a5f5209" (string)
 
    m_VCAutomaticAdd 1 (bool)
    m_VCDebugCom 0 (bool)
    m_VCDebugCmd 0 (bool)
    m_VCDebugOut 0 (bool)
    m_SemanticMergeMode 2 (int)
    m_VCShowFailedCheckout 1 (bool)

3.3 ScriptableObject

프로젝트 내에서 공유할 수 있는 데이터의 보존 방법2로 여러 방면에서 응용이 가능한 보존 방법입니다. 팀 내에 공유할 설정, 대량의 데이터를 보존하고자 할 경우 이 방법을 선택하면 됩니다.

사용 범위

ScriptableObject 는 Unity 프로젝트 내에서 데이터를 보존하기 위한 주역입니다. Unity 프로젝트 내의 에셋으로 보존할 수 있으므로 언제라도 데이터를 보존할 수 있으며, 스크립트를 통해서 언제든지 읽을 수 있습니다.

그림 3.3 인스펙터에서 값의 편집을 할 수 있습니다.

using UnityEngine;
 
[CreateAssetMenu]
public class NewBehaviourScript : ScriptableObject
{
    [Range(010)]
    public int number = 3;
 
    public bool toggle = false;
 
    public string[] texts = new string[5];
}

무엇을 보존하나요?

에디터 확장에서 작성한 에셋의 데이터, 설정 파일, 빌드 후에 게임 데이터로 사용할 데이터 베이스의 역할로써도 사용할 수 있습니다.

ScriptableObject을 보존할 장소

프로젝트의 에셋 폴더 밑에 있으면 어디라도 보존할 수 있습니다 에디터 확장 전용의 ScriptableObject의 경우 Editor 폴더 밑에 보존하면 좋습니다.

사용 방법

설명의 양이 많기 때문에 상세한 것은 4장의 「ScriptableObject」 을 참고해주세요.

3.4 JSON

텍스트 형식의 작은 량의 데이터를 기술하기 위한 포맷의 하나입니다. 주로 Web, 서버로부터 데이터를 주고 받기 위하여 사용되었지만 다양한 환경에서 이용되고 있습니다.

Unity5.3으로부터 정식으로 JsonUtilty 클래스가 추가되면서 Json을 정식으로 사용할 수 있게 되었습니다.

유니티의 Json은 다른 Json보다 고속처리는 가능하지만 제공되는 기능은 한정되어 있습니다. 객체를 Json으로 변환하는 조건은 Unity의 Serialize과 같은 조건이며 다음과 같습니다.

 

1: 클래스에 [Serializable] 속성을 지정.

2: 필드에 [SerializeField] 속성을 지정 혹은 public 변수로 선언.

3: 이외 자세한 것은 제 5장 「SerializedObject 에 대해」를 참고해주세요.

유니티의 Serialize를 사용하더라도 Json에서 제공되지 않는 부분도 존재합니다.

1: Dictionary 는 제공되지 않습니다.

2: object[]、List<object> 와 같은 객체 배열은 제공되지 않습니다.

3: 배열 개체를 있는 그대로 전달하면 Serialize가 되지 않습니다.(JsonUtility.ToJson(List<T>) -> 안됨)

사용 방법

유니티에서 JsonUtility.ToJson 과 EditorJsonUtility.FromJson을 호출하는 것으로 객체에서 Json으로, Json에서 객체로 변경할 수 있습니다.

[Serializable]
public class Example
{
    [SerializeField]
    string name = "hoge";
 
    [SerializeField]
    int number = 10;
}
 
/*
다음과 같이 JSON 데이터가 출력됩니다.
{
    "name": "hoge",
    "number": 10
}
*/
Debug.Log(JsonUtility.ToJson(new Example(), true));

JsonUtility 와 EditorJsonUtility

JsonUtility에서 UnityEngine.Object로 변경하는 것을 JSON에서 제공하지 않습니다.(단 ScriptableObject를 포함한 일부 객체는 가능합니다.)

UnityEngine.Object를 JSON으로 변환하려면 에디터 전용의 EditorJsonUtility를 사용합니다.

다만 EditorJsonUtility는 배열에 대한 처리를 해주지 않으므로 별도의 처리가 필요합니다.

EditorJsonUtility의 배열에 대한 처리는 좀 억지스럽지만 JSON 형식을 문자열로 연결시켜서 작성합니다.

/*
다음과 같이 JSON을 구합니다.
{"key 명":[{"name":"hoge"},{"name":"hoge"}]}
*/
public static string ToJson(string key, Object[] objs)
{
    var json = objs.Select(obj => EditorJsonUtility.ToJson(obj)).ToArray();
    var values = string.Join(",", json);
    return string.Format("{\"{0}\":{1}]}", key, values);
}

배열 다루기

많은 Json 라이브러리는 배열도 Serialize를 제공합니다. 하지만 Unity의 JsonUtility는 배열에 대해서 제공해주지 않습니다.

var list = new List<Example>{
  new Example(),
  new Example()
};
 
/*
원하는 결과는 다음과 같은 형식이지만
[{"name":"hoge","number":10},{"name":"hoge","number":10}]
실제로 리턴해주는 결과는 {} (-_-...)
*/
JsonUtility.ToJson(list);

배열을 Serialize를 할 경우 별도의 처리가 필요합니다.

Serialize 가능한 클래스를 구현하여 필드를 변수로 선언하여 Serialize하면 됩니다.

Serialize가 가능한 것은 클래스의 필드 변수이므로, 배열을 클래스 안에 선언합니다. 그 다음 List를 Serialize 하기 위해서 범용성으로 작성합니다.

 

SerializableList 클래스입니다.

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using UnityEngine;
 
/*
사용 방법은 리스트와 거의 동일
*/
[Serializable]
public class SerializableList<T> : Collection<T>, ISerializationCallbackReceiver
{
    [SerializeField]
    List<T> items;
 
    public void OnBeforeSerialize()
    {
        items = (List<T>)Items;
    }
 
    public void OnAfterDeserialize()
    {
        Clear();
 
        if (items != null)
        {
            foreach (var item in items)
            Add(item);
        }
 
        foreach (var item in items)
            Add(item);
    }
}

OnBeforeSerialize 에서 Items를 List<T> 타입으로 캐스팅하여 items에 대입을 합니다. 이 Items는 Collection에서 제공해주는 기능이며 Serializable을 할 때 항목들을 이 Items를 통하여 가져올 수 있습니다. 이에 대한 부분은 밑의 사이트를 참고해주세요.

 

개체 및 컬렉션 이니셜라이저 - C# 프로그래밍 가이드

개체 및 컬렉션 이니셜라이저(C# 프로그래밍 가이드)Object and Collection Initializers (C# Programming Guide) 이 문서의 내용 --> C#을 사용하면 개체 또는 컬렉션을 인스턴스화하고 단일 명령문에서 멤버 할당을 수행할 수 있습니다.C# lets you instantiate an object or collection and perform member assignments in a single statement.

docs.microsoft.com

이 것을 JsonUtility로 Serializable하면 다음과 같은 결과를 얻을 수 있습니다.

var list = new List<Example>{
  new Example(),
  new Example()
};
 
/*
다음과 같은 결과를 얻을 수 있습니다.
[{"name":"hoge","number":10},{"name":"hoge","number":10}]
*/
JsonUtility.ToJson(list);

여기서 눈 여겨 볼 부분은 ISerializationCallbackReceiver의 존재입니다. JsonUtility로 JSON으로 변경할 때는 ISerializationCallbackReceiver에서 제공하는 OnBeforeSerialize() 와 OnAfterDeserialize()를 호출합니다. 이 기능을 이용하여 ToJson을 호출할 때 객체를 Serializable 가능한 필드에 대입합니다.

Serializable가 되지만 최종적으로 JSON 형식이 아니고 배열의 형식으로 표시하고 싶은 경우도 있습니다.

JSON 형식

{"items":[{"name":"hoge","number":10},{"name":"hoge","number":10}]}

배열 형식 (Json 형식에서 키인 items를 제거한 형식)

[{"name":"hoge","number":10},{"name":"hoge","number":10}]

이 경우 SerializableList 클래스 안에 ToJson 함수를 작성해서 문자열을 커스텀 마이징 합니다.

public string ToJson()
{
    var result = "[]";
    var json = JsonUtility.ToJson(this);
    var regex = new Regex("^{\"items\":(?<array>.*)}$");
    var match = regex.Match(json);
 
    if (match.Success)
        result = match.Groups["array"].Value;
 
    return result;
}

이 ToJson 함수를 사용하면 다음과 같은 결과를 얻을 수 있습니다.

var serializedList = new SerializabeList<Example>
{
    new Example(),
    new Example()
};
 
/*
다음과 같이 문자열을 출력합니다.
[{"name":"hoge","number":10},{"name":"hoge","number":10}]
*/
Debug.Log(serializedList.ToJson());

하지만 이 경우 Deserializable를 할 수 없으므로 FromJson을 구현합니다.

public static SerializableList<T> FromJson(string arrayString)
{
    var json = "{\"items\":" + arrayString + "}";
 
    return JsonUtility.FromJson<SerializableList<T>>(json);
}

이 것으로 Deserializable로 가능합니다.

var serializedList = new SerializableList<Example>
{
    new Example(),
    new Example()
};
 
var json = serializedList.ToJson();
var serializableList = SerializableList<Example>.FromJson(json);
// Example 객체 2개를 얻을 수 있습니다.
Debug.Log(serializableList.Count == 2);

SerializableList.cs

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using UnityEngine;
 
/*
사용 방법은 리스트와 거의 동일
*/
[Serializable]
public class SerializableList<T> : Collection<T>, ISerializationCallbackReceiver
{
    [SerializeField]
    List<T> items;
 
    public void OnBeforeSerialize()
    {
        items = (List<T>)Items;
    }
 
    public void OnAfterDeserialize()
    {
        Clear();
 
        if (items != null)
        {
            foreach (var item in items)
                Add(item);
        }
 
        foreach (var item in items)
            Add(item);
    }
 
    public string ToJson(bool prettyPrint = false)
    {
        var result = "[]";
        var json = JsonUtility.ToJson(this, prettyPrint);
        var pattern = prettyPrint ? "^\\{\n\\s+\"items\":\\s(?<array>.*)\n\\s+\\]\n}$" : "^{\"items\":(?<array>.*)}$";
        var regex = new Regex(pattern, RegexOptions.Singleline);
        var match = regex.Match(json);
 
        if (match.Success)
        {
            result = match.Groups["array"].Value;
 
            if (prettyPrint)
                result += "\n]";
        }
 
        return result;
    }
 
    public static SerializableList<T> FromJson(string arrayString)
    {
        var json = "{\"items\":" + arrayString + "}";
 
        return JsonUtility.FromJson<SerializableList<T>>(json);
    }
}

Test.cs

using UnityEngine;
 
public class Test : MonoBehaviour
{
    void Start ()
    {
        SerializableList<Example> examples = new SerializableList<Example>()
        {
            new Example(){ name = "hogo", number = 10 },
            new Example(){ name = "hogo", number = 10 }
        };
 
        string json = examples.ToJson();
 
        Debug.Log("json : " + json);
 
        examples.Clear();
        examples = SerializableList<Example>.FromJson(json);
 
        if (examples != null)
        {
            Debug.Log("examples count : " + examples.Count);
 
            for (int i = 0; i < examples.Count; ++i)
            {
                Debug.Log(examples[i].name + " : " + examples[i].number);
            }
        }
    }
}

Example.cs

using System;
 
[Serializable]
public class Example
{
    public string name;
    public int number;
}

유니티 디버그 창으로 결과 출력