Tentukan kerangka kerja aplikasi UWP game

Catatan

Topik ini adalah bagian dari game Create a simple Universal Windows Platform (UWP) dengan seri tutorial DirectX. Topik di tautan itu menetapkan konteks untuk seri.

Langkah pertama dalam mengkodekan game Universal Windows Platform (UWP) adalah membangun kerangka kerja yang memungkinkan objek aplikasi berinteraksi dengan Windows, termasuk fitur runtime Windows seperti penanganan peristiwa suspend-resume, perubahan visibilitas jendela, dan gertakan.

Tujuan

  • Siapkan kerangka kerja untuk game DirectX Universal Windows Platform (UWP), dan terapkan mesin status yang menentukan aliran permainan secara keseluruhan.

Catatan

Untuk mengikuti topik ini, lihat kode sumber untuk game sampel Simple3DGameDX yang Anda unduh.

Pengantar

Dalam topik Siapkan proyek game , kami memperkenalkan fungsi wWinMain serta antarmuka IFrameworkViewSource dan IFrameworkView . Kami mengetahui bahwa kelas Aplikasi (yang dapat Anda lihat didefinisikan dalam App.cpp file kode sumber dalam proyek Simple3DGameDX ) berfungsi sebagai pabrik penyedia tampilan dan penyedia tampilan.

Topik ini mengambil dari sana, dan masuk ke lebih detail tentang bagaimana kelas Aplikasi dalam game harus menerapkan metode IFrameworkView.

Aplikasi::Metode inisialisasi

Setelah peluncuran aplikasi, metode pertama yang Windows panggilan adalah implementasi IFrameworkView: : Initialize kami.

Implementasi Anda harus menangani perilaku paling mendasar dari permainan UWP, seperti memastikan bahwa permainan dapat menangani acara penangguhan (dan kemungkinan melanjutkan nanti) dengan berlangganan acara tersebut. Kami juga memiliki akses ke perangkat adaptor tampilan di sini, sehingga kami dapat membuat sumber daya grafis yang bergantung pada perangkat.

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

Hindari pointer mentah bila memungkinkan (dan itu hampir selalu mungkin).

  • Untuk jenis Runtime Windows, Anda dapat sangat sering menghindari pointer sama sekali dan hanya membangun nilai pada tumpukan. Jika Anda memerlukan penunjuk, gunakan winrt::com_ptr (kita akan segera melihat contoh itu).
  • Untuk petunjuk unik, gunakan std::unique_ptr dan std::make_unique.
  • Untuk petunjuk bersama, gunakan std::shared_ptr dan std::make_shared.

Metode Aplikasi::SetWindow

Setelah Inisialisasi, Windows memanggil implementasi IFrameworkView::SetWindow, melewati objek CoreWindow yang mewakili jendela utama game.

Di App::SetWindow, kami berlangganan peristiwa terkait jendela, dan mengonfigurasi beberapa perilaku jendela dan tampilan. Misalnya, kami membangun penunjuk mouse (melalui kelas CoreCursor ), yang dapat digunakan oleh kontrol mouse dan sentuh. Kami juga meneruskan objek jendela ke objek sumber daya yang bergantung pada perangkat kami.

Kita akan berbicara lebih banyak tentang penanganan peristiwa dalam topik manajemen alur Game .

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

Aplikasi::Metode pemuatan

Sekarang setelah jendela utama diatur, implementasi IFrameworkView::Load kami dipanggil. Load adalah tempat yang lebih baik untuk mengambil data atau aset game sebelumnya daripada Initialize dan SetWindow.

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

Seperti yang Anda lihat, pekerjaan yang sebenarnya didelegasikan kepada konstruktor objek GameMain yang kami buat di sini. Kelas GameMain didefinisikan dalam GameMain.h dan GameMain.cpp.

The GameMain::GameMain konstruktor

Konstruktor GameMain (dan fungsi anggota lain yang disebutnya) memulai serangkaian operasi pemuatan asinkron untuk membuat objek game, memuat sumber daya grafis, dan menginisialisasi mesin state game. Kami juga melakukan persiapan yang diperlukan sebelum pertandingan dimulai, seperti menetapkan status awal atau nilai global.

