Condividi tramite


Definire l'oggetto gioco principale

Annotazioni

Questo argomento fa parte della serie di esercitazioni Creare un semplice gioco UWP (Universal Windows Platform) con DirectX. L'argomento in tale collegamento imposta il contesto per la serie.

Dopo aver disposto il framework di base del gioco di esempio e implementato una macchina a stati che gestisce i comportamenti di sistema e utente di alto livello, è necessario esaminare le regole e i meccanismi che trasformano il gioco di esempio in un gioco. Esaminiamo i dettagli dell'oggetto principale del gioco di esempio e come tradurre le regole del gioco in interazioni con il mondo del gioco.

Obiettivi

  • Scopri come applicare tecniche di sviluppo di base per implementare regole e meccanismi di gioco per un gioco DirectX UWP.

Oggetto gioco principale

Nel gioco di esempio Simple3DGameDXSimple3DGame è la classe principale dell'oggetto gioco. Un'istanza di Simple3DGame è creata, indirettamente, attraverso il metodo App::Load.

Ecco alcune delle funzionalità della classe Simple3DGame .

  • Contiene l'implementazione della logica di gioco.
  • Contiene metodi che comunicano questi dettagli.
    • Cambiamenti nello stato del gioco per la macchina a stati definita nel framework dell'app.
    • Le modifiche nello stato del gioco dall'app all'oggetto di gioco stesso.
    • Dettagli per l'aggiornamento dell'interfaccia utente del gioco (sovrapposizione e heads-up display), animazioni e fisica.

    Annotazioni

    L'aggiornamento della grafica viene gestito dalla classe GameRenderer , che contiene metodi per ottenere e usare le risorse del dispositivo grafico usate dal gioco. Per altre informazioni, vedere Framework di rendering I: Introduzione al rendering.

  • Funge da contenitore per i dati che definiscono una sessione di gioco, un livello o una durata, a seconda della modalità di definizione del gioco a un livello elevato. In questo caso, i dati sullo stato del gioco sono relativi alla durata del gioco e vengono inizializzati una volta quando un utente avvia il gioco.

Per visualizzare i metodi e i dati definiti da questa classe, vedere La classe Simple3DGame riportata di seguito.

Inizializzare e avviare il gioco

Quando un giocatore avvia il gioco, l'oggetto gioco deve inizializzarne lo stato, crearne e aggiungere la sovrimpressione, impostare le variabili che tengono traccia delle prestazioni del giocatore e creare un'istanza degli oggetti che userà per compilare i livelli. In questo esempio questa operazione viene eseguita quando l'istanza di GameMain viene creata in App::Load.

L'oggetto gioco, di tipo Simple3DGame, viene creato nel costruttore GameMain::GameMain . Viene quindi inizializzato utilizzando il metodo Simple3DGame::Initialize durante la coroutine fire-and-forget GameMain::ConstructInBackground, che è chiamata da GameMain::GameMain.

Il metodo Simple3DGame::Initialize

Il gioco di esempio configura questi componenti nell'oggetto gioco.

  • Viene creato un nuovo oggetto di riproduzione audio.
  • Vengono create matrici per le primitive grafiche del gioco, incluse matrici per le primitive di livello, munizioni e ostacoli.
  • Viene creata una posizione per il salvataggio dei dati sullo stato del gioco, denominata Game e inserita nella posizione di archiviazione delle impostazioni dei dati dell'app specificata da ApplicationData::Current.
  • Vengono creati un timer di gioco e la bitmap iniziale della sovrapposizione in-game.
  • Viene creata una nuova fotocamera con un set specifico di parametri di visualizzazione e proiezione.
  • Il dispositivo di input (il controller) è impostato sulla stessa inclinazione iniziale e rotazione della fotocamera, quindi il giocatore ha una corrispondenza uno a uno tra la posizione di controllo iniziale e la posizione della fotocamera.
  • L'oggetto giocatore viene creato e impostato su attivo. Usiamo un oggetto sfera per rilevare la vicinanza del giocatore alle pareti e agli ostacoli e per evitare che la fotocamera venga posizionata in una posizione che potrebbe interrompere l'immersione.
  • Viene creata la primitiva del mondo del gioco.
  • Vengono creati ostacoli cilindrici.
  • Gli obiettivi (oggetti Faccia) vengono creati e numerati.
  • Le sfere di munizioni vengono create.
  • I livelli vengono creati.
  • Il punteggio elevato viene caricato.
  • Viene caricato qualsiasi stato del gioco salvato in precedenza.

