DirectX

27. 엔진구조_ResourceManager

devRiripong 2024. 1. 22.
반응형

1. pch.h 헤더에서 #include 제거하기

 

헤더에 #include를 하면 꼬여서 에러가 날 수 있기 때문에

pch.h에 가서

#include "Game.h"

을 삭제한다.

 

2. wWinMain에 GGame 적용하기

빌드를 해보면 빌드가 된다.

이제 Game을 전역으로 사용하기 위해 만든 GGame만 이용해야 하는데,

GameCoding의 wWinMain에서 보면

Game game;
game.Init(hWnd);

Game game은 Stack 메모리에 임시적으로 만들어서 사용하는 건데

이제 얘는 사용하면 안 되고 GGame버전을 이용해야 한다.

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
                     _In_opt_ HINSTANCE hPrevInstance,
                     _In_ LPWSTR    lpCmdLine,
                     _In_ int       nCmdShow)
{
    // 1) 윈도우 창 정보 등록
    MyRegisterClass(hInstance);

    // 2) 윈도우 창 생성
    if (!InitInstance(hInstance, nCmdShow))
        return FALSE;

    GGame->Init(hWnd);

    MSG msg = {};

    // Main message loop:
    while (msg.message != WM_QUIT)
    {
        if (::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
        else
        {
            GGame->Update();
            GGame->Render();
        }
    }

    return (int) msg.wParam;
}

이런 식으로 Update, Render도 새 글로벌 Game을 이용해서 접근할 수 있게끔 만들어 주면 된다.

빌드를 해서 통과되는 걸 확인한다.

 

3. MeshRenderer의 Update에서 Render를 호출해서 이미지가 보이게 하기

1) MeshRenderer의 Update에서 Render를 호출하기 위해 매개 변수인 Pipeline을 MeshRenderer생성자에서 받게 하기 (취소 예정)

실행했을 때 아무것도 안 뜨는 이유는 야메로 처리했던 부분을 아직 처리 안 해서 그렇다.

SceneManager에서 Update를 하면서 그려지는 부분의 실행이 되어야 하고,

Game의 Update의 SCENE→Update() 이 함수 어딘가에서 Update를 하면서 Render가 실행이 되는 걸 유도하고 있다.

MeshRenderer가 Update를 될 때 Render라는 함수를 아무도 호출하지 않아서 물체가 안 그려지고 있는 것이다.

어차피 Render함수를 옮길 테니까 넘어가거나

아니면 MeshRenderer 생성자에서 pipeline을 받는 걸 추가해 줘서

MeshRenderer(ComPtr<ID3D11Device> device, ComPtr<ID3D11DeviceContext> deviceContext, shared_ptr<Pipeline> pipeline);
shared_ptr<Pipeline> _pipeline;

이 변수를 추가해서

MeshRenderer::MeshRenderer(ComPtr<ID3D11Device> device, ComPtr<ID3D11DeviceContext> deviceContext, shared_ptr<Pipeline> pipeline)
	: Super(ComponentType::MeshRenderer), _device(device), _pipeline(pipeline)
{

여기에 pipeline을 저장을 하고 있다가

그걸 이용해서 그려주면 해결이 되지 않을까 생각이 든다.

MeshRenderer의 Update를 할 때

void MeshRenderer::Update()
{
	_cameraData.matView = Camera::S_MatView;
	//_cameraData.matView = Matrix::Identity;
	_cameraData.matProjection = Camera::S_MatProjection;
	//_cameraData.matProjection = Matrix::Identity;
	_cameraBuffer->CopyData(_cameraData);

	_transformData.matWorld = GetTransform()->GetWorldMatrix();
	_transformBuffer->CopyData(_transformData);

	Render(_pipeline); 
}

이렇게 Render(_pipeline)을 넣어주면,

MeshRenderer만 들고 있어도 알아서 Redering 하는 부분이 실행이 된다.

그게 원래 렌더링의 역할이 맞긴 하니까 MeshRenderer는 Mesh를 그리는 역할을 하는 거니까.

이렇게 고쳐 보도록 한다.

private:
	void Render(shared_ptr<Pipeline> pipeline);

MeshRenderer의 Render 함수는 private으로 한다.

빌드를 하면 에러가 난다.

'MeshRenderer::MeshRenderer': no overloaded function takes 2 arguments

MeshRenderer를 추가하는 부분을 찾아가지고,

shared_ptr<Scene> SceneManager::LoadTestScene()
{
	shared_ptr<Scene> scene = make_shared<Scene>(); 

	// Camera
	{
		shared_ptr<GameObject> camera = make_shared<GameObject>(_graphics->GetDevice(), _graphics->GetDeviceContext());
		{
			camera->GetOrAddTransform();
			camera->AddComponent(make_shared<Camera>());
			scene->AddGameObject(camera); 
		}
	}

	// Monster
	{
		shared_ptr<GameObject> monster = make_shared<GameObject>(_graphics->GetDevice(), _graphics->GetDeviceContext());
		{
			monster->GetOrAddTransform();
			monster->AddComponent(make_shared<MeshRenderer>(_graphics->GetDevice(), _graphics->GetDeviceContext()));
			// _monster->GetTransform()->SetScale(Vec3(100.f, 100.f, 1.f)); 
			// ..
			scene->AddGameObject(monster); 
		}
	}

	return scene; 
}

이 부분에서 AddComponent로 MeshRenderer 컴포넌트를 넣어 주고 있는데 이곳에선 SceneManager가 들고 있지 않은

shared_ptr <Pipeline> pipeline

을 인자로 넘길 수 없기 때문에 에러가 발생하고 있다.

 

선택을 해야 한다. Pipeline을 받아서 넘기는 식으로 해도 되고,

임시적으로 전역인 GGame에서 _pipeline만 얻어 갈 수 있게 함수를 만들어 줄 수 있다.

싱글톤이 나쁜 게 아닌 게 이렇게 모든 구조를 뜯어고치면서 넘어갈 수밖에 없는 상황이 생길 수 밖에 없다.

GGame을 이용하는 방법으로 해본다.

 

2) 위에서 해준 이미지를 띄우기 위해 Pipeline을 MeshRenderer의 생성자의 매개 변수로 받아 Render의 매개변수로 전달하는 방법을 취소하고,  전역변수 GGame을 통해 Pipeline을 얻어, MeshRenderer의 Update에서 Render를 호출하고 매개변수로 Pipeline을 넣기

 

