I due principali vantaggi che sento costantemente lodati sui sistemi di entità sono 1) la facile costruzione di nuovi tipi di entità a causa del fatto di non dover fare i grovigli con complesse gerarchie ereditarie e 2) l'efficienza della cache.
Si noti che (1) è un vantaggio della progettazione basata su componenti , non solo ES / ECS. Puoi usare i componenti in molti modi che non hanno la parte "sistemi" e funzionano bene (e molti giochi indie e AAA usano tali architetture).
Il modello di oggetti Unity standard (utilizzo GameObject
e MonoBehaviour
oggetti) non è un ECS, ma è una progettazione basata su componenti. La più recente funzionalità di Unity ECS è un vero ECS, ovviamente.
I sistemi devono essere in grado di funzionare con più di un componente, vale a dire sia il sistema di rendering che quello fisico devono accedere al componente di trasformazione.
Alcuni ECS ordinano i loro contenitori di componenti in base all'ID entità, il che significa che i componenti corrispondenti in ciascun gruppo saranno nello stesso ordine.
Ciò significa che se stai iterando linearmente sui componenti grafici, stai anche iterando linearmente sui corrispondenti componenti di trasformazione. Potresti saltare alcune delle trasformazioni (dal momento che potresti avere volumi di trigger fisici che non esegui il rendering o simili) ma dal momento che salti sempre in memoria (e di solito non con distanze particolarmente grandi) stai ancora andando avere guadagni di efficienza.
Questo è simile al modo in cui la struttura degli array (SOA) è l'approccio consigliato per HPC. La CPU e la cache sono in grado di gestire più array lineari quasi allo stesso modo in cui possono gestire un singolo array lineare e molto meglio di quanto possano gestire l'accesso casuale alla memoria.
Un'altra strategia utilizzata in alcune implementazioni di ECS, incluso Unity ECS, è quella di allocare i componenti in base all'archetipo della loro entità corrispondente. In altre parole, tutte le Entità con esattamente l'insieme di Componenti ( PhysicsBody
, Transform
) verranno allocate separatamente dalle Entità con Componenti diversi (ad es PhysicsBody
. Transform
, E Renderable
).
I sistemi in tali progetti funzionano individuando innanzitutto tutti gli Archetipi che soddisfano i loro requisiti (che hanno il set richiesto di Componenti), ripetendo tale elenco di Archetipi e ripetendo i Componenti memorizzati all'interno di ciascun Archetipo corrispondente. Ciò consente l'accesso ai componenti O (1) completamente lineare e vero all'interno di un Archetipo e consente a Systems di trovare Entità compatibili con un sovraccarico molto basso (cercando un piccolo elenco di Archetipi piuttosto che cercare potenzialmente centinaia di migliaia di Entità).
È possibile che i componenti memorizzino i puntatori su altri componenti o i puntatori alle entità che memorizzano i puntatori sui componenti.
I componenti che fanno riferimento ad altri componenti sulla stessa entità non devono archiviare nulla. Per fare riferimento a componenti su altre entità, è sufficiente memorizzare l'ID entità.
Se un componente può esistere più di una volta per una singola entità ed è necessario fare riferimento a una particolare istanza, memorizzare l'ID dell'altra entità e un indice del componente per tale entità. Molte implementazioni ECS non consentono questo caso, in particolare perché rende queste operazioni meno efficienti.
Puoi assicurarti che ogni array di componenti sia 'n' grande, dove 'n' è il numero di entità attive nel sistema
Utilizzare le maniglie (ad es. Indici + marcatori di generazione) e non puntatori, quindi è possibile ridimensionare le matrici senza timore di rompere i riferimenti agli oggetti.
Puoi anche usare un approccio di "chunked array" (un array di array) simile a molte std::deque
implementazioni comuni (anche se senza le dimensioni pietosamente piccole di tali implementazioni) se vuoi consentire i puntatori per qualche motivo o se hai misurato problemi con prestazioni di ridimensionamento dell'array.
In secondo luogo, tutto ciò presuppone che le entità vengano elaborate linearmente in un elenco ogni frame / tick, ma in realtà ciò non accade spesso
Dipende dall'entità. Sì, per molti casi d'uso non è vero. In effetti, questo è il motivo per cui sottolineo così fortemente la differenza tra la progettazione basata su componenti (buona) e il sistema di entità (una forma specifica di CBD).
Alcuni dei tuoi componenti saranno sicuramente facili da elaborare in modo lineare. Anche in casi d'uso normalmente "ad albero pesante" abbiamo sicuramente visto un aumento delle prestazioni dall'uso di array strettamente compressi (soprattutto nei casi che coinvolgono una N di alcune centinaia al massimo, come gli agenti di intelligenza artificiale in un gioco tipico).
Alcuni sviluppatori hanno anche scoperto che i vantaggi in termini di prestazioni derivanti dall'uso di strutture di dati allocate linearmente orientate ai dati superano il vantaggio in termini di prestazioni derivante dall'utilizzo di strutture basate su alberi "più intelligenti". Dipende tutto dal gioco e dai casi d'uso specifici, ovviamente.
Supponi di utilizzare un renderizzatore di settore / portale o un ottetto per eseguire l'abbattimento dell'occlusione. Potresti essere in grado di memorizzare entità contigue all'interno di un settore / nodo, ma salterai in giro, che ti piaccia o no.
Saresti sorpreso di quanto l'array aiuti ancora. Stai saltando in una regione di memoria molto più piccola di "ovunque" e anche con tutto il salto hai ancora più probabilità di finire in qualcosa nella cache. Con un albero di una certa dimensione o meno, potresti persino essere in grado di precaricare tutto nella cache e non perdere mai la cache su quell'albero.
Ci sono anche strutture ad albero che sono costruite per vivere in matrici strette. Ad esempio, con il tuo octree, puoi utilizzare una struttura simile a un heap (genitori prima dei figli, fratelli vicini uno accanto all'altro) e assicurarti che anche quando "esegui il drill down" dell'albero stai sempre iterando in avanti nell'array, il che aiuta la CPU ottimizza gli accessi alla memoria / ricerche nella cache.
Questo è un punto importante da sottolineare. Una CPU x86 è una bestia complessa. La CPU sta effettivamente eseguendo un ottimizzatore di microcodici sul codice della tua macchina, suddividendolo in microcodici più piccoli e istruzioni di riordino, predicendo schemi di accesso alla memoria, ecc. I modelli di accesso ai dati contano più di quanto possa essere facilmente evidente se tutto ciò che hai è una comprensione di alto livello di come funziona la CPU o la cache.
Quindi hai altri sistemi, che potrebbero preferire entità memorizzate in un altro ordine.
Potresti memorizzarli più volte. Una volta ridotti gli array ai minimi dettagli, potresti trovare effettivamente un risparmio di memoria (poiché hai rimosso i puntatori a 64 bit e puoi utilizzare indici più piccoli) con questo approccio.
È possibile interlacciare il proprio array di entità invece di mantenere array separati, ma si sta ancora sprecando memoria
Questo è antitetico al buon utilizzo della cache. Se tutto ciò che ti interessa sono le trasformazioni e i dati grafici, perché fare in modo che la macchina passi il tempo a raccogliere tutti gli altri dati per la fisica e l'intelligenza artificiale, l'input, il debug e così via?
Questo è il punto di solito sottolineato a favore degli oggetti di gioco ECS vs monolitici (anche se non realmente applicabili se confrontati con altre architetture basate su componenti).
Per quello che vale, la maggior parte delle implementazioni ECS "di livello di produzione" di cui sono a conoscenza utilizzano lo storage interfogliato. Il popolare approccio Archetype che ho citato in precedenza (utilizzato in Unity ECS, ad esempio) è stato esplicitamente creato per utilizzare lo storage interlacciato per i componenti associati a un Archetype.
L'intelligenza artificiale è inutile se non può influire sulla trasformazione o sullo stato di animazione utilizzato per il rendering di un'entità.
Solo perché l'IA non può accedere in modo efficiente ai dati di trasformazione in modo lineare, ciò non significa che nessun altro sistema può utilizzare efficacemente l'ottimizzazione del layout dei dati. È possibile utilizzare un array compresso per trasformare i dati senza impedire ai sistemi di logica di gioco di fare le cose nel modo in cui i sistemi di logica di gioco di solito fanno le cose.
Dimentichi anche la cache del codice . Quando usi l'approccio sistemico di ECS (a differenza di un'architettura più ingenua dei componenti) stai garantendo che stai eseguendo lo stesso piccolo ciclo di codice e non saltando avanti e indietro attraverso le tabelle delle funzioni virtuali per un assortimento di Update
funzioni casuali sparse ovunque il tuo binario. Quindi nel caso dell'IA, vuoi davvero conservare tutti i tuoi diversi componenti dell'IA (perché sicuramente ne hai più di uno in modo da poter comporre comportamenti!) In secchi separati ed elaborare ogni elenco separatamente al fine di ottenere il miglior utilizzo della cache del codice.
Con una coda di eventi ritardata (in cui un sistema genera un elenco di eventi ma non li invia fino a quando il sistema non termina l'elaborazione di tutte le entità) è possibile assicurarsi che la cache del codice venga utilizzata correttamente mantenendo gli eventi.
Utilizzando un approccio in cui ciascun sistema sa quali code di eventi leggere per il frame, è anche possibile velocizzare la lettura degli eventi. O più veloce che senza, almeno.
Ricorda, le prestazioni non sono assolute. Non è necessario eliminare ogni ultima perdita della cache per iniziare a vedere i vantaggi in termini di prestazioni di una buona progettazione orientata ai dati.
C'è ancora una ricerca attiva per far funzionare meglio molti sistemi di gioco con l'architettura ECS e modelli di progettazione orientati ai dati. Analogamente ad alcune delle cose straordinarie che abbiamo visto fare con SIMD negli ultimi anni (ad esempio i parser JSON), stiamo vedendo sempre più cose fatte con l'architettura ECS che non sembra intuitivo per le architetture di gioco classiche ma offre una serie di vantaggi (velocità, multi-threading, testabilità, ecc.).
O forse c'è un approccio ibrido che tutti usano ma nessuno sta parlando
Questo è ciò che ho sostenuto in passato, in particolare per le persone che sono scettiche sull'architettura ECS: utilizzare buoni approcci orientati ai dati per i componenti in cui le prestazioni sono fondamentali. Usa un'architettura più semplice in cui la semplicità migliora i tempi di sviluppo. Non calpestare ogni singolo componente in una rigida sovra-definizione di componentizzazione come propone ECS. Sviluppa la tua architettura di componente in modo tale da poter facilmente utilizzare approcci simili a ECS laddove abbiano senso e utilizzare una struttura di componenti più semplice in cui l'approccio simile a ECS non ha senso (o ha meno senso di una struttura ad albero o così via) .
Personalmente sono un convertito relativamente recente al vero potere dell'ECS. Anche se per me, il fattore decisivo era qualcosa di raramente menzionato su ECS: rende quasi banali i test di scrittura per i sistemi di gioco e la logica rispetto ai progetti basati su componenti carichi di logica strettamente accoppiati con cui ho lavorato in passato. Dato che le architetture ECS mettono tutta la logica nei sistemi, che consumano solo componenti e producono aggiornamenti dei componenti, creare un insieme "finto" di componenti per testare il comportamento del sistema è abbastanza semplice; poiché la maggior parte della logica di gioco dovrebbe vivere esclusivamente all'interno dei sistemi, ciò significa che testare tutti i sistemi fornirà una copertura del codice abbastanza elevata della logica di gioco. I sistemi possono utilizzare dipendenze simulate (ad es. Interfacce GPU) per test con una complessità o un impatto sulle prestazioni di gran lunga inferiori rispetto a te "
A parte, potresti notare che molte persone parlano di ECS senza capire davvero di cosa si tratti. Vedo il classico Unity indicato come ECS con una frequenza deprimente, a dimostrazione del fatto che troppi sviluppatori di giochi identificano "ECS" con "Componenti" e praticamente ignorano del tutto la parte "Entity System". Vedi un sacco di amore accumulato su ECS su Internet quando una grande parte della gente sta davvero sostenendo la progettazione basata su componenti, non un vero ECS. A questo punto è quasi inutile discuterne; ECS è stato corrotto dal suo significato originale in un termine generico e potresti anche accettare che "ECS" non significa la stessa cosa di "ECS orientato ai dati". : /