게임의 UWP 앱 프레임워크 정의

참고 항목

이 항목은 DirectX를 사용하여 간단한 UWP(유니버설 Windows 플랫폼) 게임 만들기 자습서 시리즈의 일부입니다. 해당 링크의 항목은 시리즈의 컨텍스트를 설정합니다.

UWP(유니버설 Windows 플랫폼) 게임을 코딩하는 첫 번째 단계는 일시 중단-다시 시작 이벤트 처리, 창 표시 유형 변경 및 맞추기와 같은 Windows 런타임 기능을 포함하여 앱 개체가 Windows와 상호 작용할 수 있도록 하는 프레임워크를 빌드하는 것입니다.

목표

  • UWP(유니버설 Windows 플랫폼) DirectX 게임을 위한 프레임워크를 설정하고 전체적인 게임 흐름을 정의하는 상태 머신을 구현합니다.

참고 항목

이 문서를 따르려면 다운로드한 Simple3DGameDX 샘플 게임의 소스 코드를 살펴봅니다.

소개

게임 프로젝트 설정 문서에서 IFrameworkViewSourceIFrameworkView 인터페이스뿐만 아니라 wWinMain 기능도 소개했습니다. Simple3DGameDX 프로젝트의 App.cpp 소스 코드 파일에 정의되어 있는 클래스가 view-provider factoryview-provider 모두를 지원한다는 것을 배웠습니다.

이 문서에서는 여기서 더 나아가 게임의 클래스에서 IFrameworkView의 메서드를 구현하는 방법에 대해 자세히 설명합니다.

App::Initialize 메서드

애플리케이션을 시작할 때 Windows에서 호출하는 첫 번째 메서드는 IFrameworkView::Initialize의 구현입니다.

구현은 UWP 게임의 가장 기본적인 동작을 처리해야 합니다. 이러한 이벤트를 구독하여 게임이 일시 중단(및 나중에 다시 시작 가능)을 처리할 수 있는지 확인합니다. 또한 여기서 디스플레이 어댑터 디바이스에 액세스할 수 있으므로 디바이스에 의존하는 그래픽 리소스를 만들 수 있습니다.

void Initialize(CoreApplicationView const& applicationView)
{
    applicationView.Activated({ this, &App::OnActivated });

    CoreApplication::Suspending({ this, &App::OnSuspending });

    CoreApplication::Resuming({ this, &App::OnResuming });

    // At this point we have access to the device. 
    // We can create the device-dependent resources.
    m_deviceResources = std::make_shared<DX::DeviceResources>();
}

가능한 경우 원시 포인터를 사용하지 않습니다. 이는 거의 항상 가능합니다.

  • Windows 런타임 형식의 경우 포인터를 모두 사용하지 않고 스택에 값만 구성할 수 있습니다. 포인터가 필요한 경우 winrt::com_ptr을 사용합니다. 이에 대한 예제를 곧 살펴볼 것입니다.
  • 고유 포인터의 경우 std::unique_ptrstd::make_unique를 사용합니다.
  • 공유 포인터의 경우 std::shared_ptrstd::make_shared를 사용합니다.

App::SetWindow 메서드

초기화 후 Windows에서는 게임의 주 창을 나타내는 CoreWindow 개체를 전달하여 IFrameworkView::SetWindow 구현을 호출합니다.

App::SetWindow에서 창 관련 이벤트를 구독하고 일부 창을 구성하고 동작을 표시합니다. 예를 들어 CoreCursor 클래스를 통해 마우스 컨트롤과 터치 컨트롤 모두에서 사용할 수 있는 마우스 포인터를 구성합니다. 또한 창 개체를 디바이스 종속 리소스 개체에 전달합니다.

이벤트 처리에 대해서는 게임 흐름 관리 문서에서 자세히 알아보겠습니다.

