시연 영상
https://www.youtube.com/watch?v=d-VZS1AdvtA&list=LL&index=3
이번 기회에 win32api를 유튜브로 간단하게 독한한 뒤 "BlobFish" 개발사의 Brotato 게임을 모작으로 한번 만들어 보겠다.
제작을 시작한지는 시간이 꽤 지나긴했지만 기억을 되살려 한번 제작기를 작성해보도록 하겠다.
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
//딱히 신경쓸 건 없음. 얘내 둘은 딱히 쓰이지 않는다... 라는 걸 선언한 느낌.
//매크로임.
// TODO: 여기에 코드를 입력합니다.
LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadStringW(hInstance, IDC_CLIENT, szWindowClass, MAX_LOADSTRING);
MyRegisterClass(hInstance);
// 애플리케이션 초기화를 수행합니다:
// 여기서 윈도우 창을 생성함. 유저와 프로그램 사이의 인터페이스, 접점이라는 의미도 됨.
if (!InitInstance(hInstance, nCmdShow))
{
return FALSE;
}
//CCore 초기화
if (FAILED(CCore::GetInstance()->init(g_hWnd, POINT{960, 540}))) {
MessageBox(nullptr, L"Core 객체 초기화 실패", L"ERROR", MB_OK);
return FALSE;
}
HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_CLIENT));
MSG msg;
while (true)
{
//무한 반복 -> 메시지가 없으면 if문 바깥으로 무시.
if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
//메시지를 처리하기 전에,
if (WM_QUIT == msg.message) {
break;
}
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
//메시지가 발생하지 않는 대부분의 시간.
else {
//디자인 패턴(설계 유형)
//싱글톤 패턴
//게임 속에서 렌더링이라는 건, 매 순간순간 전부 다 삭제 후, 전부 그리기.
CCore::GetInstance()->progress();
}
}
return (int)msg.wParam;
}
기존에 있던 main()을 다음과 같은 형태로 바꿔서 제작을 할 예정이다.
기존의 main함수 방식은 message를 받아 message를 해석하고 해석한 내용에 맞게 처리해주는 방식이였다.
하지만 message를 받고 처리하는 영역이 프로그램 전체에서 차지하는 시간은 매우매우 작은 시간을 차지한다. 다른 말로 대부분의 시간은 프로그램이 놀고 있다는 소리다.
따라서 내가 원하는 게임의 대부분 렌더링들은 메세지가 발생하지 않는 시간에 처리할 것이다.
이 렌더링을 처리해줄 클래스가 바로 CCore 클래스다.
<CCore.h>
#pragma once
#include "define.h"
class CCore {
SINGLE(CCore);
private:
float m_scrollSpeed; // 스크롤 속도 (기본값: 30.0f)
HWND m_hWnd; //메인 윈도우 핸들.
POINT m_ptResolution; //메인 윈도우 해상도
//메뉴바.
HMENU m_hMenu; //Tool Scene에서만 사용.
private:
//GDI Plus 관련.
ULONG_PTR gdiplusToken;
GdiplusStartupInput gdiplusStartupInput;
private:
void Clear();
public:
void SetScrollSpeed(float speed); // 스크롤 속도 설정
float GetScrollSpeed() const; // 현재 스크롤 속도 가져오기
public:
int init(HWND _hWnd, POINT _ptResolution);
void progress();
void DockMenu();
void DivideMenu();
void ChangeWindowSize(Vec2 _vResolution, bool _bMenu);
public:
HWND GetMainHwnd() { return m_hWnd; }
POINT GetResolution() { return m_ptResolution; }
};
CCore 클래스는 다음과 같은 형태로 정의되어 있다.
이 프로젝트에서는 Direct2D를 이용해 대부분의 이미지와 객체들을 렌더링하지만 GDI+를 사용하는 경우가 딱 한가지 있다.
전투 맵을 구현할때 이미지를 32픽셀x32픽셀 단위씩 쪼개서 맵의 외곽선 이미지 파일과 내부에 그려질 바닥 이미지를 합쳐야될 때 사용한다. 물론 Direct2D를 이용해 합치는 방법도 있었지만 픽셀단위 접근이 잘 안되어서 GDI+로 두 이미지를 특정조건(픽셀값)에 따라 합친뒤 png파일로 저장하고 Direct2D로 불러오는 방식으로 만들었다.
이 부분은 나중에 따로 설명하도록 하겠다.
CCore에서 가장 중요한 부분은 init과 progess함수가 된다.
<CCore::init()>
int CCore::init(HWND _hWnd, POINT _ptResolution) {
m_hWnd = _hWnd;
m_ptResolution = _ptResolution;
m_scrollSpeed = 30.f;
GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
//해상도에 맞게 윈도우 크기 조정.
ChangeWindowSize(Vec2((float)_ptResolution.x, (float)_ptResolution.y), false);
GetClientRect(m_hWnd, &psRect);
//////////////////////////Manager initialize//////////////////////////////
CPathMgr::GetInstance()->init();
CTimeMgr::GetInstance()->init();
CkeyMgr::GetInstance()->init();
CSoundMgr::GetInstance()->init();
CCamera::GetInstance()->init();
Direct2DMgr::GetInstance()->init(m_hWnd);
CFileMgr::GetInstance()->init(CPathMgr::GetInstance()->GetContentPath());
CSceneMgr::GetInstance()->init();
CFontMgr::GetInstance()->init();
ItemMgr::GetInstance()->init();
//////////////////////////Manager initialize//////////////////////////////
return S_OK;
}
위 코드와 같이 처음에 인자로 넘겨받은 메인윈도우의 핸들값과 해상도 정보로 화면을 조정해주고, 게임이 작동되기위한 여러 Manager들을 초기화 시켜준다. 이때 Manager들은 전부 SingleTon 패턴으로 구현되어있어서 GetInstance()로 접근을 하여 사용한다.
각 매니저들은 다음과 같은 역할을 수행한다.
<CPathMgr.cpp>
#include "pch.h"
#include "CPathMgr.h"
#include "CCore.h"
CPathMgr::CPathMgr()
: m_szContentPath{}
{
}
CPathMgr::~CPathMgr() {
}
void CPathMgr::init()
{
//Ctrl + F5쓰면, 혹은 일반적으로 해도 프로젝트 폴더가 됨.
//즉 비주얼 스튜디오에서 실행하는거랑, 프로그램 자체 실행이랑
//경로값이 달라짐.
GetCurrentDirectory(255, m_szContentPath);
//상위폴더로 나가고(\\을 만날때까지 찾고 거기에 null문자 넣기.)
int dirLen = static_cast<int>(wcslen(m_szContentPath));
for (int dirIDX = dirLen - 1; dirIDX >= 0; dirIDX--) {
if ('\\' == m_szContentPath[dirIDX]) {
m_szContentPath[dirIDX] = '\0';
break;
}
}
// + bin\\content\\ 경로 붙여주기
wcscat_s(m_szContentPath, 255, L"\\bin\\content\\");
//상대 경로 설정 완료.
//SetWindowText(CCore::GetInstance()->GetMainHwnd(), m_szContentPath);
}
wstring CPathMgr::GetRelativePath(const wchar_t* _filepath)
{
wstring strFilePath = _filepath;
size_t iAbsLen = wcslen(m_szContentPath);
size_t iFullLen = strFilePath.length();
wstring strRelativePath = strFilePath.substr(iAbsLen, iFullLen - iAbsLen);
return strRelativePath;
}
현재 프로그램이 실행되고 있는 파일의 경로를 가져와서 넘겨주는 역할을 한다.
이 경로는 이미지, 사운드, 폰트 등등 과 같은 리소스를 접근하기 위해 사용된다.
<CTimeMgr.cpp>
#include "pch.h"
#include "CTimeMgr.h"
#include "CCore.h"
CTimeMgr::CTimeMgr()
:m_llCurCount{}
, m_llPrevCount{}
, m_llFrequency{}
,m_dDT(0.)
,m_iCallCount(0)
,m_dAcc(0.)
,m_iFPS(0)
,m_ftimeScale(1.f)
{
}
CTimeMgr::~CTimeMgr() {
}
void CTimeMgr::init()
{
//현재 카운트
QueryPerformanceCounter(&m_llPrevCount);
//초당 카운트가 고정되어 있는것도 아님.
//초당 카운트 횟수(1000만)
QueryPerformanceFrequency(&m_llFrequency);
SetTimeScale(1.f);
}
void CTimeMgr::update()
{
QueryPerformanceCounter(&m_llCurCount);
//이전 프레임의 카운팅고, 현재 프레임 카운팅 값의 차이를 구한다.
m_dDT = (double)(m_llCurCount.QuadPart - m_llPrevCount.QuadPart) / (double)m_llFrequency.QuadPart;
//이전 카운트 값을 현재값으로 갱신(다음번에 계산을 위해서)
m_llPrevCount = m_llCurCount;
}
void CTimeMgr::render()
{
m_iCallCount++;
m_dAcc += m_dDT; //DT 누적.
if (m_dAcc >= 1.) {
m_iFPS = m_iCallCount;
m_dAcc = 0.;
m_iCallCount = 0;
wchar_t szBuffer[255] = {};
swprintf_s(szBuffer, L"FPS : %d, DT : %f", m_iFPS, m_dDT);
SetWindowText(CCore::GetInstance()->GetMainHwnd(), szBuffer);
}
}
앞 포스팅에서 설명했던과 같이 시간동기화를 위한 DeltaTime을 구해주는 Manager다.
DeltaTime은 이전 프레임에서의 카운트값과 현재 카운트값을 비교하여 구해준다.
이때 render함수에서는 FPS와 DeltaTime을 구하는 과정도 있다.
#define fDT CTimeMgr::GetInstance()->GetfDT()
#define DT CTimeMgr::GetInstance()->GetDT()
외부에서는 fDT 또는 DT 매크로를 이용해 DeltaTime을 가져올 수 있다.
(fDT float형, DT double형)
<CKeyMgr.h>
#pragma once
#include "define.h"
//키 상태.
enum class KEY_STATE {
TAP, //막 누른 시점
HOLD, //누르고 있는
AWAY, //막 뗀 시점
NONE, // 눌리지 않은 상태. 이전에도 눌리지 않은 상태.
};
//지원해줄 수 있는 키
enum class KEY {
...
};
struct tKeyInfo{
KEY_STATE eState; //키의 상태값.
bool bPrevPush; //이전에 이 키가 눌렸는지 안 눌렸는지
};
class CkeyMgr
{
SINGLE(CkeyMgr);
private:
//벡터의 INDEX값이, 곧 KEY값.
vector<tKeyInfo> m_vecKey;
Vec2 m_vCurMousePos;
public:
void init();
void update();
public:
KEY_STATE GetKeyState(KEY _eKey) {
return m_vecKey[(int)_eKey].eState;
};
Vec2 GetMousePos() { return m_vCurMousePos; }
};
<CKeyMgr.cpp>
#include "pch.h"
#include "CkeyMgr.h"
#include "CCore.h"
int g_arrVK[(int)KEY::LAST] = {
...
};
CkeyMgr::CkeyMgr() {
}
CkeyMgr::~CkeyMgr() {
}
void CkeyMgr::init()
{
for (int keyIDX = 0; keyIDX < (int)KEY::LAST; keyIDX++) {
m_vecKey.push_back(tKeyInfo{ KEY_STATE::NONE, false });
}
}
void CkeyMgr::update() {
//윈도우 포커싱 알아내기
HWND hMainWnd = CCore::GetInstance()->GetMainHwnd();
HWND hWnd = GetFocus();//현재 포커싱 되어 있는 윈도우의 핸들값을 알려줌.
//지금 포커싱 중인게, 이 프로그램의 메인 윈도우라면 같은지 아닌지만 확인해주면 됨.
//윈도우 포커싱 중일 때, 키 이벤트 동작.
if (hMainWnd == hWnd) {
for (int keyIDX = 0; keyIDX < (int)KEY::LAST; keyIDX++) {
//키가 눌렸다면.
if (GetAsyncKeyState(g_arrVK[keyIDX]) & 0x8000) {
if (m_vecKey[keyIDX].bPrevPush) {
//이전에 눌려있었다.
m_vecKey[keyIDX].eState = KEY_STATE::HOLD;
}
else {
//이전에 눌려있지 않았다.
m_vecKey[keyIDX].eState = KEY_STATE::TAP;
}
m_vecKey[keyIDX].bPrevPush = true;
}
else {//현재 키가 눌려있지 않다면.
if (m_vecKey[keyIDX].bPrevPush) {
//이전에 키가 눌려있다면.
m_vecKey[keyIDX].eState = KEY_STATE::AWAY;
}
else {
//이전에 키가 눌려있지 않았다면.
m_vecKey[keyIDX].eState = KEY_STATE::NONE;
}
m_vecKey[keyIDX].bPrevPush = false;
}
}
//Mouse 위치 계산
POINT ptPos = {};
GetCursorPos(&ptPos);
ScreenToClient(CCore::GetInstance()->GetMainHwnd(), &ptPos);
m_vCurMousePos = Vec2((float)ptPos.x, (float)ptPos.y);
}
else {
//윈도우 포커싱 해제 상태
for (int keyIDX = 0; keyIDX < (int)KEY::LAST; keyIDX++) {
m_vecKey[keyIDX].bPrevPush = false;
if (m_vecKey[keyIDX].eState == KEY_STATE::TAP || m_vecKey[keyIDX].eState == KEY_STATE::HOLD) {
m_vecKey[keyIDX].eState = KEY_STATE::AWAY;
}
else if (m_vecKey[keyIDX].eState == KEY_STATE::AWAY) {
m_vecKey[keyIDX].eState = KEY_STATE::NONE;
}
}
}
}
CKeyMgr은 키 입력 관리 클래스이다.
이 클래스는 키의 상태를 관리하고, 프레임 단위로 키 입력을 처리하여 게임의 논리적 일관성을 유지한다.
예를들어 키 입력을 위와 같이 관리하지 않게 되면 한 프레임 내에서 어떤 객체에서는 특정키가 눌렸다고 판단하지만 다른 객체에서는 안눌렸다고 판단하는 경우가 생겨 일관성이 없게된다. 따라서 키 입력도 SingleTon 패턴으로 구현하였다.
update() 함수에서는 현재 창이 포커싱 중일때와 아닐때를 나누어 생각했다.
- 포커싱 해제 상태 : 모든 키의 상태를 NONE으로 초기화
- 포커싱 중인 상태 : 각 키의 상태를 업데이트. 키가 눌렸는지, 이전 프레임에 따라 현재가 AWAY인지 HOLD인지 여러 상황을 판단한다.
<예제>
if (KEY_TAP(KEY::K))
{
...
}
if (KEY_HOLD(KEY::L))
{
...
}
if (KEY_AWAY(KEY::J))
{
...
}
///////////////////////////////////////////////////////
#define KEY_HOLD(key) KEY_CHECK(key, KEY_STATE::HOLD)
#define KEY_TAP(key) KEY_CHECK(key, KEY_STATE::TAP)
#define KEY_NONE(key) KEY_CHECK(key, KEY_STATE::NONE)
#define KEY_AWAY(key) KEY_CHECK(key, KEY_STATE::AWAY)
///////////////////////////////////////////////////////
다음과 같은 매크로로 키의 상태를 확인할 수 있게 만들었다.
<CSoundMgr.cpp>
#include "pch.h"
#include "CSoundMgr.h"
#include "CResMgr.h"
#include "CSound.h"
#include "CPathMgr.h"
#include "CTimeMgr.h"
#include "CCore.h"
CSoundMgr::CSoundMgr()
: m_pSystem(nullptr)
, m_pChannel(nullptr)
, m_pSound(nullptr)
, m_fMasterRatio(1.f)
, m_fBGMRatio(1.f)
, m_fSFXRatio(1.f)
, m_iStepIdx(0)
{
InitVolumePointer();
}
CSoundMgr::~CSoundMgr()
{
Release();
}
HRESULT CSoundMgr::init()
{
FMOD_RESULT ret;
ret = FMOD::System_Create(&m_pSystem);
if (ret != FMOD_OK) {
return E_FAIL;
}
ret = m_pSystem->init(SOUNDBUFFERSIZE, FMOD_INIT_NORMAL, 0);
if (ret != FMOD_OK) {
return E_FAIL;
}
ret = m_pSystem->createChannelGroup("BGM", &m_pBGMChannelGroup);
if (ret != FMOD_OK) {
return E_FAIL;
}
ret = m_pSystem->createChannelGroup("SFX", &m_pSFXChannelGroup);
if (ret != FMOD_OK) {
return E_FAIL;
}
return S_OK;
}
void CSoundMgr::Release()
{
//Sound는 내부에 해제하는 것이 있어서 SAFE_DELETE_MAP 함수 안 쓴다.
for (auto soundData : m_mapSounds) {
soundData.second->m_pSound->release();
delete soundData.second;
}
m_mapSounds.clear();
if (m_pBGMChannelGroup) {
m_pBGMChannelGroup->release();
m_pBGMChannelGroup = nullptr;
}
if (m_pSFXChannelGroup) {
m_pSFXChannelGroup->release();
m_pSFXChannelGroup = nullptr;
}
if (nullptr != m_pSystem) {
m_pSystem->release();
m_pSystem->close();
}
}
void CSoundMgr::update()
{
m_pSystem->update();
}
void CSoundMgr::AddSound(wstring _keyName, wstring _fileName, bool _bgm, bool _loop)
{
FMOD_RESULT ret;
auto soundIter = m_mapSounds.find(_keyName);
if (soundIter != m_mapSounds.end()) {
return;
}
SoundInfo* info = new SoundInfo;
info->isBGM = _bgm;
wstring strFilePath = CPathMgr::GetInstance()->GetContentPath();
strFilePath += _fileName;
std::wstring_convert<std::codecvt_utf8<wchar_t>> converter;
std::string strFilePathNarrow = converter.to_bytes(strFilePath);
if (_bgm) {
//BGM일때.
ret = m_pSystem->createStream(strFilePathNarrow.c_str(), FMOD_LOOP_NORMAL, nullptr, &info->m_pSound);
}
else {
//BGM이 아닌 SFX일때.
if (_loop) {
ret = m_pSystem->createSound(strFilePathNarrow.c_str(), FMOD_LOOP_NORMAL, nullptr, &info->m_pSound);
}
else {
ret = m_pSystem->createSound(strFilePathNarrow.c_str(), FMOD_DEFAULT, nullptr, &info->m_pSound);
}
}
if (ret != FMOD_OK) assert(false);
m_mapSounds.insert(make_pair(_keyName, info));
}
void CSoundMgr::Play(wstring _keyName, float _volume)
{
auto soundIter = m_mapSounds.find(_keyName);
if (soundIter == m_mapSounds.end()) {
assert(false);
}
if (soundIter->second->isBGM) {
m_pSystem->playSound(soundIter->second->m_pSound, m_pBGMChannelGroup, false, &soundIter->second->m_pChannel);
soundIter->second->m_pChannel->setVolume(_volume);
}
else {
m_pSystem->playSound(soundIter->second->m_pSound, m_pSFXChannelGroup, false, &soundIter->second->m_pChannel);
soundIter->second->m_pChannel->setVolume(_volume);
}
}
void CSoundMgr::Stop(wstring _keyName)
{
auto soundIter = m_mapSounds.find(_keyName);
if (soundIter == m_mapSounds.end()) {
assert(false);
}
soundIter->second->m_pChannel->stop();
}
void CSoundMgr::Pause(wstring _keyName)
{
auto soundIter = m_mapSounds.find(_keyName);
if (soundIter == m_mapSounds.end()) {
assert(false);
}
soundIter->second->m_pChannel->setPaused(true);
}
void CSoundMgr::Resume(wstring _keyName)
{
auto soundIter = m_mapSounds.find(_keyName);
if (soundIter == m_mapSounds.end()) {
assert(false);
}
soundIter->second->m_pChannel->setPaused(false);
}
bool CSoundMgr::IsPlaySound(wstring& _keyName)
{
auto soundIter = m_mapSounds.find(_keyName);
if (soundIter == m_mapSounds.end()) {
assert(false);
}
bool isPlay = false;
soundIter->second->m_pChannel->isPlaying(&isPlay);
return isPlay;
}
bool CSoundMgr::IsPauseSound(wstring _keyName)
{
return false;
}
void CSoundMgr::InitVolumePointer()
{
m_pMasterSoundSlider = nullptr;
m_pBGMSoundSlider = nullptr;
m_pSFXSoundSlider = nullptr;
m_pMasterSoundRatio = nullptr;
m_pBGMSoundRatio = nullptr;
m_pSFXSoundRatio = nullptr;
}
bool CSoundMgr::IsOptionPanel()
{
if (nullptr != m_pMasterSoundSlider &&
nullptr != m_pMasterSoundRatio &&
nullptr != m_pBGMSoundSlider &&
nullptr != m_pBGMSoundRatio &&
nullptr != m_pSFXSoundSlider &&
nullptr != m_pSFXSoundRatio) {
return true;
}
return false;
}
void CSoundMgr::SetBGMChannelVolume(float _fVolume)
{
if (m_pBGMChannelGroup) {
FMOD_RESULT result = m_pBGMChannelGroup->setVolume(_fVolume);
//wprintf(L"BGM Volume: %f\n", _fVolume);
if (result != FMOD_OK)
{
// 에러 처리: 필요시 로그 출력 또는 assert 처리
assert(false);
}
}
}
void CSoundMgr::SetSFXChannelVolume(float _fVolume)
{
if (m_pSFXChannelGroup) {
FMOD_RESULT result = m_pSFXChannelGroup->setVolume(_fVolume);
//wprintf(L"SFX Volume: %f\n", _fVolume);
if (result != FMOD_OK)
{
// 에러 처리: 필요시 로그 출력 또는 assert 처리
assert(false);
}
}
}
void CSoundMgr::PlayWalkSound()
{
//Play(walkSoundKey[0], 1.f);
Play(walkSoundKey[m_iStepIdx], 0.3f);
m_iStepIdx = (m_iStepIdx + 1) % 4;
}
사운드(배경음악(BGM), SFX(효과음))를 처리해주는 클래스이다.
이 클래스는 FMOD라이브러리를 이용하여 사운드를 관리한다.
AddSound()함수를 이용해 unordered_map <wstring, SoundInfo*> 자료구조로 sound를 불러오고 저장을 한다.
저장된 사운드들은 _keyName을 이용해 Play(), Stop(), Pause(), Resume(), IsPlaySound() 의 함수들을 제공한다.
SetBgrmChannelVolume(), SetSFXChannelVolume() 함수들은 게임상에서 옵션으로 음향을 설정한 비율에 따라 볼륨을 조절해주는 기능을 제공한다.
<CCamera.cpp>
#include "pch.h"
#include "CCamera.h"
#include "CObject.h"
#include "CCore.h"
#include "CCore.h"
#include "CkeyMgr.h"
#include "CTimeMgr.h"
#include "CResMgr.h"
#include "CTexture.h"
CCamera::CCamera()
: m_pTargetObj(nullptr)
, m_fTime(0.5f)
, m_fSpeed(0.f)
, m_fAccTime(0.5f)
//, m_pVeilTex(nullptr)
{
}
CCamera::~CCamera() {
}
void CCamera::init()
{
Vec2 vResolution = CCore::GetInstance()->GetResolution();
//m_pVeilTex = CResMgr::GetInstance()->CreateTexture(L"cameraVeil", (UINT)vResolution.x, (UINT)vResolution.y);
m_vMinBounds = Vec2(vResolution.x / 2.f, vResolution.y / 2.f);
m_vMaxBounds = Vec2(18.f * TILE_SIZE - vResolution.x / 2.f, 18.f * TILE_SIZE - vResolution.y / 2.f);
m_pTargetObj = nullptr;
}
void CCamera::update()
{
if (m_pTargetObj != nullptr) {
if (m_pTargetObj->IsDead()) m_pTargetObj = nullptr;
m_vLookAt = m_pTargetObj->GetPos();
m_vLookAt.x = floor(m_vLookAt.x);
m_vLookAt.y = floor(m_vLookAt.y);
}
if (KEY_HOLD(KEY::UP)) {
m_vLookAt.y -= 500.f * fDT;
}
if (KEY_HOLD(KEY::DOWN)) {
m_vLookAt.y += 500.f * fDT;
}
if (KEY_HOLD(KEY::RIGHT)) {
m_vLookAt.x += 500.f * fDT;
}
if (KEY_HOLD(KEY::LEFT)) {
m_vLookAt.x -= 500.f * fDT;
}
m_vLookAt.x = max(m_vMinBounds.x, min(m_vLookAt.x, m_vMaxBounds.x));
m_vLookAt.y = max(m_vMinBounds.y, min(m_vLookAt.y, m_vMaxBounds.y)); //최대범위,최소범위 설정
//화면이동 m_fTime동안 부드럽게 이동하게
CalDiff();
}
void CCamera::CalDiff()
{
//이전 LookAt과 현재 Look의 차이값을 보정해서 현재의 LookAt을 구한다.
//방향 벡터.
m_fAccTime += fDT;
if (m_fAccTime >= m_fTime) {
m_vCurLookAt = m_vLookAt;
}
else {
Vec2 vLookDir = m_vLookAt - m_prevLookAt;
if (!vLookDir.IsZero()) {
m_vCurLookAt = m_prevLookAt + vLookDir.Normalize() * m_fSpeed * fDT;
}
}
Vec2 vResolution = CCore::GetInstance()->GetResolution();
Vec2 vCenter = vResolution / 2;
//
m_vDiff = m_vCurLookAt - vCenter;
m_prevLookAt = m_vCurLookAt;
}
//1. 최적화
//카메라가 찍는 범위들의 타일들만 렌더링 되도록.
카메라 클래스는 화면의 시점을 제어하고, 특정 객체 ( player )를 추적하거나 사용자 입력에 따라 이동하게 만들었다.(카메라 강제 이동은 구현은 되어있으나 사용은 하지 않는다.)
m_pTargetObj는 게임상에서는 player를 항상 쫓아 다니게 설정해놓았다.
이때 m_vLookAt의 값들을 floor()로 소숫점을 버리도록 설정하였는데, 이는 카메라 좌표가 소숫점을 가지게 되면 전투맵에서 타일간의 미세한 차이가 흰색 줄의 형태로 나타나는 현상이 발생하게 된다. 따라서 실제 player는 소숫점의 좌표를 가지지만 카메라는 소수점을 버리는 형태로 해당 현상을 제거하였다.
또한 CalDiff()함수를 이용해 원하는 지점까지 m_fTime동안 카메라 시점이 이동을 하되, 지정한 시간이 되면 강제로 그 지점을 보도록 설정했다.
카메라의 부드러운 이동 : m_fAccTime이 m_fTime에 도달할 때까지, 카메라는 목표 위치로 부드럽게 이동. 이는 m_fSpeed에 따라 조절된다.
- 목표 위치 도달 : 현재 카메라 시점과 화면 중앙과의 차이를 계산하여 렌더링에 사용
- 이전 시점 저장 : 현재 시점을 이전 시점으로 저장하여 다음 프레임에서 사용
<CFileMgr.cpp>
#include "pch.h"
#include "CFileMgr.h"
#include "CPathMgr.h"
#include "CSoundMgr.h"
#include "Direct2DMgr.h"
CFileMgr::CFileMgr()
{
}
CFileMgr::~CFileMgr()
{
}
void CFileMgr::init(const wstring& folderPath)
{
// 주어진 폴더 경로에서 탐색 시작
SearchFolder(folderPath);
}
void CFileMgr::SearchFolder(const wstring& folderPath)
{
WIN32_FIND_DATA findData;
HANDLE hFind = INVALID_HANDLE_VALUE;
// 폴더 내 모든 파일 및 디렉터리를 검색하기 위한 경로
wstring searchPath = folderPath + L"\\*";
// 첫 번째 파일/디렉터리 검색
hFind = FindFirstFile(searchPath.c_str(), &findData);
if (hFind == INVALID_HANDLE_VALUE)
{
wprintf(L"Failed to open directory: %s\n", folderPath.c_str());
return;
}
do
{
if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
{
if (wcscmp(findData.cFileName, L".") != 0 && wcscmp(findData.cFileName, L"..") != 0)
{
// 하위 디렉터리를 재귀적으로 탐색
wstring subFolderPath = folderPath + L"\\" + findData.cFileName;
SearchFolder(subFolderPath);
}
}
else
{
// 파일 경로 생성
std::wstring fileName = findData.cFileName;
std::wstring filePath = folderPath + L"\\" + fileName;
// 파일 처리
ProcessFile(filePath);
}
} while (FindNextFile(hFind, &findData) != 0);
FindClose(hFind);
}
void CFileMgr::ProcessFile(const std::wstring& filePath)
{
// 파일 확장자 확인
size_t lastSlashPos = filePath.find_last_of(L"\\/");
wstring fileName = (lastSlashPos == wstring::npos) ? filePath : filePath.substr(lastSlashPos + 1);
wstring extension = fileName.substr(fileName.find_last_of(L'.') + 1);
// PNG 파일 처리
const wchar_t* contentPath = CPathMgr::GetInstance()->GetContentPath();
wstring relativePath = CPathMgr::GetInstance()->GetRelativePath(filePath.c_str());
// 확장자에 따라 처리
if (extension == L"png")
{
wstring tag = GetFileNameWithoutExtension(fileName);
Direct2DMgr::GetInstance()->LoadAndStoreBitmap(relativePath.c_str(), tag.c_str(), false);
wprintf(L"Loaded PNG: %s\n", tag.c_str());
}
else if (extension == L"mp3" || extension == L"wav")
{
// MP3 또는 WAV 파일 처리
wstring key = GetFileNameWithoutExtension(fileName);
if (L"main_title_bgm" == key) {
CSoundMgr::GetInstance()->AddSound(key, relativePath.c_str(), true, false);
wprintf(L"BGM: %s\n", key.c_str());
}
else {
CSoundMgr::GetInstance()->AddSound(key, relativePath.c_str(), false, false);
}
//CSoundMgr::GetInstance()->AddSound(key, relativePath.c_str(), false, false);
wprintf(L"Loaded sound: %s\n", key.c_str());
}
}
wstring CFileMgr::GetFileNameWithoutExtension(const wstring& fileName)
{
size_t dotPos = fileName.find_last_of(L'.');
if (dotPos != wstring::npos)
{
return fileName.substr(0, dotPos); // 확장자를 제외한 파일 이름 반환
}
return fileName; // 확장자가 없는 경우 그대로 반환
}
기존에는 각 CScene에서 Direct2DMgr을 이용해 png파일을 불러오는 방식을 사용했었는데, 방식을 바꾸어 게임 처음에 켜지면 Direct2DMgr와 CSoundMgr을 이용해 png파일, mp3파일, wav파일을 모두 로드한뒤 tag를 이용해서 각각 파일에 접근할 수 있도록 만들었다.
init() 함수에서 넘겨 받은 폴더 경로는 CPathMgr를 이용해 받아오고, 하위에 있는 모든 폴더들에 대해서 재귀적으로 탐색하여 png파일, mp3파일, wav파일들만 로딩하는 방식을 사용하였다.
해당 파일들은 Direct2DMgr에서는 다음과 같은 자료구조로 png파일을 저장한다.
이때 저장할때의 태그는 파일이름의 확장자를 제거한 이름이 태그가 된다. 이 작업은 GetFileNameWithoutExtension()함수에서 수행한다.
<CItemMgr.h>
#pragma once
#include "CWeapon.h"
class CImage;
struct Item
{
// 모든 아이템이 공통적으로 가져야 할 정보
wstring tag; // 이미지 태그 정보 (아이템창에 표시할 아이콘 이미지)
ITEM_TYPE m_eItemType; // WEAPON, PASSIVE
int m_iBasePrice; // 기본 가격
wstring m_sName; // 한국어 이름
// 아이템이 가질 수 있는 능력치들 (weapon 또는 passive)
union {
tWeaponInfo m_tWeaponInfo; // 무기 정보 (WEAPON 타입일 때 사용)
struct {
float m_fDefaultSpeed; // 기본 스피드
int m_iAddMaxHP; // + 최대 체력
float m_fDamageCoef; // 최종 데미지 %
float m_fMeleeCoef; // 근거리 최종 데미지 %
float m_fRangeCoef; // 원거리 최종 데미지 %
float m_fAttackSpeedCoef; // 공격 속도 %
int m_iCriticalAcc; // 크리티컬 확률 %
float m_fSpeed; // 속도 계수
} PassiveStats; // 패시브 아이템 관련 정보 (PASSIVE 타입일 때 사용)
};
// 복사 생성자
Item(const Item& other) : tag(other.tag), m_eItemType(other.m_eItemType), m_iBasePrice(other.m_iBasePrice), m_sName(other.m_sName)
{
if (m_eItemType == ITEM_TYPE::WEAPON) {
m_tWeaponInfo = other.m_tWeaponInfo; // 무기 정보 복사
}
else {
PassiveStats = other.PassiveStats; // 패시브 정보 복사
}
}
// 대입 연산자
Item& operator=(const Item& other)
{
if (this != &other) {
tag = other.tag;
m_eItemType = other.m_eItemType;
m_iBasePrice = other.m_iBasePrice;
m_sName = other.m_sName;
if (m_eItemType == ITEM_TYPE::WEAPON) {
m_tWeaponInfo = other.m_tWeaponInfo; // 무기 정보 복사
}
else {
PassiveStats = other.PassiveStats; // 패시브 정보 복사
}
}
return *this;
}
// 생성자 (초기화 편의를 위해 추가)
Item(ITEM_TYPE type) : m_eItemType(type) {
if (type == ITEM_TYPE::WEAPON) {
memset(&m_tWeaponInfo, 0, sizeof(m_tWeaponInfo)); // 무기 정보 초기화
}
else {
memset(&PassiveStats, 0, sizeof(PassiveStats)); // 패시브 정보 초기화
}
}
// 기본 생성자
Item() : tag(L""), m_eItemType(ITEM_TYPE::PASSIVE), m_iBasePrice(0), m_sName(L"") {}
// 소멸자
~Item() {}
};
class ItemMgr
{
SINGLE(ItemMgr);
private:
unordered_map<wstring, Item*> m_sItems; //모든 아이템 정보 저장
vector<Item*> m_vWeaponsItems; //무기류 저장
vector<Item*> m_vPassiveItems; //패시브류 저장
Item* m_basicCharacter;
bool m_bBaseCharacterIsAdded; //캐릭터가 세팅되면 한번만 추가하게
public:
vector<Item*>& GetWeaponItems() { return m_vWeaponsItems; }
const vector<Item*>& GetPassiveItems() { return m_vPassiveItems; }
size_t GetPassiveItemssize() { return m_vPassiveItems.size(); }
Item* GetBasicCharacter() { return m_basicCharacter; }
Item* GetItem(const wstring& _tag) { return m_sItems[_tag]; }
public:
void update();
public:
void AddWeapons(Item* _weapon) { m_vWeaponsItems.push_back(_weapon); }
void AddPassive(Item* _passive) { m_vPassiveItems.push_back(_passive); }
void SetBasicCharacter(Item* _basicCharacter) { m_basicCharacter = _basicCharacter; }
public:
void Clear();
public:
void init();
void init_character();
void init_passive();
void init_weapon();
friend class CScene_Select_Character;
};
<CItemMgr.cpp>
#include "pch.h"
#include "ItemMgr.h"
ItemMgr::ItemMgr()
: m_bBaseCharacterIsAdded(false)
{
}
ItemMgr::~ItemMgr()
{
}
void ItemMgr::update()
{
if (m_basicCharacter != nullptr && m_bBaseCharacterIsAdded == false)
{
m_vPassiveItems.push_back(m_basicCharacter);
m_bBaseCharacterIsAdded = true;
}
}
void ItemMgr::Clear()
{
Safe_Delete_Vec(m_vWeaponsItems);
Safe_Delete_Vec(m_vPassiveItems);
m_basicCharacter = nullptr;
m_bBaseCharacterIsAdded = false;
}
void ItemMgr::init()
{
init_character();
init_passive();
init_weapon();
}
void ItemMgr::init_character()
{
//////////////////////다재다능///////////////////////////////////
Item* well_rounded = new Item(ITEM_TYPE::CHARACTER);
well_rounded->m_sName = L"다재다능";
well_rounded->tag = L"well_rounded";
well_rounded->m_eItemType = ITEM_TYPE::CHARACTER;
well_rounded->PassiveStats.m_iAddMaxHP = 3;
well_rounded->PassiveStats.m_fDamageCoef = 1.f;
well_rounded->PassiveStats.m_fMeleeCoef = 1.f;
well_rounded->PassiveStats.m_fRangeCoef = 1.f;
m_sItems.insert(make_pair(L"well_rounded", well_rounded));
//////////////////////다재다능///////////////////////////////////
//...
}
void ItemMgr::init_passive()
{
Item* tree = new Item(ITEM_TYPE::PASSIVE);
tree->m_sName = L"나무";
tree->tag = L"tree";
tree->m_eItemType = ITEM_TYPE::PASSIVE;
tree->m_iBasePrice = 5;
tree->PassiveStats.m_iAddMaxHP = 3;
m_sItems.insert(make_pair(L"tree", tree));
//...
}
void ItemMgr::init_weapon()
{
Item* knife = new Item(ITEM_TYPE::WEAPON);
knife->m_sName = L"칼";
knife->tag = L"knife";
knife->m_eItemType = ITEM_TYPE::WEAPON;
knife->m_iBasePrice = 10;
knife->m_tWeaponInfo.m_sIconImageKey = L"knife_icon";
knife->m_tWeaponInfo.m_iDMG = 10;
knife->m_tWeaponInfo.m_fMeleeCoef = 20.f;
m_sItems.insert(make_pair(L"knife", knife));
//...
}
이 클래스는 게임에서 아이템을 관리하는데 사용된다.
여러 아이템 타입(무기, 패시브)를 포함한다. ( 원작 게임에서는 패시브에서도 다양한 종류로 나뉘지만, 이번에는 단순한 패시브 아이템(획득시 스탯증가)만 생각하기로 했다.)
Item 구조체는 무기와 패시브 아이템 모두 갖추어야 할 tag, 아이템 타입, 가격, 이름을 포함하고 무기와 패시브 아이템을 구분 지어서 union으로 tWeaponInfo 또는 PassiveStats로 작성하였다. 무기의 경우는 tWeaponInfo, 패시브아이템의 경우는 PassiveStats을 사용한다.
멤버 변수에서 m_sItems는 이 게임내에 존재하는 모든 아이템들에 대한 정보를 가지고 있다.
m_vWeaponsItems, m_vPassiveItems는 각각 player가 소유하고 있는 무기, 패시브 아이템 정보를 담는 벡터이다. 하지만 무기 벡터는 player객체에서 관리하기로 해서 실제로는 사용하지않는 벡터다.
m_basicCharacter는 캐릭터 선택화면에서 선택한 캐릭터의 정보를 담고 있다. 선택한 캐릭터도 패시브 아이템으로 취급하여 관리한다.
여기까지
CPathMgr, CTimeMgr, CKeyMgr, CSoundMgr, CCamera, CFileMgr, CItemMgr을 설명하였고 나머지 Direct2DMgr, CCollisionMgr, CSceneMgr, CFontMgr ,CEventMgr, CUIMgr은 다음 포스트에서 설명하도록 하겠다.

'WinApi > Brotato 모작' 카테고리의 다른 글
[Win32 API Brotato 모작] 6. Scene_Main (0) | 2025.03.13 |
---|---|
[Win32 API Brotato 모작] 5. Object (0) | 2025.03.12 |
[Win32 API Brotato 모작] 4. Core & Scene (0) | 2025.03.11 |
[Win32 API Brotato 모작] 3. Main & Core & Manager (3) (0) | 2025.03.11 |
[Win32 API Brotato 모작] 2. Main & Core & Manager (2) (0) | 2025.03.11 |