코승호딩의 메모장

[InputManager와 GameTimer] 본문

DirectX12/DirectX12 응용

[InputManager와 GameTimer]

코승호딩 2023. 10. 7. 18:28

이번 글에서는 키 입력을 쉽게 받기 위한 InputManager 클래스와 시간 관리를 위한 GameTimer를 생성하도록 한다. 이 두 클래스는 엔진에서 한 개만 존재해야 하기 때문에 싱글턴 패턴을 이용하여 간편하게 관리하도록 한다.

#define DECLARE_SINGLE(type)		\
private:				\
	type() {}			\			
	~type() {}			\		
public:						
	static type* GetInstance()	\
	{				\
		static type instance;	\
		return &instance;	\
	}				\

#define GET_SINGLE(type)	type::GetInstance()

ex) GET_SINGLE(Timer)->GetDeltaTime();

우선 싱글턴 패턴을 위해 #define을 이용하여 클래스에서 쉽게 사용하도록 한다. 그리고 다른 파일에서 싱글턴 객체들을 불러와서 사용하기 위해 GET_SINGLE을 추가하여 내부적으로 가지고 있는 자신의 Instance를 반환하도록 한다.


InputManager

 

enum class KEY_STATE 
{	
	NONE, 
	TAP, 
	PRESSED, 
	AWAY
};

struct KeyInfo {
	KEY_STATE mState{};
	bool mIsPrevPushed{};
};

다음은 키가 가지고 있어야 할 정보들이다. TAP은 이전에 눌린 적이 없는 상태일 때 눌린 것이고 PRESSED는 이전에 눌린 것이 아직도 눌려지고 있는 상태이다. AWAY는 이전에 눌려 있었지만 현재는 안 눌려 있는 상태이다.

 

int keyList[] =
{
	VK_ESCAPE, VK_F1, VK_F2, VK_F3, ...
};

keyList는 int 배열로 관리되며 각자 다른 값을 가지고 있기 때문에 이 키 리스트를 통해 mKeys의 키 값으로 사용한다.

 

class InputManager {
	DECLARE_SINGLE(InputManager)

public:
	void Init(const WindowInfo& info);
	void Update();

public:
	KEY_STATE GetKeyState(int key) { return mKeys[key].mState; }
    
private:
	WindowInfo _info;
	std::unordered_map<int, KeyInfo> mKeys;
};

InputManager 클래스는 내부적으로 키에 대한 정보를 unoredered_map을 통해서 가지고 있다. mKeys의 키 값은 키 리스트 즉 어떠한 키보드의 키인지를 가지고 있고 밸류 값으로는 KeyInfo 즉, 현재 키보드의 상태가 어떤 상태인지를 가지고 있다.

 

void InputManager::Init(const WindowInfo& info)
{
	_info = info;

	for (int key : keyList) {
		mKeys[key] = KeyInfo{ KEY_STATE::NONE, false };
	}
}

Init에서 모든 키를 NONE과 false로 초기화 해준다.

 

Update에서는 for (auto& keyInfo : mKeys) 을 돌며 mKeys에 저장된 키 값에 대한 내용들이 변화가 있는지를 계속 확인한다. 그리고 각 State에 따라 변화가 있다면 변경해 주는 것이다. 

 

#define KEY_NONE(key)		GET_SINGLE(InputManager)->GetKeyState(key) == KEY_STATE::NONE
#define KEY_TAP(key)		GET_SINGLE(InputManager)->GetKeyState(key) == KEY_STATE::TAP
#define KEY_PRESSED(key)	GET_SINGLE(InputManager)->GetKeyState(key) == KEY_STATE::PRESSED
#define KEY_AWAY(key)		GET_SINGLE(InputManager)->GetKeyState(key) == KEY_STATE::AWAY

ex) if (KEY_PRESSED('W')) { move(y + 1) };

마지막으로 편리하게 키 값을 얻어올 수 있는 매크로 명령을 정의한다. 


GameTimer

Timer에 대한 세부 변수들은 아래 포스팅에서 확인할 수 있다.

 

[시간 측정]

컴퓨터의 성능은 하드웨어마다 다르기 때문에 게임의 물체들을 경과 시간에 기초하여 적절히 갱신하기 위해서는 애니메이션의 프레임들을 렌더링할 때는 프레임들 사이에 시간이 얼마나 흘렀

suengho2257.tistory.com

 