void SetWindow(CoreWindow const& window)
{
    //CoreWindow window = CoreWindow::GetForCurrentThread();
    window.Activate();

    window.PointerCursor(CoreCursor(CoreCursorType::Arrow, 0));

    PointerVisualizationSettings visualizationSettings{ PointerVisualizationSettings::GetForCurrentView() };
    visualizationSettings.IsContactFeedbackEnabled(false);
    visualizationSettings.IsBarrelButtonFeedbackEnabled(false);

    m_deviceResources->SetWindow(window);

    window.Activated({ this, &App::OnWindowActivationChanged });

    window.SizeChanged({ this, &App::OnWindowSizeChanged });

    window.Closed({ this, &App::OnWindowClosed });

    window.VisibilityChanged({ this, &App::OnVisibilityChanged });

    DisplayInformation currentDisplayInformation{ DisplayInformation::GetForCurrentView() };

    currentDisplayInformation.DpiChanged({ this, &App::OnDpiChanged });

    currentDisplayInformation.OrientationChanged({ this, &App::OnOrientationChanged });

    currentDisplayInformation.StereoEnabledChanged({ this, &App::OnStereoEnabledChanged });

    DisplayInformation::DisplayContentsInvalidated({ this, &App::OnDisplayContentsInvalidated });
}

App::Load 메서드

이제 주 창이 설정되었으므로 IFrameworkView::Load의 구현을 호출합니다. 게임 데이터 또는 자산을 미리 가져오는 경우 초기화SetWindow보다 로드가 더 좋습니다.

void Load(winrt::hstring const& /* entryPoint */)
{
    if (!m_main)
    {
        m_main = winrt::make_self<GameMain>(m_deviceResources);
    }
}

보시다시피 실제 작업은 여기서 만드는 GameMain 개체의 생성자에게 위임됩니다. GameMain 클래스는 GameMain.hGameMain.cpp에서 정의됩니다.

GameMain::GameMain 생성자

GameMain 생성자 및 이 생성자가 호출하는 다른 멤버 함수는 비동기 로딩 작업 집합을 시작하여 게임 개체를 만들고, 그래픽 리소스를 로드하고, 게임의 상태 머신을 초기화합니다. 또한 시작 상태 또는 전역 값 설정과 같이 게임을 시작하기 전에 필요한 준비를 수행합니다.

Windows에서는 입력 처리를 시작하기 전에 게임 시간이 제한됩니다. 따라서 여기에서와 같이 비동기를 사용하면 시작된 작업이 백그라운드에서 계속되는 동안 로드가 빠르게 반환 될 수 있습니다. 로드하는 데 시간이 오래 걸리거나 리소스가 많은 경우 사용자에게 자주 업데이트되는 진행률 표시줄을 제공하는 것이 좋습니다.

비동기 프로그래밍이 처음이라면 C++/WinRT를 사용한 동시성 및 비동기 작업을 참조하세요.

GameMain::GameMain(std::shared_ptr<DX::DeviceResources> const& deviceResources) :
    m_deviceResources(deviceResources),
    m_windowClosed(false),
    m_haveFocus(false),
    m_gameInfoOverlayCommand(GameInfoOverlayCommand::None),
    m_visible(true),
    m_loadingCount(0),
    m_updateState(UpdateEngineState::WaitingForResources)
{
    m_deviceResources->RegisterDeviceNotify(this);

    m_renderer = std::make_shared<GameRenderer>(m_deviceResources);
    m_game = std::make_shared<Simple3DGame>();

    m_uiControl = m_renderer->GameUIControl();

    m_controller = std::make_shared<MoveLookController>(CoreWindow::GetForCurrentThread());

    auto bounds = m_deviceResources->GetLogicalSize();

    m_controller->SetMoveRect(
        XMFLOAT2(0.0f, bounds.Height - GameUIConstants::TouchRectangleSize),
        XMFLOAT2(GameUIConstants::TouchRectangleSize, bounds.Height)
        );
    m_controller->SetFireRect(
        XMFLOAT2(bounds.Width - GameUIConstants::TouchRectangleSize, bounds.Height - GameUIConstants::TouchRectangleSize),
        XMFLOAT2(bounds.Width, bounds.Height)
        );

    SetGameInfoOverlay(GameInfoOverlayState::Loading);
    m_uiControl->SetAction(GameInfoOverlayCommand::None);
    m_uiControl->ShowGameInfoOverlay();

    // Asynchronously initialize the game class and load the renderer device resources.
    // By doing all this asynchronously, the game gets to its main loop more quickly
    // and in parallel all the necessary resources are loaded on other threads.
    ConstructInBackground();
}

