Loop di gioco "ottimale" per scroller laterale 2D


14

È possibile descrivere un layout "ottimale" (in termini di prestazioni) per un loop di gioco a scorrimento laterale 2D? In questo contesto il "loop di gioco" prende l'input dell'utente, aggiorna gli stati degli oggetti di gioco e disegna gli oggetti di gioco.

Ad esempio avere una classe base GameObject con una gerarchia di eredità profonda potrebbe essere utile per la manutenzione ... puoi fare qualcosa del tipo:

foreach(GameObject g in gameObjects) g.update();

Tuttavia, penso che questo approccio possa creare problemi di prestazioni.

D'altro canto, tutti i dati e le funzioni di tutti gli oggetti di gioco potrebbero essere globali. Che sarebbe un mal di testa da manutenzione ma potrebbe essere più vicino a un loop di gioco dalle prestazioni ottimali.

qualche idea? Sono interessato ad applicazioni pratiche con una struttura del loop di gioco quasi ottimale ... anche se ricevo mal di testa da manutenzione in cambio di grandi prestazioni.


Risposte:


45

Ad esempio avere una classe base GameObject con una gerarchia di eredità profonda potrebbe essere utile per la manutenzione ...

In realtà, le gerarchie profonde sono generalmente peggiori per la manutenibilità rispetto a quelle poco profonde, e lo stile architettonico moderno per gli oggetti di gioco tende a approcci superficiali e basati su aggregazioni .

Tuttavia, penso che questo approccio possa creare problemi di prestazioni. D'altra parte, tutti i dati e le funzioni di tutti gli oggetti di gioco potrebbero essere globali. Che sarebbe un mal di testa da manutenzione ma potrebbe essere più vicino a un loop di gioco dalle prestazioni ottimali.

