Definir a estrutura do aplicativo UWP do jogoDefine the game's UWP app framework

Observação

Este tópico faz parte do jogo criar um simples plataforma universal do Windows (UWP) com a série de tutoriais do DirectX .This topic is part of the Create a simple Universal Windows Platform (UWP) game with DirectX tutorial series. O tópico nesse link define o contexto para a série.The topic at that link sets the context for the series.

A primeira etapa na codificação de um jogo da Plataforma Universal do Windows (UWP) é criar a estrutura que permite que o objeto de aplicativo interaja com o Windows, incluindo recursos de Windows Runtime, como a manipulação de eventos de suspensão/retomada, alterações no foco da janela e encaixe.The first step in coding a Universal Windows Platform (UWP) game is building the framework that lets the app object interact with Windows, including Windows Runtime features such as suspend-resume event handling, changes in window focus, and snapping.

ObjetivosObjectives

  • Configure a estrutura para um jogo do DirectX Plataforma Universal do Windows (UWP) e implemente o computador de estado que define o fluxo de jogo geral.Set up the framework for a Universal Windows Platform (UWP) DirectX game, and implement the state machine that defines the overall game flow.

Observação

Para acompanhar este tópico, examine o código-fonte do jogo de exemplo Simple3DGameDX que você baixou.To follow along with this topic, look in the source code for the Simple3DGameDX sample game that you downloaded.

IntroduçãoIntroduction

No tópico Configurar o projeto de jogo , apresentamos a função wWinMain , bem como as interfaces IFrameworkViewSource e IFrameworkView .In the Set up the game project topic, we introduced the wWinMain function as well as the IFrameworkViewSource and IFrameworkView interfaces. Aprendemos que a classe de aplicativo (que você pode ver definida no App.cpp arquivo de código-fonte no projeto Simple3DGameDX ) serve como fábrica de provedor de exibição e provedor de exibição.We learned that the App class (which you can see defined in the App.cpp source code file in the Simple3DGameDX project) serves as both view-provider factory and view-provider.

Este tópico escolhe a partir daí e entra em muito mais detalhes sobre como a classe de aplicativo em um jogo deve implementar os métodos de IFrameworkView.This topic picks up from there, and goes into much more detail about how the App class in a game should implement the methods of IFrameworkView.

O método App:: InitializeThe App::Initialize method

Após a inicialização do aplicativo, o primeiro método que o Windows chama é nossa implementação de IFrameworkView:: Initialize.Upon application launch, the first method that Windows calls is our implementation of IFrameworkView::Initialize.

Sua implementação deve lidar com os comportamentos mais fundamentais de um jogo UWP, como certificar-se de que o jogo possa lidar com um evento de suspensão (e um possível reinício posterior) inscrevendo-se nesses eventos.Your implementation should handle the most fundamental behaviors of a UWP game, such as making sure that the game can handle a suspend (and a possible later resume) event by subscribing to those events. Também temos acesso ao dispositivo adaptador de vídeo aqui, portanto, podemos criar recursos gráficos que dependem do dispositivo.We also have access to the display adapter device here, so we can create graphics resources that depend on the device.

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>();
}