Il gioco ha ora istanze di tutti i componenti principali: il mondo, il giocatore, gli ostacoli, i bersagli e le sfere di munizioni. Include anche istanze dei livelli, che rappresentano le configurazioni di tutti i componenti precedenti e i relativi comportamenti per ogni livello specifico. Vediamo ora come il gioco crea i livelli.

Compilare e caricare i livelli di gioco

La maggior parte del lavoro pesante per la creazione dei livelli viene svolto nei file Level[N].h/.cpp che si trovano nella cartella GameLevels della soluzione di esempio. Poiché si concentra su un'implementazione molto specifica, non verranno trattate qui. È importante che il codice per ogni livello venga eseguito come oggetto Level[N] separato. Se vuoi estendere il gioco, puoi creare un oggetto Level[N] che accetta un numero assegnato come parametro e posiziona in modo casuale gli ostacoli e gli obiettivi. In alternativa, è possibile caricare i dati di configurazione del livello da un file di risorse o anche da Internet.

Definire il gioco

A questo punto, abbiamo tutti i componenti necessari per sviluppare il gioco. I livelli sono stati costruiti in memoria a partire dalle primitive e sono pronti per permettere al giocatore di iniziare a interagire con essi.

I migliori giochi reagiscono immediatamente all'input del giocatore e forniscono feedback immediato. Questo è vero per qualsiasi tipo di gioco, dai giochi d'azione twitch, agli sparatutto in prima persona in tempo reale, fino ai giochi di strategia ponderati e a turni.

Metodo Simple3DGame::RunGame

Mentre è in corso un livello di gioco, il gioco si trova nello stato Dynamics.

GameMain::Update è il ciclo di aggiornamento principale che aggiorna lo stato dell'applicazione una volta per fotogramma, come mostrato di seguito. Il ciclo di aggiornamento chiama il metodo Simple3DGame::RunGame per gestire il lavoro se il gioco si trova nello stato dynamics .

