Bagikan melalui


Tentukan kerangka kerja aplikasi UWP game

Catatan

Topik ini adalah bagian dari membuat permainan Platform Windows Universal sederhana (UWP) dengan seri tutorial DirectX. Topik di tautan tersebut mengatur konteks untuk seri.

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

Tujuan

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

Catatan

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

Pendahuluan

Dalam topik Siapkan proyek game, kami memperkenalkan fungsi wWinMain serta antarmuka IFrameworkViewSource dan IFrameworkView. Kami mempelajari 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 diambil dari sana, dan masuk ke jauh lebih detail tentang bagaimana kelas Aplikasi dalam game harus menerapkan metode IFrameworkView.

Metode App::Initialize

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

Implementasi Anda harus menangani perilaku paling mendasar dari game UWP, seperti memastikan bahwa game dapat menangani peristiwa yang ditangguhkan (dan kemungkinan dilanjutkan nanti) dengan berlangganan peristiwa 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 jika memungkinkan (dan hampir selalu memungkinkan).

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

Metode App::SetWindow

Setelah Inisialisasi, Windows memanggil implementasi IFrameworkView::SetWindow kami, 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 membuat 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 menangani 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 });
}

Metode App::Load

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

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

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

Konstruktor GameMain::GameMain

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

Windows memberlakukan batasan waktu yang dapat diambil permainan Anda sebelum mulai memproses input. Jadi menggunakan asinkron, 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 baik.

Jika Anda baru menggunakan 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 kerangka urutan pekerjaan yang dimulai oleh konstruktor.

  • Membuat dan menginisialisasi objek jenis GameRenderer. Untuk informasi selengkapnya, lihat Merender kerangka kerja I: Pengandalan penyajian.
  • Membuat dan menginisialisasi objek jenis Simple3DGame. Untuk informasi selengkapnya, lihat Menentukan objek game utama.
  • Buat objek kontrol UI game, dan tampilkan overlay 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 (sentuhan, mouse, atau pengontrol permainan). Untuk informasi selengkapnya, lihat Menambahkan kontrol.
  • Tentukan dua area persegi panjang di sudut kiri bawah dan kanan bawah layar untuk kontrol sentuhan pemindahan dan kamera. Pemutar menggunakan persegi panjang kiri bawah (didefinisikan dalam panggilan ke SetMoveRect) sebagai pad kontrol virtual untuk memindahkan kamera maju dan mundur, dan berdampingan. Persegi kanan bawah (didefinisikan oleh metode SetFireRect ) digunakan sebagai tombol virtual untuk menembakkan amunisi.
  • Gunakan coroutines untuk memecah pemuatan sumber daya menjadi tahap terpisah. Akses ke konteks perangkat Direct3D dibatasi untuk utas tempat konteks perangkat dibuat; sementara akses ke perangkat Direct3D untuk pembuatan objek berulir bebas. Akibatnya, coroutine GameRenderer::CreateGameDeviceResourcesAsync dapat berjalan pada utas terpisah dari tugas penyelesaian (GameRenderer::FinalizeCreateGameDeviceResources), yang berjalan pada utas 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 alur game).

Metode App::OnActivated

Selanjutnya, peristiwa CoreApplicationView::Activated dinaikkan. Jadi, penanganan aktivitas OnActivated apa pun 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 kita lakukan di sini adalah mengaktifkan CoreWindow utama. Atau, Anda dapat memilih untuk melakukannya di App::SetWindow.

Metode App::Run

Inisialisasi, SetWindow, dan Load telah mengatur tahap. Setelah permainan berjalan, implementasi IFrameworkView::Run kami dipanggil.

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

Sekali lagi, pekerjaan didelegasikan ke GameMain.

Metode GameMain::Run

GameMain::Run adalah perulangan utama gim ini; Anda dapat menemukannya di GameMain.cpp. Logika dasarnya adalah bahwa sementara jendela untuk game Anda tetap terbuka, mengirim semua peristiwa, memperbarui timer, lalu merender dan menyajikan hasil alur grafis. Juga di sini, peristiwa yang digunakan untuk transisi antara status permainan dikirim dan diproses.

Kode di sini juga berkaitan dengan dua negara bagian dalam mesin game mesin status.

  • UpdateEngineState::D diaktifkan. Ini menentukan bahwa jendela permainan dinonaktifkan (telah kehilangan fokus) atau diciutkan.
  • UpdateEngineState::TooSmall. Ini menentukan bahwa area klien terlalu kecil untuk merender permainan.

Dalam salah satu status ini, game menangguhkan pemrosesan peristiwa, dan menunggu jendela diaktifkan, untuk membatalkan penggantian, atau untuk diubah ukurannya.

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

Ketika permainan tidak terlihat (Window.Visible adalah false), atau ketika ditangguhkan, atau ketika terlalu kecil (di-snap), Anda tidak ingin mengonsumsi sumber daya apa pun yang bersepeda untuk mengirim pesan yang tidak akan pernah tiba. Dalam hal ini, game Anda harus menggunakan opsi ProcessOneAndAllPending . Opsi itu memblokir hingga mendapatkan peristiwa, dan kemudian memproses peristiwa tersebut (serta yang lain yang tiba dalam antrean proses selama pemrosesan yang pertama). CoreWindowDispatch.ProcessEvents kemudian segera kembali setelah antrean 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 atau tidak (kehilangan fokus), terlalu kecil (diciutkan), 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.
}

Metode App::Uninitialize

Ketika permainan berakhir, implementasi IFrameworkView::Uninitialize kami dipanggil. Ini adalah kesempatan kita untuk melakukan pembersihan. Menutup jendela aplikasi tidak mematikan proses aplikasi; tetapi sebaliknya menulis status aplikasi singleton ke memori. Jika ada sesuatu yang istimewa harus terjadi ketika sistem mengklaim kembali memori ini, termasuk pembersihan sumber daya khusus, maka letakkan kode untuk pembersihan tersebut di Tidak Diinisialisasi.

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 sambungkan penanganan aktivitas dasar.
  • Gunakan SetWindow untuk berlangganan peristiwa khusus jendela apa pun, dan untuk meneruskan jendela utama Anda ke objek sumber daya yang bergantung pada perangkat Anda sehingga dapat menggunakan jendela tersebut saat membuat rantai pertukaran.
  • Gunakan Muat untuk menangani penyiapan 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, lakukan juga di sini.

Langkah berikutnya

Topik ini telah mencakup beberapa struktur dasar game UWP yang menggunakan DirectX. Ada baiknya untuk mengingat metode ini, karena kita akan merujuk kembali ke beberapa dari mereka dalam topik selanjutnya.

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