Evite ponteiros brutos sempre que possível (e é quase sempre possível).Avoid raw pointers whenever possible (and it's nearly always possible).

  • Para tipos de Windows Runtime, muitas vezes você pode evitar ponteiros completamente e apenas construir um valor na pilha.For Windows Runtime types, you can very often avoid pointers altogether and just construct a value on the stack. Se você precisar de um ponteiro, use winrt:: com_ptr (Vamos ver um exemplo disso em breve).If you do need a pointer, then use winrt::com_ptr (we'll see an example of that soon).
  • Para ponteiros exclusivos, use std:: unique_ptr e std:: make_unique.For unique pointers, use std::unique_ptr and std::make_unique.
  • Para ponteiros compartilhados, use std:: shared_ptr e std:: make_shared.For shared pointers, use std::shared_ptr and std::make_shared.

O método App:: SetWindowThe App::SetWindow method

Após a inicialização, o Windows chama nossa implementação de IFrameworkView:: SetWindow, passando um objeto CoreWindow que representa a janela principal do jogo.After Initialize, Windows calls our implementation of IFrameworkView::SetWindow, passing a CoreWindow object representing the game's main window.

Em App:: SetWindow, assinamos eventos relacionados à janela e configuramos alguns comportamentos de janela e exibição.In App::SetWindow, we subscribe to window-related events, and configure some window and display behaviors. Por exemplo, construímos um ponteiro do mouse (por meio da classe CoreCursor ), que pode ser usado por controles de toque e mouse.For example, we construct a mouse pointer (via the CoreCursor class), which can be used by both mouse and touch controls. Também passamos o objeto Window para nosso objeto de recursos dependente de dispositivo.We also pass the window object to our device-dependent resources object.

Falaremos mais sobre como manipular eventos no tópico Gerenciamento de fluxo de jogos .We'll talk more about handling events in the Game flow management topic.

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

O método App:: LoadThe App::Load method

Agora que a janela principal está definida, nossa implementação de IFrameworkView:: Load é chamada.Now that the main window is set, our implementation of IFrameworkView::Load is called. A carga é um local melhor para buscar previamente dados de jogos ou ativos do que inicializar e SetWindow.Load is a better place to pre-fetch game data or assets than Initialize and SetWindow.

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

Como você pode ver, o trabalho real é delegado para o construtor do objeto GameMain que fazemos aqui.As you can see, the actual work is delegated to the constructor of the GameMain object that we make here. A classe GameMain é definida em GameMain.h e GameMain.cpp .The GameMain class is defined in GameMain.h and GameMain.cpp.

O Construtor GameMain:: GameMainThe GameMain::GameMain constructor

O construtor GameMain (e as outras funções de membro que ele chama) inicia um conjunto de operações de carregamento assíncronas para criar os objetos do jogo, carregar recursos gráficos e inicializar a máquina de estado do jogo.The GameMain constructor (and the other member functions that it calls) begins a set of asynchronous loading operations to create the game objects, load graphics resources, and initialize the game's state machine. Também fazemos os preparativos necessários antes do jogo começar, como a definição de quaisquer Estados iniciais ou valores globais.We also do any necessary preparations before the game begins, such as setting any starting states or global values.

O Windows impõe um limite no tempo que seu jogo pode tomar antes de começar a processar a entrada.Windows imposes a limit on the time your game can take before it begins processing input. Portanto, usar asyc, como fazemos aqui, significa que a carga pode retornar rapidamente enquanto o trabalho que ele iniciou continua em segundo plano.So using asyc, as we do here, means that Load can return quickly while the work that it has begun continues in the background. Se o carregamento levar muito tempo ou se houver muitos recursos, então fornecer aos usuários uma barra de progresso atualizada com frequência é uma boa ideia.If loading takes a long time, or if there are lots of resources, then providing your users with a frequently updated progress bar is a good idea.

Se você for novo na programação assíncrona, consulte operações de simultaneidade e assíncronas com C++/WinRT.If you're new to asynchronous programming, then see Concurrency and asynchronous operations with 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 have focus. 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.
    ...
}

Aqui está uma descrição da sequência de trabalho que é inicializada pelo construtor.Here's an outline of the sequence of work that's kicked off by the constructor.

  • Crie e inicialize um objeto do tipo GameRenderer.Create and initialize an object of type GameRenderer. Para obter mais informações, consulte Estrutura de renderização I: introdução à renderização.For more information, see Rendering framework I: Intro to rendering.
  • Crie e inicialize um objeto do tipo Simple3DGame.Create and initialize an object of type Simple3DGame. Para obter mais informações, consulte Definir o objeto principal do jogo.For more information, see Define the main game object.
  • Crie o objeto de controle de interface do usuário do jogo e exiba a sobreposição de informações do jogo para mostrar uma barra de progresso conforme os arquivos de recurso são carregados.Create the game UI control object, and display game info overlay to show a progress bar as the resource files load. Para saber mais, consulte Adicionando uma interface do usuário.For more information, see Adding a user interface.
  • Crie um objeto de controlador para ler a entrada do controlador (toque, mouse ou controlador sem fio Xbox).Create a controller object to read input from the controller (touch, mouse, or Xbox wireless controller). Para obter mais informações, consulte Adicionando controles.For more information, see Adding controls.
  • Defina duas áreas retangulares nos cantos inferior esquerdo e inferior direito da tela para os controles de toque mover e câmera, respectivamente.Define two rectangular areas in the lower-left and lower-right corners of the screen for the move and camera touch controls, respectively. O Player usa o retângulo inferior esquerdo (definido na chamada para SetMoveRect) como um painel de controle virtual para mover a câmera para frente e para trás e lado a lado.The player uses the lower-left rectangle (defined in the call to SetMoveRect) as a virtual control pad for moving the camera forward and backward, and side to side. O retângulo inferior direito (definido pelo método SetFireRect ) é usado como um botão virtual para acionar o Ammo.The lower-right rectangle (defined by the SetFireRect method) is used as a virtual button to fire the ammo.
  • Use corrotinas para interromper o carregamento de recursos em estágios separados.Use coroutines to break resource loading into separate stages. O acesso ao contexto do dispositivo Direct3D é restrito ao thread no qual o contexto do dispositivo foi criado; Embora o acesso ao dispositivo Direct3D para a criação de objetos seja de segmento livre.Access to the Direct3D device context is restricted to the thread on which the device context was created; while access to the Direct3D device for object creation is free-threaded. Consequentemente, a corotina GameRenderer:: CreateGameDeviceResourcesAsync pode ser executada em um thread separado da tarefa de conclusão (GameRenderer:: FinalizeCreateGameDeviceResources), que é executada no thread original.Consequently, the GameRenderer::CreateGameDeviceResourcesAsync coroutine can run on a separate thread from the completion task (GameRenderer::FinalizeCreateGameDeviceResources), which runs on the original thread.
  • Usamos um padrão semelhante para carregar recursos de nível com Simple3DGame:: LoadLevelAsync e Simple3DGame:: FinalizeLoadLevel.We use a similar pattern for loading level resources with Simple3DGame::LoadLevelAsync and Simple3DGame::FinalizeLoadLevel.

Veremos mais de GameMain:: InitializeGameState no próximo tópico (gerenciamento defluxo de jogos).We'll see more of GameMain::InitializeGameState in the next topic (Game flow management).

O método App:: OnActivatedThe App::OnActivated method

Em seguida, o evento CoreApplicationView:: Activated é gerado.Next, the CoreApplicationView::Activated event is raised. Portanto, qualquer manipulador de eventos OnActivated que você tenha (como nosso método App:: OnActivated ) é chamado.So any OnActivated event handler that you have (such as our App::OnActivated method) is called.

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

O único trabalho que fazemos aqui é ativar o CoreWindowprincipal.The only work we do here is to activate the main CoreWindow. Como alternativa, você pode optar por fazer isso em App:: SetWindow.Alternatively, you can choose to do that in App::SetWindow.

O método App:: RunThe App::Run method

Inicializar, SetWindowe Load definiram o estágio.Initialize, SetWindow, and Load have set the stage. Agora que o jogo está em funcionamento, nossa implementação de IFrameworkView:: Run é chamada.Now that the game is up and running, our implementation of IFrameworkView::Run is called.

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

Novamente, o trabalho é delegado para GameMain.Again, work is delegated to GameMain.

O método GameMain:: RunThe GameMain::Run method

GameMain:: Run é o loop principal do jogo; Você pode encontrá-lo em GameMain.cpp .GameMain::Run is the main loop of the game; you can find it in GameMain.cpp. A lógica básica é que, embora a janela do seu jogo permaneça aberta, despache todos os eventos, atualize o temporizador e, em seguida, processe e apresente os resultados do pipeline de gráficos.The basic logic is that while the window for your game remains open, dispatch all events, update the timer, and then render and present the results of the graphics pipeline. Além disso, os eventos usados para fazer a transição entre os Estados do jogo são expedidos e processados.Also here, the events used to transition between game states are dispatched and processed.

O código aqui também se preocupa com dois dos Estados na máquina de estado do mecanismo de jogo.The code here is also concerned with two of the states in the game engine state machine.

  • UpdateEngineState::D eactivated.UpdateEngineState::Deactivated. Isso especifica que a janela do jogo está desativada (tem o foco perdido) ou está encaixada.This specifies that the game window is deactivated (has lost focus) or is snapped.
  • UpdateEngineState:: TooSmall.UpdateEngineState::TooSmall. Isso especifica que a área do cliente é muito pequena para renderizar o jogo.This specifies that the client area is too small to render the game in.

Em qualquer um desses Estados, o jogo suspende o processamento de eventos e aguarda a janela se concentrar, desencaixar ou ser redimensionado.In either of these states, the game suspends event processing, and waits for the window to focus, to unsnap, or to be resized.

Quando seu jogo tem foco, você deve lidar com todos os eventos na fila de mensagens conforme eles chegam. Por isso, é preciso chamar CoreWindowDispatch.ProcessEvents com a opção ProcessAllIfPresent.When your game has focus, you must handle every event in the message queue as it arrives, and so you must call CoreWindowDispatch.ProcessEvents with the ProcessAllIfPresent option. Outras opções podem causar atrasos no processamento de eventos de mensagem, que podem fazer com que seu jogo fique sem resposta ou que resulte em comportamentos de toque que se sintam lentos.Other options can cause delays in processing message events, which can make your game feel unresponsive, or result in touch behaviors that feel sluggish.

Quando o jogo não está visível, suspenso nem encaixado, você não deseja que ele consuma nenhum ciclo de recursos para enviar mensagens que nunca chegarão.When the game is not visible, suspended, nor snapped, you don't want it to consume any resources cycling to dispatch messages that will never arrive. Nesse caso, o jogo deve usar a opção ProcessOneAndAllPending .In this case, your game must use the ProcessOneAndAllPending option. Essa opção é bloqueada até obter um evento e, em seguida, processa esse evento (bem como qualquer outro que chegue na fila de processos durante o processamento do primeiro).That option blocks until it gets an event, and then processes that event (as well as any others that arrive in the process queue during the processing of the first). CoreWindowDispatch. ProcessEvents , em seguida, retorna imediatamente depois que a fila é processada.CoreWindowDispatch.ProcessEvents then immediately returns after the queue has been processed.

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.
}

