Condividi tramite


Gestione del flusso di gioco

Nota

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.

Il gioco ha ora una finestra, ha registrato alcuni gestori di eventi e ha caricato gli asset in modo asincrono. Questo argomento illustra l'uso degli stati di gioco, come gestire stati di gioco chiave specifici e come creare un ciclo di aggiornamento per il motore di gioco. Quindi apprenderemo il flusso dell'interfaccia utente e, infine, ne sapremo di più sui gestori di eventi necessari per un gioco UWP.

Stati di gioco utilizzati per gestire il flusso di gioco

Facciamo uso di stati di gioco per gestire il flusso del gioco.

Quando il gioco di esempio Simple3DGameDX viene eseguito per la prima volta su una macchina, si trova in uno stato in cui non è stato avviato alcun gioco. Le volte successive in cui viene eseguito il gioco, può trovarsi in uno qualsiasi di questi stati:

  • Nessun gioco è mai stato avviato o il gioco non si trova tra livelli (il punteggio più alto è zero).
  • Il ciclo di gioco è in esecuzione e si trova al centro di un livello.
  • Il ciclo di gioco non è in esecuzione a causa del completamento di un gioco (il punteggio più alto ha un valore diverso da zero).

Il gioco può avere tutti gli stati che gli sono necessari. Ma non dimenticare che può essere terminato in qualsiasi momento. E quando riprende, l'utente si aspetta che venga ripreso nello stato in cui si trovava quando è stato terminato.

Gestione dello stato del gioco

Dunque, durante l'inizializzazione del gioco, si dovrà supportare un avvio a freddo del gioco nonché la sua ripresa dopo averlo arrestato in corsa. L'esempio Simple3DGameDX salva sempre lo stato del gioco per dare l'impressione che non si sia mai arrestato.

In risposta a un evento di sospensione, è l'esperienza di gioco che viene sospesa, ma le risorse del gioco sono ancora presenti in memoria. Analogamente, l'evento di ripresa viene gestito per garantire che il gioco di esempio venga prelevato nello stato in cui si trovava quando è stato sospeso o è terminato. A seconda dello stato, vengono presentate diverse opzioni al giocatore.

  • Se il gioco riprende a un livello intermedio, viene visualizzato in pausa e una sovrimpressione offre la possibilità di continuare.
  • Se il gioco riprende in uno stato in cui il gioco è completato, visualizza i punteggi più alti e un'opzione per giocare un nuovo gioco.
  • Infine, se il gioco riprende prima dell'avvio di un livello, la sovrimpressione presenta all'utente un'opzione di avvio.

Il gioco di esempio non distingue se il gioco è in avvio a freddo, in avvio per la prima volta senza un evento di sospensione o in ripresa da uno stato sospeso. Questa è la progettazione corretta per qualsiasi app UWP.

In questo esempio, l'inizializzazione degli stati del gioco si verifica in GameMain::InitializeGameState (una struttura di tale metodo viene visualizzata nella sezione successiva).

Ecco un diagramma di flusso che consente di visualizzare il flusso. Illustra sia l'inizializzazione che il ciclo di aggiornamento.

  • L'inizializzazione ha inizio nel nodo Start quando si controlla lo stato corrente del gioco. Per il codice del gioco, vedere GameMain::InitializeGameState nella sezione successiva.

la macchina a stati principale per il nostro gioco

Il metodo GameMain::InitializeGameState

Il metodo GameMain::InitializeGameState viene chiamato indirettamente tramite il costruttore della classe GameMain , che è il risultato della creazione di un'istanza GameMain all'interno di App::Load.

GameMain::GameMain(std::shared_ptr<DX::DeviceResources> const& deviceResources) : ...
{
    m_deviceResources->RegisterDeviceNotify(this);
    ...
    ConstructInBackground();
}