winrt::fire_and_forget GameMain::ConstructInBackground()
{
    auto lifetime = get_strong();

    m_game->Initialize(m_controller, m_renderer);

    co_await m_renderer->CreateGameDeviceResourcesAsync(m_game);

    // The finalize code needs to run in the same thread context
    // as the m_renderer object was created because the D3D device context
    // can ONLY be accessed on a single thread.
    // co_await of an IAsyncAction resumes in the same thread context.
    m_renderer->FinalizeCreateGameDeviceResources();

    InitializeGameState();

    if (m_updateState == UpdateEngineState::WaitingForResources)
    {
        // In the middle of a game so spin up the async task to load the level.
        co_await m_game->LoadLevelAsync();

        // The m_game object may need to deal with D3D device context work so
        // again the finalize code needs to run in the same thread
        // context as the m_renderer object was created because the D3D
        // device context can ONLY be accessed on a single thread.
        m_game->FinalizeLoadLevel();
        m_game->SetCurrentLevelToSavedState();
        m_updateState = UpdateEngineState::ResourcesLoaded;
    }
    else
    {
        // The game is not in the middle of a level so there aren't any level
        // resources to load.
    }

    // Since Game loading is an async task, the app visual state
    // may be too small or not be activated. Put the state machine
    // into the correct state to reflect these cases.

    if (m_deviceResources->GetLogicalSize().Width < GameUIConstants::MinPlayableWidth)
    {
        m_updateStateNext = m_updateState;
        m_updateState = UpdateEngineState::TooSmall;
        m_controller->Active(false);
        m_uiControl->HideGameInfoOverlay();
        m_uiControl->ShowTooSmall();
        m_renderNeeded = true;
    }
    else if (!m_haveFocus)
    {
        m_updateStateNext = m_updateState;
        m_updateState = UpdateEngineState::Deactivated;
        m_controller->Active(false);
        m_uiControl->SetAction(GameInfoOverlayCommand::None);
        m_renderNeeded = true;
    }
}

void GameMain::InitializeGameState()
{
    // Set up the initial state machine for handling Game playing state.
    ...
}

다음은 생성자에 의해 시작되는 작업 시퀀스의 개요입니다.

  • GameRenderer 유형의 개체를 만들고 초기화합니다. 자세한 내용은 렌더링 프레임워크 I: 렌더링 소개를 참조하세요.
  • Simple3DGame 유형의 개체를 만들고 초기화합니다. 자세한 내용은 주 게임 개체 정의를 참조하세요.
  • 게임 UI 컨트롤 개체를 만들고 게임 정보 오버레이를 표시하여 리소스 파일이 로드됨에 따라 진행률 표시줄을 보여 줍니다. 자세한 내용은 사용자 인터페이스 추가를 참조하세요.
  • 컨트롤러(터치, 마우스 또는 게임 컨트롤러)에서 입력을 읽을 컨트롤러 개체를 생성합니다. 자세한 내용은 컨트롤 추가를 참조하세요.
  • 화면의 왼쪽 아래와 오른쪽 아래 모서리에 각각 이동 및 카메라 터치 컨트롤을 위해 2개의 사각형 영역을 정의합니다. 플레이어는 SetMoveRect 호출로 정의된 왼쪽 아래 사각형을 카메라를 전후좌우로 이동하기 위한 가상 컨트롤 패드로 사용합니다. SetFireRect 메서드로 정의된 오른쪽 아래 사각형은 탄약을 발사하는 가상 버튼로 사용됩니다.
  • 코루틴을 사용하여 리소스 로드를 별도의 단계로 구분합니다. Direct3D 디바이스 컨텍스트에 대한 액세스는 디바이스 컨텍스트가 만들어진 스레드로 제한됩니다. 개체를 만들기 위한 Direct3D 디바이스에 대한 액세스는 자유 스레드입니다. 결과적으로 GameRenderer::CreateGameDeviceResourcesAsync 코루틴은 원래 스레드에서 실행되는 완료 작업인 GameRenderer::FinalizeCreateGameDeviceResources와 별개의 스레드에서 실행됩니다.
  • Simple3DGame::LoadLevelAsyncSimple3DGame::FinalizeLoadLevel을 사용하는 로드 수준 리소스에는 비슷한 패턴을 사용합니다.

