코승호딩의 메모장

[Component] 본문

DirectX12/DirectX12 응용

[Component]

코승호딩 2023. 10. 7. 21:09

이번 글에서는 유니티의 컴포넌트 구조와 비슷하게 엔진을 컴포넌트 구조로 설계하려고 한다. 오브젝트당 하나씩만 필요로 하는 컴포넌트로는 Transform, MeshRenderer 등이 있으며 그 밖의 직접 작성한 컴포넌트들은 스크립트라 하여 MONO_BEHAVIOUR을 상속 받는다. 따라서 고정 컴포넌트는 현재는 Transform과 MeshRenderer에 해당한다.


GameObject

게임 오브젝트는 게임 상에 존재하는 캐릭터, 소품, 배경 등 컴포넌트의 컨테이너 역할을 하는 객체이다. 광원이 게임 오브젝트가 될 수도 있고, 카메라가 게임 오브젝트가 될 수도 있다. 그러나 씬에 배치하기 위해서는 게임 오브젝트는 무조건적으로 Transform 즉 위치 정보는 가지고 있어야 한다. 이를 기반으로 게임 오브젝트를 구현해보자

 

class GameObject : public enable_shared_from_this<GameObject>
{
public:
	GameObject();
	virtual ~GameObject();

public:
	void Init();

public:
	void Awake();
	void Start();
	void Update();
	void LateUpdate();

public:
	shared_ptr<Transform> GetTransform();
	void AddComponent(shared_ptr<Component> component);

private:
	array<shared_ptr<Component>, FIXED_COMPONENT_COUNT> _components;
	vector<shared_ptr<MonoBehaviour>> _scripts;
};

다음과 같이 라이프 사이클에 따라 필요한 함수들이 정의되어 있으며 Transform 컴포넌트는 자주 사용되기 때문에 편의 함수로 따로 뺐다. 그리고 내부 변수로는 무조건 한 개씩만 필요한 고정 컴포넌트 배열을 가지며 언제든 늘어날 수 있는 사용자 정의 클래스인 스크립트를 벡터로 받도록 하였다.

 

void GameObject::Init()
{
	AddComponent(make_shared<Transform>());
}

앞서 말했듯이 게임 오브젝트는 Transform 정보를 가지고 있어야 하므로 초기화 함수에서 Transform 컴포넌트를 추가하였다.

 

void GameObject::Awake() // Start(), Update(), LateUpdate()
{
	for (shared_ptr<Component>& component : _components)
	{
		if (component)
			component->Awake() // Start(), Update(), LateUpdate()
	}

	for (shared_ptr<MonoBehaviour>& script : _scripts)
	{
		script->Awake() // Start(), Update(), LateUpdate()
	}
}

게임 오브젝트는 라이프 사이클에 따른 함수들을 호출할 때, 자신이 가지고 있는 모든 컴포넌트와 스크립트도 함께 실행한다. 

 

shared_ptr<Transform> GameObject::GetTransform()
{
	uint8 index = static_cast<uint8>(COMPONENT_TYPE::TRANSFORM);
	return static_pointer_cast<Transform>(_components[index]);
}

자주 사용하는 Transform을 반환 받도록 GetTransform을 정의한다. Transform은 Component를 상속받고 있는 구조이기 때문에 게임 오브젝트는 자신이 가진 컴포넌트들을 반환할 때, 캐스팅하여 반환해야 한다.

 

void GameObject::AddComponent(shared_ptr<Component> component)
{
	component->SetGameObject(shared_from_this());

	uint8 index = static_cast<uint8>(component->GetType());
	if (index < FIXED_COMPONENT_COUNT)
	{
		_components[index] = component;
	}
	else
	{
		_scripts.push_back(dynamic_pointer_cast<MonoBehaviour>(component));
	}
}

 컴포넌트를 추가하는 함수이다. 만약 해당 컴포넌트가 고정 컴포넌트라면 고정 컴포넌트 배열에 넣어주고 해당 컴포넌트가 MonoBehaviour을 상속받은 스크립트라면 스크립트 동적 배열에 넣어준다. 참고로 컴포넌트는 자신을 들고 있는 게임 오브젝트의 정보를 가지고 있으며 자주 사용되기 때문에 컴포넌트를 추가할 때, shared_from_this를 이용해 자신을 넣어준다. 예를 들어 카메라 컴포넌트가 Transform 정보를 사용하고 싶다면 사용자 편의 함수를 사용하지 않고 GetGameObject를 통해서 자신의 게임 오브젝트에 접근하여 게임 오브젝트의 내부 함수 및 변수를 사용할 수 있다.


Component

enum class COMPONENT_TYPE : uint8
{
	TRANSFORM,
	MESH_RENDERER,
	MONO_BEHAVIOUR,
	END,
};

enum
{
	FIXED_COMPONENT_COUNT = static_cast<uint8>(COMPONENT_TYPE::END) - 1
};

컴포넌트는 다음과 같이 고정 컴포넌트와 스크립트로 나눌 수 있다. 

 

class Component
{
public:
	Component(COMPONENT_TYPE type);
	virtual ~Component();

public:
	virtual void Awake() { }
	virtual void Start() { }
	virtual void Update() { }
	virtual void LateUpdate() { }

public:
	COMPONENT_TYPE GetType() { return _type; }
	bool IsValid() { return _gameObject.expired() == false; }

	sptr<GameObject> GetGameObject();
	sptr<Transform> GetTransform();

private:
	friend class GameObject;
	void SetGameObject(sptr<GameObject> gameObject) { _gameObject = gameObject; }

protected:
	COMPONENT_TYPE _type;
	wptr<GameObject> _gameObject;
};

