본문 바로가기

Language/Unity

[Unity3D] 유니티 직렬화 (Unity Serialization)

직렬화(Serialization)란?



직렬화에 대한 개념은 다음의 링크를 참조하도록 하자.

요약하자면 C#등에서 제공하는 Serialization API로 스트림형태의 데이터변환을 하여 파일로 저장하는 방식이다.


여기서 사용할 유니티 에디터의 시리얼라이저는 실시간 게임환경에서 실행되기 때문에 다른 프로그래밍 환경의 시리얼라이저와는 다르게 동작한다.

클래스 내의 필드가 직렬화되도록 하려면 다음의 조건을 만족해야한다.


  1. public이거나 [SerializeField] 속성이 있어야한다.
  2. static이 아니어야한다.
  3. const가 아니어야한다.
  4. readonly가 아니어야한다.
  5. 직렬화가 가능한 필드타입이어야한다. (단순 필드 타입만 가능하다.)

마지막 조건을 보면 모든 타입이 다 직렬화가 가능하지 않다는 것을 알 수 있다.
직렬화가 가능한 단순 필드 타입은 다음을 얘기한다.

  1. [Serializable] 속성이 있는 비 추상, 일반 클래스
  2. [Serializable] 속성이 있는 커스텀 구조체
  3. UnityEngine.Object에서 파생된 오브젝트
  4. 프리미티브 데이터 타입(int, float, double, bool. string 등)
  5. 열거형 타입
  6. 특정 Unity 타입 : Vector2, Vector3, Vector4, Rect, Quaternion, Matrix4x4, Color, Color32, LayerMask, AnimationCurve, Gradient, RectOffset, GUIStyle

추가적으로, 단순 필드 타입의 배열과 List<T>도 직렬화가 가능하다.

그러나 다단계 타입(다차원 배열, 가변 배열, 중첩 컨테이너 타입)의 직렬화를 지원하지는 않는다.


아예 불가능하지는 않다. 중첩 타입을 클래스 또는 구조체 랩핑, ISerializatinCallbackReceiver콜백을 사용하여 커스텀 직렬화를 수행한다면 가능하다.

( 참고 : https://docs.unity3d.com/kr/current/Manual/script-Serialization-Custom.html )





직렬화 - 세이브, 로드



데이터를 직렬화, 역직렬화하여 파일에 담는 순서를 살펴보자.


  • 세이브
    1. 프로그램에서 사용할 변수 선언 및 할당.
    2. 직렬화용 Serializable클래스 선언.
    3. FileStream 클래스를 이용하여 파일 생성.
    4. Serializable클래스에 프로그램에서 사용하는 변수 값 담기. (1->2)
    5. BinaryFormatter 클래스를 이용하여 4를 직렬화 -> 3의 파일에 담기.
    6. 생성된 파일 닫기.
  • 로드
    1. FileStream 클래스를 이용하여 파일 열기.
    2. 불러온 데이터를 BinaryFormatter 클래스를 이용해 역직렬화.
    3. 역직렬화한 데이터를 Serializable 클래스에 담기.
    4. Serializable 클래스에 담긴 데이터를 실제 프로그램에서 쓰는 변수에 재 할당.
    5. 프로그램 사용.


위의 순서를 보면 Serializable 클래스가 데이터 직렬화, 역직렬화할 때 사용하는 일종의 임시 클래스로 사용된다는 것을 볼 수 있다.

실제 데이터는 일반 클래스를 사용하고, 저장하고자 하는 데이터만 Serializable 클래스에 담아 파일 세이브 및 로드를 진행한다.



다음은 직렬화 및 역직렬화를 진행할 수 있는 코드다.



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
public class SuperSerialization<T>
{
    private string ext = ".dat";
 
    private BinaryFormatter bf;
    private FileStream file;
 
    public void Serialization(T tempData, string filename, string path)
    {
        bf = new BinaryFormatter();
        file = File.Create(path + "/" + filename + ext);
 
        bf.Serialize(file, tempData);
        file.Close();
    }
 
    public void DeSerialization(string filename, string path, out T deserialized)
    {
        bf = new BinaryFormatter();
        file = File.Open(path + "/" + filename + ext, FileMode.Open);
 
        if (file != null && file.Length > 0)
        {
            deserialized = (T)bf.Deserialize(file);
        } else
        {
            deserialized = default(T);
        }
 
        file.Close();
    }
}
cs

[참조 1]


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
48
49
50
51
52
53
[Serializable]
public class TempSerializeData
{
    public string uid;
    public string username;
}
 
public class CSerialization: SuperSerialization<TempSerializeData>
{
    private TempSerializeData tempData;
    private string filename = "test";
 
    public void Serialization(string path)
    {
        if (tempData == null)
        {
            DebugConsole.Log("tempData에 값이 채워지지 않음.");
            return;
        }
 
        Serialization(tempData, filename, path);
    }
 
    public void DeSerialization(string path, out object[] data)
    {
        TempSerializeData output;
 
        DeSerialization(filename, path, out output);
 
        if (output == default(TempSerializeData))
        {
            data = default(object[]);
            DebugConsole.Log("불러오는 데 오류 발생");
            return;
        }
 
        data = new object[2];
        data[0= output.uid;
        data[1= output.username;
    }
 
 
    public void SetArguments(string uid, string username)
    {
        if (tempData == null)
        {
            tempData = new TempSerializeData();
        }
        tempData.uid = uid;
        tempData.username = username;
    }
}
 
cs

[참조 2]



참조1 (SuperSerialization)은 파일 이름, 경로, Serializable객체만 받아서 직렬화, 역직렬화만 진행한다. 

수시로 바뀔 수 있는 파일 이름, 경로, 객체는 참조2에 있는 클래스(CSerialization)에서 처리한다.




예외처리



  • 파일에 저장하던 데이터의 필드명이 바뀌는 경우?

기존 값은 계속 사용하지만, 그 값에 대한 변수 명이 바뀌게 되면 역직렬화할 때 값을 찾을 수 없다.

이 경우를 대비해 유니티에서는 [FormerlySerializedAs] 키워드를 지원한다.

바뀐 필드 명 위에 해당 키워드를 선언하여 이전에 사용하던 필드명을 등록해 놓으면 필드명을 바꿔도 값을 찾을 수 있게된다.


1
2
3
4
5
6
public class MyClass
{
  [FormerlySerializedAs("m_MyVariable")]
  [SerializeField]
  private string m_ABetterName;
}

cs


예를 들어 필드명을 m_MyVariable -> mABetterName 으로 바꾸고자 할 때, 위와 같이 사용한다.

단, [SerializeField]키워드를 사용하고 있는 변수만 해당 키워드가 적용된다.


만약 클래스 자체가 [Serializable]키워드로 만들어져서 사용되는 경우, [FormerlySerializedAs]가 소용없다.

즉, 이 기능을 수동으로 구현할 필요가 있는데 OnBeforeSerialize, OnAfterDeserialize 이벤트가 그것이다.

이 두 이벤트는 각각 직렬화하기 전, 역직렬화가 끝난 후에 호출되는데 이벤트가 호출될 때 임시변수에 불러온 데이터를 넣고 새로운 필드에 재할당 시켜준다.


OnBeforeSerialize, OnAfterDeserialize 이벤트는 ISerializationCallbackReceiver 인터페이스를 상속받아야 사용이 가능하다.