Suggerirei di iniziare leggendo le 3 grandi bugie di Mike Acton, perché ne violate due. Sono serio, questo cambierà il modo in cui progetti il tuo codice: http://cellperformance.beyond3d.com/articles/2008/03/three-big-lies.html
Quindi, quale violi?
Bugia n. 3 - Il codice è più importante dei dati
Parli dell'iniezione di dipendenza, che può essere utile in alcuni (e solo in alcuni) casi, ma dovrebbe sempre suonare un campanello d'allarme grande se lo usi, specialmente nello sviluppo del gioco! Perché? Perché è un'astrazione spesso non necessaria. E le astrazioni nei posti sbagliati sono orribili. Quindi hai un gioco. Il gioco ha gestori per diversi componenti. I componenti sono tutti definiti. Quindi crea una classe da qualche parte nel tuo codice di loop di gioco principale che "abbia" i gestori. Piace:
private CollissionManager _collissionManager;
private BulletManager _bulletManager;
Dagli alcune funzioni getter per ottenere ogni classe manager (getBulletManager ()). Forse questa stessa classe è un Singleton o è raggiungibile da uno (probabilmente hai un Singleton centrale di gioco da qualche parte). Non c'è niente di sbagliato nei dati e nel comportamento ben definiti.
Non creare un ManagerManager che ti consenta di registrare Manager usando una chiave, che può essere recuperata usando quella chiave da altre classi che vogliono usare il manager. È un sistema eccezionale e molto flessibile, ma in cui si parla di un gioco qui. Sai esattamente quali sistemi sono presenti nel gioco. Perché fingere di no? Perché questo è un sistema per persone che pensano che il codice sia più importante dei dati. Diranno "Il codice è flessibile, i dati lo riempiono e basta". Ma il codice è solo dati. Il sistema che ho descritto è molto più semplice, più affidabile, più facile da mantenere e molto più flessibile (ad esempio, se il comportamento di un manager differisce dagli altri manager, devi solo cambiare alcune righe invece di rielaborare l'intero sistema)
Lie # 2 - Il codice dovrebbe essere progettato attorno a un modello del mondo
Quindi hai un'entità nel mondo di gioco. L'entità ha un numero di componenti che ne definiscono il comportamento. Quindi si crea una classe Entity con un elenco di oggetti Component e una funzione Update () che chiama la funzione Update () di ciascun Component. Giusto?
No :) Si tratta di progettare un modello del mondo: hai proiettili nel tuo gioco, quindi aggiungi una classe Bullet. Quindi aggiorni ogni punto elenco e passi a quello successivo. Questo ucciderà assolutamente le tue prestazioni e ti darà una base di codice contorta orribile con codice duplicato ovunque e nessuna strutturazione logica di codice simile. (Controlla la mia risposta qui per una spiegazione più dettagliata del perché il design OO tradizionale fa schifo o cerca Design orientato ai dati)
Diamo un'occhiata alla situazione senza il nostro pregiudizio OO. Vogliamo quanto segue, né più né meno (si noti che non è necessario creare una classe per entità o oggetto):
- Hai un mucchio di entità
- Le entità comprendono un numero di componenti che definiscono il comportamento dell'entità
- Vuoi aggiornare ogni componente del gioco in ogni frame, preferibilmente in modo controllato
- Oltre a identificare i componenti come appartenenti insieme, non c'è nulla che l'entità stessa debba fare. È un collegamento / ID per un paio di componenti.
E diamo un'occhiata alla situazione. Il tuo sistema di componenti aggiornerà il comportamento di ogni oggetto nel gioco in ogni frame. Questo è sicuramente un sistema critico per il tuo motore. Le prestazioni sono importanti qui!
Se hai familiarità con l'architettura del computer o il Design orientato ai dati, sai come si ottengono le migliori prestazioni: memoria compatta e raggruppando l'esecuzione del codice. Se esegui frammenti di codice A, B e C in questo modo: ABCABCABC, non otterrai le stesse prestazioni di quando lo esegui in questo modo: AAABBBCCC. Questo non è solo perché le istruzioni e la cache dei dati verranno utilizzate in modo più efficiente, ma anche perché se si eseguono tutte le "A" una dopo l'altra, c'è molto spazio per l'ottimizzazione: rimozione di codice duplicato, precalcolo dei dati utilizzati da tutte le "A", ecc.
Quindi, se vogliamo aggiornare tutti i componenti, non rendiamoli classi / oggetti con una funzione di aggiornamento. Non chiamiamo quella funzione di aggiornamento per ciascun componente in ciascuna entità. Questa è la soluzione "ABCABCABC". Raggruppiamo tutti gli aggiornamenti identici dei componenti. Quindi possiamo aggiornare tutti i componenti A, seguiti da B, ecc. Di cosa abbiamo bisogno per farlo?
In primo luogo, abbiamo bisogno dei componenti manager. Per ogni tipo di componente nel gioco, abbiamo bisogno di una classe manager. Ha una funzione di aggiornamento che aggiornerà tutti i componenti di quel tipo. Ha una funzione di creazione che aggiungerà un nuovo componente di quel tipo e una funzione di rimozione che distruggerà il componente specificato. Potrebbero esserci altre funzioni di supporto per ottenere e impostare dati specifici per quel componente (es: impostare il modello 3D per Componente modello). Si noti che il gestore è in qualche modo una scatola nera per il mondo esterno. Non sappiamo come sono memorizzati i dati di ciascun componente. Non sappiamo come ogni componente viene aggiornato. Non ci interessa, purché i componenti si comportino come dovrebbero.
Quindi abbiamo bisogno di un'entità. Potresti rendere questo un corso, ma non è assolutamente necessario. Un'entità non può essere altro che un ID intero univoco o una stringa con hash (quindi anche un numero intero). Quando si crea un componente per l'Entità, si passa l'ID come argomento al Manager. Quando si desidera rimuovere il componente, si passa nuovamente l'ID. Potrebbero esserci alcuni vantaggi nell'aggiungere un po 'più di dati all'entità invece di renderlo un ID, ma quelli saranno solo funzioni di supporto perché, come ho elencato nei requisiti, tutto il comportamento dell'entità è definito dai componenti stessi. È il tuo motore, quindi fai ciò che ha senso per te.
Ciò di cui abbiamo bisogno è un Entity Manager. Questa classe genererà ID univoci se si utilizza la soluzione solo ID o può essere utilizzata per creare / gestire oggetti Entity. Può anche tenere un elenco di tutte le entità del gioco se ne hai bisogno. L'Entity Manager potrebbe essere la classe centrale del tuo sistema di componenti, memorizzando i riferimenti a tutti i ComponentManager nel tuo gioco e chiamando le loro funzioni di aggiornamento nell'ordine giusto. In questo modo tutto il loop di gioco deve fare è chiamare EntityManager.update () e l'intero sistema è ben separato dal resto del tuo motore.
Questa è la visione a volo d'uccello, diamo un'occhiata a come funzionano i gestori dei componenti. Ecco cosa ti serve:
- Crea dati componente quando viene chiamato create (entityID)
- Elimina i dati dei componenti quando viene chiamato remove (entityID)
- Aggiorna tutti i dati dei componenti (applicabili) quando viene chiamato update () (ovvero non tutti i componenti devono aggiornare ogni frame)
L'ultimo è dove si definisce il comportamento / la logica dei componenti e dipende completamente dal tipo di componente che si sta scrivendo. Il componente Animation aggiorna i dati di animazione in base al frame in cui si trova. DragableComponent aggiornerà solo un componente che viene trascinato dal mouse. PhysicsComponent aggiornerà i dati nel sistema fisico. Tuttavia, poiché si aggiornano tutti i componenti dello stesso tipo in una volta, è possibile eseguire alcune ottimizzazioni che non sono possibili quando ogni componente è un oggetto separato con una funzione di aggiornamento che può essere chiamata in qualsiasi momento.
Si noti che non ho ancora mai richiesto la creazione di una classe XxxComponent per contenere i dati dei componenti. Dipende da te. Ti piace il design orientato ai dati? Quindi strutturare i dati in matrici separate per ogni variabile. Ti piace il design orientato agli oggetti? (Non lo consiglierei, ucciderà comunque le tue prestazioni in molti punti) Quindi crea un oggetto XxxComponent che conterrà i dati di ciascun componente.
La cosa grandiosa dei gestori è l'incapsulamento. Ora l'incapsulamento è una delle filosofie più orribilmente abusate nel mondo della programmazione. Ecco come dovrebbe essere usato. Solo il gestore sa quali dati dei componenti sono archiviati dove, come funziona la logica di un componente. Ci sono alcune funzioni per ottenere / impostare i dati, ma il gioco è fatto. Potresti riscrivere l'intero gestore e le sue classi sottostanti e se non cambi l'interfaccia pubblica, nessuno se ne accorge nemmeno. Motore fisico modificato? Riscrivi PhysicsComponentManager e il gioco è fatto.
Quindi c'è un'ultima cosa: comunicazione e condivisione dei dati tra i componenti. Ora questo è difficile e non esiste una soluzione unica per tutti. È possibile creare funzioni get / set nei gestori per consentire ad esempio al componente di collisione di ottenere la posizione dal componente position (ovvero PositionManager.getPosition (entityID)). È possibile utilizzare un sistema di eventi. Potresti archiviare alcuni dati condivisi in entità (la soluzione più brutta a mio avviso). È possibile utilizzare (spesso utilizzato) un sistema di messaggistica. Oppure usa una combinazione di più sistemi! Non ho il tempo o l'esperienza per entrare in ciascuno di questi sistemi, ma Google e Stackoverflow sono i tuoi amici.