Questo è difficile. Proverò solo ad affrontare alcune delle domande basate sulle mie esperienze particolari (YMMV):
I componenti devono accedere ai dati di altri componenti. Ad esempio, il metodo di disegno del componente di rendering deve accedere alla posizione del componente di trasformazione. Questo crea dipendenze nel codice.
Non sottovalutare la quantità e la complessità (non il grado) di accoppiamento / dipendenze qui. Potresti guardare la differenza tra questo (e questo diagramma è già ridicolmente semplificato a livelli simili a giocattoli, e l'esempio del mondo reale avrebbe interfacce tra per allentare l'accoppiamento):
... e questo:
... o questo:
I componenti possono essere polimorfici, il che introduce ulteriore complessità. Ad esempio, potrebbe esserci un componente di rendering sprite che sovrascrive il metodo di disegno virtuale del componente di rendering.
Così? L'equivalente analogico (o letterale) di un invio virtuale e virtuale può essere invocato tramite il sistema piuttosto che l'oggetto che nasconde il suo stato / dati sottostanti. Il polimorfismo è ancora molto pratico e fattibile con l'implementazione "pura" di ECS quando la vable analogica o il / i puntatore / i di funzione si trasforma in "dati" di sorta per il sistema da invocare.
Poiché il comportamento polimorfico (ad es. Per il rendering) deve essere implementato da qualche parte, è semplicemente esternalizzato nei sistemi. (ad es. il sistema di rendering sprite crea un nodo di rendering sprite che eredita il nodo di rendering e lo aggiunge al motore di rendering)
Così? Spero che questo non stia venendo fuori come sarcasmo (non è il mio intento anche se ne sono stato spesso accusato, ma vorrei poter comunicare meglio le emozioni attraverso il testo), ma in questo caso il comportamento polimorfico di "esternalizzazione" non comporta necessariamente un ulteriore costo per la produttività.
La comunicazione tra i sistemi può essere difficile da evitare. Ad esempio, il sistema di collisione potrebbe aver bisogno del riquadro di delimitazione che viene calcolato da qualsiasi componente di rendering in cemento ci sia.
Questo esempio mi sembra particolarmente strano. Non so perché un renderer restituirebbe i dati alla scena (in genere considero i renderer di sola lettura in questo contesto) o se un renderer sta studiando gli AABB invece di un altro sistema per farlo sia per il renderer che per collisione / fisica (potrei essere bloccato sul nome "componente di rendering" qui). Eppure non voglio essere troppo preso da questo esempio poiché mi rendo conto che non è questo il punto che stai cercando di fare. Tuttavia, la comunicazione tra i sistemi (anche in forma indiretta di lettura / scrittura nel database ECS centrale con sistemi che dipendono piuttosto direttamente dalle trasformazioni effettuate da altri) non dovrebbe essere frequente, se non del tutto necessario. Quello'
Ciò può portare a problemi se l'ordine di chiamata delle funzioni di aggiornamento del sistema non è definito.
Questo dovrebbe assolutamente essere definito. L'ECS non è la soluzione definitiva per riorganizzare l'ordine di valutazione dell'elaborazione del sistema di ogni possibile sistema nella base di codice e restituire esattamente lo stesso tipo di risultati all'utente finale che si occupa di frame e FPS. Questa è una delle cose, nel progettare un ECS, che suggerirei almeno fortemente di anticipare un po 'in anticipo (anche se con un sacco di spazio per perdonare per cambiare idea in seguito, a condizione che non stia alterando gli aspetti più critici dell'ordine di invocazione / valutazione del sistema).
Tuttavia, ricalcolare l'intera tilemap ogni frame è costoso. Pertanto, sarebbe necessario un elenco per tenere traccia di tutte le modifiche apportate per aggiornarle nel sistema. Nel modo OOP questo potrebbe essere incapsulato dal componente della mappa delle tessere. Ad esempio, il metodo SetTile () aggiornerebbe l'array di vertici ogni volta che viene chiamato.
Non ho capito bene questo tranne per il fatto che si tratta di una preoccupazione orientata ai dati. E non ci sono insidie sulla rappresentazione e l'archiviazione dei dati in un ECS, inclusa la memoizzazione, per evitare tali insidie delle prestazioni (le più grandi con un ECS tendono a riguardare cose come i sistemi che richiedono query per istanze disponibili di particolari tipi di componenti che è uno dei aspetti più difficili dell'ottimizzazione di un ECS generalizzato). Il fatto che la logica e i dati siano separati in un ECS "puro" non significa che devi improvvisamente ricalcolare le cose che potresti altrimenti memorizzare nella cache / memorizzare in una rappresentazione OOP. Questo è un punto controverso / irrilevante, a meno che non abbia riflettuto su qualcosa di molto importante.
Con l'ECS "puro" è ancora possibile memorizzare questi dati nel componente della mappa delle tessere. L'unica grande differenza è che la logica per aggiornare questo array di vertici si sposterà da qualche parte su un sistema.
Puoi persino appoggiarti all'ECS per semplificare l'invalidazione e la rimozione di questa cache dall'entità se crei un componente separato come TileMapCache
. A quel punto quando la cache è desiderata ma non disponibile in un'entità con un TileMap
componente, è possibile calcolarla e aggiungerla. Quando è invalidato o non è più necessario, è possibile rimuoverlo tramite ECS senza dover scrivere altro codice specifico per tale invalidamento e rimozione.
Le dipendenze tra i componenti esistono ancora sebbene siano nascoste nei sistemi
Non c'è dipendenza tra i componenti in un rappresentante "puro" (non credo sia giusto dire che le dipendenze vengono nascoste qui dai sistemi). I dati non dipendono dai dati, per così dire. La logica dipende dalla logica. E un ECS "puro" tende a promuovere la logica da scrivere in modo da dipendere dal sottoinsieme minimo assoluto di dati e logica (spesso nessuno) che un sistema richiede per funzionare, il che è diverso da molte alternative che spesso incoraggiano a seconda molte più funzionalità di quelle necessarie per l'attività effettiva. Se stai usando il puro ECS giusto, una delle prime cose che dovresti apprezzare sono i vantaggi del disaccoppiamento mentre allo stesso tempo metti in discussione tutto ciò che hai imparato ad apprezzare in OOP sull'incapsulamento e in particolare sul nascondere le informazioni.
Per disaccoppiamento intendo in particolare la quantità di informazioni necessarie ai sistemi per funzionare. Il tuo sistema di movimento non ha nemmeno bisogno di sapere qualcosa di molto più complesso come un Particle
o Character
(lo sviluppatore del sistema non ha nemmeno bisogno di sapere che tali idee di entità esistono anche nel sistema). Deve solo conoscere i dati minimi nudi come un componente di posizione che potrebbe essere semplice come alcuni float in una struttura. Sono anche meno informazioni e meno dipendenze esterne rispetto a quelle che un'interfaccia pura IMotion
tende a portare con sé. È principalmente dovuto a questa minima conoscenza che ogni sistema richiede di funzionare, il che rende l'ECS spesso così indulgente a gestire i cambiamenti di progetto molto imprevisti con il senno di poi senza affrontare rotture dell'interfaccia a cascata in tutto il luogo.
L'approccio "impuro" che suggerisci diminuisce in qualche modo il vantaggio dato che ora la tua logica non è localizzata rigorosamente in sistemi in cui i cambiamenti non causano rotture a cascata. La logica verrebbe ora centralizzata in una certa misura nei componenti a cui accedono più sistemi che ora devono soddisfare i requisiti di interfaccia di tutti i vari sistemi che potrebbero usarlo, e ora è come se ogni sistema avesse bisogno di conoscenza di (dipende da) altro informazioni di quelle strettamente necessarie per lavorare con quel componente.
Dipendenze dai dati
Una delle cose controverse dell'ECS è che tende a sostituire quelle che potrebbero altrimenti essere dipendenze da interfacce astratte con solo dati grezzi, e questa è generalmente considerata una forma meno desiderabile e più stretta di accoppiamento. Ma nei tipi di domini come i giochi in cui ECS può essere molto utile, spesso è più semplice progettare la rappresentazione dei dati in anticipo e mantenerlo stabile piuttosto che progettare cosa si può fare con quei dati a un livello centrale del sistema. È qualcosa che ho dolorosamente osservato anche tra i veterani esperti in codebase che utilizza più di un approccio di interfaccia pura in stile COM con cose come IMotion
.
Gli sviluppatori hanno continuato a trovare motivi per aggiungere, rimuovere o modificare funzioni a questa interfaccia centrale, e ogni modifica è stata orribile e costosa perché tenderebbe a spezzare ogni singola classe implementata IMotion
insieme a ogni posto nel sistema che ha usato IMotion
. Nel frattempo per tutto il tempo con così tanti cambiamenti dolorosi e in cascata, gli oggetti implementati IMotion
stavano semplicemente memorizzando una matrice 4x4 di float e l'intera interfaccia si preoccupava solo di come trasformare e accedere a quei float; la rappresentazione dei dati era stabile fin dall'inizio e molto dolore avrebbe potuto essere evitato se questa interfaccia centralizzata, così incline a cambiare con esigenze di progettazione impreviste, non esistesse nemmeno in primo luogo.
Tutto ciò potrebbe sembrare disgustoso quasi quanto le variabili globali, ma la natura di come l'ECS organizza questi dati in componenti recuperati in modo esplicito per tipo attraverso i sistemi lo rende tale, mentre i compilatori non possono applicare nulla come nascondere le informazioni, i luoghi che accedono e mutano i dati sono generalmente molto espliciti e abbastanza ovvi da mantenere ancora efficacemente gli invarianti e prevedere che tipo di trasformazioni ed effetti collaterali si verificano da un sistema all'altro (in realtà in modi che possono essere probabilmente più semplici e prevedibili di OOP in determinati domini dato come il sistema si trasforma in una sorta di pipeline piatta).
Infine, vorrei porre la domanda su come gestire l'animazione in un ECS puro. Attualmente ho definito un'animazione come un funzione che manipola un'entità sulla base di alcuni progressi tra 0 e 1. Il componente animazione ha un elenco di animatori con un elenco di animazioni. Nella sua funzione di aggiornamento applica quindi le animazioni attualmente attive all'entità.
Siamo tutti pragmatici qui. Anche in Gamedev avrai probabilmente idee / risposte contrastanti. Anche l'ECS più puro è un fenomeno relativamente nuovo, un territorio pionieristico, per il quale le persone non hanno necessariamente formulato le opinioni più forti su come pelare i gatti. La mia reazione istintiva è un sistema di animazione che incrementa questo tipo di avanzamento dell'animazione nei componenti animati per la visualizzazione del sistema di rendering, ma questo ignora tanta sfumatura per la particolare applicazione e contesto.
Con l'ECS non è un proiettile d'argento e mi trovo ancora con la tendenza ad entrare e aggiungere nuovi sistemi, rimuoverne alcuni, aggiungere nuovi componenti, cambiare un sistema esistente per raccogliere quel nuovo tipo di componente, ecc. Non capisco le cose vanno bene per la prima volta ancora. Ma la differenza nel mio caso è che non sto cambiando nulla di centrale quando non riesco a prevedere in anticipo determinate esigenze di progettazione. Non sto ottenendo l'effetto increspato delle rotture a cascata che mi richiedono di andare ovunque e cambiare così tanto codice per gestire alcune nuove esigenze che si accumulano, e questo è piuttosto un risparmio di tempo. Sto trovando anche più facile il mio cervello perché quando mi siedo con un particolare sistema, non ho bisogno di sapere / ricordare molto di tutto ciò che non riguarda i componenti rilevanti (che sono solo dati) per lavorarci.