본문 바로가기

- GameProgramming/- Unity 3D

★ 9. Unity Editor 확장 입문 - [3] 데이터의 보존

반응형

[3] 데이터의 보존




에디터 확장에서 기능을 넣을 때, 수치를 보존해서 다음에도 사용하고 싶을 때가 있을겁니다. 그 수치는 에디터 확장 설정에 관한 파라미터일 수도 있고 게임에 관련된 파라미터일 수도 있고 다양합니다. 유니티에서는 데이터를 보존하는 수단이 다양합니다. 크게 3개의 패턴으로 나눠볼 수 있는데 그것에 대해서 알아보겠습니다.



3.1 EditorPrefs

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

[영향을 미치는 범위]


저장한 수치는 메이저 버전(4.x, 5.x)이 같다면 공유가 가능합니다.

[무엇을 저장하는가?]

윈도우의 위치, 사이즈, 유니티 에디터의 환경설정(Preferences에 있는 설정) 수치들을 저장합니다. 독자적인 에셋이어도 환경에 따른 설정이라면 EditorPrefs를 사용하는 것이 좋습니다. EditorPrefs 경유해서 저장한 수치는 모두 평문으로 보존되며 결코 패스워드 등의 중요한 정보는 보존하지 않는 편이 좋습니다.

[EditorPrefs가 저장되는 장소]

Window (Unity4.x) - HKEY_CURRENT_USER\Software\Unity Technologies\UnityEditor 4.x
Window (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는 메이저 버전끼리 나누어서 보존하게 되고, 특히 Windows는 레지스트리에 수치를 보존합니다.
EditorPrefs로만 경유하면 문제는 없겠지만 직접 레지스트리를 건드려서 수정하면 그 과정에서 실수로 잘못된 설정을 넣으면 윈도우 자체가 돌아가지 않기도 합니다.

[사용법]

OnEnable 등의 1회만 호출되는 함수 안에서 타이밍 잡고 수치를 얻습니다.
수치의 변경 타이밍에 EditorPrefs에 저장하는 방식으로하면 됩니다.

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 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);
        }
    }
}
cs

또한 윈도우 사이즈를 저장할 경우 그렇게 중요한 수치가 아니기 때문에 OnDisable에서 수치를 저장하는 것이 적당합니다. 절대 OnGUI에서 매 회 저장하지 않도록 해야합니다. OnGUI는 호출 빈도가 어마어마하게 많은 함수이기 때문에 OnGUI함수 내에서 저장할 경우 부하가 심하게 걸립니다.

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
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()
    {
        float width = EditorPrefs.GetFloat(SIZE_WIDTH_KEY, 600);
        float height = EditorPrefs.GetFloat(SIZE_HEIGHT_KEY, 600);
        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);
    }
}
cs



3.2 EditorUserSettings.Set/GetConfigValue

프로젝트 안에서 공유할 수 있는 데이터의 보존 방법입니다. 여기서 저장되는 수치의 경우 암호화되며 개인적인 정보인 패스워드 등을 저장하기에 적당합니다.

[영향 범위와 저장 장소]

이 API로 저장하는 데이터는 프로젝트 안에서만 영향을 주게 됩니다. 데이터의 저장 장소는 Library/EditorUserSettings.asset 에 있고 Library 폴더를 다른 사람이 공유하지 않는 한, 정보를 타인과 공유할 수 없습니다.

[무엇을 저장하는가?]

다양한 툴을 만들게되면 로그인을 위한 메일 주소, 패스워드가 필요할 때가 있습니다. Oauth의 접근 토근 등.
EditorUserSettings.asset은 바이너리 형식으로 저장되어 있어 간단하게 내용을 볼 수 없습니다. 하지만 유니티가 제공하는 binary2text를 사용하면 바이너리를 텍스트 형식으로 변환시켜 볼 수 있으므로 주의해야합니다.

[사용법]

<저장>

1
2
3
4
5
6
7
8
9
10
11
using UnityEditor;
 
 
public class NewBehaviourScript
{
    [InitializeOnLoadMethod]
    static void SaveConfig()
    {
        EditorUserSettings.SetConfigValue("Data 1""text");
    }
}
cs

<저장 확인>

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

<결과>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)
cs

저는 파일 경로를 도저히 찾을 수 없어서 직접 실습하지 못해 원문을 올렸습니다.



3.3 ScriptableObject

프로젝트 안에서 공유할 수 있는 저장 방법. 다양한 응용이 가능하며 팀 내에서 공유하고 싶은 설정, 대량의 데이터를 저장하고 싶을 때 사용합니다.

