Quando / dove aggiornare i componenti


10

Invece della mia solita eredità motori di gioco pesanti sto giocando con un approccio più basato sui componenti. Tuttavia, faccio fatica a giustificare dove lasciare che i componenti facciano le loro cose.

Supponiamo di avere un'entità semplice con un elenco di componenti. Naturalmente l'entità non sa quali siano questi componenti. Potrebbe esserci un componente presente che dà all'entità una posizione sullo schermo, un altro potrebbe essere lì per disegnare l'entità sullo schermo.

Affinché questi componenti funzionino devono aggiornare ogni fotogramma, il modo più semplice per farlo è camminare sull'albero della scena e quindi per ogni entità aggiornare ciascun componente. Ma alcuni componenti potrebbero richiedere un po 'più di gestione. Ad esempio, un componente che rende un'entità negoziabile deve essere gestito da qualcosa che può sovrintendere a tutti i componenti raccoglibili. Un componente che rende un'entità estraibile ha bisogno di qualcuno che controlli tutti gli altri componenti estraibili per capire l'ordine di estrazione, ecc.

Quindi la mia domanda è, dove posso aggiornare i componenti, qual è un modo pulito per farli arrivare ai manager?

Ho pensato di usare un oggetto gestore singleton per ciascuno dei tipi di componenti ma che ha i soliti svantaggi dell'utilizzo di un singleton, un modo per alleviare questo è un po 'usando l'iniezione di dipendenza ma suona eccessivo per questo problema. Potrei anche camminare sull'albero della scena e poi riunire i diversi componenti in elenchi usando una sorta di modello di osservatore, ma sembra un po 'dispendioso fare ogni fotogramma.


1
Stai usando i sistemi in qualche modo?
Asakeron,

I sistemi componenti sono il solito modo per farlo. Personalmente, chiamo aggiornamento su tutte le entità, che chiama aggiornamento su tutti i componenti, e ho alcuni casi "speciali" (come il gestore spaziale per il rilevamento delle collisioni, che è statico).
ashes999,

Sistemi componenti? Non ne avevo mai sentito parlare prima. Inizierò a cercare su Google, ma accetterei con favore qualsiasi link consigliato.
Roy T.

1
I sistemi di entità sono il futuro dello sviluppo MMOG è una grande risorsa. E, ad essere sincero, sono sempre confuso da questi nomi di architettura. La differenza con l'approccio suggerito è che i componenti contengono solo dati e sistemi che li elaborano. Anche questa risposta è molto pertinente.
Asakeron,

1
Ho scritto un post sul blog piuttosto
intricato

Risposte:


15

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.


Trovo questa risposta molto interessante. Solo una domanda (spero che tu o qualcuno possa rispondermi). Come riesci a eliminare l'entità su un sistema basato su componenti DOD? Persino Artemide usa Entity come classe, non sono sicuro che sia molto evasivo.
Wolfrevo Kcats il

1
Cosa intendi per eliminarlo? Intendi un sistema di entità senza una classe Entity? Il motivo per cui Artemis ha un'entità è perché in Artemis la classe Entity gestisce i propri componenti. Nel sistema che ho proposto, le classi ComponentManager gestiscono i componenti. Quindi, invece di richiedere una classe Entity, puoi avere un ID intero univoco. Supponiamo quindi che tu abbia l'entità 254, che ha un componente di posizione. Quando si desidera modificare la posizione, è possibile chiamare PositionCompMgr.setPosition (int id, Vector3 newPos), con 254 come parametro id.
Mart

Ma come gestite gli ID? Cosa succede se si desidera rimuovere un componente da un'entità per assegnarlo successivamente ad altri? Cosa succede se si desidera rimuovere un'entità e aggiungerne una nuova? Cosa succede se si desidera condividere un componente tra due o più entità? Sono davvero interessato a questo.
Wolfrevo Kcats il

1
EntityManager può essere utilizzato per fornire nuovi ID. Potrebbe anche essere usato per creare entità complete basate su modelli predefiniti (es. Creare "EnemyNinja" che genera un nuovo ID e crea tutti i componenti che compongono un ninja nemico come renderizzabili, collisione, AI, forse alcuni componenti per il combattimento corpo a corpo , eccetera). Può anche avere una funzione removeEntity che chiama automaticamente tutte le funzioni di rimozione di ComponentManager. ComponentManager può verificare se dispone di dati componente per l'entità specificata e, in tal caso, eliminare tali dati.
Mart