Game.h에 이 함수를 추가한다.

shared_ptr<Pipeline> GetPipeline() { return _pipeline; }
#include "Pipeline.h"

이것도 추가한다.

다시 SceneManager::LoadTestScene()로 돌아가서 monster를 만들 때 넘겨받으면 된다.

아니면, 지금처럼 _pipeline을 받는 함수를 만들었다면 굳이 넘겨받을 필요가 없다는 생각이 되면,

MeshRenderer의 생성자에서 pipelien을 넘겨받는 부분을 다시 삭제하고

MeshRenderer(ComPtr<ID3D11Device> device, ComPtr<ID3D11DeviceContext> deviceContext);
MeshRenderer::MeshRenderer(ComPtr<ID3D11Device> device, ComPtr<ID3D11DeviceContext> deviceContext)
	: Super(ComponentType::MeshRenderer), _device(device)
{

MeshRenderer.h에서

shared_ptr<Pipeline> _pipeline;

도 삭제하고

전역에서 빼서 사용할 수 있다.

void MeshRenderer::Update()에서

void MeshRenderer::Update()
{
	_cameraData.matView = Camera::S_MatView;
	//_cameraData.matView = Matrix::Identity;
	_cameraData.matProjection = Camera::S_MatProjection;
	//_cameraData.matProjection = Matrix::Identity;
	_cameraBuffer->CopyData(_cameraData);

	_transformData.matWorld = GetTransform()->GetWorldMatrix();
	_transformBuffer->CopyData(_transformData);

	// Render	
	Render(GGame->GetPipeline());
}

GGame->GetPipeline() 이렇게 꺼내서 쓰는 것도 하나의 방법이 될 수 있다.

 

MeshRenderer.cpp에

#include "Game.h"
#include "Pipeline.h"

을 추가한다.

실행을 하면 이미지가 잘 뜬다.

어차피 MeshRenderer를 다시 고칠 것이긴 하다.

 

4. TimeManager, InputManager을 Game에 추가하기

InputManager와 TimeManager는 WinAPI에서 했고 중요한 부분은 아니라 복붙 해서 적용만 시킨다. 코드를 다운로드하여 InputManager와 TimeManager만 코드가 들어있는 폴더 복붙하고, 드래그 앤 드롭으로 각 필터에 위치시킨다.

 

1) TimeManager 클래스

클래스 선언: TimeManager

  • public 메서드:
    • Init(): 시간 측정을 초기화합니다.
    • Update(): 각 프레임에서 호출되어 시간 관련 데이터를 업데이트합니다.
    • GetFps(): 현재 FPS를 반환합니다.
    • GetDeltaTime(): 마지막 두 프레임 간의 시간 차이(델타 타임)를 반환합니다.
  • private 멤버 변수:
    • _frequency: 시스템의 고성능 타이머 주파수를 저장합니다.
    • _prevCount: 이전 프레임의 타이머 카운트를 저장합니다.
    • _deltaTime: 마지막 두 프레임 간의 시간 차이를 저장합니다.
    • _frameCount: 현재 초(measurement second) 동안 처리된 프레임 수를 세는 데 사용됩니다.
    • _frameTime: 현재 초 동안 누적된 시간을 추적합니다.
    • _fps: 현재 계산된 프레임 속도(FPS)를 저장합니다.