Il ciclo che hai mostrato potenzialmente ha problemi di prestazioni, ma non, come è implicito nella tua dichiarazione successiva, perché hai dati di istanza e funzioni membro nella GameObjectclasse. Piuttosto, il problema è che se trattate tutti gli oggetti nel gioco esattamente come gli stessi, probabilmente non state raggruppando quegli oggetti in modo intelligente - sono probabilmente sparsi casualmente in tutto l'elenco. Potenzialmente, quindi, ogni chiamata al metodo di aggiornamento per quell'oggetto (indipendentemente dal fatto che quel metodo sia una funzione globale o meno e che l'oggetto abbia dati di istanza o "dati globali" fluttuanti in una tabella in cui stai indicizzando o qualunque altra cosa) è diverso dalla chiamata di aggiornamento nelle iterazioni dell'ultimo ciclo.

Ciò può aumentare la pressione sul sistema in quanto potrebbe essere necessario sfogliare la memoria con la relativa funzione dentro e fuori memoria e riempire più frequentemente la cache delle istruzioni, causando un ciclo più lento. Se questo è osservabile a occhio nudo (o anche in un profiler) dipende esattamente da cosa è considerato un "oggetto di gioco", quanti di essi esistono in media e cos'altro accade nella tua applicazione.

I sistemi di oggetti orientati ai componenti sono una tendenza popolare in questo momento, sfruttando la filosofia secondo cui l' aggregazione è preferibile all'eredità . Tali sistemi potenzialmente consentono di dividere la logica di "aggiornamento" dei componenti (in cui "componente" è approssimativamente definito come un'unità di funzionalità, come la cosa che rappresenta la parte fisicamente simulata di un oggetto, che viene elaborata dal sistema fisico ) su più thread, discriminati dal tipo di componente, se possibile e desiderato, che potrebbero avere un guadagno in termini di prestazioni. Come minimo è possibile organizzare i componenti in modo tale che tutti i componenti di un determinato tipo si aggiornino insieme , facendo un uso ottimale della cache della CPU. Un esempio di tale sistema orientato ai componenti è discusso in questo thread .

Tali sistemi sono spesso altamente disaccoppiati, il che è anche un vantaggio per la manutenzione.

La progettazione orientata ai dati è un approccio correlato: si tratta di orientarsi intorno ai dati richiesti dagli oggetti come una priorità assoluta, in modo che i dati possano essere effettivamente elaborati in blocco (ad esempio). Ciò significa generalmente un'organizzazione che cerca di mantenere insieme i dati utilizzati per lo stesso cluster di scopi e di operare contemporaneamente. Non è fondamentalmente incompatibile con il design OO e puoi trovare alcune chiacchiere sull'argomento qui su GDSE in questa domanda .

In effetti, al posto del tuo originale sarebbe un approccio più ottimale al loop di gioco

foreach(GameObject g in gameObjects) g.update();

qualcosa di più simile

ProcessUserInput();
UpdatePhysicsForAllObjects();
UpdateScriptsForAllObjects();
UpdateRenderDataForAllObjects();
RenderEverything();

In un mondo del genere, ogni GameObjectpotrebbe avere un puntatore o riferimento al proprio PhysicsDatao di Scripto RenderData, per i casi in cui si potrebbe aver bisogno di interagire con gli oggetti su base individuale, ma l'attuale PhysicsData, Scripts, RenderData, eccetera saranno tutti di proprietà dei rispettivi sottosistemi (simulatore di fisica, ambiente di hosting degli script, renderer) ed elaborati in blocco come indicato sopra.

È molto importante notare che questo approccio non è un proiettile magico e non sempre produrrà un miglioramento delle prestazioni (sebbene sia generalmente una progettazione migliore di un albero di eredità profondo). In particolare, noterai praticamente nessuna differenza di prestazione se hai pochissimi oggetti o molti oggetti per i quali non puoi parallelizzare efficacemente gli aggiornamenti.

Sfortunatamente, non esiste un loop magico così ottimale: ogni gioco è diverso e potrebbe essere necessario ottimizzare le prestazioni in diversi modi. Quindi è molto importante misurare (profilare) le cose prima di andare alla cieca seguendo il consiglio di un ragazzo a caso su Internet.


5
Buon Dio, questa è un'ottima risposta.
Raveline,

2

Lo stile di programmazione di oggetti di gioco va bene per i progetti più piccoli: man mano che il progetto diventa davvero enorme, finirai con una gerarchia di eredità profonda e la base di oggetti di gioco diventerà davvero pesante!

Ad esempio ... Supponiamo che tu abbia iniziato a creare una base di oggetti di gioco con attributi minimi come Position (per xey), displayObject (si riferisce all'immagine se è un elemento di disegno), larghezza, altezza ecc ...

Ora in seguito, se è necessario aggiungere una sottoclasse che avrà uno stato di animazione, probabilmente si sposta 'codice controllo animazione' (diciamo currentAnimationId, Numero di frame per questa animazione, ecc.) Su GameObjectBase nel pensiero che la maggior parte di gli oggetti avranno un'animazione.
Successivamente, se si desidera aggiungere un corpo rigido statico (senza animazioni), è necessario controllare il "codice di controllo animazione" nella funzione di aggiornamento di GameObject Base (anche se è un controllo se l'animazione è presente o meno. ..è importante!) Contrariamente a questo, se segui la Struttura basata su componenti, avrai un 'Componente di controllo animazione' collegato al tuo oggetto. Se necessario, lo collegherai. Per un corpo statico non collegherai quel componente. modo, possiamo evitare controlli non necessari.

Quindi, la scelta dello stile dipende dalla dimensione del progetto. La struttura di GameObject è facile da padroneggiare solo per progetti più piccoli. Spero che questo aiuti un po '.


@Josh Petrie - Davvero un'ottima risposta!
Ayyappa,

@Josh Petrie - Davvero una buona risposta con link utili! (Sry non so come inserire questo commento vicino al tuo post: P)
Ayyappa

Puoi fare clic sul link "aggiungi commento" sotto la mia risposta, normalmente, ma ciò richiede una reputazione maggiore di quella che hai attualmente (vedi gamedev.stackexchange.com/privileges/comment ). E grazie!
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.