[영향 범위]

유니티 프로젝트 내부에서 데이터를 보존하기 위해 주로 사용하는 형식입니다. 프로젝트 내에서 Asset으로 보존되어 있어 언제든지 데이터를 저장할 수 있으며 언제나 스크립트에서 데이터를 불러들일 수 있습니다.


















1
2
3
4
5
6
7
8
9
10
11
12
13
using UnityEngine;
using UnityEditor;
 
[CreateAssetMenu]
public class NewBehaviourScript : ScriptableObject
{
    [Range(010)]
    public int number = 3;
 
    public bool toggle = false;
 
    public string[] texts = new string[5];
}
cs

위 코드처럼 스크립트 파일을 만들면 Project뷰 Create 메뉴에 ScriptableObject를 만드는 메뉴가 생깁니다.


제가 코드를 작성한 스크립트 이름이 NewBehaviourScript 이기 때문에 제일 상단에 New BehaviourScript의 이름으로 생기게 됩니다.
더 알아보고 싶으신 분은 http://archive.fo/5Mifm 에 들려주시면 될 것 같습니다.

[무엇을 저장하는가?]

에디터 확장에서 작성한 Asset의 데이터나 설정 파일, 빌드 후 게임 데이터로 사용하는 데이터베이스로써의 역할을 합니다.

[저장되는 장소]

Project의 Assets 폴더 내부라면 어디에든 저장할 수 있습니다. 에디터 확장 전용의 ScriptableObject라면 Editor 폴더 내부에 작성하는 편이 좋습니다.

[사용법]

사용법이 방대하므로 다음 [4]편에서 다루겠습니다.



3.4 JSON

텍스트 형식의 경량 데이터 작성 언어의 일종입니다. 일반적으로 Web이나 서버에서 데이터를 얻을 때에 데이터 형식으로 사용합니다. 뿐만 아니라 폭 넓은 분야에 이용되기도 하죠.
유니티 5.3 버전부터 정식으로 jSonUtility 클래스가 추가되었으며 JSON에 정식적으로 대응할 수 있게 되었습니다. 단, 보통 사용하는 JSON 라이브러리보다 고속이지만 고성능인 것은 아니며 사용에 한계가 있습니다. 오브젝트를 JSON으로 변환하는 조건은 유니티의 Serializer에서의 조건과 같으며 다음과 같습니다.

1 - 클래스에 [Serializable] 속성이 있을 것.
2 - 필드에 [SerializeField] 속성이 있을 것. 혹은 public 변수일 것.
3 - 그 외, 세부 사항은 [[5]편 SerializedObject에 대해서] 를 참조하세요.

유니티의 Serializer를 사용한다는 것은 Serializer가 대응할 수 없는 것은 JSON으로도 못한다는 뜻입니다.

1 - Dictionary는 Serialize할 수 없습니다.
2 - object[], List<object> 같은 object 배열은 Serialize 할 수 없습니다.
3 - 배열 오브젝트를 그대로 넘겨주어도 Serialize할 수 없습니다. (JsonUtility.ToJson(List<T>)가 불가능)

[사용법]

단순하게 JsonUtility.ToJson과 JsonUtility.FromJson을 호출하면 오브젝트와 JSON의 변환이 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[Serializable]
public class Example
{
    [SerializeField]
    string name = "hihi";
 
    [SerializeField]
    int number = 10;
}
 
/*
아래와 같은 JSON 데이터가 출력됩니다.
{
"name": "hihi",
"number": 10
}
*/
Debug.Log(JsonUtility.ToJson(new Example(), true));
cs

[JsonUtility와 EditorJsonUtility의 차이]

JsonUtility로는 UnityEngine.Object는 JSON으로 변환할 수 없습니다.(변환할 수 없는걸로 알고 있지만 ScriptableObject를 포함한 일부 오브젝트는 가능합니다.)

UnityEngine.Object를 JSON으로 변환시키려면 에디터 전용의 EditorJsonUtility를 사용합니다. 단, EditorJsonUtility는 배열에는 대응할 수 없어서 연구가 필요합니다.

EditorJsonUtility의 배열 대응은 좀 무리가 있지만 최종적인 JSON 형식의 문자열의 연결로 작성합니다.

1
2
3
4
5
6
7
8
9
10
/*
아래와 같은 JSON을 얻을 수 있습니다.
{"Key이름":[{"name":"hihi"},{"name":"hihi"}]}
*/
public static string ToJson(string key, object[] objs)
{
    var json = objs.Select(objs => EditorJsonUtility.ToJson(obj)).ToArray();
    var values = string.Join(",", json);
    return string.Format("{\"{0}\":{1}]}", key, values);
}
cs