메서드 구현부

  • Init():
    • QueryPerformanceFrequency: 시스템의 고성능 타이머 주파수를 얻습니다.
    • QueryPerformanceCounter: 현재 타이머 카운트를 가져와 _prevCount에 저장합니다. 이는 프레임 간 시간 차이를 계산하는 기준점으로 사용됩니다.
  • Update():
    • 현재 타이머 카운트를 새롭게 얻습니다.
    • _deltaTime을 계산하기 위해 이전 카운트(_prevCount)와의 차이를 측정하고 주파수로 나눕니다.
    • 프레임 카운트와 시간을 업데이트합니다.
    • _frameTime이 1초 이상이면, FPS를 계산하고 _frameTime과 _frameCount를 리셋합니다.

코드 분석 

  • 이 클래스는 고정 주기로 Update() 메서드를 호출하여 정확한 FPS 계산과 각 프레임 간의 델타 타임을 추적할 수 있습니다.
  • QueryPerformanceCounter와 QueryPerformanceFrequency 함수는 Windows API의 일부로, 매우 정확한 시간 측정을 가능하게 합니다.
  • 이 클래스는 게임 루프나 다른 실시간 그래픽 애플리케이션에서 성능 측정과 최적화에 유용하게 사용될 수 있습니다.
#pragma once

class TimeManager
{
public:
	void Init();
	void Update();

	uint32 GetFps() { return _fps; }
	float GetDeltaTime() { return _deltaTime; }

private:
	uint64	_frequency = 0;
	uint64	_prevCount = 0;
	float	_deltaTime = 0.f;

private:
	uint32	_frameCount = 0;
	float	_frameTime = 0.f;
	uint32	_fps = 0;
};
#include "pch.h"
#include "TimeManager.h"

void TimeManager::Init()
{
	::QueryPerformanceFrequency(reinterpret_cast<LARGE_INTEGER*>(&_frequency));
	::QueryPerformanceCounter(reinterpret_cast<LARGE_INTEGER*>(&_prevCount)); // CPU 클럭
}

void TimeManager::Update()
{
	uint64 currentCount;
	::QueryPerformanceCounter(reinterpret_cast<LARGE_INTEGER*>(&currentCount));

	_deltaTime = (currentCount - _prevCount) / static_cast<float>(_frequency);
	_prevCount = currentCount;

	_frameCount++;
	_frameTime += _deltaTime;

	if (_frameTime > 1.f)
	{
		_fps = static_cast<uint32>(_frameCount / _frameTime);

		_frameTime = 0.f;
		_frameCount = 0;
	}
}

2) InputManager클래스

클래스 및 열거형 선언

  1. KEY_TYPE 열거형: 다양한 키보드 및 마우스 버튼을 나타냅니다. 예를 들어, 방향키, WASD 키, 몇 가지 추가 키(Q, E, Z, C), 숫자 키(1-4), 그리고 마우스의 왼쪽 및 오른쪽 버튼입니다. 이 값들은 VK_UP, VK_DOWN 등의 Windows 가상 키 코드를 사용합니다.
  2. KEY_STATE 열거형: 키의 상태를 나타냅니다. NONE, PRESS, DOWN, UP, END와 같은 상태들이 있습니다.
  3. 상수 선언: KEY_TYPE_COUNT는 가능한 키 유형의 수를 나타내고, KEY_STATE_COUNT는 KEY_STATE 열거형의 항목 수를 나타냅니다.

InputManager 클래스

  • 메서드:
    • Init(HWND hwnd): 윈도우 핸들을 설정하고 키 상태 벡터를 초기화합니다.
    • Update(): 현재 활성 윈도우를 확인하고 키보드의 상태를 업데이트하며 마우스 위치를 가져옵니다.
  • 키 상태 확인 메서드:
    • GetButton(KEY_TYPE key): 주어진 키가 눌려있는지 확인합니다.
    • GetButtonDown(KEY_TYPE key): 주어진 키가 막 눌렸는지 확인합니다.
    • GetButtonUp(KEY_TYPE key): 주어진 키가 막 떼어졌는지 확인합니다.
  • 마우스 위치 가져오기: GetMousePos()를 통해 현재 마우스 위치를 반환합니다.