다음 문서인 게임 흐름 관리에서는 GameMain::InitializeGameState에 대해 자세히 알아보겠습니다.

App::OnActivated 메서드

다음으로 CoreApplicationView::Activated 이벤트가 발생합니다. 따라서 App::OnActivated 메서드와 같이 보유하고 있는 모든 OnActivated 이벤트 처리기가 호출됩니다.

void OnActivated(CoreApplicationView const& /* applicationView */, IActivatedEventArgs const& /* args */)
{
    CoreWindow window = CoreWindow::GetForCurrentThread();
    window.Activate();
}

여기서 수행하는 유일한 작업은 주 CoreWindow를 활성화하는 것입니다. 또는 App::SetWindow에서 해당 작업을 수행하도록 선택할 수 있습니다.

App::Run 메서드

초기화, SetWindow, 로드가 단계를 설정합니다. 이제 게임이 시작되고 실행되었으므로 IFrameworkView::Run의 구현이 호출됩니다.

void Run()
{
    m_main->Run();
}

다시 작업이 GameMain에 위임됩니다.

GameMain::Run 메서드

GameMain::Run은 게임의 주 루프로, GameMain.cpp에서 찾을 수 있습니다. 기본 논리는 게임 창이 열려 있는 경우 모든 이벤트를 전달하고, 타이머를 업데이트한 다음, 그래픽 파이프라인의 결과를 렌더링하고 표시한다는 것입니다. 또한 여기서 게임 상태 간 전환에 사용되는 이벤트는 디스패치 및 처리됩니다.

여기에 있는 코드는 게임 엔진 상태 머신의 두 가지 상태와도 관련이 있습니다.

  • UpdateEngineState::Deactivated. 이렇게 하면 게임 창이 비활성화되어 포커스가 끊어지거나 맞춰지게 지정됩니다.
  • UpdateEngineState::TooSmall. 이렇게 하면 클라이언트 영역이 너무 작아서 게임을 렌더링할 수 없습니다.

이러한 상태 중 하나를 통해 게임은 이벤트 처리를 일시 중단하고 창이 활성화, 스냅 취소, 또는 크기가 조정될 때까지 대기합니다.

게임 창이 표시되는 경우, 즉 Window.Visibletrue인 경우 메시지가 도착할 때 메시지 큐의 모든 이벤트를 처리해야 하므로 ProcessAllIfPresent 옵션을 사용하여 CoreWindowDispatch.ProcessEvents를 호출해야 합니다. 다른 옵션을 사용하면 메시지 이벤트 처리가 지연되어 게임이 응답하지 않는 것처럼 보이거나 터치 동작이 느린 것처럼 느끼게 됩니다.

게임이 표시되지 않은 경우(Window.Visiblefalse인 경우), 또는 게임이 일시 중단되거나, 맞출 때 너무 작아진 경우에는 도착하지 않는 메시지를 계속 배포하느라 리소스가 소비되는 일이 없어야 합니다. 이 경우 게임은 ProcessOneAndAllPending 옵션을 사용해야 합니다. 이 옵션은 이벤트를 받을 때까지 차단한 다음, 해당 이벤트 및 첫 번째 처리 중 프로세스 큐에 도착하는 다른 이벤트를 처리합니다. 그러면 큐가 처리된 후 바로 CoreWindowDispatch.ProcessEvents가 반환됩니다.

