DirectX11/Eternal Return 모작

[DirectX 11 Eternal Return 모작] 6. Scene

Vfly 2025. 9. 2. 23:56

전체 코드 : https://github.com/HyangRim/DirectX11-Engine-Client

 

GitHub - HyangRim/DirectX11-Engine-Client

Contribute to HyangRim/DirectX11-Engine-Client development by creating an account on GitHub.

github.com


Scene 시스템 개발기

1. Scene 아키텍쳐 개요

1.1 전체 Scene 구조

Scene (기본 클래스)
├── StartScene (게임 시작 화면)
├── CharacterSelectScene (캐릭터 선택)
└── LumiaIsland (실제 플레이 Scene)

SceneManager (Scene 전환 관리)
└── SceneObjectManager (객체 분리 관리)
    ├── 일반 객체 (unordered_set)
    ├── UI 객체 (unordered_set)
    └── QuadTree 기반 피킹 시스템

 

1.2 Scene 전환 시스템

Scene 전환은 SceneManager를 통해 안전하게 처리

class SceneManager {
public:
    template<typename T>
    void ChangeScene(shared_ptr<T> _scene) {
        m_nextScene = _scene;
        m_sceneChangeRequested = true; // 즉시 전환하지 않고 플래그만 설정
    }

    void ProcessSceneChange() {
        if (m_nextScene == nullptr) {
            m_sceneChangeRequested = false;
            return;
        }

        // 현재 Scene 안전하게 정리
        if (m_curScene) {
            m_curScene->SetDestroying(true); // 소멸 플래그 설정
            
            // 모든 GameObject들의 OnDestroy 호출
            auto& objectManager = m_curScene->GetObjectManager();
            for (auto& obj : objectManager->GetObjects()) {
                if (obj) obj->OnDestroy();
            }
            for (auto& obj : objectManager->GetUIObjects()) {
                if (obj) obj->OnDestroy();
            }
        }

        // 새로운 Scene으로 안전하게 전환
        m_curScene = m_nextScene;
        m_nextScene = nullptr;
        m_sceneChangeRequested = false;

        if (m_curScene) {
            m_curScene->Start();
        }
    }
};

 

 

 

2. 멀티스레드 로딩 시스템 구현

백그라운드 로딩의 필요성

게임에서 대용량 모델, 텍스쳐, 사운드 파일을 로딩할 때 메인 스레드가 블록되면 게임이 멈춘 것처럼 보인다. 이를 위해 백그라운드 로딩을 구현했다.

 

멀티 스레드 로딩시스템 설계

class LumiaIsland : public Scene
{
private:
    // 멀티스레드 관련 변수
    CRITICAL_SECTION m_loadingCS;
    HANDLE m_loadingThread;
    atomic<bool> m_loadingComplete{false};
    
    // 메인 스레드 작업 큐
    queue<function<void()>> m_mainThreadTasks;
    CRITICAL_SECTION m_mainThreadTasksCS;
    atomic<bool> m_objectsCreated{false};
};

 

백그라운드 로딩 스레드 구현

DWORD WINAPI LumiaIsland::BackgroundLoadingThread(LPVOID _param)
{
    LumiaIsland* scene = static_cast<LumiaIsland*>(_param);
    
    try {
        // 백그라운드에서 안전하게 로딩할 수 있는 작업들
        EnterCriticalSection(&scene->m_loadingCS);
        
        // 리소스 로딩 작업
        // 예: 모델 파일 읽기, 텍스처 로딩 등
        
        LeaveCriticalSection(&scene->m_loadingCS);
        
        // 메인 스레드에서 실행해야 할 작업들을 큐에 추가
        EnterCriticalSection(&scene->m_mainThreadTasksCS);
        
        scene->m_mainThreadTasks.push([scene]() {
            // GPU 리소스 생성 등 메인 스레드에서만 가능한 작업
            scene->CreateNavMesh();
            scene->m_objectsCreated = true;
        });
        
        LeaveCriticalSection(&scene->m_mainThreadTasksCS);
        
        scene->m_loadingComplete = true;
        
    } catch (...) {
        // 예외 처리
        scene->m_loadingComplete = true;
    }
    
    return 0;
}

 

 

메인 스레드 동기화

void LumiaIsland::ProcessMainThreadTasks()
{
    EnterCriticalSection(&m_mainThreadTasksCS);
    
    while (!m_mainThreadTasks.empty()) {
        auto task = m_mainThreadTasks.front();
        m_mainThreadTasks.pop();
        
        LeaveCriticalSection(&m_mainThreadTasksCS);
        
        // 메인 스레드에서 작업 실행
        task();
        
        EnterCriticalSection(&m_mainThreadTasksCS);
    }
    
    LeaveCriticalSection(&m_mainThreadTasksCS);
}