InputManager::Update() 메서드 

  • 현재 활성 윈도우를 확인하여, 입력 관리자가 초기화된 윈도우와 다르다면 모든 키 상태를 NONE으로 설정합니다.
  • GetKeyboardState 함수를 사용하여 현재 키보드 상태를 가져옵니다.
  • 각 키에 대하여, 키가 눌려있는지 여부를 확인하고 상태를 업데이트합니다.
    • 키가 눌려있다면 (0x80 비트가 설정되어 있으면), 이전 상태가 PRESS 또는 DOWN이었다면 PRESS 상태로 유지하고, 아니라면 DOWN 상태로 설정합니다.
    • 키가 눌려있지 않다면, 이전 상태가 PRESS 또는 DOWN이었다면 UP 상태로 설정하고, 그렇지 않다면 NONE 상태로 유지합니다.
  • 마우스 위치를 가져와 화면 좌표로 변환합니다.

이 코드는 Windows API를 사용하여 키보드 및 마우스 입력을 처리하고, 각 키의 상태(눌려있는지, 막 눌렸는지, 막 뗐는지)를 추적합니다. 이는 게임 개발이나 다른 대화형 응용 프로그램에서 사용자 입력을 관리하는 데 유용할 수 있습니다.

#pragma once

enum class KEY_TYPE
{
	UP = VK_UP,
	DOWN = VK_DOWN,
	LEFT = VK_LEFT,
	RIGHT = VK_RIGHT,

	W = 'W',
	A = 'A',
	S = 'S',
	D = 'D',

	Q = 'Q',
	E = 'E',
	Z = 'Z',
	C = 'C',

	KEY_1 = '1',
	KEY_2 = '2',
	KEY_3 = '3',
	KEY_4 = '4',

	LBUTTON = VK_LBUTTON,
	RBUTTON = VK_RBUTTON,
};

enum class KEY_STATE
{
	NONE,
	PRESS,
	DOWN,
	UP,
	END
};

enum
{
	KEY_TYPE_COUNT = static_cast<int32>(UINT8_MAX + 1),
	KEY_STATE_COUNT = static_cast<int32>(KEY_STATE::END),
};

class InputManager
{
public:
	void Init(HWND hwnd);
	void Update();

	// 누르고 있을 때
	bool GetButton(KEY_TYPE key) { return GetState(key) == KEY_STATE::PRESS; }
	// 맨 처음 눌렀을 때
	bool GetButtonDown(KEY_TYPE key) { return GetState(key) == KEY_STATE::DOWN; }
	// 맨 처음 눌렀다 뗐을 때
	bool GetButtonUp(KEY_TYPE key) { return GetState(key) == KEY_STATE::UP; }
	
	const POINT& GetMousePos() { return _mousePos; }

private:
	inline KEY_STATE GetState(KEY_TYPE key) { return _states[static_cast<uint8>(key)]; }

private:
	HWND _hwnd;
	vector<KEY_STATE> _states;
	POINT _mousePos = {};
};
#include "pch.h"
#include "InputManager.h"

void InputManager::Init(HWND hwnd)
{
	_hwnd = hwnd;
	_states.resize(KEY_TYPE_COUNT, KEY_STATE::NONE);
}

void InputManager::Update()
{
	HWND hwnd = ::GetActiveWindow();
	if (_hwnd != hwnd)
	{
		for (uint32 key = 0; key < KEY_TYPE_COUNT; key++)
			_states[key] = KEY_STATE::NONE;

		return;
	}

	BYTE asciiKeys[KEY_TYPE_COUNT] = {};
	if (::GetKeyboardState(asciiKeys) == false)
		return;

	for (uint32 key = 0; key < KEY_TYPE_COUNT; key++)
	{
		// 키가 눌려 있으면 true
		if (asciiKeys[key] & 0x80)
		{
			KEY_STATE& state = _states[key];

			// 이전 프레임에 키를 누른 상태라면 PRESS
			if (state == KEY_STATE::PRESS || state == KEY_STATE::DOWN)
				state = KEY_STATE::PRESS;
			else
				state = KEY_STATE::DOWN;
		}
		else
		{
			KEY_STATE& state = _states[key];

			// 이전 프레임에 키를 누른 상태라면 UP
			if (state == KEY_STATE::PRESS || state == KEY_STATE::DOWN)
				state = KEY_STATE::UP;
			else
				state = KEY_STATE::NONE;
		}
	}

	::GetCursorPos(&_mousePos);
	::ScreenToClient(_hwnd, &_mousePos);
}

얘네들을 추가한다면 Game에 가서 그 부분만 추가를 해주면 된다.

 

Game.h에

shared_ptr<InputManager> _input; 
shared_ptr<TimeManager> _time;

을 추가하고

shared_ptr<InputManager> GetInputManager() { return _input; }
shared_ptr<TimeManager> GetTimeManager() { return _time; }
class InputManager;
class TimeManager;

를 추가한다.

 

Game.cpp에

#include "InputManager.h"
#include "TimeManager.h"

를 추가한다.

얘네들도 자주 활용할 거 같으면 pch.h 에

#define			INPUT		GAME->GetInputManager()
#define			TIME		GAME->GetTimeManager()

이런 식으로 매크로를 만들어 주면 타이핑을 줄일 수 있다.

