반응형

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

크게 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로 구분한다. 

 

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

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

반응형
반응형

ECS란?

Entity, Component, System의 약자로써

Entity와 Component와 System에 대한 역할 분리를 철저히 한 설계 방식이다.

 

내가 이해한 바를 적어보자면,


Entity : 객체를 Entity의 id, key 등으로 분류하여 매핑시켜 Coponent와 System을 연결해주는 객체

Component : 역할에 필요한 "데이터, 즉 변수"만 가지고 있는 객체

System :  Component의 데이터를 기반으로 실제 역할에 필요한 행위를 하는 객체


라고 이해하였다.

하지만 이건 개념일 뿐이고, ECS를 사용하는 이유는 간단하다.

바로 선형적 데이터 구조를 통해 캐시친화율을 극단적으로 올려 성능 향상을 꾀하는 것이 목표다.

그러기 위해서 주의해야 할 점이 있다.

첫 번쨰.

간혹 ecs를 검색해보면, unordered_map을 사용하는데, 이것이 과연 캐시친화적 설계인가? 하는 의문이 든다. 매핑의 기본은 해시맵에서 시작한다지만 과연 캐시친화적 설계에 해시맵이 적절한지 고려해볼 필요가 있다. 최대한 선형적 데이터구조를 띄는 설계를 해야하지 않을까? 

두 번째,

 

 

내가 생각하는 최적화 기법

1. Entity의 컴포넌트 배열을 bit_set을 사용한다.

2. 

반응형
반응형

애니메이션

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

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

pose구성도

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

Pose Controller 구성도

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

Animator Component 구성

 

반응형
반응형

내가 inline키워드를 명시적으로 쓰는 타입의 프로그래머인데, 링커에러가 뜬다? 그러면 이 글을 보시길 바란다.

 

3D자체엔진을 제작하려고 3D리소스 래핑을 하는 도중에 inline에 대하여 엄청난 오용을 하고 있었다는 사실을 깨달았다.

// Material.h
// FBX를 로드할 때 가져오는 정보. 
class MaterialResource : public IGraphicsResource
{
	using MaterialMapArray = std::array<std::shared_ptr<Texture>, static_cast<UINT>(eMaterialMapType::SIZE)>;
public:
	explicit MaterialResource(std::wstring_view _name);
	virtual ~MaterialResource();
public:
	inline void SetMaterialMap(eMaterialMapType _mapType, std::shared_ptr<Texture> _pTexture);
	inline std::shared_ptr<Texture> GetMaterialMap(eMaterialMapType _mapType);
private:
	MaterialMapArray mMaterialMaps;
};

// Material.cpp
MaterialResource::MaterialResource(std::wstring_view _name)
	: IGraphicsResource(_name)
{
}
MaterialResource::~MaterialResource()
{
}
inline void MaterialResource::SetMaterialMap(eMaterialMapType _mapType, std::shared_ptr<Texture> _pTexture)
{
	mMaterialMaps[static_cast<UINT>(_mapType)] = _pTexture;
}
inline std::shared_ptr<Texture> MaterialResource::GetMaterialMap(eMaterialMapType _mapType)
{
	return mMaterialMaps[static_cast<UINT>(_mapType)];
}

이런 클래스가 있다.

이 클래스는 일단 SetMaterialMap() 함수를 사용하면 에러를 이렇게 낸다.

개같은...링커에러

그렇다. 그 악명높은 링커에러가 뜬다.

근데 정의와 구현부도 제대로 되어있고, 헤더가 꼬인거도 아니고....당연하게도 main문은 있다.

애초에 SetMaterialMap() 함수를 사용한 코드에서 나는 에러다. 해당 코드를 지우면 제대로 빌드가 된다.

 

뭐가 문제임? 하는 사람도 있을거고,

왜 이따구로 짬? 하는 사람도 있을거다...

일단 확실한건, 대부분은 "뭐가 문제임?" 혹은 "inline이 뭐임?" 하는 반응이 대부분일 것이다.

내가 아는 inline의 역할은 컴파일 단계에서 함수 호출을 없애고, 호출 위치에 함수의 코드를 그대로 붙여넣어 함수 호출비용을 줄이는 것으로 알고있었다.

물론 이것도 맞다. 하지만 보조적으로 한 가지 역할이 더 있다.

그건 바로 여러 파일에서 #include한 함수가 중복 정의되어도 괜찮다고 링커에게 알려주는 역할이라는 것이다......

약간 처음 들으면 애매할 수 있는데, 이해하고 보면 당연한 말이다.

이해를 돕기 위해 gpt쌤의 말을 가져오면,

더보기

inline을 쓰면 헤더 파일에서 구현해야 하는 이유는 컴파일러와 링커의 작동 방식 때문입니다. 이를 차근차근 설명드릴게요.


1. 컴파일러는 각 번역 단위에서 따로 작동

  • C++에서 코드를 컴파일할 때 **각 소스 파일(.cpp)**은 독립적인 "번역 단위"로 처리됩니다.
  • inline 함수는 컴파일러에게 "이 함수는 어디에서든 중복 정의될 수 있지만, 중복되어도 괜찮다"는 뜻을 전달합니다.