컴포넌트는 생성자에서 자신의 타입을 받아 저장한다. 게임 오브젝트와 마찬가지로 라이프 사이클에 따른 함수들이 있으며 이 함수들은 게임 오브젝트가 가지고 있는 라이프 사이클과 동일해야 한다. 게임 오브젝트가 이들을 호출할 때, 함께 호출해야 하기 때문이다. 그리고 컴포넌트는 자신이 어떤 게임 오브젝트에 속해있는지 정보를 가지고 있어야 유용하다. 따라서 게임 오브젝트의 AddComponent를 할 때, 컴포넌트의 SetGameObject를 통해서 게임 오브젝트가 자기 자신을 인자로 넣어줘야 하는데 이 때, SetGameObject를 외부에서 건드릴 수 없도록 해야 안전하기 때문에 private로 지정하였지만 GameObject에서는 접근할 수 있도록 friend class로 GameObject를 선언해줬다. 컴포넌트에서는 안전하게 게임 오브젝트를 사용하기 위해서 weak 포인터를 사용하였으며 사용할 때, lock 함수를 사용해줬다.

 

#include "Component.h"

class MonoBehaviour : public Component
{
public:
	MonoBehaviour();
	virtual ~MonoBehaviour();
};

MonoBehaviour은 가변 길이의 컴포넌트로 스크립트에 해당한다. 스크립트를 작성하기 위해서는 MonoBehaviour을 상속 받아야 하며 이 또한 생성자에서 자신의 컴포넌트 타입인 MONO_BEHAVIOUR을 넘겨준다.

 

class MeshRenderer : public Component
{
public:
	MeshRenderer();
	virtual ~MeshRenderer();

	void SetMesh(sptr<Mesh> mesh) { _mesh = mesh; }
	void SetMaterial(sptr<Material> material) { _material = material; }

	virtual void Update() override { Render(); }

	void Render();

private:
	sptr<Mesh> _mesh;
	sptr<Material> _material;
};

다음은 고정 컴포넌트인 MeshRenderer이다. 이 컴포넌트를 게임 오브젝트에 추가하면 해당 메쉬와 머티리얼이 게임 오브젝트에 저장된다. 이전에는 메쉬가 머티리얼을 가지고 있었지만 이제는 MeshRenderer에서 메쉬와 머티리얼을 관리하기 때문에 더욱 편리해졌다. 업데이트에서는 자신의 Render함수를 호출하고 이 함수에서는 머티리얼의 업데이트와 메쉬의 렌더 함수를 호출한다.

 

class Transform : public Component
{
public:
	Transform();
	virtual ~Transform();
};

Transform 또한 고정 컴포넌트이며 자세한 작성은 다음에 하도록 하자.

 

sptr<GameObject> gameObject = make_shared<GameObject>();

void Game::Init(const WindowInfo& info)
{
	//...
	gameObject->Init();
	
	sptr<MeshRenderer> meshRenderer = make_shared<MeshRenderer>();
	{
		shared_ptr<Mesh> mesh = make_shared<Mesh>();
		mesh->Init(vec, indexVec);
		meshRenderer->SetMesh(mesh);
	}

	{
		shared_ptr<Shader> shader = make_shared<Shader>();
		shader->Init(L"..\\Resources\\Shader\\Default.hlsl");

		shared_ptr<Texture> texture = make_shared<Texture>();
		texture->Init(L"..\\Resources\\Texture\\newjeans3.dds");

		shared_ptr<Material> material = make_shared<Material>();
		material->SetShader(shader);
		material->SetDiffuse(Vec4(0.5f, 0.5f, 0.5f, 1.f));
		material->SetFresnel(Vec3(0.01f, 0.01f, 0.01f));
		material->SetRoughness(0.5f);
		material->SetTexOn(1.f);
		material->SetTexture(0, texture);
		meshRenderer->SetMaterial(material);
	}

	gameObject->AddComponent(meshRenderer);
}

void Game::Update()
{
	gEngine->Update();
	gEngine->RenderBegin();
	gameObject->Update();
	gEngine->RenderEnd();
}

이제 마지막으로 Game에서 게임 오브젝트 하나를 생성하고 MeshRenderer도 생성하여 머티리얼과 메쉬를 설정한다. 그리고 빈 깡통에 해당하는 게임 오브젝트에 MeshRenderer을 붙이게 되면 게임 오브젝트가 렌더링이 된다.


컴포넌트 패턴을 사용한 설계는 굉장히 중요한 개념이다. 코드를 커플링 없이 깔끔하게 만들어주고 사용할 것만 선택적으로 오브젝트에 붙여서 사용하면 바로 기능이 되기 때문에 굉장히 좋은 구조이다. 실제 구현해보니 유니티 엔진과 똑같다할 정도로 비슷하게 느껴졌고 사용하기 굉장히 편리했다. 컴포넌트를 통해 무엇이든 붙일 수 있다는 생각을 하니 무적이 된 느낌까지 받았다. 누가 개발한지는 모르겠지만 컴포넌트 패턴 짱..

'DirectX12 > DirectX12 응용' 카테고리의 다른 글

[Resources]  (1) 2023.10.10
[Camera]  (1) 2023.10.09
[SceneManager]  (1) 2023.10.08
[InputManager와 GameTimer]  (1) 2023.10.07
[Material]  (0) 2023.10.07
[Texture Mapping]  (0) 2023.10.07
[Mesh]  (1) 2023.10.07