처음에 게임이 시작되고 인풋을 할 때

Game::Init에서

void Game::Init(HWND hwnd)
{
	_hwnd = hwnd;

	_graphics = make_shared<Graphics>(hwnd); 
	_pipeline = make_shared<Pipeline>(_graphics->GetDeviceContext());

	_input = make_shared<InputManager>(); 
	_input->Init(hwnd);

	_time = make_shared<TimeManager>(); 
	_time->Init(); 

	_scene = make_shared<SceneManager>(_graphics);
	_scene->Init(); 

	SCENE->LoadScene(L"Test"); 	
}

이런 식으로 만들고, 초기화하는 부분이 들어가면 되겠고,

void Game::Update()
{
	_graphics->RenderBegin();

	TIME->Update(); 
	INPUT->Update(); 
	SCENE->Update(); 	
	
	_graphics->RenderEnd();
}

이렇게 하면 시간도 체크할 수 있게 되었고,

키보드 입력도 언제든지 InputManager를 통해 얻어올 수 있게 되었다고 볼 수 있다.

빌드를 해보면 된다.

 

5. Resource

이제 다음으로 넘어간다.

 

아직 MeshRenderer가 불안한 요소가 있다. 너무 몰빵이 되어 있다.

똑같은 물체 1000개 만들면 물체마다 MeshRenderer를 들고 있을 텐데,

MeshRenderer 생성자를 보면

Geomestry, VertexBuffer, IndexBuffer, Shader도 1000번 모든 것들을 물체 개수만큼 해주고 있어.

 

하지만 만약 유니티로 작업을 한다면 유니티짱이라는 Mesh와 Asset은 하나만 있고, 이걸 이용해서 여러 개 만든다면 기본적으로 다 동일한 리소스를 사용할 것이다. 마찬가지로 큐브도 여러개 있을 수 있는데 큐브가 갖고 있는 Mesh, Material 자체는 하나만 공용으로 만들어서 사용할 것이다.

 

이런 식으로 어떤 애들은 공용으로 사용하는 게 필요하고, 그래야지만 큐브가 몇 만개가 되어도 Material이나 Mesh를 그 개수만큼 만들 수고를 덜어줄 수 있다.

그런 게 Resource의 개념이다.

 

GameObject, Compnent 다음으로 이제 Resource에 대한 개념을 익히기 시작해야 한다.

00.Engine에 Resource라는 필터를 만든다.

Resource는 다양한 게 있을 수 있다. 인게임에서 파일로 관리하고 있다가 한 번만 로드해서 모든 물체들이 공용으로 활용하는 걸 Resource라고 한다.

Resource필터에 ResourceBase클래스, ResourceManager 클래스를 생성한다.

 

1) ResourceBase 클래스

ResourceBase.h에

enum class ResourceType : uint8
{
	None = -1, 
	Mesh,
	Shader, 
	Texture,
	Material,
	Animation, 

	End
};

이렇게 리소스 타입을 추가한다. 사운드 파일 같은 것도 Resource라고 할 수 있다.

End는 카운팅을 하기 위한 거다.

enum
{
	RESOURCE_TYPE_COUNT = static_cast<uint8>(ResourceType::End)
};

이렇게 만들어 본다.

#pragma once

enum class ResourceType : uint8
{
	Node = -1, 
	Mesh,
	Shader, 
	Texture,
	Material,
	Animation, 

	End
};

enum
{
	RESOURCE_TYPE_COUNT = static_cast<uint8>(ResourceType::End)
};

class ResourceBase : public enable_shared_from_this<ResourceBase>
{
public:
	ResourceBase(ResourceType type); 
	virtual ~ResourceBase(); 

	ResourceType GetType() { return _type; }

	void SetName(const wstring& name) { _name = name; }
	const wstring& GetName() { return _name; }
	uint32 GetID() { return _id; }

protected:
	virtual void Load(const wstring& path) { } 
	virtual void Save(const wstring& path) { }
// 저장하는 포멧은 리소스마다 다르기 때문에 오버로딩해서 정하라고 넘겨주는 거  

protected:
	ResourceType _type = ResourceType::None; 
	wstring _name; 

	wstring _path; 
	uint32 _id = 0; 
};
#include "pch.h"
#include "ResourceBase.h"

ResourceBase::ResourceBase(ResourceType type)
	: _type(type)
{
}

ResourceBase::~ResourceBase()
{
}

ResourceBase는 이렇게 만들어 주었다.

기존 Resource 필터는 ResourceManager 역할을 하고 있으니까 Manager 필터 안으로 옮기고,

00.Engine에 Resoruce필터를 새로 만든다.

여기다가 Texture나 Material 같은 애들을 늘려주면 된다.

 

2) Texture 클래스