중요: inline 함수는 중복 정의를 허용하지만, 컴파일러는 이를 보장하기 위해 번역 단위마다 해당 함수의 정의를 알고 있어야 합니다.


2. 헤더 파일은 모든 번역 단위에 포함됨

  • 헤더 파일에 inline 함수를 정의하면 이를 포함하는 모든 번역 단위에서 동일한 함수 정의가 생깁니다.
  • 이 정의가 동일하다면, 링커는 이를 하나로 합쳐 문제가 없게 처리합니다.

예제:

cpp
코드 복사
// header.h inline int Add(int a, int b) { return a + b; }
  • 이 코드가 포함된 각 .cpp 파일마다 Add 함수의 정의가 복사됩니다.
  • 링커는 "이건 모두 동일한 함수다!"라고 인식하고 하나로 합칩니다.

3. CPP 파일에 정의하면 발생하는 문제

  • inline 함수는 헤더 파일에 정의되지 않으면 다른 번역 단위에서 해당 정의를 볼 수 없습니다.
  • 링커는 다른 번역 단위에서 함수의 정의를 찾을 수 없고, "이 기호를 찾을 수 없다"는 에러를 발생시킵니다.

문제 상황:

cpp
코드 복사
// header.h inline int Add(int a, int b); // 선언만 있고 구현 없음 // source.cpp #include "header.h" int Add(int a, int b) { // 구현이 cpp 파일에만 존재 return a + b; }
  • 링커는 Add 함수가 헤더에 정의되어 있어야 한다고 가정하지만, 실제 정의는 CPP에 있으므로 충돌합니다.

4. inline의 의미와 동작

  • inline의 의미: 함수가 호출될 때 **코드 삽입(inline expansion)**으로 대체될 수 있다.
  • 컴파일러가 함수의 코드를 호출자 위치에 직접 삽입하려면, 함수의 **정의(body)**를 컴파일 시점에 알아야 합니다.
  • 따라서 inline 함수는 반드시 모든 번역 단위에서 동일한 정의를 볼 수 있도록 헤더 파일에 구현해야 합니다.

5. 중복 정의를 피하기 위한 설계

  • 일반적인 함수는 여러 번 정의되면 링커가 에러를 발생시킵니다(One Definition Rule 위반).
  • 그러나 inline 함수는 특별히 "같은 정의라면 여러 번 있어도 괜찮다"는 규칙이 있습니다.
  • 이 규칙을 제대로 활용하려면, 모든 번역 단위에서 동일한 정의가 필요하므로 헤더 파일에 구현해야 합니다.

요약: 헤더에 구현해야 하는 이유

  1. 모든 번역 단위에서 동일한 함수 정의를 보장하기 위해.
  2. 함수 호출 위치에 직접 코드 삽입하려면, 컴파일러가 함수의 정의를 알고 있어야 하기 때문에.
  3. 중복 정의로 인한 링킹 에러를 방지하기 위해.

따라서, inline 함수는 헤더 파일에 정의를 작성하고, CPP 파일에서는 구현을 배제해야 안정적으로 동작합니다! 😊

따라서, 쉽게 말하면 템플릿이랑 컴파일과정이 비슷하다고 볼 수 있습니다.

템플릿도 헤더에 구현을 하지 않으면 에러가 뜨므로.....

 

그래서 이렇게 바꾸니까 되었습니다.

class Texture;

// FBX를 로드할 때 가져오는 정보. 
class MaterialResource : public IGraphicsResource
{
	using MaterialMapArray = std::array<std::shared_ptr<Texture>, static_cast<UINT>(eMaterialMapType::SIZE)>;
public:
	explicit MaterialResource(std::wstring_view _name);
	virtual ~MaterialResource();
public:
	inline void SetMaterialMap(eMaterialMapType _mapType, std::shared_ptr<Texture> _pTexture) {
		mMaterialMaps[static_cast<UINT>(_mapType)] = _pTexture; 
	}
	inline std::shared_ptr<Texture> GetMaterialMap(eMaterialMapType _mapType) { 
		return mMaterialMaps[static_cast<UINT>(_mapType)];
	}
private:
	MaterialMapArray mMaterialMaps;
};
MaterialResource::MaterialResource(std::wstring_view _name)
	: IGraphicsResource(_name)
{
}
MaterialResource::~MaterialResource()
{
}
MaterialState::MaterialState(MaterialResource* _pMaterialResource)
	: mMaterialResource(_pMaterialResource)
{
	for (int mapType = 0; mapType < static_cast<UINT>(eMaterialMapType::SIZE); ++mapType)
	{
		mCBuffer.UseMap[mapType] = TRUE;
	}
}

cpp에 구현을 없애고 h에 구현을 했습니다.

 

근데 걍 느낀게....

처음에 inlline 굳이 쓴건, 어차피 컴파일러가 자동으로 inline화를 판단하지만, inline키워드를 씀으로써 const키워드처럼 필요없지만 (물론 const는 항상 필요없는건 아니다) 명시적으로 넣어주는 습관을 가지려고 쓴건데... 이렇게 복잡한 키워드인지 몰랐다.

하...걍 inline써도 컴파일러가 inline화 무시할 때도 있고, inline안써도 컴파일러가 알아서 inline화 시킬 때도 있는데,
걍 쓰지 맙시다.....

 

반응형

+ Recent posts