유니티의 직렬화는 어떤 구조로 구성되어있을까?

크게 GUID와 fileID로 파일과 객체를 구분하여 저장한다.


GUID

GUID는 파일 단위의 유니크한 식별자이다. 때문에 에셋 파일에만 부여되는 ID값이다. (GameObject, Component 등은 없음)

예외로 Scene은 파일로 관리되므로 GUID가 있음

헷갈릴 수 있는 사실은 Scene과 Prefab도 Asset으로 관리되기에 GUID가 있다는 점이다.

특히 Prefab은 GameObject랑 혼동될 수 있으니 주의해야한다.

GameObject는 Scene파일 내에서 관리하고, Prefab은 별도의 에셋이다.


fileID

GUID가 부여된 Asset파일 안에 있는 오브젝트를 식별하기 위한 ID값이다. (Scene안의 GameObject, Component와 Fbx안의 Mesh, Metarial등이 해당한다). 따라서 서브에셋이 없는 에셋(예를 들어 텍스쳐, 스크립트 등)은 fileID가 존재하지 않는다.

fileID는 에셋 단위에 식별하는 값이므로 겹칠 수 있다.

실제로 Scene파일의 Transform 계층 정보나, MeshFilter 컴포넌트의 멤버 직렬화 구조가 이런 방식으로 되어있다.

fileID와 GUID

이 MeshFilter의 직렬화 구조는 어떤 파일(GUID)의 어떤 메쉬(fileID)를 사용하겠다. 라는 뜻이다.

아래 사진은 순서대로 해당 모델의 fbx에셋 GUID, 그리고 모델 해당 메쉬의 fileID이다.

 

 


type

사실 별로 알 필요는 없다. 하지만 굳이 적어보자면, 해당 에셋이 무슨 타입인지 지정하는 것이다.

에디터를 쓰는 사람 입장에선 이럴 것이다.
Transform에 들어간 정보면 어차피 Transform일테고, MeshFilter에 들어간 정보면 당연히 Mesh일텐데 뭐하러 type이 있는가?

 

정답은 아무도 모르겠지만 내 생각은 이렇다. 우리는 사람이라 무슨 타입인지 당연히 특정할 수 있지만, 컴퓨터는 이게 무슨 타입인지 모른다.

무슨 말이냐면, 해당 파일(GUID)의 객체(fileID)에 접근했으나, 해당 객체가 무슨 타입(class)인지 알아야 해당 멤버를 사용할 수 있을텐데 컴퓨터입장에선 모르므로, 유니티 내부에서 type이라는 정수값을 통해 타입캐스팅을 해주는 것이 아닌가 하는 추측이다. 참고로 정확한 정보를 찾지 못해 어떤 정수가 어떤 타입을 의미하는지는 모르겠다.

 

보면 Script도, MeshFilter도 type값이 3이다. 컴포넌트 자체가 3을 의미할 가능성이 높다.


InstanceID

사실 직렬화랑 관계는 없지만 헷갈릴 가능성이 있어서 번외로 적었다.

InstanceID는 직렬화 시에는 저장하지 않고 메모리에 올라가는 동적 객체들을 식별하기 위해 쓰는 ID값이다.

실제로 프로그램을 껏다 키면 InstanceID가 바뀌어 있는 것을 확인할 수 있다.

초기 세팅을 위한 직렬화할때는 guid와 fileID를 사용하고, 그 후 런타임엔 InstanceID를 사용하는 듯 하다.

InstanceID는 생성규칙이 있는데, 파일에서 로드된 객체는 양수가 부여되고 런타임에 만들어진 개체는 음수가 부여된다는 것이다.

 

처음엔 이걸 굳이 왜쓰지? C++의 포인터랑 다른게 뭐지 했는데, 약간 포인터의 단점을 보완한 것 같다.

나의 추측은 이렇다.

1. 객체가 이동 시, 주소 값이 바뀐다. 이 경우에 해당 객체를 가리키던 모든 포인터들이 댕글링 상태가 되어 위험한데, InstanceID로 식별할 경우 객체가 이동해도 매핑테이블의 정보만 바꿔주면 댕글링포인터를 접근할 가능성이 줄어든다.