예전에 만들어 놓은 것 중에서 Pipeline / 04. PixelShader 필터 안에 있던 Texture 클래스를 Resource로 옮겨주고 첫 번째로 작업을 해본다.

 

Texture.h에

#include "ResourceBase.h"

class Texture : public ResourceBase
{
	using Super = ResourceBase;

이렇게 추가하고,

생성자도

Texture::Texture(ComPtr<ID3D11Device> device) 
	: Super(ResourceType::Texture), _device(device)
{
}

이렇게 추가한다.

 

3) ResourceManager 클래스

ResourceManager로 돌아가서 보면

이건 온갖 Resource들을 관리하는 역할이라고 보면 된다.

어떻게 하면 효율적으로 관리할 수 있을까는 고민을 할 필요가 있다.

#pragma once

#include "ResourceBase.h"

class ResourceManager
{
public:
	ResourceManager(ComPtr<ID3D11Device> device); 

	void Init(); 

	template<typename T>
	shared_ptr<T> Load(const wstring& key, const wstring& path); // 지금은 구현안함

	template<typename T>
	bool Add(const wstring& key, shared_ptr<T> obj); 

	template<typename T>
	shared_ptr<T> Get(const wstring& key);

	// Get<Texture>()
	template<typename T>
	ResourceType GetResourceType(); 

private:

private:
	ComPtr<ID3D11Device> _device; 

	using KeyObjMap = map<wstring/*key*/, shared_ptr<ResourceBase>>; 

	// [ map map map map map ]
	array<KeyObjMap, RESOURCE_TYPE_COUNT> _resources; 
};

이렇게 헤더를 작성하고 각 함수를 구현한다.

핵심은 원하는 타입의 Resource를 원하는 키값으로 KeyObjMap에다가 때려 박는 게 목적이다라고 결론을 내릴 수 있는 거다.

private:
	void CreateDefaultTexture(); 
	void CreateDefaultMesh(); 
	void CreateDefaultShader(); 
	void CreateDefaultMaterial(); 
	void CreateDefaultAnimation();

를 추가한다. 나중에는 Scene에 만드는 게 직관적이고 Scene이 아니면 툴 상에서 드래그 앤 드롭을 했을 떄 그거에 따라 가지고 어떤 씬에 들어가는 식으로 만드는게 일반적일 것이다.

 

지금은 ResourceManager가 시작이 될 때 이 5개를 로드해서 시작하게끔 유도를 해준다.

void ResourceManager::Init()
{
	CreateDefaultTexture();
	CreateDefaultMesh();
	CreateDefaultShader();
	CreateDefaultMaterial();
	CreateDefaultAnimation();
}
void ResourceManager::CreateDefaultTexture()
{
	{
		auto texture = make_shared<Texture>(_device);
		texture->SetName(L"chiikawa");
		texture->Create(L"chiikawa.png");
		Add(texture->GetName(), texture);
	}
}

이렇게 해주면,

Get<Texture>(L"chiikawa"); 

이걸 호출해 주면 다시 만들 필요 없이 하나의 아이를 계속 재활용할 수 있게 된다.

 

이게 ResourceManager의 위력이다.

MeshRenderer의 Mesh, Material로 묶어 줄 수 있는 부분은 묶어서 관리하게 되면 MeshRenerer를 사용할 때 필요한 애들만 연결해 주면 되는 것이다.

그게 유니티에서 Mesh, Material을 고르는 부분이라고 보면 된다.

동일한 MeshRenderer라고 해도 Mesh, Material 부분을 어떤 리소스로 채워줬냐에 따라가지고, 다르게 동작할 수 있다.

리소스를 사용할 때 모든 애들이 같은 리소스를 바라보면서 공유해서 사용하는 개념이다.

예를 들어 모두 hair 마테리얼을 쓰고 있었는데 누군가가 hair 머테리얼의 옵션을 바꿨다면 사용하는 모든 애들이 영향을 받는다.

혼자만 다르게 쓰려면 머테리얼 인스턴스를 만들거나, 새로운 걸 만들어서 사용해야 한다.

리소스가 등장하면 이 5개의 Create함수에 넣어줄 예정이다.

#pragma once

#include "ResourceBase.h"

class ResourceManager
{
public:
	ResourceManager(ComPtr<ID3D11Device> device); 

	void Init(); 

	template<typename T>
	shared_ptr<T> Load(const wstring& key, const wstring& path); // 지금은 구현안함

	template<typename T>
	bool Add(const wstring& key, shared_ptr<T> object); 

	template<typename T>
	shared_ptr<T> Get(const wstring& key);

	// Get<Texture>()
	template<typename T>
	ResourceType GetResourceType(); 

private:
	void CreateDefaultTexture(); 
	void CreateDefaultMesh(); 
	void CreateDefaultShader(); 
	void CreateDefaultMaterial(); 
	void CreateDefaultAnimation(); 

private:
	ComPtr<ID3D11Device> _device; 

