Sdílet prostřednictvím


Definujte rámec aplikace UWP pro hru

Poznámka:

Toto téma je součástí Vytvoření jednoduché hry pro Univerzální platformu Windows (UPW) pomocí série kurzů DirectX. Téma na tomto odkazu nastaví kontext pro řadu.

Prvním krokem při kódování hry univerzální platformy Windows (UPW) je vytvoření architektury, která umožňuje objektu aplikace pracovat s Windows, včetně funkcí prostředí Windows Runtime, jako je pozastavení zpracování událostí, změny viditelnosti okna a přichycení.

Cíle

  • Nastavte architekturu pro hru DirectX pro Univerzální platformu Windows (UPW) a implementujte stavový počítač, který definuje celkový tok hry.

Poznámka:

Pokud chcete postupovat podle tohoto tématu, podívejte se do zdrojového kódu na Simple3DGameDX ukázkovou hru, kterou jste stáhli.

Úvod

V tématu Nastavení herního projektu jsme představili funkci wWinMain a také IFrameworkViewSource a IFrameworkView rozhraní. Zjistili jsme, že třída App (kterou můžete vidět v souboru zdrojového kódu v projektu Simple3DGameDX) slouží jako továrna na zprostředkovatele zobrazení a zprostředkovatel zobrazení.

Toto téma navazuje na předchozí a podrobněji rozebírá, jak by třída App ve hře měla implementovat metody rozhraní IFrameworkView.

Metoda App::Initialize

Při spuštění aplikace první metoda, kterou Windows volá, je naše implementace IFrameworkView::Initialize.

Vaše implementace by měla zvládnout nejzákladnější chování hry UWP, například zajistit, aby hra zvládla pozastavení (a možné pozdější obnovení) událostí přihlášením k odběru těchto událostí. Máme zde také přístup k grafickému adaptéru, abychom mohli vytvořit grafické zdroje, které závisí na zařízení.

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

Vyhněte se nezpracovaným ukazatelům, kdykoli je to možné (a téměř vždy je to možné).

  • U typů prostředí Windows Runtime se můžete velmi často vyhnout ukazatelům úplně a jednoduše vytvořit hodnotu v zásobníku. Pokud potřebujete ukazatel, použijte winrt::com_ptr (brzy uvidíme příklad).
  • U jedinečných ukazatelů použijte std::unique_ptr a std::make_unique.
  • U sdílených ukazatelů použijte std::shared_ptr a std::make_shared.

Metoda App::SetWindow

Po Initialize, Windows volá naši implementaci IFrameworkView::SetWindow, předáním CoreWindow objektu představujícího herní okno.

V App::SetWindowse přihlásíme k odběru událostí souvisejících s okny a nakonfigurujeme některé okno a chování zobrazení. Například vytvoříme ukazatel myši (prostřednictvím třídy CoreCursor), kterou mohou používat ovládací prvky myši i dotykové ovládání. Objekt okna také předáváme objektu prostředků závislých na zařízení.

Další informace o zpracování událostí si řekneme v tématu řízení toků her.

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

Metoda App::Load

Teď, když je hlavní okno nastaveno, je volána naše implementace IFrameworkView::Load. Load je lepším místem k předběžnému načtení herních dat nebo prostředků než Initialize a SetWindow.

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

Jak vidíte, skutečná práce je delegována na konstruktor objektu GameMain, který zde vytvoříme. Třída GameMain je definována v GameMain.h a GameMain.cpp.

Konstruktor GameMain::GameMain

GameMain konstruktor (a ostatní členské funkce, které volá) zahájí sadu asynchronních operací načítání pro vytvoření herních objektů, načtení grafických prostředků a inicializaci stavového počítače hry. Před zahájením hry provádíme také všechny nezbytné přípravy, například nastavení počátečních stavů nebo globálních hodnot.

Systém Windows omezuje dobu, kterou může hra trvat, než začne zpracovávat vstup. Použití async, jak to děláme zde, znamená, že Load se může rychle vrátit k dispozici, zatímco práce, kterou začal, pokračuje na pozadí. Pokud načítání trvá příliš dlouho nebo pokud existuje spousta prostředků, je vhodné poskytnout uživatelům často aktualizovaný indikátor průběhu.

Pokud s asynchronním programováním teprve začínáte, podívejte se na Asynchronní operace a souběžnost sC++/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.
    ...
}

Tady je přehled posloupnosti práce, kterou konstruktor zahájil.

  • Vytvoření a inicializace objektu typu GameRenderer. Další informace naleznete v tématu Rendering Framework I: Úvod k vykreslování.
  • Vytvoření a inicializace objektu typu Simple3DGame. Další informace naleznete v tématu Definovat hlavní herní objekt.
  • Vytvořte objekt ovládacího prvku herního UI a zobrazte překryvnou vrstvu s informacemi o hře, aby se během načítání zdrojových souborů zobrazil indikátor průběhu. Další informace naleznete v tématu Přidání uživatelského rozhraní.
  • Vytvořte objekt kontroleru pro čtení vstupu z ovladače (dotykové ovládání, myš nebo herní ovladač). Další informace naleznete v tématu Přidání ovládacích prvků.
  • Definujte dvě obdélníkové oblasti v levém dolním a pravém dolním rohu obrazovky pro ovládací prvky pro pohyb a ovládání dotykového ovládání fotoaparátu. Hráč používá levý dolní obdélník (definovaný ve volání SetMoveRect) jako virtuální ovládací prvek pro pohybování kamerou dopředu a dozadu a ze strany na stranu. Obdélník v pravém dolním rohu (definovaný metodou SetFireRect) se používá jako virtuální tlačítko pro vyvolání ammo.
  • Pomocí korutin rozdělte načítání prostředků do samostatných fází. Přístup k kontextu zařízení Direct3D je omezen na vlákno, na kterém byl kontext zařízení vytvořen; přístup k zařízení Direct3D pro vytvoření objektu je bez vláken. V důsledku toho se GameRenderer::CreateGameDeviceResourcesAsync coroutine může spustit na samostatném vlákně od úlohy dokončení procesu (GameRenderer::FinalizeCreateGameDeviceResources), jež běží na původním vlákně.
  • Podobný vzorec používáme pro načítání úrovňových prostředků s Simple3DGame::LoadLevelAsync a Simple3DGame::FinalizeLoadLevel.