2. 실제로 댕글링포인터 접근을 막아준다. 객체가 삭제시 매핑테이블에 자신의 InstanceID값에 있는 데이터를 NULL로 바꿔 놓으면 다음에 접근할 때, 객체가 삭제되어있는지 알 수 있기때문에 댕글링포인터 접근 가능성이 거의 사라진다고 보면 된다.


Script의 Serialize

스크립트를 작성해본 사람들은 SerializeField가 뭔지 알것이다.

SerializeField는 해당 변수를 직렬화와 역직렬화 지원을 하겠다는 뜻이다. 그런데 어떤 구조로 저장할까?

이러한 스크립트가 있다. 말 그대로 MyInt를 직렬화하겠다는 내용이다.

이걸 GameObject에 부착시키고, Scene파일에서 해당 GameObject의 직렬화 정보를 보면 아래와 같다.

여기서 MyInt를 없애고 MyFloat라는 변수를 추가해 보았다.

어떻게 변할까?

그냥 단순하게 myInt가 삭제되고 myFloat가 추가된다.

myInt가 사라진 걸 아는거보니까, 저장 시에 아예 처음부터 다 덮어씌우나보다.

 

근데 이상한건 MonoBehaviour로 저장이 되어있는데 스크립트들을 어떻게 구분한걸까?

그건 아까도 말했듯, GUID로 구분한다. 

 

마지막으로, 유용한 글이 있어 남긴다.

이걸 보고 포스팅했으면 더 확실했을 것 같은데 귀찮아서 패스.
[번역] 에셋과 오브젝트, 그리고 직렬화 :: 코드쿡

프로젝트 개요

  • 프로젝트 명: LastLight
  • 장르: 3D 로컬/멀티 협동 액션 플랫포머
  • 개발 기간: 2025.01.20 ~ 2025.02.10
  • 플레이 타임: 10~15분
  • 소개: 평화로웠던 물건 나라에 갑자기 나타난 어둠. 물건들은 영혼을 잃고 세계는 빛을 잃어 얼어붙기 시작한다.
     세상에 남은 유일한 빛인 양초형제는 세상에 빛을 전하기 위해 등대로 향하는데.... 빛이 꺼지지 않게 2명이서 협력하며 등대의 빛을 밝혀라!

개발 환경

  • 개발 인원: 10명 (프로그래머 4명, 아트 3명, 기획 3명)
  • 개발 언어: C++17
  • 개발 도구: Microsoft Visual Studio 2022
  • 사용 라이브러리: DX11, assimp, fmod, PhysX, Xinput, nlohmann, ImGui

개발 내용

  • 담당 업무: 게임 엔진 제작, 에디터 툴 제작, 리소스 시스

게임 엔진 제작

더보기

엔진 라이프 사이클 구성

- Tick: FixedUpdate 보다 먼저 업데이트가 필요한 경우가 생겨 만들게 되었다. 

- Render: Window에 SwapChain의 화면을 출력하기 위한 일련의 모든 과정이다. 이 과정에 Camera의 Draw()와 UIManager의 Draw()등이 포함된다.

- Camera::Draw(): Render()중에 호출되는 메소드로, 각 월드의 Camera 컴포넌트 마다 해당 장면을 SwapChain에 그려준다.

- UIManager::Draw(): Render()중에 호출되는 메소드로, UICanvas의 UI들을 마지막에 한번에 SwapChain에 그려준다.


※엔진 기본 씬 그래프 구성


World

- Object의 모음을 관리하는 객체이다.

- Object의 생성과 삭제 및 검색을 담당한다.

- World는 기본적으로 Active(활성화), Non-Active(비활성화), Persistance(유지)의 3가지 상태로 나뉜다.

  Active(활성화)상태는 하나의 World만 활성화될 수 있으며, 해당 World는 엔진의 라이프 사이클 업데이트를 돌게 된다.

  Persistance(유지)상태는 Active상태가 아님에도 해당 World의 라이프 사이클 업데이트를 돌게 해주고 싶을 때 활성화 해준다. 캐릭터나 HUD UI 등과 같은 요소는 스테이지가 넘어가도 파괴되거나 비활성화 되면 안되기에 만들게 되었다.