winrt::fire_and_forget GameMain::ConstructInBackground()
{
    ...
    m_renderer->FinalizeCreateGameDeviceResources();

    InitializeGameState();
    ...
}

void GameMain::InitializeGameState()
{
    // Set up the initial state machine for handling Game playing state.
    if (m_game->GameActive() && m_game->LevelActive())
    {
        // The last time the game terminated it was in the middle
        // of a level.
        // We are waiting for the user to continue the game.
        ...
    }
    else if (!m_game->GameActive() && (m_game->HighScore().totalHits > 0))
    {
        // The last time the game terminated the game had been completed.
        // Show the high score.
        // We are waiting for the user to acknowledge the high score and start a new game.
        // The level resources for the first level will be loaded later.
        ...
    }
    else
    {
        // This is either the first time the game has run or
        // the last time the game terminated the level was completed.
        // We are waiting for the user to begin the next level.
        ...
    }
    m_uiControl->ShowGameInfoOverlay();
}

Aggiornare il motore di gioco

Il metodo App::Run chiama GameMain::Run. All'interno di GameMain::Run è presente una macchina a stati di base per la gestione di tutte le azioni principali che un utente può eseguire. Il livello più alto di questa macchina a stati riguarda il caricamento di un gioco, la riproduzione di un livello specifico o la continuazione di un livello dopo che il gioco è stato sospeso (dal sistema o dall'utente).

Nel gioco di esempio, sono presenti 3 stati principali (rappresentati dall'enumerazione UpdateEngineState ) in cui il gioco può trovarsi.

  1. UpdateEngineState::WaitingForResources. Il ciclo di gioco è in ciclo, incapace di eseguire la transizione fino a quando non sono disponibili le risorse (in particolare risorse grafiche). Al termine delle attività asincrone di caricamento delle risorse, aggiorniamo lo stato a UpdateEngineState::ResourcesLoaded. Ciò si verifica in genere tra i livelli quando il livello carica nuove risorse dal disco, da un server di gioco o da un backend su cloud. Nel gioco di esempio, simuliamo questo comportamento, perché l'esempio non richiede risorse aggiuntive per livello in quel momento.
  2. UpdateEngineState::WaitingForPress. Il ciclo del gioco è in ciclo, in attesa di un input specifico da parte dell'utente. Questo input è un'azione del giocatore per caricare un gioco, avviare un livello o continuare un livello. Il codice di esempio fa riferimento a questi stati secondari tramite l'enumerazione PressResultState.
  3. UpdateEngineState::Dynamics. Il ciclo del gioco è in esecuzione con l'utente che gioca. Mentre l'utente sta giocando, il gioco verifica la presenza di 3 condizioni su cui può eseguire la transizione:
  • GameState::TimeExpired. Scadenza del limite di tempo per un livello.
  • GameState::LevelComplete. Completamento di un livello da parte del giocatore.
  • GameState::GameComplete. Completamento di tutti i livelli da parte del giocatore.

Un gioco è semplicemente una macchina a stati contenente più macchine a stati più piccole. Ogni stato specifico deve essere definito da criteri molto specifici. Le transizioni da uno stato a un altro devono essere basate sull'input discreto dell'utente o su azioni di sistema, ad esempio il caricamento di risorse grafiche.

Durante la pianificazione del gioco, valutare la possibilità di disegnare l'intero flusso di gioco per accertarsi di aver risolto tutte le possibili azioni che l'utente o il sistema può eseguire. Un gioco può essere molto complicato, quindi una macchina a stati è uno strumento potente per aiutare a visualizzare questa complessità e renderla più gestibile.

Esaminiamo ora il codice per il ciclo di aggiornamento.

Il metodo GameMain::Update

SI tratta della struttura della macchina a stati utilizzata per aggiornare il motore di gioco.

void GameMain::Update()
{
    // The controller object has its own update loop.
    m_controller->Update(); 

    switch (m_updateState)
    {
    case UpdateEngineState::WaitingForResources:
        ...
        break;

    case UpdateEngineState::ResourcesLoaded:
        ...
        break;

    case UpdateEngineState::WaitingForPress:
        if (m_controller->IsPressComplete())
        {
            ...
        }
        break;

    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)
            {
            case GameState::TimeExpired:
                ...
                break;

            case GameState::LevelComplete:
                ...
                break;

            case GameState::GameComplete:
                ...
                break;
            }
        }

        if (m_updateState == UpdateEngineState::WaitingForPress)
        {
            // Transitioning state, so enable waiting for the press event.
            m_controller->WaitForPress(
                m_renderer->GameInfoOverlayUpperLeft(),
                m_renderer->GameInfoOverlayLowerRight());
        }
        if (m_updateState == UpdateEngineState::WaitingForResources)
        {
            // Transitioning state, so shut down the input controller
            // until resources are loaded.
            m_controller->Active(false);
        }
        break;
    }
}