아래 표시된 예제 코드에서는 m_visible 데이터 멤버가 창의 가시성을 나타냅니다. 게임이 일시 중단되면 해당 창이 표시되지 않습니다. 창이표시되면 m_updateState(UpdateEngineState 열거) 값은 창이 포커스가 손실되어 비활성화되었는지, 맞출 때 너무 작아졌는지, 올바른 크기에 있는지 추가로 결정합니다.

void GameMain::Run()
{
    while (!m_windowClosed)
    {
        if (m_visible)
        {
            switch (m_updateState)
            {
            case UpdateEngineState::Deactivated:
            case UpdateEngineState::TooSmall:
                if (m_updateStateNext == UpdateEngineState::WaitingForResources)
                {
                    WaitingForResourceLoading();
                    m_renderNeeded = true;
                }
                else if (m_updateStateNext == UpdateEngineState::ResourcesLoaded)
                {
                    // In the device lost case, we transition to the final waiting state
                    // and make sure the display is updated.
                    switch (m_pressResult)
                    {
                    case PressResultState::LoadGame:
                        SetGameInfoOverlay(GameInfoOverlayState::GameStats);
                        break;

                    case PressResultState::PlayLevel:
                        SetGameInfoOverlay(GameInfoOverlayState::LevelStart);
                        break;

                    case PressResultState::ContinueLevel:
                        SetGameInfoOverlay(GameInfoOverlayState::Pause);
                        break;
                    }
                    m_updateStateNext = UpdateEngineState::WaitingForPress;
                    m_uiControl->ShowGameInfoOverlay();
                    m_renderNeeded = true;
                }

                if (!m_renderNeeded)
                {
                    // The App is not currently the active window and not in a transient state so just wait for events.
                    CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(CoreProcessEventsOption::ProcessOneAndAllPending);
                    break;
                }
                // otherwise fall through and do normal processing to get the rendering handled.
            default:
                CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(CoreProcessEventsOption::ProcessAllIfPresent);
                Update();
                m_renderer->Render();
                m_deviceResources->Present();
                m_renderNeeded = false;
            }
        }
        else
        {
            CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(CoreProcessEventsOption::ProcessOneAndAllPending);
        }
    }
    m_game->OnSuspending();  // Exiting due to window close, so save state.
}

App::Uninitialize 메서드

게임이 끝나면 IFrameworkView::Uninitialize의 구현을 호출합니다. 정리를 수행할 수 있는 기회입니다. 앱 창을 닫아도 앱의 프로세스가 중단되지 않지만 대신 앱 싱글톤의 상태를 메모리에 기록합니다. 시스템에서 이 메모리를 확보해야 할 때 리소스를 특별 정리하는 등 특별한 조치가 필요한 경우에는 Uninitialize에 해당 정리를 위한 코드를 포함시킵니다.

이 경우 App::Uninitialize는 실행 중지입니다.

void Uninitialize()
{
}

게임을 직접 개발하는 경우 이 문서에 설명된 이러한 메서드를 기준으로 시작 코드를 설계합니다. 다음에는 각 메서드에 대한 기본 제한 사항 목록이 간략히 나와 있습니다.

  • Initialize를 사용하여 기본 클래스를 할당하고 기본 이벤트 처리기를 연결합니다.
  • SetWindow를 사용하여 창별 이벤트를 구독하고 스왑 체인을 만들 때 해당 창을 사용할 수 있도록 주 창을 디바이스 종속 리소스 개체에 전달합니다.
  • Load를 사용하여 나머지 설정을 처리하고 개체의 비동기 만들기 및 리소스 로드를 시작합니다. 단계적으로 생성되는 자산과 같은 임시 파일이나 데이터를 만들어야 하는 경우에도 이 메서드를 사용합니다.

다음 단계

이 문서에서는 DirectX를 사용하는 UWP 게임의 기본 구조 중 일부를 다루었습니다. 이후 문서에서 일부 메서드를 다시 참조할 예정이므로 이러한 메서드를 염두에 두는 것이 좋습니다.

다음 문서인 게임 흐름 관리로 가서 게임 흐름이 계속될 수 있도록 게임 상태와 이벤트 처리를 관리하는 방법을 심층적으로 살펴보겠습니다.