다음을 통해 공유


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

비고

이 항목은 DirectX 자습서 시리즈를 사용하여 간단한 UWP(유니버설 Windows 플랫폼) 게임 만들기의 일부입니다. 그 링크에 있는 주제가 시리즈의 맥락을 설정합니다.

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

목표

  • UWP(유니버설 Windows 플랫폼) DirectX 게임에 대한 프레임워크를 설정하고 전체 게임 흐름을 정의하는 상태 컴퓨터를 구현합니다.

비고

이 항목을 따라가려면 다운로드한 Simple3DGameDX 샘플 게임의 소스 코드를 확인합니다.

소개

게임 프로젝트 설정 항목에서는 wWinMain 함수와 IFrameworkViewSourceIFrameworkView 인터페이스를 소개했습니다. App 클래스(App.cpp 프로젝트의 소스 코드 파일에서 정의됨)가 뷰 공급자 팩터리뷰 공급자역할을 한다는 것을 배웠습니다.

이 항목에서는 거기서부터 이어받아, 게임에서 클래스가 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_ptr 및 std::make_shared사용합니다.

App::SetWindow 메서드

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

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는 게임이 입력 처리를 시작하기 전에 걸릴 수 있는 시간에 제한을 적용합니다. 따라서 여기서처럼 비동기를 사용하면 시작된 작업이 백그라운드에서 계속되는 동안 Load 가 신속하게 반환될 수 있습니다. 로드하는 데 시간이 오래 걸리거나 리소스가 많은 경우 사용자에게 자주 업데이트되는 진행률 표시줄을 제공하는 것이 좋습니다.

비동기 프로그래밍이 처음이라면 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 제어 객체를 생성하고, 그리고 게임 정보 오버레이를 표시하여 리소스 파일이 로드되는 동안 진행 표시줄을 제공합니다. 자세한 내용은 사용자 인터페이스 추가를 참조하세요.
  • 컨트롤러(터치, 마우스 또는 게임 컨트롤러)에서 입력을 읽을 컨트롤러 개체를 만듭니다. 자세한 내용은 컨트롤 추가참조하세요.
  • 이동 및 카메라 터치 컨트롤에 대한 화면의 왼쪽 아래와 오른쪽 아래 모서리에 각각 두 개의 사각형 영역을 정의합니다. 플레이어는 왼쪽 아래 사각형(SetMoveRect호출에 정의됨)을 카메라를 좌우로 이동하기 위한 가상 컨트롤 패드로 사용합니다. 오른쪽 아래 사각형(SetFireRect 메서드에 의해 정의됨)은 탄약을 발사하는 가상 단추로 사용됩니다.
  • 코루틴을 사용하여 리소스 로드를 별도의 단계로 분리합니다. Direct3D 디바이스 컨텍스트에 대한 액세스는 디바이스 컨텍스트가 만들어진 스레드로 제한됩니다. 개체를 만들기 위한 Direct3D 디바이스에 대한 액세스는 자유 스레드입니다. 따라서 GameRenderer::CreateGameDeviceResourcesAsync 코루틴은 완료 작업(GameRenderer::FinalizeCreateGameDeviceResources)을 실행하는 원래 스레드와 별도의 스레드에서 실행될 수 있습니다.
  • Simple3DGame::LoadLevelAsync 및 simple3DGame::FinalizeLoadLevel수준 리소스를 로드하는 데 유사한 패턴을 사용합니다.

다음 항목에서 GameMain::InitializeGameState 자세히 살펴보겠습니다(게임 흐름 관리).

App::OnActivated 메서드

다음으로 CoreApplicationView::Activated 이벤트가 발생합니다. 따라서 보유하고 있는 모든 OnActivated 이벤트 처리기(예: App::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::비활성화된. 이렇게 하면 게임 창이 비활성화되거나(포커스가 손실됨) 스냅됩니다.
  • UpdateEngineState::TooSmall. 이렇게 하면 클라이언트 영역이 너무 작아 게임을 렌더링할 수 없습니다.

이러한 상태 중 하나에서 게임은 이벤트 처리를 일시 중단하고 창이 활성화되거나 스냅이 해제되거나 크기가 조정될 때까지 기다립니다.

게임 창이 표시되는 동안(Window.Visibletrue), 도착하는 메시지 큐의 모든 이벤트를 처리해야 하므로 ProcessAllIfPresent 옵션을 사용하여 CoreWindowDispatch.ProcessEvents를 호출해야 합니다. 다른 옵션으로 인해 메시지 이벤트 처리가 지연되어 게임이 응답하지 않거나 터치 동작이 느려질 수 있습니다.

게임이 표시되지 경우(Window.Visible ), 일시 중단되거나 너무 작을 때(스냅됨) 리소스 순환을 사용하여 도착하지 않는 메시지를 디스패치하는 것을 원하지 않습니다. 이 경우 게임에서 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 구현이 호출됩니다. 이것은 정리를 수행 할 수있는 기회입니다. 앱 창을 닫으면 앱의 프로세스가 중단되지 않습니다. 하지만 대신 앱 싱글톤 상태를 메모리에 씁니다. 시스템에서 이 메모리를 회수할 때, 리소스의 특수 정리를 포함하여 특별한 일이 발생해야 한다면 그 정리에 대한 코드를 초기화 해제에 넣으십시오.

우리의 경우, App::Uninitialize는 no-op입니다.

void Uninitialize()
{
}

고유한 게임을 개발할 때 이 항목에 설명된 방법을 중심으로 시작 코드를 디자인합니다. 다음은 각 메서드에 대한 기본 제안의 간단한 목록입니다.

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

다음 단계

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

다음 항목에서는 게임 흐름 관리게임 흐름을 유지하기 위해 게임 상태 및 이벤트 처리를 관리하는 방법을 자세히 살펴보겠습니다.