[배열의 사용]

많은 Json 라이브러리에서는 배열의 Serialize도 할 수 있도록 되어 있습니다. 하지만 유니티는 사용하더라도 Serialize가 되지 않습니다.

1
2
3
4
5
6
7
8
9
10
11
12
var list = new List<Example>
{
    new Example(),
    new Example
};
 
/*
{} 가 반환됩니다.
우리가 얻고 싶은 것은 아래와 같은 배열입니다.
[{"name":"hihi","number":10},{"name":"hihi","number":10}]
*/
JsonUtility.ToJson(list)
cs

꼭 배열만을 Serialize하고 싶다면 더 연구해봐야합니다.

[Serialize 가능한 클래스의 필드 변수이면 Serialize 할 수 있음]
Serialize할 수 있는 것은 클래스의 필드 변수이므로, 먼저 그 상태를 만들어봅시다. 다음 코드는 List를 Serialize하기 위해 범용성을 생각해 작성한 SerializableList 클래스입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
사용 법은 List와 거의 같음(AddRange가 없기에 자작했습니다.)
*/
[Serializable]
public class SerializableList<T> : Collection<T>, IserializationCallbackReceiver
{
    [erializeField]
    List<T> items;
 
    public void OnBeforeSerialize()
    {
        items = (List<T>)Items;
    }
 
    public void OnAfterDeserialize()
    {
        Clear();
        foreach (var item in items)
            Add(item);
    }
}
cs

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

1
2
3
4
5
6
7
8
9
10
11
var serializedList = new SerializableList<Example>
{
    new Example(),
    new Example()
};
 
/*
아래와 같은 JSON을 얻을 수 있음
{"items":[{"name":"hihi","number":10},{"name":"hihi","number":10}]}
*/
Debug.Log(JsonUtility.ToJson(serializedList));
cs

이걸로 남은 것은 ISerializationCallbackReceiver의 존재입니다.
JsonUtility에서 JSON으로 변환할 때 ISerialization CallbackReceiver의 OnBeforeSerialize와 OnAfterDeserialize가 호출됩니다. 이들을 이용해서 ToJson이 호출될 때만 오브젝트를 Serialize 가능한 필드로 대입시키는 것으로 목적을 달성할 수 있습니다.

Serialize 가능한 것은 좋지만 최종적으로는 JSON 형식이 아니라 배열 형식으로 표시 가능하게 하는 편이 좋을 것 같습니다. ("items"의 key가 필요 없어짐)

그래서 SerializableList 클래스 안에 ToJson 함수를 작성해서 문자열 커스터마이즈가 가능하도록 하였습니다.

1
2
3
4
5
6
7
8
9
10
11
12
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;
}
cs

이 ToJson 함수를 사용하면 아래의 결과를 얻을 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
var serializedList = new SerializableList<Example>
{
    new Example(),
    new Example()
};
 
/*
아래의 문자열이 출력됩니다.
[{"name":"hihi","number":10},{"name":"hihi","number":10}]
*/
Debug.Log(serializedList.ToJson());
cs

단, 이런 방식이라면 Deserialize를 할 수 없으므로 FromJson도 작성합니다.

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

이것으로 Deserialize도 할 수 있게 되었습니다.

1
2
3
4
5
6
7
8
9
10
var serializedList = new SerializableList<Example>
{
    new Example(),
    new Example()
};
 
var json = serializedList.ToJson();
var serializableList = SerializableList<Example>.FromJson(json);
// Example 오브젝트를 두 개 얻을 수 있습니다.
Debug.Log(serializableList.Count == 2);
cs

SerializableList.cs 소스코드입니다.

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
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text.RegularExpressions;
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();
        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);
    }
}
cs

[Dictionary의 사용]

JsonUtility에서 Dictionary의 사용은 매우 어렵습니다. 일반적으로 JSON 라이브러리같은 Serialize는 유니티에서는 할 수 없기 때문에 거의 모든 기능을 스스로 만들어야합니다. 이렇게 된다면 JsonUtility를 사용하는 의미도 없어지므로 MiniJSON을 사용하든 유니티에 대응하는 다른 JSON 라이브러리를 사용하는게 좋습니다.




Unity Editor 을 활용해 데이터의 보존 방법에 대해 알아보았습니다.

다음 강좌에서는 ScriptableObject에 대해서 자세히 알아보겠습니다.





반응형