- World는 각자 자신의 World가 무슨 Resource가 필요한지 알고, 활성화되어 있을 시 해당 Resource를 로드한다.


Object

- Object는 컴포넌트의 모음을 관리하는 객체이다.

- Component의 추가 및 검색을 할 수 있다. 

- 중복 Component에 대한 상황이 생각보다 많아 중복 Component를 추가할 수 있게 만들었다.


Component

- Object에 소속되어 특정 역할을 수행하는 객체이다.

- 라이프 사이클마다 특정 역할을 수행하거나, 특정 정보를 지니는 역할을 한다.


Component의 종류는 여러 개지만, 대표적으로 다음이 있다.

Transform: Object의 위치 정보를 제공하고, 갱신하는 역할을 한다. 또한 Object간의 계층 구조를 이룰 수 있도록 해준다.

MeshRenderer / SkinnedMeshRenderer: Transform 컴포넌트를 통해 가져온 해당 위치에 Mesh를 렌더링하는 역할을 한다. 또한 Material을 적용시켜 질감을 표현할 수 있다.

Animator: Bone의 Transform 위치 정보를 애니메이션 프레임에 따라 바꿔주는 역할을 한다.

Camera: 윈도우의 특정 영역에 Transform의 위치 정보와 여러 속성을 통해 화면을 출력한다.



리소스 시스템

더보기

※리소스 시스템


Resource Handle

- 기존에는 Path를 Key값으로 동일하게 사용하려 했으나, 경로가 바뀔 경우에 각 Resource를 사용하는 모든 Object의 Key값을 바꿔줘야하는 경우가 발생하였다.

- 따라서 Key값과 Path를 같이 들고 있는 Handle을 만들어 관리하도록 변경하였다.

- Resource Manager는 Resource Handle과 실질적인 Resource를 매핑하여 관리한다.

- Resource는 Alloc과 Free상태로 나뉘며, Handle을 등록하면 기본적으로 Free상태이다.


에디터 툴 제작

더보기

3D는 3차원 공간이므로 에디터가 없다면 레벨배치가 힘들 것이라고 판단하여 에디터를 제작하였다.


※에디터 툴 구성

 

HierarchyView

- 월드의 속하는 오브젝트를 계층적으로 나열하여 보여주는 역할을 한다.

- 드래그&드랍 액션으로 오브젝트의 계층 구조를 변경하거나, 오른쪽 클릭을 통해 오브젝트를 삭제할 수 있다.


InspectorView

- Object, Resource, World 등의 모든 Entity들의 정보를 보여주거나 수정할 수 있게해주는 역할을 한다.


ResourceView

- 클라이언트가 쓴, 즉 등록된 모든 Resource를 보여주는 역할을 한다.

- Resource는 Handle을 통해 매핑하여 관리한다. Handle에는 Key와 Path 등이 포함된다.

- Alloc상태의 Resource는 초록색 탭, Free상태의 Resource는 빨간색 탭으로 보여준다.


 

애니메이션

기본적으로 유니티의 애니메이션은 다음으로 구성되어 있다.

Pose : 각 한 모션에 대한 애니메이션의 키 값(각 프레임별로 메쉬가 어느 위치에 있는지)들로 구성된 확장자.

pose구성도

Pose Controller : 각 Pose들을 FSM식으로 구성하여 하나의 모델 애니메이션 사이클을 나타내는 정보. 

Pose Controller 구성도

Animator Component : Pose Controller를 참조해 모션을 직접 컨트롤 하는 개체.

Animator Component 구성

 

'프로그래밍 > 게임제작' 카테고리의 다른 글

[게임제작] LastLight  (0) 2025.03.05
[WinAPI] 유피의 소원  (2) 2024.10.07
[연습작] 콘솔 오목게임  (0) 2024.04.03
[연습작] 던파 플래시 모작  (1) 2024.02.12
[연습작] 비행기 게임  (1) 2024.02.12

+ Recent posts