class Timer
{
	DECLARE_SINGLE(Timer)

public:
	void Init(const WindowInfo info);
	void Update();
	void Reset();
	void Start(); 
	void Stop();  

public:
	float TotalTime() const;
	float DeltaTime() const;
	float GetFps() const;

private:
	void CalculateFrameStats();

private:
	//... value
};

위와 같이 Timer 또한 싱글턴으로 생성하고 나머지는 위 포스팅에서 확인한다. 주목할 점은 CalculateFrameStats 함수를 사용하여 프레임을 계산하고 프레임을 응용 프로그램 시작 시 상단 캡션 바에 표시해 주는 것이다.

 

void Timer::CalculateFrameStats()
{
	static int frameCnt = 0;
	static float timeElapsed = 0.0f;

	frameCnt++;

	if ((TotalTime() - timeElapsed) >= 1.0f)
	{
		mFps = (float)frameCnt;
		mSpf = 1000.0f / mFps;

		wstring fpsStr = to_wstring(mFps);
		wstring mspfStr = to_wstring(mSpf);
		wstring wndCaption = L"DirectX Game";
		wstring windowText = wndCaption + L"    fps: " + fpsStr + L"   mspf: " + mspfStr;

		SetWindowText(mInfo.hwnd, windowText.c_str());

		// Reset for next average.
		frameCnt = 0;
		timeElapsed += 1.0f;
	}
}

이 함수를 업데이트할 때마다 호출하게 되면 계속 캡션에 fps와 mspf의 정보를 확인할 수 있을 것이다. 다만 주의할 점은 Timer을 생성하여 시작할 때, Init 함수에 Reset을 넣어줘야 한다. 그렇지 않으면 BaseTime 값이 0이 되어버리기 때문에 위 함수의 TotalTime() 부분이 엄청 높은 숫자가 되어 계속 1, 1000의 값만 표시가 될 것이다.

 

void Engine::Init(const WindowInfo& info)
{
	GET_SINGLE(InputManager)->Init(info);
	GET_SINGLE(Timer)->Init(info);
}

void Engine::Update()
{
	GET_SINGLE(InputManager)->Update();
	GET_SINGLE(Timer)->Update();
}

이제 엔진 코드의 초기화와 업데이트 부분에서 싱글턴 인스턴스를 얻어와서 각각 객체들을 초기화해주고 업데이트해준다. 


void Game::Update()
{
	gEngine->Update();

	gEngine->RenderBegin();

	{
		static ObjectConstants o;

		if (KEY_PRESSED('W'))
			o.offset.y += 1.f * GET_SINGLE(Timer)->DeltaTime();
		if (KEY_PRESSED('D'))
			o.offset.x += 1.f * GET_SINGLE(Timer)->DeltaTime();
		if (KEY_PRESSED('S'))
			o.offset.y -= 1.f * GET_SINGLE(Timer)->DeltaTime();
		if (KEY_PRESSED('A'))
			o.offset.x -= 1.f * GET_SINGLE(Timer)->DeltaTime();

		mesh->SetObjectConstant(o);

		mesh->Render();
	}

	gEngine->RenderEnd();
}

이제 mesh 객체의 ObjectConstatns 정보에 각 키마다의 값을 넘겨주어 더하거나 빼주고 상수 버퍼에 넘겨주면 객체가 이동이 잘 될 것이다. 다음은 객체를 오른쪽으로 몇 초간 이동한 모습을 보여준다. 이제 정확한 시간에 따라 객체를 움직일 수 있다.

 


이번 코드를 작성하면서 싱글턴은 벌집안에 든 꿀과 같다는 생각이 들었다. 이렇게 사용하기 편리하고 구현하기 쉬운 장점이 있지만 싱글턴을 남발하다 보면 화를 면치 못할 것이기 때문이다. 그러나 InputManager와 같이 편리하게 사용할 수 있다면 위에서 작성한 코드처럼 편리하며 개발자가 아닌 사람들도 어느 정도 이해를 할 수 있을 정도이다. 또한 상수 버퍼를 자유롭고 유동적으로 사용할 수 있는 능력도 기를 수 있었다.

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

[Camera]  (1) 2023.10.09
[SceneManager]  (1) 2023.10.08
[Component]  (0) 2023.10.07
[Material]  (0) 2023.10.07
[Texture Mapping]  (0) 2023.10.07
[Mesh]  (1) 2023.10.07
[ConstantBuffer와 FrameResource]  (0) 2023.10.07