1
Spostare un componente da un'entità all'altra? Basta aggiungere una funzione swapComponentOwner (int oldEntity, int newEntity) a ciascun ComponentManager. I dati sono tutti presenti in ComponentManager, tutto ciò che serve è una funzione per cambiare il proprietario a cui appartiene. Ogni ComponentManager avrà qualcosa come un indice o una mappa per memorizzare quali dati appartengono a quale ID entità. Basta cambiare l'ID entità dal vecchio al nuovo ID. Non sono sicuro che condividere i componenti sia facile nel sistema che ho pensato, ma quanto può essere difficile? Invece di un collegamento ID componente <-> Dati componente nella tabella dell'indice ci sono più.
Mart

3

Affinché questi componenti funzionino devono aggiornare ogni fotogramma, il modo più semplice per farlo è camminare sull'albero della scena e quindi per ogni entità aggiornare ciascun componente.

Questo è il tipico approccio ingenuo agli aggiornamenti dei componenti (e non c'è nulla di necessariamente sbagliato nell'essere ingenuo, se funziona per te). Uno dei maggiori problemi su cui hai effettivamente toccato: stai operando attraverso l'interfaccia del componente (ad esempio IComponent), quindi non sai nulla di ciò che hai appena aggiornato. Probabilmente, quindi, non sai nulla sull'ordinamento dei componenti all'interno dell'entità

  1. è probabile che aggiorni frequentemente componenti di tipi diversi (in sostanza una scarsa localizzazione del codice di riferimento)
  2. questo sistema non si presta bene agli aggiornamenti simultanei perché non sei in grado di identificare le dipendenze dei dati e quindi suddividere gli aggiornamenti in gruppi locali di oggetti non correlati.

Ho pensato di usare un oggetto gestore singleton per ciascuno dei tipi di componenti ma che ha i soliti svantaggi dell'utilizzo di un singleton, un modo per alleviare questo è un po 'usando l'iniezione di dipendenza ma suona eccessivo per questo problema.

Un singleton non è davvero necessario qui, e quindi dovresti evitarlo perché porta i lati negativi che hai citato. L'iniezione di dipendenza non è eccessiva - il cuore del concetto è che passi cose di cui un oggetto ha bisogno a quell'oggetto, idealmente nel costruttore. Non hai bisogno di un framework DI pesante (come Ninject ) per questo - basta passare un parametro extra a un costruttore da qualche parte.

Un renderer è un sistema fondamentale e probabilmente supporta la creazione e la gestione della durata di un gruppo di oggetti renderizzabili che corrispondono a oggetti visivi nel tuo gioco (sprite o modelli, probabilmente). Allo stesso modo, un motore fisico ha probabilmente il controllo a vita su cose che rappresentano le entità che possono muoversi nella simulazione fisica (corpi rigidi). Ciascuno di tali sistemi pertinenti dovrebbe possedere, in qualche modo, tali oggetti ed essere responsabile dell'aggiornamento.

I componenti che usi nel tuo sistema di composizione delle entità di gioco dovrebbero essere semplicemente avvolgenti attorno alle istanze di quei sistemi di livello inferiore: il tuo componente di posizione potrebbe semplicemente avvolgere un corpo rigido, il tuo componente visivo avvolge semplicemente uno sprite o un modello renderizzabile, eccetera.

Quindi il sistema stesso che possiede gli oggetti di livello inferiore è responsabile dell'aggiornamento e può farlo in blocco e in un modo che gli consenta di eseguire il multithread di tale aggiornamento, se appropriato. Il tuo ciclo di gioco principale controlla l'ordine grezzo in cui si aggiornano quei sistemi (prima la fisica, poi il renderer o altro). Se si dispone di un sottosistema che non ha controllo della durata o dell'aggiornamento sulle istanze distribuite, è possibile creare un semplice wrapper per gestire in blocco anche tutti i componenti rilevanti per quel sistema e decidere dove posizionarlo aggiornamento rispetto al resto degli aggiornamenti di sistema (questo accade spesso, trovo, con componenti "script").

Questo approccio è talvolta noto come approccio del componente esterno , se stai cercando maggiori dettagli.

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.