WinApi/TBI(더 바인딩 오브 아이작) 모작
[Win32 API TBI 모작] 10. Scene & 메인메뉴
Vfly
2025. 5. 31. 04:58
이번에는 게임화면과 관련된 클래스 CScene과 메인화면을 담당하는 CScene_Main 클래스에 대해 알아보자
전체 소스코드 : https://github.com/vfly1189/TBI
화면 구성
- 게임을 실행하면 바로 나오는 화면으로 Enter키와 Space바 키를 누르면 아래 화면으로 넘어간다.
- 메뉴를 선택하는 화면으로 현재는 새도전 메뉴만 구현해놓은 상태
- 방향키 상, 하를 이용해 메뉴의 화살표를 움직이고 Enter키와 Space바 키로 선택 할 수 있다.
- 새도전 메뉴에서 넘어오면 위 사진과 같이 캐릭터를 선택할 수 있다.
- 방향키 좌,우를 이용해 총 4개의 캐릭터 ( 아이작, 막달레나, 카인, 유다 ) 를 선택할 수 있다.
- 이때 선택된 캐릭터에 따라 CScene_Fight에서 생성되는 캐릭터가 달라진다.
화면 배치
위에 나온 화면은 모두 CScene_Main 클래스에서 배치된 객체 들이고 각 객체들은 다음과 같은 구조를 가진다.
해상도가 960x540일때 메인화면의 좌표가 (0,0)이면 메뉴선택은 (0, 540), 캐릭터 선택은 (0,1080) 에 위치해 있고 Space바 키나 Enter키를 누르면 카메라가 비추고 있는 좌표를 변경하는 방식으로 작동한다.
🏗️ 아키텍처 분석
1. 클래스 구조
// 기반 클래스
class CScene
{
static bool isPause;
vector<CObject*> m_arrObj[(UINT)GROUP_TYPE::END]; // 오브젝트 그룹 관리
SCENE_TYPE m_eType;
CObject* m_pPlayer; //플레이어는 더 이상 Scene에서 관리하지 않고 CPlayerMgr을 통해 관리
// ... 타일 정보, 씬 이름 등
};
// 메인 메뉴 구현 클래스
class CScene_Main : public CScene
{
// UI 관리
CSpriteUI* Cursor; // 메뉴 커서
CSpriteUI* m_CurShowingCharacter; // 캐릭터 선택 UI
CSpriteUI* veil; // 페이드 효과용
// 상태 관리
int m_iCursorPos; // 커서 위치
int m_iCurPage; // 현재 페이지 (0:타이틀, 1:메뉴, 2:캐릭터선택)
int m_iCurCharacterIndex; // 선택된 캐릭터 인덱스
bool m_bPageControl; // 페이지 전환 제어
bool m_bChangeSceneFlag; // 씬 전환 플래그
// 효과 관리
float m_fAccTime; // 누적 시간
float m_fFadeAlpha; // 페이드 알파값
float m_fFadeDuration; // 페이드 지속시간
};
2. 핵심 설계 패턴
컴포넌트 패턴
- CObject: 게임 오브젝트 기반 클래스
- CSpriteUI: UI 스프라이트 컴포넌트
- CAnimator: 애니메이션 시스템
- CRigidBody: 물리 시스템
상태 패턴
// 페이지 상태에 따른 입력 처리
void CScene_Main::HandleInputs()
{
if (m_iCurPage == 1) // 메뉴 페이지
{
if (KEY_TAP(KEY::UP)) MoveCursor(-1);
else if (KEY_TAP(KEY::DOWN)) MoveCursor(1);
}
else if (m_iCurPage == 2) // 캐릭터 선택 페이지
{
if (KEY_TAP(KEY::LEFT)) ChangeCharacter(-1);
else if (KEY_TAP(KEY::RIGHT)) ChangeCharacter(1);
}
}
🎨 UI 시스템 구현
1. 계층적 UI 구조
// 부모-자식 관계로 UI 구성
CSpriteUI* MainPanel = new CSpriteUI;
CSpriteUI* MainCharacter = MainPanel->AddChild<CSpriteUI>(Vec2(-10.f, 60.f));
CSpriteUI* MainLogoShadow = MainPanel->AddChild<CSpriteUI>(Vec2(0.f, -115.f));
CSpriteUI* MainLogoText = MainLogoShadow->AddChild<CSpriteUI>(Vec2(0.f, 0.f));
- Scene에서 특정 객체를 동적할당하고 해당씬에 등록시키기 위해서는 AddObject() 함수로 해당씬 m_arrObj 벡터에 추가해주어야 한다.
- 위와 같이 MainPanel의 child UI로써 MainCharacter, MainLogoShadow, MainLogoText는 m_arrrObj에 추가하지 않아도 MainPanel만 추가해주면 MainPanel 객체에서 update()와 render() 실행될때 child UI들도 update()와 render()가 실행된다.
2. 동적 비트맵 분할
// 하나의 큰 텍스처에서 필요한 부분만 추출
pD2DMgr->SplitBitmap(pD2DMgr->GetStoredBitmap(L"titlemenu_2"), L"title_background",
D2D1::Point2F(0.f, 0.f), D2D1::Point2F(480.f, 270.f));
pD2DMgr->SplitBitmap(pD2DMgr->GetStoredBitmap(L"titlemenu_2"), L"title_character",
D2D1::Point2F(0.f, 405.f), D2D1::Point2F(480.f, 540.f));
3. 애니메이션 시스템
// 프레임 기반 애니메이션 생성
MainCharacter->GetAnimator()->CreateAnimation(L"title_character",
pD2DMgr->GetStoredBitmap(L"title_character"),
Vec2(0.f, 0.f), Vec2(160.f, 135.f), Vec2(160.f, 0.f),
0.4f, 2, true, Vec2(0.f, 0.f));
MainCharacter->GetAnimator()->Play(L"title_character", true, 1);
🎮 상태 관리 시스템
1. 페이지 전환 시스템
enum PageState {
TITLE_PAGE = 0, // 타이틀 화면
MENU_PAGE = 1, // 게임 메뉴
CHARACTER_SELECT = 2 // 캐릭터 선택
};
// 각 페이지별 카메라 위치 설정
Vec2 MainCameraPos[3] = {
Vec2(0.f, 0.f), // 타이틀
Vec2(0.f, 540.f), // 메뉴
Vec2(0.f, 1080.f) // 캐릭터 선택
};
2. 입력 제어 시스템
// 키 입력 중복 방지를 위한 제어 플래그
bool m_bPageControl;
void HandlePageChange()
{
bool keyAction = KEY_TAP(KEY::ENTER) || KEY_TAP(KEY::SPACE);
bool keyRelease = KEY_AWAY(KEY::ENTER) || KEY_AWAY(KEY::SPACE);
if (keyRelease) {
m_bPageControl = false; // 키 해제 시 제어 해제
return;
}
if (!m_bPageControl && keyAction) {
// 페이지 전환 로직
m_bPageControl = true; // 제어 잠금
}
}
🎭 효과 시스템
1. 페이드 효과
void HandleSceneTransition()
{
if (!m_bChangeSceneFlag) return;
m_fAccTime += fDT; // 델타 타임 누적
// 페이드 인/아웃 계산
float fDelta = (m_fAccTime < 1.f) ? 1.f : -1.f;
m_fFadeAlpha += (1.f / m_fFadeDuration) * fDelta * fDT;
m_fFadeAlpha = max(0.f, min(m_fFadeAlpha, 1.f));
UpdateFadeEffect();
if (m_fAccTime > 1.f) {
ChangeScene(SCENE_TYPE::FIGHT); // 씬 전환
}
}
2. 동적 비트맵 합성
// 런타임에서 여러 이미지를 하나로 합성
void CreateLoadImage(Vec2 _Resolution)
{
// 랜덤 로딩 이미지 선택
int loadImageNum = rand() % 57;
// 오프스크린 렌더 타겟 생성
ComPtr<ID2D1BitmapRenderTarget> pBitmapRT = nullptr;
Direct2DMgr::GetInstance()->GetRenderTarget()->CreateCompatibleRenderTarget(
D2D1::SizeF((FLOAT)compositeWidth, (FLOAT)compositeHeight), &pBitmapRT);
// 두 이미지를 하나로 합성
pBitmapRT->BeginDraw();
pBitmapRT->DrawBitmap(pBitmap1, destRect1, 1.0f, D2D1_BITMAP_INTERPOLATION_MODE_LINEAR);
pBitmapRT->DrawBitmap(pBitmap2, destRect2, 1.0f, D2D1_BITMAP_INTERPOLATION_MODE_LINEAR);
pBitmapRT->EndDraw();
pBitmapRT->GetBitmap(&pCompositeBitmap);
}
🔧 핵심 기능 구현
1. 메뉴 내비게이션
void MoveCursor(int direction)
{
if (m_iCurPage != 1) return;
// 순환 인덱스 계산 (0~2 범위)
m_iCursorPos = (m_iCursorPos + direction + 3) % 3;
Cursor->SetOffset(CursorPos[m_iCursorPos]);
CSoundMgr::GetInstance()->Play(L"menu_scroll");
}
2. 캐릭터 선택 시스템
void ChangeCharacter(int direction)
{
if (m_CurShowingCharacter == nullptr || m_iCurPage != 2) return;
// 캐릭터 인덱스 순환
m_iCurCharacterIndex = (m_iCurCharacterIndex + direction + 4) % 4;
UpdateCharacterImage(); // 이미지 업데이트
UpdateCharacterNameTag(); // 이름 태그 업데이트
// 방향에 따른 사운드 재생
CSoundMgr::GetInstance()->Play(direction > 0 ?
L"character select right" : L"character select left");
}
3. 게임 시작 초기화
void InitializePlayerForGameStart()
{
CPlayer* player = CPlayerMgr::GetInstance()->GetPlayer();
// 선택된 캐릭터 정보로 플레이어 설정
player->SetPlayerStat(vecCharacterInfo[m_iCurCharacterIndex].m_stat);
float maxVelocity = vecCharacterInfo[m_iCurCharacterIndex].m_stat.m_fMoveSpd;
player->GetRigidBody()->SetMaxVelocity(Vec2(maxVelocity, maxVelocity));
player->SetCharacterName(vecCharacterInfo[m_iCurCharacterIndex].m_characterName);
player->SetCharacterIdx(m_iCurCharacterIndex);
CPlayerMgr::GetInstance()->SettingImageAndAnimations(m_iCurCharacterIndex);
}
🎵 오디오 시스템
1. 배경음악 관리
void Enter()
{
CSoundMgr::GetInstance()->StopAllSound(); // 기존 사운드 정지
wstring mainTitleBGMKey = L"genesis_retake_light_loop";
if (!CSoundMgr::GetInstance()->IsPlaySound(mainTitleBGMKey)) {
CSoundMgr::GetInstance()->Play(mainTitleBGMKey, 0.2f); // 볼륨 설정
}
}
2. 효과음 시스템
// 페이지 전환 효과음
void PlayPageChangeSound() {
CSoundMgr::GetInstance()->Play(L"book page turn");
}
// 게임 시작 효과음
CSoundMgr::GetInstance()->Stop(L"genesis_retake_light_loop");
CSoundMgr::GetInstance()->Play(L"game_start");