Windows memberlakukan batasan waktu yang dapat diambil game Anda sebelum mulai memproses input. Jadi menggunakan asyc, seperti yang kita lakukan di sini, berarti bahwa Load dapat kembali dengan cepat sementara pekerjaan yang telah dimulai berlanjut di latar belakang. Jika pemuatan membutuhkan waktu lama, atau jika ada banyak sumber daya, maka memberi pengguna Anda bilah kemajuan yang sering diperbarui adalah ide yang bagus.

Jika Anda baru mengenal pemrograman asinkron, lihat Operasi Konkurensi dan asinkron dengan 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.
    ...
}

Berikut adalah garis besar urutan pekerjaan yang dimulai oleh konstruktor.

  • Buat dan inisialisasi objek jenis GameRenderer. Untuk informasi selengkapnya, lihat Rendering framework I: Intro to rendering.
  • Buat dan inisialisasi objek tipe Simple3DGame. Untuk informasi selengkapnya, lihat Menentukan objek game utama.
  • Buat objek kontrol UI game, dan tampilkan hamparan info game untuk menampilkan bilah kemajuan saat file sumber daya dimuat. Untuk informasi selengkapnya, lihat Menambahkan antarmuka pengguna.
  • Buat objek pengontrol untuk membaca input dari pengontrol (sentuh, mouse, atau pengontrol nirkabel Xbox). Untuk informasi selengkapnya, lihat Menambahkan kontrol.
  • Tentukan dua area persegi panjang di sudut kiri bawah dan kanan bawah layar untuk kontrol sentuh bergerak dan kamera. Pemain menggunakan persegi panjang kiri bawah (didefinisikan dalam panggilan ke SetMoveRect) sebagai pad kontrol virtual untuk menggerakkan kamera ke depan dan ke belakang, dan sisi ke sisi. Persegi panjang kanan bawah (didefinisikan oleh metode SetFireRect ) digunakan sebagai tombol virtual untuk menembakkan amunisi.
  • Gunakan coroutines untuk memecah pemuatan sumber daya menjadi beberapa tahap terpisah. Akses ke konteks perangkat Direct3D dibatasi pada utas tempat konteks perangkat dibuat; sementara akses ke perangkat Direct3D untuk pembuatan objek berulir bebas. Akibatnya, gameRenderer::CreateGameDeviceResourcesAsync coroutine dapat berjalan pada thread terpisah dari tugas penyelesaian (GameRenderer::FinalizeCreateGameDeviceResources), yang berjalan pada thread asli.
  • Kami menggunakan pola serupa untuk memuat sumber daya tingkat dengan Simple3DGame::LoadLevelAsync dan Simple3DGame::FinalizeLoadLevel.

Kita akan melihat lebih banyak GameMain::InitializeGameState di topik berikutnya (Manajemen aliran game).

Aplikasi::Metode Aktif Diaktifkan

Selanjutnya, acara CoreApplicationView::Diaktifkan dinaikkan. Jadi setiap penangan peristiwa OnActivated yang Anda miliki (seperti metode App::OnActivated kami) dipanggil.

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

Satu-satunya pekerjaan yang kami lakukan di sini adalah mengaktifkan CoreWindow utama. Atau, Anda dapat memilih untuk melakukannya di App::SetWindow.

Metode Aplikasi::Jalankan

Inisialisasi, SetWindow, dan Load telah mengatur panggung. Sekarang setelah game berjalan dan berjalan, implementasi IFrameworkView::Run kami dipanggil.

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

Sekali lagi, pekerjaan didelegasikan ke GameMain.

Metode GameMain::Jalankan

GameMain::Run adalah loop utama dari permainan; Anda dapat menemukannya di GameMain.cpp. Logika dasarnya adalah bahwa sementara jendela untuk game Anda tetap terbuka, kirim semua acara, perbarui timer, dan kemudian render dan sajikan hasil alur grafis. Juga di sini, peristiwa yang digunakan untuk transisi antara negara-negara permainan dikirim dan diproses.