Aggiornare l'interfaccia utente

Dobbiamo mantenere il giocatore informato dello stato del sistema e consentire allo stato del gioco di cambiare a seconda delle azioni del giocatore e delle regole che definiscono il gioco. Molti giochi, incluso questo gioco di esempio, utilizzano in genere elementi dell'interfaccia utente per presentare queste informazioni al giocatore. L'interfaccia utente contiene rappresentazioni dello stato del gioco e altre informazioni specifiche del gioco, quali punteggio, munizioni o il numero di probabilità residue. L'interfaccia utente viene anche chiamata sovrimpressione perché viene eseguito il rendering separatamente dalla pipeline grafica principale e posizionato sopra la proiezione 3D.

Alcune informazioni sull'interfaccia utente vengono anche presentate come un heads-up display (HUD) per consentire all'utente di visualizzare tali informazioni senza distogliere gli occhi completamente dall'area di gioco principale. Nel gioco di esempio creiamo questa sovrimpressione tramite le API Direct2D. In alternativa, è possibile creare questa sovrimpressione utilizzando XAML, che verrà illustrato in Estensione del gioco di esempio.

Esistono due componenti per l'interfaccia utente.

  • L'HUD che contiene il punteggio e le informazioni sullo stato corrente del gioco.
  • Il bitmap di pausa, ovvero un rettangolo nero con testo in sovraimpressione durante lo stato di pausa/sospensione del gioco. Questa è la sovrimpressione del gioco. Verrà illustrata più avanti in Aggiunta di un'interfaccia utente.

Non stupisce che anche la sovrimpressione dispone di una macchina a stati. La sovrimpressione può visualizzare un messaggio di avvio livello o di gioco concluso. Si tratta essenzialmente di un'area su cui possiamo restituire informazioni sullo stato del gioco che vogliamo visualizzare al giocatore mentre il gioco è in pausa o sospeso.

La sovrimpressione sottoposta renderizzata può essere una delle seguenti sei schermate, a seconda dello stato del gioco.

  1. Schermata della progressione del caricamento delle risorse all'inizio del gioco.
  2. Schermata delle statistiche di gioco.
  3. Schermata del messaggio di avvio livello.
  4. Schermata di gioco concluso quando tutti i livelli vengono completati senza che il tempo si sia esaurito.
  5. Schermata di gioco concluso quando il tempo scade.
  6. Schermata del menu di pausa.

La separazione dell'interfaccia utente dalla pipeline grafica del gioco consente di lavorare su di essa in modo indipendente dal motore di rendering della grafica del gioco e riduce significativamente la complessità del codice del gioco.

Ecco come il gioco di esempio struttura la macchina a stati della sovrimpressione.