Uvidíme více o GameMain::InitializeGameState v dalším tématu (správa herního toku).

Metoda App::OnActivated

Dále se vyvolá událost CoreApplicationView::Activated. Všechny obslužné rutiny události typu OnActivated, které máte (například naše metoda App::OnActivated), se volají.

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

Jediné, co zde děláme, je aktivovat hlavní CoreWindow. Můžete se také rozhodnout to provést v App::SetWindow.

Metoda App::Run

Inicializace, SetWindowa Načtení připravily prostředí. Nyní, když hra běží, naše implementace IFrameworkView::Run je volána.

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

Práce je opět delegována na GameMain.

Metoda GameMain::Run

GameMain::Run je hlavní smyčka hry; najdete ho v GameMain.cpp. Základní logika spočívá v tom, že zatímco okno vaší hry zůstává otevřené, zpracují se všechny události, aktualizuje se časovač a poté se vykreslí a zobrazí výsledky grafického řetězce. Zde se také události použité k přechodu mezi stavy her odesílají a zpracovávají.

Zde uvedený kód se také zabývá dvěma stavy ve stavovém automatu herního enginu.

  • UpdateEngineState::Deaktivované. Určuje, že okno hry je deaktivováno (ztratilo pozornost) nebo je přicvaknuté.
  • UpdateEngineState::TooSmall. To určuje, že klientská oblast je pro zobrazení hry příliš malá.

V některém z těchto stavů hra pozastaví zpracování událostí a počká, až se okno aktivuje, zruší nebo změní velikost.

I když je okno hry viditelné (Window.Visible je true), musíte zpracovat každou událost ve frontě zpráv, jakmile přijde, a proto musíte volat CoreWindowDispatch.ProcessEvents s možností ProcessAllIfPresent. Jiné možnosti mohou způsobit zpoždění při zpracování událostí zpráv, což může působit, že vaše hra nereaguje, nebo vést k opožděným reakcím dotykového ovládání.

Pokud hra není viditelná (Window.Visible je false), nebo když je pozastavená nebo příliš malá (přichycená), nechcete, aby spotřebovávala žádné prostředky při pokusu o odesílání zpráv, které nikdy nedorazí. V tomto případě je nutné, aby vaše hra použila volbu ProcessOneAndAllPending. Tato možnost blokuje, dokud nezístane událost, a pak tuto událost zpracuje (stejně jako všechny ostatní, které přicházejí do fronty procesu během zpracování první události). CoreWindowDispatch.ProcessEvents se pak okamžitě vrátí po zpracování fronty.

V ukázkovém kódu uvedeném níže představuje datový člen m_visible viditelnost okna. Když je hra pozastavena, její okno není viditelné. Pokud je okno viditelné, hodnota m_updateState, což je výčet UpdateEngineState, dále určuje, zda je okno deaktivováno (ztratilo fokus), příliš malé (přichycené) nebo správné velikosti.

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

Metoda App::Uninitialize

Když hra skončí, naše implementace IFrameworkView::Uninitialize je volána. Toto je naše příležitost k vyčištění. Zavření okna aplikace nezabíjí proces aplikace; ale místo toho zapíše stav singletonu aplikace do paměti. Pokud je nutné, aby se při uvolnění této paměti systémem něco zvláštního stalo, včetně zvláštního vyčištění prostředků, vložte kód pro toto vyčištění do Uninitialize.

V našem případě App::Uninitialize je no-op.

void Uninitialize()
{
}

Tipy

Při vývoji vlastní hry navrhněte spouštěcí kód kolem metod popsaných v tomto tématu. Tady je jednoduchý seznam základních návrhů pro každou metodu.

  • K inicializaci použijte Inicializovat pro přidělení hlavních tříd a připojení základních obslužných rutin událostí.
  • Pomocí SetWindow se registrovat na jakékoli události specifické pro okno a předat hlavní okno objektu zdrojů závislých na zařízení, aby tento objekt mohl použít toto okno při vytváření swap chainu.
  • Použijte Load ke zpracování všech zbývajících nastavení a k zahájení asynchronního vytváření objektů a načítání prostředků. Pokud potřebujete vytvořit nějaké dočasné soubory nebo data, například procedurálně generovaná aktiva, udělejte to také zde.

Další kroky

Toto téma se zabývá některými základními strukturami hry pro UPW, která používá DirectX. Je vhodné mít na paměti tyto metody, protože na některé z nich se budeme v pozdějších tématech odkazovat.

V dalším tématu ,řízení toku hry– se podrobněji podíváme na to, jak spravovat stavy her a zpracování událostí, abychom udrželi herní tok.