Definire l'oggetto principale del 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.
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, sarà 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 il modo per tradurre le regole del gioco in interazioni con il mondo del gioco.
Obiettivi
- Informazioni su come applicare tecniche di sviluppo di base per implementare regole e meccanismi di gioco per un gioco UWP DirectX.
Oggetto principale del gioco
Nel gioco di esempio Simple3DGameDX, Simple3DGame è la classe dell'oggetto principale del gioco. Un'istanza di Simple3DGame viene costruita, indirettamente, tramite il metodo App::Load.
Ecco alcune delle funzionalità della classe Simple3DGame.
- Contiene l'implementazione della logica di gioco.
- Contiene metodi che comunicano questi dettagli.
- Modifiche nello stato del gioco alla macchina a stati definita nel framework dell'app.
- Modifiche nello stato del gioco dall'app all'oggetto del gioco stesso.
- Dettagli per l'aggiornamento dell'interfaccia utente del gioco (sovrimpressione ed heads-up display), animazioni e fisica (le dinamiche).
Nota
L'aggiornamento della grafica viene gestito dalla classe GameRenderer, che contiene metodi per ottenere e utilizzare le risorse grafiche del dispositivo utilizzate dal gioco. Per maggiori 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 realizzare i livelli. In questo esempio questa operazione viene eseguita quando viene creata l'istanza GameMain in App::Load.
L'oggetto del gioco, di tipo Simple3DGame, viene creato nel costruttore GameMain::GameMain. Questo viene quindi inizializzato utilizzando il metodo Simple3DGame::Initialize durante la coroutine "fire-and-forget" GameMain::ConstructInBackground, che viene chiamata da GameMain::GameMain.
Il metodo Simple3DGame::Initialize
Il gioco di esempio configura questi componenti nell'oggetto del 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 del gioco e la bitmap iniziale da sovrapporre all'interno del gioco stesso.
- Viene creata una nuova telecamera con una serie specifica di parametri di visuale e proiezione.
- Il dispositivo di input (il controller) è impostato sulle stesse inclinazione e imbardata iniziali della telecamera, quindi il giocatore ha una corrispondenza da 1 a 1 tra la posizione di controllo iniziale e la posizione della telecamera.
- L'oggetto del giocatore viene creato e impostato su attivo. Utilizziamo un oggetto sfera per rilevare la vicinanza del giocatore alle pareti e agli ostacoli e per evitare che la telecamera venga collocata in una posizione che potrebbe interrompere l'immersione.
- Viene creata la primitiva globale del gioco.
- Vengono creati gli ostacoli del cilindro.
- I bersagli (oggetti Face) vengono creati e numerati.
- Vengono create le sfere di munizioni.
- Vengono creati i livelli.
- Viene caricato il punteggio elevato.
- Viene caricato qualsiasi stato del gioco salvato in precedenza.
Il gioco ha ora istanze di tutti i componenti chiave, 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.
Realizzare e caricare i livelli di gioco
La maggior parte dei carichi pesanti per la costruzione del livello viene eseguita nei file Level[N].h/.cpp
presenti nella cartella GameLevels della soluzione di esempio. Poiché si tratta di un'implementazione molto specifica, non le tratteremo in questa sede. La cosa importante è che il codice per ogni livello venga eseguito come oggetto Level[N] separato. Se si desidera estendere il gioco, è possibile creare un oggetto Level[N] che accetta un numero assegnato come parametro e posiziona in modo casuale gli ostacoli e i target. In alternativa, è possibile far caricare i dati di configurazione del livello da un file di risorse o addirittura da Internet.
Definire l'esperienza di gioco
A questo punto, abbiamo tutti i componenti necessari per sviluppare il gioco. I livelli sono stati costruiti in memoria dalle primitive e sono pronti per iniziare a interagire con il giocatore.
I migliori giochi reagiscono immediatamente all'input del giocatore e forniscono un feedback immediato. Questo è vero per qualsiasi tipo di gioco, dai giochi frenetici e pieni d'azione, agli sparatutto in prima persona in tempo reale, a giochi di strategia ponderati e basati su turni.
Il 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 frame, come illustrato 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 dell'esperienza di 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 fa il conto alla rovescia fino al completamento del livello e verifica se il tempo del livello è scaduto. Questa è una delle regole del gioco: quando il tempo scade, se non tutti i bersagli sono stati colpiti, allora il gioco si conclude.
- Se il tempo è scaduto, il metodo imposta lo stato di gioco TimeExpired e torna al metodo Update nel codice precedente.
- Se rimane tempo, viene eseguito il polling del controller move-look per un aggiornamento sulla posizione della telecamera; in particolare, un aggiornamento sull'angolo di visuale normale proiettato dal piano della telecamera (dove il giocatore sta guardando) e di quanto questo angolo sia stato spostato rispetto all'ultimo polling del controller.
- La telecamera viene aggiornata in base ai nuovi dati del controller move-look.
- Le dinamiche, o le animazioni e i comportamenti degli oggetti nel mondo del gioco indipendentemente dal controllo 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 attivate, l'animazione degli ostacoli dei pilastri e il movimento dei bersagli. Per maggiori informazioni, vedere Aggiornare il mondo del 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 o meno dell'ultimo livello (di 6). Se è l'ultimo livello, allora il metodo restituisce lo stato di gioco GameState::GameComplete; in caso contrario, restituisce lo stato di gioco GameState::LevelComplete.
- Se il livello non è completo, allora il metodo imposta lo stato di gioco su GameState::Active e torna indietro.
Aggiornare il mondo del gioco
In questo esempio, quando il gioco è in esecuzione, viene chiamato il metodo Simple3DGame::UpdateDynamics 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 quale UpdateDynamics chiama tutti i metodi utilizzati per impostare il mondo del gioco in movimento, indipendentemente dall'input del giocatore, per creare un'esperienza di gioco immersiva e rendere attivo il 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 giocatore. Nel gioco, ciò potrebbe includere alberi ondeggianti nel vento, onde che si increspano lungo litorali, fumo di macchinari e mostri alieni che si allungano e si spostano nell'ambiente. 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 è chiamato dinamica, e comprende la salita e la discesa degli ostacoli dei pilastri, e il movimento e i comportamenti fisici delle sfere di munizioni quando vengono sparate e in movimento.
Il metodo Simple3DGame::UpdateDynamics
Questo metodo gestisce le seguenti quattro serie di calcoli:
- Le posizioni delle sfere di munizioni sparate nel mondo.
- L'animazione degli ostacoli dei pilastri.
- L'intersezione tra il giocatore e i confini del mondo.
- Le collisioni delle sfere di munizioni con gli ostacoli, i bersagli, altre sfere di munizioni e il mondo.
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 una serie 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.
Rivedere il flusso
Ora che abbiamo aggiornato tutti gli oggetti della scena e calcolate le eventuali collisioni, dobbiamo utilizzare queste informazioni per disegnare le modifiche di visuale corrispondenti.
Dopo che GameMain::Update ha completato l'iterazione corrente del ciclo di gioco, l'esempio chiama immediatamente GameRenderer::Render per acquisire i dati dell'oggetto aggiornati 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
Consigliamo di aggiornare spesso la grafica in un gioco, preferibilmente con la stessa frequenza con cui si ripete il ciclo principale del gioco. Durante l'iterazione del ciclo, lo stato del mondo del gioco viene aggiornato, con o senza l'input da parte del giocatore. In questo modo, animazioni e comportamenti calcolati possono essere visualizzati senza problemi. Si immagini se avessimo una semplice scena di acqua che si muove solo quando il giocatore preme un pulsante. Non sarebbe realistico; in un buon gioco deve sembrare liscia e fluida per tutto il tempo.
Richiamare il ciclo del gioco di esempio come illustrato sopra in GameMain::Run. Se la finestra principale del gioco è visibile e non è ancorata o disattivata, il gioco continua ad aggiornare ed eseguire il rendering dei risultati di tale aggiornamento. Il metodo GameRenderer::Render che esaminiamo successivamente esegue il rendering di 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 descritto nella sezione precedente.
GameRenderer::Render disegna la proiezione del mondo 3D e quindi disegna la sovrimpressione Direct2D sopra di essa. Al termine, presenta la catena di scambio finale con i buffer combinati per la visualizzazione.
Nota
Esistono due stati per la sovrimpressione Direct2D del gioco di esempio: uno in cui il gioco visualizza la sovrimpressione delle informazioni sul gioco che contengono la bitmap per il menu di pausa e una in cui il gioco visualizza i mirini insieme ai rettangoli per il controller move-look del touchscreen. Il testo del punteggio viene disegnato in entrambi gli stati. Per maggiori informazioni, vedere 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
);
}
...
}
}
La classe Simple3DGame
Si tratta dei metodi e dei membri di dati definiti dalla classe Simple3DGame.
Funzioni membro
Le funzioni membro pubbliche definite da Simple3DGame includono quelle seguenti:
- Initialize. Imposta i valori iniziali delle variabili globali e inizializza gli oggetti del gioco. Questo argomento è illustrato nella sezione Inizializzare e avviare il gioco.
- LoadGame. Inizializza un nuovo livello e inizia a caricarlo.
- LoadLevelAsync. Coroutine che inizializza il livello e quindi richiama un'altra coroutine nel renderer per caricare le risorse a un livello specifico del dispositivo. Questo metodo viene eseguito in un thread separato; di conseguenza, è possibile chiamare solo metodi ID3D11Device (anziché metodi ID3D11DeviceContext) da questo thread. Tutti i metodi del contesto di dispositivo vengono chiamati nel metodo FinalizeLoadLevel. Se non si ha familiarità con la programmazione asincrona, vedere Concorrenza e operazioni asincrone con C++/WinRT.
- FinalizeLoadLevel. Completa tutte le operazioni per il caricamento di livello che deve essere eseguito sul thread principale. Sono incluse tutte le chiamate ai metodi di contesto del dispositivo Direct3D 11 (ID3D11DeviceContext).
- StartLevel. Avvia l'esperienza di gioco per un nuovo livello.
- PauseGame. Mette in pausa il gioco.
- RunGame. Esegue un'iterazione del ciclo di gioco. Viene chiamato da App::Update una volta ogni iterazione del ciclo di gioco se lo stato del gioco è Active.
- OnSuspending e OnResuming. Sospendere/riprendere l'audio del gioco, rispettivamente.
Ecco le funzioni membro private:
- LoadSavedState e SaveState. Caricare/salvare lo stato corrente del gioco, rispettivamente.
- LoadHighScore e SaveHighScore. Caricare/salvare il punteggio più alto tra i giochi, rispettivamente.
- InitializeAmmo. Riporta lo stato di ogni oggetto sfera utilizzato come munizioni allo stato originale per l'inizio di ciascun round.
- UpdateDynamics. Questo è un metodo importante perché aggiorna tutti gli oggetti di gioco in base a routine di animazione predefinite, fisica e input di controllo. Si tratta del cuore dell'interattività che definisce il gioco. Questa sezione è descritta 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 di dati
Questi oggetti vengono aggiornati durante l'esecuzione del ciclo di gioco.
- Oggetto MoveLookController. Rappresenta l'input del giocatore. Per maggiori informazioni, vedere Aggiunta di controlli.
- Oggetto GameRenderer. Rappresenta un renderer Direct3D 11, che gestisce tutti gli oggetti specifici del dispositivo e il relativo rendering. Per maggiori informazioni, vedere Framework di rendering I.
- Oggetto audio. Controlla la riproduzione audio per il gioco. Per maggiori informazioni, vedere Aggiunta di suoni.
Il resto delle variabili di gioco contiene le liste delle primitive e i rispettivi quantitativi in gioco, nonché dati specifici del gameplay e vincoli.
Passaggi successivi
Dobbiamo ancora parlare del motore di rendering effettivo: come le chiamate ai metodi Render sulle primitive aggiornate vengono trasformate in pixel sullo schermo. Questi aspetti sono trattati in due parti: Framework di rendering I: Introduzione al rendering e Framework di rendering II: Rendering del gioco. Se si è più interessati a come il giocatore controlla l'aggiornamento dello stato del gioco, vedere Aggiunta di controlli.