O método App:: UninitializeThe App::Uninitialize method

Quando o jogo termina, nossa implementação de IFrameworkView:: Uninitialize é chamada.When the game ends, our implementation of IFrameworkView::Uninitialize is called. Essa é a nossa oportunidade para executar a limpeza.This is our opportunity to perform cleanup. Fechar a janela do aplicativo não interrompe o processo do aplicativo; Mas, em vez disso, ele grava o estado do singleton do aplicativo na memória.Closing the app window doesn't kill the app's process; but instead it writes the state of the app singleton to memory. Se algo especial precisar ocorrer quando o sistema recuperar essa memória, incluindo qualquer limpeza especial de recursos, coloque o código para essa limpeza em uninitializeize.If anything special must happen when the system reclaims this memory, including any special cleanup of resources, then put the code for that cleanup in Uninitialize.

Em nosso caso, App:: Uninitialize é um não op.In our case, App::Uninitialize is a no-op.

void Uninitialize()
{
}

DicasTips

Ao desenvolver seu próprio jogo, crie seu código de inicialização em volta dos métodos descritos neste tópico.When developing your own game, design your startup code around the methods described in this topic. Aqui está uma lista simples de sugestões básicas para cada método.Here's a simple list of basic suggestions for each method.

  • Use Initialize para alocar suas classes principais e conecte os manipuladores de eventos básicos.Use Initialize to allocate your main classes, and connect up the basic event handlers.
  • Use SetWindow para assinar qualquer evento específico de janela e passe sua janela principal para o objeto de recursos dependente de dispositivo para que ele possa usar essa janela ao criar uma cadeia de permuta.Use SetWindow to subscribe to any window-specific events, and to pass your main window to your device-dependent resources object so that it can use that window when creating a swap chain.
  • Use carregar para lidar com qualquer configuração restante e para iniciar a criação assíncrona de objetos e o carregamento de recursos.Use Load to handle any remaining setup, and to initiate the asynchronous creation of objects, and loading of resources. Se você precisar criar arquivos temporários ou dados, como ativos gerados em procedimentos, faça isso aqui também.If you need to create any temporary files or data, such as procedurally generated assets, then do that here, too.

Próximas etapasNext steps

Este tópico abordou parte da estrutura básica de um jogo UWP que usa o DirectX.This topic has covered some of the basic structure of a UWP game that uses DirectX. É uma boa ideia manter esses métodos em mente, pois iremos nos referir a alguns deles em tópicos posteriores.It's a good idea to keep these methods in mind, because we'll be referring back to some of them in later topics.

No próximo tópico — Gerenciamento de fluxo de jogos — , vamos analisar detalhadamente como gerenciar Estados de jogos e manipulação de eventos para manter o jogo fluindo.In the next topic—Game flow management—we'll take an in-depth look at how to manage game states and event handling in order to keep the game flowing.