// Updates the application state once per frame.
void GameMain::Update()
{
    // The controller object has its own update loop.
    m_controller->Update();

    switch (m_updateState)
    {
    ...
    case UpdateEngineState::Dynamics:
        if (m_controller->IsPauseRequested())
        {
            ...
        }
        else
        {
            // When the player is playing, work is done by Simple3DGame::RunGame.
            GameState runState = m_game->RunGame();
            switch (runState)
            {
                ...

Simple3DGame::RunGame gestisce il set di dati che definisce lo stato corrente del gioco per l'iterazione corrente del ciclo del gioco.

Ecco la logica del flusso del gioco in Simple3DGame::RunGame.

  • Il metodo aggiorna il timer che conta i secondi fino al completamento del livello e verifica se l'ora del livello è scaduta. Questa è una delle regole del gioco—quando il tempo si esaurisce, se non tutti i bersagli sono stati sparati, allora è finita.
  • Se il tempo è scaduto, il metodo imposta lo stato del gioco TimeExpired e torna al metodo Update nel codice precedente.
  • Se il tempo lo permette, il controller di movimento viene interrogato per un aggiornamento della posizione della fotocamera; in particolare, un aggiornamento dell'angolo della vista normale proiettata dal piano della fotocamera (da cui il giocatore osserva) e la distanza di cui l'angolo si è spostato dall'ultimo controllo del controller.
  • La fotocamera viene aggiornata in base ai nuovi dati del controller di movimento.
  • Le dinamiche, ovvero le animazioni e i comportamenti degli oggetti nel mondo del gioco senza l'intervento del giocatore, vengono aggiornati. In questo gioco di esempio viene chiamato il metodo Simple3DGame::UpdateDynamics per aggiornare il movimento delle sfere di munizioni che sono state sparate, l'animazione degli ostacoli a forma di pilastro e il movimento dei bersagli. Per altre informazioni, vedere Aggiorna il mondo di gioco.
  • Il metodo verifica se sono stati soddisfatti i criteri per il completamento corretto di un livello. In tal caso, finalizza il punteggio per il livello e verifica se si tratta dell'ultimo livello (di 6). Se è l'ultimo livello, il metodo restituisce lo stato del gioco GameState::GameComplete ; in caso contrario, restituisce lo stato del gioco GameState::LevelComplete .
  • Se il livello non è completo, il metodo imposta lo stato del gioco su GameState::Active e restituisce.

Aggiornare il mondo del gioco

In questo esempio, quando il gioco è in esecuzione, il metodo Simple3DGame::UpdateDynamics viene chiamato dal metodo Simple3DGame::RunGame (chiamato da GameMain::Update) per aggiornare gli oggetti di cui viene eseguito il rendering in una scena di gioco.

Un ciclo come UpdateDynamics chiama tutti i metodi usati per impostare il mondo del gioco in movimento, indipendentemente dall'input del giocatore, per creare un'esperienza di gioco immersiva e far prendere vita al livello. Sono inclusi elementi grafici di cui è necessario eseguire il rendering e cicli di animazione in esecuzione per creare un mondo dinamico anche quando non è presente alcun input del lettore. Nel tuo gioco, ciò potrebbe includere alberi che ondeggiano nel vento, onde che si infrangono lungo le linee di riva, macchinari fumanti e mostri alieni che si allungano e si muovono. Comprende anche l'interazione tra oggetti, incluse le collisioni tra la sfera del giocatore e il mondo, o tra le munizioni e gli ostacoli e i bersagli.

Tranne quando il gioco viene messo in pausa in modo specifico, il ciclo del gioco deve continuare ad aggiornare il mondo del gioco; che si tratti di logica del gioco, algoritmi fisici o se è semplicemente casuale.

Nel gioco di esempio, questo principio viene chiamato dinamiche, e comprende l'innalzamento e l'abbassamento dei pilastri ostacolo e il movimento e i comportamenti fisici delle sfere di munizioni mentre vengono sparate e sono in movimento.

Metodo Simple3DGame::UpdateDynamics

Questo metodo gestisce questi quattro set di calcoli.

  • Le posizioni delle sfere di munizioni sparate nel mondo.
  • Animazione degli ostacoli del pilastro.
  • L'intersezione tra il giocatore e i confini del mondo.
  • Le collisioni delle sfere di munizione con gli ostacoli, i bersagli, altre sfere di munizione e l'ambiente circostante.

L'animazione degli ostacoli avviene in un ciclo definito nei file di codice sorgente Animate.h/.cpp . Il comportamento delle munizioni e delle collisioni è definito da algoritmi di fisica semplificati, forniti nel codice e parametrizzati da un set di costanti globali per il mondo del gioco, incluse le proprietà di gravità e materiale. Tutto questo viene calcolato nello spazio di coordinate del mondo del gioco.

Esaminare il flusso

Ora che tutti gli oggetti della scena sono stati aggiornati e sono stati calcolati eventuali conflitti, è necessario usare queste informazioni per disegnare le modifiche visive corrispondenti.

Dopo che GameMain::Update ha completato l'iterazione corrente del ciclo di gioco, l'esempio chiama immediatamente GameRenderer::Render per acquisire i dati aggiornati degli oggetti e generare una nuova scena da presentare al giocatore, come illustrato di seguito.

void GameMain::Run()
{
    while (!m_windowClosed)
    {
        if (m_visible)
        {
            switch (m_updateState)
            {
            case UpdateEngineState::Deactivated:
            case UpdateEngineState::TooSmall:
                ...
                // Otherwise, fall through and do normal processing to perform rendering.
            default:
                CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(
                    CoreProcessEventsOption::ProcessAllIfPresent);
                // GameMain::Update calls Simple3DGame::RunGame. If game is in Dynamics
                // state, uses Simple3DGame::UpdateDynamics to update game world.
                Update();
                // Render is called immediately after the Update loop.
                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.
}

Eseguire il rendering della grafica del mondo del gioco

Raccomandiamo di aggiornare spesso la grafica in un gioco, idealmente esattamente tutte le volte che il ciclo principale del gioco itera. Durante l'iterazione del ciclo, lo stato del mondo del gioco viene aggiornato, con o senza input del giocatore. In questo modo le animazioni calcolate e i comportamenti possono essere visualizzati senza problemi. Immagina se avessimo una semplice scena d'acqua che si muoveva solo quando il giocatore premeva un pulsante. Non sarebbe realistico; un buon gioco sembra liscio e fluido in ogni momento.

Ricorda il ciclo del gioco di esempio come illustrato in precedenza in GameMain::Run. Se la finestra principale del gioco è visibile, e non è ancorata o disattivata, il gioco continua ad aggiornare e a eseguire il rendering dei risultati dell'aggiornamento. Il metodo GameRenderer::Render che esaminiamo successivamente rende una rappresentazione di tale stato. Questa operazione viene eseguita immediatamente dopo una chiamata a GameMain::Update, che include Simple3DGame::RunGame per aggiornare gli stati, come illustrato nella sezione precedente.

GameRenderer::Render disegna la proiezione del mondo 3D e quindi sovrappone l'overlay Direct2D su di essa. Al termine, presenta la catena di scambio finale con i buffer combinati per la visualizzazione.

Annotazioni

Esistono due stati per la sovrimpressione Direct2D del gioco di esempio: uno in cui il gioco visualizza la sovrimpressione delle informazioni sul gioco che contiene la bitmap per il menu di pausa e uno in cui il gioco visualizza i mirini insieme ai rettangoli per il controller di movimento e visuale del touchscreen. Il testo del punteggio viene disegnato in entrambi gli stati. Per maggiori informazioni, consultare Framework di rendering I: Introduzione al rendering.

Il metodo GameRenderer::Render

void GameRenderer::Render()
{
    bool stereoEnabled{ m_deviceResources->GetStereoState() };

    auto d3dContext{ m_deviceResources->GetD3DDeviceContext() };
    auto d2dContext{ m_deviceResources->GetD2DDeviceContext() };

    ...
        if (m_game != nullptr && m_gameResourcesLoaded && m_levelResourcesLoaded)
        {
            // This section is only used after the game state has been initialized and all device
            // resources needed for the game have been created and associated with the game objects.
            ...
            for (auto&& object : m_game->RenderObjects())
            {
                object->Render(d3dContext, m_constantBufferChangesEveryPrim.get());
            }
        }

        d3dContext->BeginEventInt(L"D2D BeginDraw", 1);
        d2dContext->BeginDraw();

        // To handle the swapchain being pre-rotated, set the D2D transformation to include it.
        d2dContext->SetTransform(m_deviceResources->GetOrientationTransform2D());

        if (m_game != nullptr && m_gameResourcesLoaded)
        {
            // This is only used after the game state has been initialized.
            m_gameHud.Render(m_game);
        }

        if (m_gameInfoOverlay.Visible())
        {
            d2dContext->DrawBitmap(
                m_gameInfoOverlay.Bitmap(),
                m_gameInfoOverlayRect
                );
        }
        ...
    }
}

Classe Simple3DGame

Si tratta dei metodi e dei membri dati definiti dalla classe Simple3DGame .

Funzioni membro

Le funzioni membro pubbliche definite da Simple3DGame includono quelle seguenti.

  • Inizializzare. Imposta i valori iniziali delle variabili globali e inizializza gli oggetti gioco. Questo argomento è illustrato nella sezione Inizializzare e avviare il gioco .
  • LoadGame. Inizializza un nuovo livello e inizia a caricarlo.
  • LoadLevelAsync. Una coroutine che inizializza il livello e quindi richiama un'altra coroutine nel renderer per caricare le risorse del livello specifiche del dispositivo. Questo metodo viene eseguito in un thread separato; Di conseguenza, solo metodi id3D11Device (anziché metodi ID3D11DeviceContext) possono essere chiamati da questo thread. Nel metodo FinalizeLoadLevel vengono chiamati tutti i metodi del contesto di dispositivo. Se sei nuovo alla programmazione asincrona, vedere Concorrenza e operazioni asincrone con C++/WinRT.
  • FinalizeLoadLevel. Completa tutte le operazioni per il bilanciamento del carico che devono essere eseguite sul thread principale. Sono incluse tutte le chiamate ai metodi del contesto dispositivo Direct3D 11 (ID3D11DeviceContext).
  • StartLevel. Avvia il gioco per un nuovo livello.
  • PauseGame. Sospende il gioco.
  • RunGame. Esegue un'iterazione del ciclo di gioco. Viene chiamato da App::Update una volta a ogni iterazione del ciclo di gioco se lo stato del gioco è Attivo.
  • OnSuspending e OnResuming. Sospendi/riprendi l'audio del gioco, rispettivamente.

Ecco le funzioni membro private.

  • LoadSavedState e SaveState. Caricare/salvare rispettivamente lo stato corrente del gioco.
  • LoadHighScore e SaveHighScore. Carica/salva rispettivamente il punteggio massimo tra i giochi.
  • InitializeAmmo. Reimposta lo stato di ogni sfera utilizzata come munizione allo stato originale per l'inizio di ogni round.
  • UpdateDynamics. Questo è un metodo importante perché aggiorna tutti gli oggetti di gioco in base a routine di animazione predefinite, fisiche e input di controllo. Questo è il cuore dell'interattività che definisce il gioco. Questo argomento è illustrato nella sezione Aggiornare il mondo del gioco.

Gli altri metodi pubblici sono funzioni di accesso alle proprietà che restituiscono informazioni specifiche del gioco e della sovrimpressione al framework dell'app per la visualizzazione.

Membri dati

Questi oggetti vengono aggiornati durante l'esecuzione del ciclo del gioco.

  • MoveLookController oggetto. Rappresenta l'input del giocatore. Per ulteriori informazioni, vedere Aggiungi controlli.
  • oggetto GameRenderer. Rappresenta un renderer Direct3D 11, che gestisce tutti gli oggetti specifici del dispositivo e il relativo rendering. Per altre informazioni, vedere Framework di rendering I.
  • oggetto audio. Controlla la riproduzione audio per il gioco. Per ulteriori informazioni, vedere Aggiunta di suono.

Le altre variabili del gioco contengono gli elenchi delle primitive, i rispettivi importi nel gioco, e i dati e vincoli specifici del gioco.

Passaggi successivi

Dobbiamo ancora parlare del motore di rendering effettivo, ovvero come le chiamate ai metodi Render sulle primitive aggiornate vengono trasformate in pixel sul tuo schermo. Questi aspetti sono trattati in due parti:Framework di rendering I: Introduzione al rendering e Framework di rendering II: Rendering del gioco. Se sei più interessato a come i controlli del giocatore aggiornano lo stato del gioco, vedi Aggiunta di controlli.