void LumiaIsland::Update()
{
    // 매 프레임마다 메인 스레드 작업 처리
    ProcessMainThreadTasks();
    
    // 로딩 완료 후 게임 로직 실행
    if (m_objectsCreated) {
        m_uiManager->Update();
        CheckPickedItemBox();
        ControlPlayerStatus();
        HandleSkillLevelUpInput();
    }
    
    Scene::Update();
}

 

 

 

3. UI / 일반 객체 분리 관리 시스템

게임 성능 최적화를 위해 UI 객체와 일반 객체를 완전히 분리하여 관리

class SceneObjectManager {
private:
    unordered_set<shared_ptr<GameObject>> m_gameObjects;    // 일반 객체
    unordered_set<shared_ptr<GameObject>> m_uiObjects;      // UI 객체
    vector<shared_ptr<GameObject>> m_uiParents;             // UI 부모들
    vector<shared_ptr<GameObject>> m_uiChildren;            // UI 자식들

public:
    void AddUIObject(shared_ptr<GameObject> _object, bool isParent = false) {
        m_uiObjects.insert(_object);
        
        if (isParent) {
            m_uiParents.push_back(_object);
        } else {
            m_uiChildren.push_back(_object);
        }
    }

    // 계층적 UI 삭제
    void MarkUIObjectForDestroyWithChildren(shared_ptr<GameObject> obj) {
        if (!obj || CURSCENE->IsDestroying()) return;

        // 자식 객체들 먼저 수집
        vector<shared_ptr<GameObject>> allChildren;
        CollectUIChildren(obj, allChildren);

        // 자식들부터 삭제 마크 (역순으로)
        for (auto it = allChildren.rbegin(); it != allChildren.rend(); ++it) {
            MarkUIObjectForDestroy(*it);
        }

        // 부모 객체 삭제 마크
        MarkUIObjectForDestroy(obj);
    }
};

 

 

4. QuadTree 기반 피킹 시스템

수백 개의 객체 중에서 마우스로 선택할 객체를 효율적으로 찾는 시스템

shared_ptr<GameObject> SceneObjectManager::PickObjectOrUI() {
    if (INPUT->GetButtonDown(KEY_TYPE::LBUTTON) == false)
        return nullptr;

    POINT screenPt = INPUT->GetMousePos();
    shared_ptr<Camera> camera = GetMainCamera()->GetCamera();
    
    // Ray 생성
    Ray ray = CreateRayFromScreen(Vec2(screenPt.x, screenPt.y), camera);
    
    // QuadTree로 후보 객체 수집 (O(log n))
    vector<shared_ptr<GameObject>> candidates = m_quadTree->Query(ray, camera);
    
    // 음수 좌표 필터링 (화면 밖 객체 제외)
    vector<shared_ptr<GameObject>> validCandidates;
    for (auto& obj : candidates) {
        RECT objBounds = m_quadTree->GetObjectScreenBounds(obj, camera);
        int screenCenterX = (objBounds.left + objBounds.right) / 2;
        int screenCenterY = (objBounds.top + objBounds.bottom) / 2;

        if (screenCenterX < 0 || screenCenterY < 0) continue;
        validCandidates.push_back(obj);
    }

    // 유효한 후보들만 대상으로 Ray 교차 검사
    float minDistance = FLT_MAX;
    shared_ptr<GameObject> picked;

    for (auto& gameObject : validCandidates) {
        if (camera->IsCulled(gameObject->GetLayerIndex())) continue;
        if (gameObject->GetCollider() == nullptr) continue;

        float distance = 0.f;
        if (gameObject->GetCollider()->Intersects(ray, OUT distance) == false) continue;

        if (distance < minDistance) {
            minDistance = distance;
            picked = gameObject;
        }
    }

    return picked;
}

 

 

5. StartScene - 게임 시작 화면

동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.

 

 

6. Character Select Scene - 캐릭터 선택 화면

동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.

 

 

7. LumiaIsland - 플레이 Scene

동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.

 

 

8. Scene 전환 플로우

graph TD
    A[StartScene] -->|게임 시작| B[CharacterSelectScene]
    B -->|캐릭터 선택| C[LumiaIsland]
    C -->|멀티스레드 로딩| D[백그라운드 리소스 로딩]
    C -->|메인 스레드| E[맵/환경 생성]
    D -->|완료| F[UI 시스템 초기화]
    E -->|완료| F
    F --> G[게임플레이 시작]

 

결론

이 시스템을 통해

  • 게일 실행 중 끊김 살짝 있는 리소스 로딩 ( 아직 최적화가 덜 된거 같긴함.... 아직 끊김 ) 약 7% 성능 향상
  • 메모리 사용량 최적화
  • 안정적인 Scene 전환
  • 피킹 성능 : QuadTree로 O(n) -> O(log n)