void GameMain::SetGameInfoOverlay(GameInfoOverlayState state)
{
    m_gameInfoOverlayState = state;
    switch (state)
    {
    case GameInfoOverlayState::Loading:
        m_uiControl->SetGameLoading(m_loadingCount);
        break;

    case GameInfoOverlayState::GameStats:
        ...
        break;

    case GameInfoOverlayState::LevelStart:
        ...
        break;

    case GameInfoOverlayState::GameOverCompleted:
        ...
        break;

    case GameInfoOverlayState::GameOverExpired:
        ...
        break;

    case GameInfoOverlayState::Pause:
        ...
        break;
    }
}

Gestione degli eventi

Come abbiamo visto nell'argomento Definire il framework dell'app UWP di un gioco, molti dei metodi del provider di viste della classe App registrano gestori di eventi. Questi metodi devono gestire correttamente questi eventi importanti prima di aggiungere meccanismi di gioco o avviare lo sviluppo della grafica.

La corretta gestione degli eventi in questione è fondamentale per l'esperienza dell'app UWP. Poiché un'app UWP può essere attivata, disattivata, ridimensionata, bloccata, sbloccata, sospesa o ripresa in qualsiasi momento, il gioco deve registrarsi per questi eventi non appena possibile e gestirli in modo da mantenere un'esperienza uniforme e prevedibile per il giocatore.

Questi sono i gestori di eventi utilizzati in questo esempio e gli eventi gestiti.

Gestore di eventi Descrizione
OnActivated Gestisce CoreApplicationView::Activated. L'app di gioco è stata portata in primo piano, quindi la finestra principale viene attivata.
OnDpiChanged Gestisce Graphics::Display::DisplayInformation::DpiChanged. Il valore DPI dello schermo è cambiato e il gioco regola le sue risorse di conseguenza.
Nota Le coordinate CoreWindow si trovano in pixel indipendenti dal dispositivo (DIP) per Direct2D. Di conseguenza, è necessario notificare a Direct2D la modifica in DPI per visualizzare correttamente qualsiasi asset o primitive 2D.
OnOrientationChanged Gestisce Graphics::Display::DisplayInformation::OrientationChanged. L'orientamento della visualizzazione cambia e il rendering deve essere aggiornato.
OnDisplayContentsInvalidated Gestisce Graphics::Display::DisplayInformation::DisplayContentsInvalidated. Lo schermo richiede che l'interfaccia venga ridisegnata e il rendering del gioco deve essere nuovamente eseguito.
OnResuming Gestisce CoreApplication::Resuming. L'app di gioco ripristina il gioco da uno stato sospeso.
OnSuspending Gestisce CoreApplication::Suspending. L'app del gioco salva il suo stato sul disco. Ha 5 secondi per salvare lo stato nella memoria di archiviazione.
OnVisibilityChanged Gestisce CoreWindow::VisibilityChanged. L'app del gioco ha cambiato visibilità ed è diventata visibile o resa invisibile da un'altra app che diventa visibile.
OnWindowActivationChanged Gestisce CoreWindow::Activated. La finestra principale dell'app del gioco è stata disattivata o attivata, quindi deve rimuovere lo stato attivo e mette in pausa il gioco o riprendere lo stato attivo. In entrambi i casi, la sovrimpressione indica che il gioco è in pausa.
OnWindowClosed Gestisce CoreWindow::Closed. L'app del gioco chiude la finestra principale e sospende il gioco.
OnWindowSizeChanged Gestisce CoreWindow::SizeChanged. L'app di gioco rialloca le risorse grafiche e la sovrimpressione per adattarle alla modifica delle dimensioni e quindi aggiorna il target di rendering.

Passaggi successivi

In questo argomento abbiamo visto come viene gestito il flusso generale del gioco utilizzando gli stati del gioco e che un gioco è costituito da più macchine a stati diverse. Abbiamo anche visto come aggiornare l'interfaccia utente e gestire i gestori di eventi dell'app chiave. Ora siamo pronti per approfondire il ciclo di rendering, il gioco e i suoi meccanismi.

È possibile esaminare gli argomenti rimanenti che documentano questo gioco in qualsiasi ordine.