Kode di sini juga berkaitan dengan dua negara bagian dalam mesin keadaan mesin permainan.

  • UpdateEngineState::D diaktifkan. Ini menentukan bahwa jendela permainan dinonaktifkan (telah kehilangan fokus) atau tersentak.
  • UpdateEngineState::Toosmall. Ini menentukan bahwa area klien terlalu kecil untuk membuat game masuk.

Di salah satu negara bagian ini, game menangguhkan pemrosesan acara, dan menunggu jendela diaktifkan, untuk membuka hambatan, atau diubah ukurannya.

Saat jendela permainan Anda terlihat (Window.Visible adalah true), Anda harus menangani setiap peristiwa dalam antrian pesan saat tiba, jadi Anda harus memanggil CoreWindowDispatch.ProcessEvents dengan opsi ProcessAllIfPresent . Opsi lain dapat menyebabkan penundaan dalam memproses acara pesan, yang dapat membuat game Anda terasa tidak responsif, atau mengakibatkan perilaku sentuh yang terasa lamban.

Ketika permainan tidak terlihat (Window.Visible adalah false), atau ketika itu ditangguhkan, atau ketika itu terlalu kecil (itu bentak), Anda tidak ingin itu untuk mengkonsumsi sumber daya bersepeda untuk mengirim pesan yang tidak akan pernah tiba. Dalam hal ini, game Anda harus menggunakan opsi ProcessOneAndAllPending . Opsi itu memblokir sampai mendapat acara, dan kemudian memproses peristiwa itu (serta yang lain yang tiba dalam antrian proses selama pemrosesan yang pertama). CoreWindowDispatch.ProcessEvents kemudian segera kembali setelah antrian diproses.

Dalam contoh kode yang ditunjukkan di bawah ini, anggota data m_visible mewakili visibilitas jendela. Ketika permainan ditangguhkan, jendelanya tidak terlihat. Ketika jendela terlihat , nilai m_updateState (enum UpdateEngineState ) menentukan lebih lanjut apakah jendela dinonaktifkan (kehilangan fokus), terlalu kecil (bentak), atau ukuran yang tepat.

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

Aplikasi::Metode yang tidak tahu

Ketika permainan berakhir, implementasi IFrameworkView kami: : Uninitialize dipanggil. Ini adalah kesempatan kami untuk melakukan pembersihan. Menutup jendela aplikasi tidak membunuh proses aplikasi; tetapi sebaliknya menulis keadaan singleton aplikasi ke memori. Jika sesuatu yang istimewa harus terjadi ketika sistem merebut kembali memori ini, termasuk pembersihan sumber daya khusus, maka letakkan kode untuk pembersihan itu di Uninitialize.

Dalam kasus kami, App::Uninitialize adalah no-op.

void Uninitialize()
{
}

Tips

Saat mengembangkan game Anda sendiri, rancang kode startup Anda di sekitar metode yang dijelaskan dalam topik ini. Berikut adalah daftar sederhana saran dasar untuk setiap metode.

  • Gunakan Inisialisasi untuk mengalokasikan kelas utama Anda, dan hubungkan penangan peristiwa dasar.
  • Gunakan SetWindow untuk berlangganan acara khusus jendela, dan untuk meneruskan jendela utama Anda ke objek sumber daya yang bergantung pada perangkat sehingga dapat menggunakan jendela itu saat membuat rantai swap.
  • Gunakan Beban untuk menangani pengaturan yang tersisa, dan untuk memulai pembuatan objek asinkron, dan pemuatan sumber daya. Jika Anda perlu membuat file atau data sementara, seperti aset yang dihasilkan secara prosedural, maka lakukan di sini juga.

Langkah berikutnya

Topik ini telah membahas beberapa struktur dasar permainan UWP yang menggunakan DirectX. Ini adalah ide yang baik untuk menjaga metode ini dalam pikiran, karena kita akan mengacu kembali ke beberapa dari mereka dalam topik kemudian.

Dalam topik berikutnya—Manajemen alur game—kita akan melihat secara mendalam cara mengelola status game dan penanganan peristiwa agar game tetap mengalir.