	using KeyObjMap = map<wstring/*key*/, shared_ptr<ResourceBase>>; 

	// [ map map map map map ]
	array<KeyObjMap, RESOURCE_TYPE_COUNT> _resources; 
};

template<typename T>
inline shared_ptr<T> ResourceManager::Load(const wstring& key, const wstring& path)
{
	auto objectType = GetResourceType<T>();
	KeyObjMap& keyObjMap = _resources[static_cast<uint8>(objectType)];

	auto findIt = keyObjMap.find(key);
	if (findIt != keyObjMap.end())
		return static_pointer_cast<T>(findIt->second);

	shared_ptr<T> object = make_shared<T>();
	object->Load(path);
	keyObjMap[key] = object;

	return object;
}

template<typename T>
inline bool ResourceManager::Add(const wstring& key, shared_ptr<T> object)
{
	ResourceType resourceType = GetResourceType<T>();
	KeyObjMap& keyObjMap = _resources[static_cast<uint8>(resourceType)];

	auto findIt = keyObjMap.find(key);
	if (findIt != keyObjMap.end())
		return false; 

	keyObjMap[key] = object;

	return true; 
}

template<typename T>
inline shared_ptr<T> ResourceManager::Get(const wstring& key)
{
	ResourceType resourceType = GetResourceType<T>();
	KeyObjMap& keyObjMap = _resources[static_cast<uint8>(resourceType)]; 

	auto findIt = keyObjMap.find(key); 
	if (findIt != keyObjMap.end())
		return static_pointer_cast<T>(findIt->second); 

	return nullptr; 
}

template<typename T>
inline ResourceType ResourceManager::GetResourceType()
{
	if (std::is_same_v<T, Texture>) // 어떤 template 타입이 Texture타입과 일치하는지, 컴파일 타임에 실행되기 원하면 constexpr를 붙인다. 지금은 런타임 실행
		return REsourceType::Texture; 

	assert(false); 
	return ResourceType::None;
}
#include "pch.h"
#include "ResourceManager.h"
#include "Texture.h"

ResourceManager::ResourceManager(ComPtr<ID3D11Device> device)
	: _device(device)
{

}

void ResourceManager::Init()
{
	CreateDefaultTexture();
	CreateDefaultMesh();
	CreateDefaultShader();
	CreateDefaultMaterial();
	CreateDefaultAnimation();
}

void ResourceManager::CreateDefaultTexture()
{
	{
		auto texture = make_shared<Texture>(_device);
		texture->SetName(L"chiikawa");
		texture->Create(L"chiikawa.png");
		Add(texture->GetName(), texture);
	}
}

void ResourceManager::CreateDefaultMesh()
{
}

void ResourceManager::CreateDefaultShader()
{
}

void ResourceManager::CreateDefaultMaterial()
{
}

void ResourceManager::CreateDefaultAnimation()
{
}

 

4) ResourceManager를 Game에 추가하기

그다음에 Game에 가서

새로운 매니저를 추가해 줘야 한다.

Game.h에

class ResourceManager;

이렇게 전방선언 해주고 ,

shared_ptr<ResourceManager> GetResourceManager() { return _resource; }

Get 함수를 추가하고,

shared_ptr<ResourceManager> _resource;

변수도 추가한다.

 

Game::Init으로 가서

_resource = make_shared<ResourceManager>(_graphics->GetDevice());
_resource->Init();

를 추가하고

#include "ResourceManager.h"

를 추가한다.

 

그리고 pch.h로 가서 매크로를 만들어준다.

#define			RESOURCES	GAME->GetResourceManager()

Resources라고 한 이유는 유니티 엔진에서 Resources라고 되어 있다.

Resources로 뭔가 가져오고 싶으면 RESOURCES를 쓰면 된다.

빌드를 하면 통과한다.

 

5) Shader 클래스를 ShaderBase클래스로 이름 변경하기

해야 할게 많다.

Texture를 만들어 줬으니 Mesh, Shader, Material, Animation 등을 만들어주면 된다.

Shader는 만들어 두긴 했다. 하지만 VertexShader와 PixelShader를 세트로 묶어서 그걸 셰이더 리소스로 사용할 건데 지금은 기본적으로 각각이 물고 있는 상위 클래스를 Shader로 해 놨다.

앞으로 리소스를 사용할 때는 이 Shader가 아니라 VertexShader랑 PixelShader를 동시에 들고 있는 걸 만드는 게 좋기 때문에 Shader 클래스의 이름을 ShaderBase로 바꿔준다.

 

6) ResourceBase 클래스를 상속받은 Shader, Mesh, Material, Animation, Animation 리소스 타입 클래스

