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");