그리고 Shader resource type 클래스는 조금 더 Resource에 맞는, 모든 애들을 다 물고 있는 애를 만들어 준다.

Resource 필터에 ResourceBase를 상속받은 Shader라는 클래스를 만든다.

그리고 Material, Mesh, Animation클래스도 각각 ResourceBase를 상속받아 만든다.

ResoureBase를 만들게 되면 생성자에 type을 채워주지 않게 되면 만들 수 없으니까 만들어 주면서 채워준다.

 

Shader부터 만들면

#pragma once
#include "ResourceBase.h"
class Shader : public ResourceBase
{
	using Super = ResourceBase; 
public:
	Shader(); 
	virtual ~Shader(); 
};
#include "pch.h"
#include "Shader.h"

Shader::Shader() : Super(ResourceType::Shader)
{
}

Shader::~Shader()
{
}

이 작업을 반복해서 해주면 된다.

 

Mesh

#pragma once
#include "ResourceBase.h"
class Mesh :  public ResourceBase
{
	using Super = ResourceBase;

public:
	Mesh(); 
	virtual ~Mesh(); 
};
#include "pch.h"
#include "Mesh.h"

Mesh::Mesh() : Super(ResourceType::Mesh)
{
}

Mesh::~Mesh()
{
}

 

Material

#pragma once
#include "ResourceBase.h"
class Material :  public ResourceBase
{
	using Super = ResourceBase;
public:
	Material(); 
	virtual ~Material(); 
};
#include "pch.h"
#include "Material.h"

Material::Material() : Super(ResourceType::Material)
{
}

Material::~Material()
{
}

 

Animation

#pragma once
#include "ResourceBase.h"
class Animation : public ResourceBase
{
	using Super = ResourceBase;
public:
	Animation(); 
	virtual ~Animation(); 
};
#include "pch.h"
#include "Animation.h"

Animation::Animation() : Super(ResourceType::Animation)
{
}

Animation::~Animation()
{
}

 

7) ResourceManager::GetResourceType()에서 Mesh, Shader, Material, Animation 판정하는 부분 추가하기

ResourceManager에서 채워주는 부분까지 해줘야 한다.

ResourceManager::GetResourceType에 가서

template<typename T>
inline ResourceType ResourceManager::GetResourceType()
{
	if (std::is_same_v<T, Texture>) // 어떤 template 타입이 Texture타입과 일치하는지, 컴파일 타임에 실행되기 원하면 constexpr를 붙인다. 지금은 런타임 실행
		return ResourceType::Texture; 

	if (std::is_same_v<T, Mesh>) 
		return ResourceType::Mesh;

	if (std::is_same_v<T, Shader>)
		return ResourceType::Shader;

	if (std::is_same_v<T, Material>)
		return ResourceType::Material;

	if (std::is_same_v<T, Animation>)
		return ResourceType::Animation;

	assert(false); 
	return ResourceType::None;
}

이렇게 채워준다.

class Mesh; 
class Material; 
class Shader; 
class Animation; 
class Texture;

이렇게 전방선언을 하고

빌드를 하면 잘 된다.

 

8) 다음 시간에 할 것, 절대경로, 상대경로

준비는 끝났으니까 나머지는 ResourceManager::CreateDefault~() 함수들을 채워주면 된다.

 

지금은 경로 자체를 절대 경로를 이용해서 관리하고 있다. 코드 있는 곳에 같이 넣고 있는데 나중에는 솔루션 파일 있는 곳에 Resources 폴더를 만든 다음에 Texture, Sound 같은 폴더를 만들고 체계적으로 리소스를 관리해야 될 것이고, 리소스 파일의 경로를 ResourceManager에서 관리를 해서 그 경로를 토대로 작업 작동하게 만들어줘야 한다.

 

보통 실행 파일이 있는 Binaries 폴더 안에 설정 파일을 만들어 놓는다. 실행이 될 때 설정 파일을 읽어서 설정 파일에 게임이 어떻게 실행될지, 리소스가 어디에 있는지 등등을 파일에 적어 놓은 다음에 그걸 이용해서 리소스 폴더의 상위 폴더 경로를 ResourceManager가 들고 있다가 그걸 이용해서 key나 path는 그 전체적인 ResourceManager를 기준으로 한 경로로 접근하게 해주는 경우가 많다.

 

유니티에서도 Resources폴더를 만들면 거기에 있는 파일 경로를 찾아서 로드하는 식으로 되어 있다. 지금은 새 방식이 나오긴 했다.

반응형

'DirectX' 카테고리의 다른 글

29. 엔진구조_Material  (0) 2024.01.26
28. 엔진구조_ RenderManager  (0) 2024.01.23
26. 엔진구조_SceneManager  (0) 2024.01.21
25. 엔진구조_MeshRenderer  (0) 2024.01.19
24. 엔진구조_Component  (0) 2024.01.17

댓글