È ragionevole costruire applicazioni (non giochi) usando un'architettura componente-entità-sistema?


24

So che quando si creano applicazioni (native o web) come quelle dell'App Store Apple o dell'app Google Play, è molto comune usare un'architettura Model-View-Controller.

Tuttavia, è ragionevole creare anche applicazioni utilizzando l'architettura Component-Entity-System comune nei motori di gioco?


1
Scopri l'architettura del tavolo luminoso: chris-granger.com/2013/01/24/the-ide-as-data
Hakan Deryal,

Risposte:


39

Tuttavia, è ragionevole creare anche applicazioni utilizzando l'architettura Component-Entity-System comune nei motori di gioco?

Per me assolutamente. Lavoro in effetti visivi e ho studiato un'ampia varietà di sistemi in questo campo, le loro architetture (incluso CAD / CAM), affamati di SDK e qualsiasi documento che mi darebbe un senso dei pro e dei contro delle decisioni architettoniche apparentemente infinite che potrebbe essere realizzato, anche con i più sottili non sempre avere un impatto sottile.

VFX è piuttosto simile ai giochi in quanto esiste un concetto centrale di "scena", con finestre che mostrano i risultati del rendering. Tende inoltre a esserci un sacco di elaborazione a ciclo centrale che ruota attorno a questa scena costantemente in contesti di animazione, dove potrebbero esserci eventi fisici, emettitori di particelle che generano particelle, mesh animate e renderizzate, animazioni di movimento, ecc. E, infine, renderle tutto per l'utente alla fine.

Un altro concetto simile ai motori di gioco almeno molto complessi era la necessità di un aspetto "designer" in cui i progettisti potessero progettare in modo flessibile scene, inclusa la possibilità di eseguire una leggera programmazione propria (script e nodi).

Ho scoperto, nel corso degli anni, che ECS ha fatto la scelta migliore. Ovviamente questo non è mai completamente separato dalla soggettività, ma direi che è sembrato fortemente dare il minor numero di problemi. Ha risolto molti problemi più importanti con cui abbiamo sempre lottato, dandoci in cambio solo alcuni nuovi problemi minori.

OOP tradizionale

Gli approcci OOP più tradizionali possono essere molto efficaci quando si hanno una solida conoscenza dei requisiti di progettazione in anticipo ma non dei requisiti di implementazione. Sia attraverso un approccio ad interfaccia multipla più piatta o un approccio ABC gerarchico più nidificato, tende a cementare il design e rendere più difficile la modifica, rendendo l'implementazione più semplice e sicura. C'è sempre bisogno di instabilità in qualsiasi prodotto che superi una singola versione, quindi gli approcci OOP tendono a distorcere la stabilità (difficoltà di cambiamento e mancanza di motivi per il cambiamento) verso il livello di progettazione e l'instabilità (facilità di cambiamento e ragioni del cambiamento) a livello di implementazione.

Tuttavia, a fronte dell'evoluzione dei requisiti dell'utente finale, potrebbe essere necessario cambiare frequentemente sia la progettazione che l'implementazione. Potresti trovare qualcosa di strano come un forte bisogno dell'utente finale per la creatura analogica che deve essere allo stesso tempo pianta e animale, invalidando completamente l'intero modello concettuale che hai costruito. I normali approcci orientati agli oggetti non ti proteggono qui e talvolta possono rendere ancora più difficili tali cambiamenti imprevisti e di rottura dei concetti. Quando sono coinvolte aree molto critiche dal punto di vista delle prestazioni, le ragioni della progettazione cambiano ulteriormente.

La combinazione di più interfacce granulari per formare l'interfaccia conforme di un oggetto può aiutare molto a stabilizzare il codice client, ma non aiuta a stabilizzare i sottotipi che a volte possono ridurre il numero di dipendenze client. Puoi avere un'interfaccia utilizzata solo da una parte del tuo sistema, ad esempio, ma con mille sottotipi diversi che implementano quell'interfaccia. In tal caso, mantenere i sottotipi complessi (complessi perché hanno così tante responsabilità di interfaccia disparate da soddisfare) può diventare l'incubo piuttosto che il codice che li utilizza attraverso un'interfaccia. OOP tende a trasferire la complessità a livello di oggetto, mentre ECS la trasferisce a livello di client ("sistemi"), e ciò può essere ideale quando ci sono pochissimi sistemi ma un intero gruppo di "oggetti" ("entità") conformi.

inserisci qui la descrizione dell'immagine

Una classe possiede anche i suoi dati privatamente, e quindi può mantenere gli invarianti da sola. Tuttavia, ci sono invarianti "grossolani" che in realtà possono essere ancora difficili da mantenere quando gli oggetti interagiscono tra loro. Affinché un sistema complesso nel suo insieme sia in uno stato valido, spesso deve considerare un grafico complesso di oggetti, anche se i loro singoli invarianti sono adeguatamente mantenuti. Gli approcci tradizionali in stile OOP possono aiutare a mantenere invarianti granulari, ma in realtà può rendere difficile mantenere invarianti ampi e grossolani se gli oggetti si concentrano su aspetti giovanili del sistema.

È qui che questi tipi di approcci o varianti ECS che creano lego-block building possono essere così utili. Inoltre, poiché i sistemi sono più grossolani nel design rispetto al solito oggetto, diventa più facile mantenere quei tipi di invarianti grossolani alla vista dall'alto del sistema. Molte interazioni tra oggetti adolescenti si trasformano in un unico grande sistema focalizzato su un compito ampio invece di piccoli oggetti adolescenti che si concentrano su piccoli compiti adolescenti con un grafico delle dipendenze che coprirebbe un chilometro di carta.

Tuttavia, ho dovuto guardare al di fuori del mio campo, l'industria del gioco, per conoscere l'ECS, sebbene fossi sempre stato un approccio orientato ai dati. Inoltre, stranamente, mi sono quasi avvicinato all'ECS da solo, ripetendo e cercando di elaborare progetti migliori. Non ci sono riuscito fino in fondo e ho perso un dettaglio molto cruciale, che è la formalizzazione della parte "sistemi" e la compressione dei componenti fino ai dati grezzi.

Proverò a capire come ho finito per accontentarmi di ECS e come ha risolto tutti i problemi con precedenti iterazioni di progettazione. Penso che ciò contribuirà ad evidenziare esattamente perché la risposta qui potrebbe essere un "sì" molto forte, che ECS è potenzialmente applicabile ben oltre il settore dei giochi.

Brute Force Architecture degli anni '80

La prima architettura su cui ho lavorato nel settore VFX aveva una lunga eredità che andava già avanti da un decennio da quando sono entrato in azienda. Era una codifica C bruta a forza bruta fino in fondo (non una pendenza su C, poiché amo C, ma il modo in cui veniva usato qui era davvero grezzo). Una porzione in miniatura e troppo semplicistica assomigliava a dipendenze come questa:

inserisci qui la descrizione dell'immagine

E questo è un diagramma enormemente semplificato di un minuscolo pezzo del sistema. Ognuno di questi client nel diagramma ("Rendering", "Fisica", "Movimento") otterrebbe un oggetto "generico" attraverso il quale controllerebbe un campo di tipo, in questo modo:

void transform(struct Object* obj, const float mat[16])
{
    switch (obj->type)
    {
        case camera:
            // cast to camera and do something with camera fields
            break;
        case light:
            // cast to light and do something with light fields
            break;
        ...
    }
}

Ovviamente con un codice significativamente più brutto e più complesso di così. Spesso da questi casi di switch vengono chiamate funzioni aggiuntive che ricorrono ripetutamente allo switch ancora e ancora e ancora. Questo schema e il codice potrebbe quasi apparire come ECS-lite, ma non c'era una forte distinzione soggetto-componente ( " è l'oggetto di una fotocamera?", Non 'fa questo oggetto fornisce movimento?'), E nessuna formalizzazione del 'sistema' ( solo un mucchio di funzioni nidificate che vanno dappertutto e confondono le responsabilità). In quel caso, quasi tutto era complicato, qualsiasi funzione era un potenziale per un disastro in attesa di accadere.

La nostra procedura di test qui spesso doveva controllare cose come maglie separate da altri tipi di oggetti, anche se accadeva la stessa cosa ad entrambi, poiché la natura della forza bruta della codifica qui (spesso accompagnata da un sacco di copia e incolla) spesso fatta è molto probabile che quella che è la stessa identica logica potrebbe fallire da un tipo di elemento a quello successivo. Cercare di estendere il sistema per gestire nuovi tipi di oggetti è stato piuttosto senza speranza, anche se c'era un bisogno fortemente espresso dell'utente finale, dato che era troppo difficile quando stavamo lottando così tanto solo per gestire i tipi di oggetti esistenti.

Alcuni professionisti:

  • Uhh ... non ho esperienza di ingegneria, immagino? Questo sistema non richiede alcuna conoscenza anche di concetti di base come il polimorfismo, è una forza totalmente bruta, quindi immagino che anche un principiante potrebbe essere in grado di comprendere parte del codice anche se un professionista al debugging riesce a malapena a mantenerlo.

Alcuni contro:

  • Incubo di manutenzione. Il nostro team di marketing ha sentito la necessità di vantare di aver risolto oltre 2000 bug unici in un ciclo di 3 anni. Per me è qualcosa di cui essere imbarazzati dal fatto che abbiamo avuto così tanti bug in primo luogo, e quel processo probabilmente ha ancora risolto solo circa il 10% del totale dei bug che crescevano in numero per tutto il tempo.
  • Informazioni sulla soluzione più flessibile possibile.

Architettura degli anni '90 COM

La maggior parte del settore VFX utilizza questo stile di architettura da quello che ho raccolto, leggendo documenti sulle loro decisioni di progettazione e dando un'occhiata ai loro kit di sviluppo software.

Potrebbe non essere esattamente COM a livello ABI (alcune di queste architetture potrebbero avere solo plugin scritti usando lo stesso compilatore), ma condividono molte caratteristiche simili con query di interfaccia fatte su oggetti per vedere quali interfacce supportano i loro componenti.

inserisci qui la descrizione dell'immagine

Con questo tipo di approccio, la transformfunzione analogica sopra è arrivata ad assomigliare a questa forma:

void transform(Object obj, const Matrix& mat)
{
    // Wrapper that performs an interface query to see if the 
    // object implements the IMotion interface.
    MotionRef motion(obj);

    // If the object supported the IMotion interface:
    if (motion.valid())
    {
        // Transform the item through the IMotion interface.
        motion->transform(mat);
        ...
    }
}

Questo è l'approccio su cui la nuova squadra di quella vecchia base di codice ha optato per rifatturare. Ed è stato un notevole miglioramento rispetto all'originale in termini di flessibilità e manutenibilità, ma c'erano ancora alcuni problemi che tratterò nella prossima sezione.

Alcuni professionisti:

  • Drammaticamente più flessibile / estensibile / mantenibile rispetto alla precedente soluzione di forza bruta.
  • Promuove una forte conformità a molti principi di SOLID rendendo ogni interfaccia completamente astratta (apolidi, nessuna implementazione, solo interfacce pure).

Alcuni contro:

  • Un sacco di boilerplate. I nostri componenti dovevano essere pubblicati attraverso un registro al fine di creare un'istanza degli oggetti, le interfacce supportate richiedevano sia ereditare ("implementare" in Java) l'interfaccia e fornire del codice per indicare quali interfacce erano disponibili in una query.
  • Promosso logica duplicata in tutto il luogo a causa delle interfacce pure. Ad esempio, tutti i componenti implementati IMotionavrebbero sempre lo stesso stato esatto e la stessa implementazione esatta per tutte le funzioni. Per mitigare questo, inizieremmo a centralizzare le classi di base e la funzionalità di supporto in tutto il sistema per le cose che tendono ad essere implementate in modo ridondante allo stesso modo per la stessa interfaccia, e possibilmente con eredità multipla in corso dietro il cofano, ma era piuttosto disordinato sotto il cofano, anche se il codice client è stato facile.
  • Inefficienza: le sessioni di vtune hanno spesso mostrato che la QueryInterfacefunzione di base si presentava quasi sempre come hotspot medio-alto, e occasionalmente anche l'hotspot n. 1. Per mitigarlo, faremmo cose come rendere parti della cache di codebase un elenco di oggetti già noti per supportareIRenderable, ma ciò ha notevolmente aumentato la complessità e i costi di manutenzione. Allo stesso modo, questo è stato più difficile da misurare, ma abbiamo notato alcuni rallentamenti rispetto alla codifica in stile C che stavamo facendo prima, quando ogni singola interfaccia richiedeva una spedizione dinamica. Cose come le previsioni errate delle filiali e le barriere di ottimizzazione sono difficili da misurare al di fuori di una piccola sfaccettatura del codice, ma gli utenti stavano semplicemente notando la reattività dell'interfaccia utente e cose del genere che peggioravano confrontando le versioni precedenti e più recenti del software fianco a fianco- lato per le aree in cui la complessità algoritmica non è cambiata, solo le costanti.
  • Era ancora difficile ragionare sulla correttezza a un livello di sistema più ampio. Anche se era significativamente più semplice dell'approccio precedente, era ancora difficile cogliere le complesse interazioni tra oggetti in questo sistema, specialmente con alcune delle ottimizzazioni che iniziarono a diventare necessarie contro di esso.
  • Abbiamo avuto problemi a correggere le nostre interfacce. Anche se nel sistema potrebbe esserci solo un ampio spazio che utilizza un'interfaccia, i requisiti dell'utente finale cambieranno rispetto alle versioni e finiremmo per dover fare modifiche in cascata a tutte le classi che implementano l'interfaccia per accogliere una nuova funzione aggiunta a l'interfaccia, ad esempio, a meno che non ci fosse qualche classe di base astratta che stava già centralizzando la logica sotto il cofano (alcuni di questi si manifesterebbero nel mezzo di questi cambiamenti a cascata nella speranza di non ripetere più e più volte).

inserisci qui la descrizione dell'immagine

Risposta pragmatica: composizione

Una delle cose che stavamo notando prima (o almeno lo ero) che stava causando problemi era che IMotionpotevano essere implementate da 100 classi diverse ma con la stessa identica implementazione e stato associati. Inoltre, sarebbe utilizzato solo da una manciata di sistemi come rendering, movimento dei fotogrammi chiave e fisica.

Quindi, in tal caso, potremmo avere una relazione 3 a 1 tra i sistemi che usano l'interfaccia per l'interfaccia e una relazione 100 a 1 tra i sottotipi che implementano l'interfaccia per l'interfaccia.

La complessità e la manutenzione sarebbero quindi drasticamente distorte per l'implementazione e la manutenzione di 100 sottotipi, invece di 3 sistemi client da cui dipendono IMotion. Ciò ha spostato tutte le nostre difficoltà di manutenzione nella manutenzione di questi 100 sottotipi, non nei 3 posti che utilizzano l'interfaccia. Aggiornamento di 3 posizioni nel codice con pochi o nessun "accoppiamento efferente indiretto" (come nelle dipendenze ad esso, ma indirettamente attraverso un'interfaccia, non una dipendenza diretta), non è un grosso problema: aggiornare 100 posizioni di sottotipo con un carico di "accoppiamenti efferenti indiretti" , un grosso problema *.

* Mi rendo conto che è strano e sbagliato sbagliare con la definizione di "accoppiamenti efferenti" in questo senso dal punto di vista dell'implementazione, non ho trovato un modo migliore per descrivere la complessità di manutenzione associata quando sia l'interfaccia che le implementazioni corrispondenti di cento sottotipi deve cambiare.

Quindi ho dovuto insistere, ma ho proposto di provare a diventare un po 'più pragmatici e rilassare l'intera idea di "interfaccia pura". Per me non aveva senso realizzare qualcosa di IMotioncompletamente astratto e apolide a meno che non ne vedessimo un vantaggio avendo una ricca varietà di implementazioni. Nel nostro caso, IMotionavere una ricca varietà di implementazioni si trasformerebbe in realtà in un incubo di manutenzione, poiché non volevamo varietà. Invece stavamo iterando nel tentativo di realizzare una singola implementazione motion che fosse davvero buona contro il cambiamento dei requisiti del cliente, e spesso stavamo lavorando molto attorno alla pura idea dell'interfaccia cercando di forzare ogni implementatore IMotiona usare la stessa implementazione e lo stato associato in modo da non donare " t obiettivi duplicati.

Le interfacce sono diventate quindi più simili a quelle Behaviorsassociate a un'entità. IMotiondiventerebbe semplicemente un Motion"componente" (ho cambiato il modo in cui abbiamo definito "componente" da COM a uno in cui è più vicino alla solita definizione, di un pezzo che costituisce un'entità "completa").

Invece di questo:

class IMotion
{
public:
    virtual ~IMotion() {}
    virtual void transform(const Matrix& mat) = 0;
    ...
};

Lo abbiamo evoluto in qualcosa di più simile a questo:

class Motion
{
public:
    void transform(const Matrix& mat)
    {
        ...
    }
    ...

private:
    Matrix transformation;
    ...
};

Questa è una palese violazione del principio di inversione di dipendenza per iniziare a spostarsi dall'astratto al concreto, ma per me un tale livello di astrazione è utile solo se possiamo prevedere una reale necessità in qualche futuro, oltre un ragionevole dubbio e non esercitare ridicoli scenari "what if" completamente distaccati dall'esperienza dell'utente (che probabilmente richiederebbe comunque un cambio di progettazione), per tale flessibilità.

Quindi abbiamo iniziato ad evolvere in questo design. QueryInterfaceè diventato più simile QueryBehavior. Inoltre, ha iniziato a sembrare inutile usare l'eredità qui. Invece abbiamo usato la composizione. Gli oggetti si sono trasformati in una raccolta di componenti la cui disponibilità potrebbe essere interrogata e iniettata in fase di esecuzione.

inserisci qui la descrizione dell'immagine

Alcuni professionisti:

  • Nel nostro caso era molto più facile da mantenere rispetto al precedente sistema in puro stile COM con interfaccia. Sorprese impreviste come un cambiamento nei requisiti o i reclami relativi al flusso di lavoro potrebbero essere accolti più facilmente con Motionun'implementazione molto centrale e ovvia , ad esempio, e non dispersi tra cento sottotipi.
  • Ha fornito un livello completamente nuovo di flessibilità del tipo di cui avevamo effettivamente bisogno. Nel nostro sistema precedente, poiché l'ereditarietà modella una relazione statica, potremmo definire efficacemente nuove entità solo in fase di compilazione in C ++. Non siamo riusciti a farlo dal linguaggio di scripting, ad esempio con l'approccio della composizione, potremmo mettere insieme nuove entità al volo in fase di esecuzione semplicemente collegando i componenti a loro e aggiungendoli a un elenco. Una "entità" si è trasformata in una tela bianca su cui potremmo semplicemente mettere insieme un collage di tutto ciò di cui avevamo bisogno al volo, con i sistemi pertinenti che riconoscono ed elaborano automaticamente queste entità come risultato.

Alcuni contro:

  • Abbiamo ancora avuto difficoltà nel reparto efficienza e manutenibilità nelle aree critiche per le prestazioni. Ogni sistema finirebbe comunque per voler memorizzare nella cache componenti di entità che hanno fornito questi comportamenti per evitare di ripercorrerli ripetutamente tutti e verificare ciò che era disponibile. Ogni sistema che richiede prestazioni lo farebbe in modo leggermente diverso, ed era incline a una diversa serie di bug nel non aggiornare questo elenco memorizzato nella cache e possibilmente una struttura di dati (se una qualche forma di ricerca fosse coinvolta come abbattimento di frustum o raytracing) su alcuni oscuro evento di cambio scena, ad es
  • C'era ancora qualcosa di imbarazzante e complesso su cui non potevo mettere il dito in relazione a tutti questi piccoli oggetti granulari comportamentali e semplici. Abbiamo comunque generato molti eventi per gestire le interazioni tra questi oggetti "comportamentali" che a volte erano necessari e il risultato è stato un codice molto decentralizzato. Ogni piccolo oggetto era facile da verificare per la correttezza e, preso individualmente, spesso erano perfettamente corretti. Eppure ci sentivamo ancora come se stessimo cercando di mantenere un enorme ecosistema composto da piccoli villaggi e provando a ragionare su ciò che fanno tutti individualmente e si sommano per fare insieme. La base di codice anni '80 in stile C sembrava una megalopoli epica e sovrappopolata che era sicuramente un incubo di manutenzione,
  • Perdita di flessibilità a causa della mancanza di astrazione, ma in un'area in cui non abbiamo mai realmente avuto un reale bisogno, quindi difficilmente una soluzione pratica (anche se sicuramente almeno teorica).
  • Preservare la compatibilità ABI è sempre stato difficile e ciò ha reso più difficile la richiesta di dati stabili e non solo un'interfaccia stabile associata a un "comportamento". Tuttavia, potremmo facilmente aggiungere nuovi comportamenti e semplicemente deprecare quelli esistenti se fosse necessario un cambio di stato, e questo è probabilmente più facile che fare backflip sotto le interfacce a livello di sottotipo per gestire i problemi di versioning.

Un fenomeno che si è verificato è stato che, poiché abbiamo perso l'astrazione su questi componenti comportamentali, ne abbiamo avuti di più. Ad esempio, invece di un IRenderablecomponente astratto , associamo un oggetto con un calcestruzzo Mesho un PointSpritescomponente. Il sistema di rendering saprebbe come renderizzare Meshe PointSpritescomponenti e troverebbe entità che forniscono tali componenti e ne disegnano. Altre volte, disponevamo di vari render render del genere SceneLabelche avevamo scoperto col senno di poi, e quindi SceneLabelin quei casi avremmo attaccato a entità rilevanti (possibilmente oltre a Mesh). L'implementazione del sistema di rendering verrebbe quindi aggiornata per sapere come eseguire il rendering delle entità che le hanno fornite e questa è stata una modifica abbastanza semplice da apportare.

In questo caso, un'entità composta da componenti potrebbe anche essere utilizzata come componente per un'altra entità. Costruiremmo le cose in quel modo collegando blocchi di lego.

ECS: sistemi e componenti di dati grezzi

L'ultimo sistema era fino a quando l'ho realizzato da solo e lo stavamo ancora bastardando con COM. Sembrava che volesse diventare un sistema a componenti di entità ma all'epoca non ne avevo familiarità. Stavo guardando esempi in stile COM che hanno saturato il mio campo, quando avrei dovuto guardare i motori di gioco AAA per l'ispirazione architettonica. Alla fine ho iniziato a farlo.

Quello che mi mancava erano diverse idee chiave:

  1. La formalizzazione di "sistemi" per elaborare "componenti".
  2. I "componenti" sono dati grezzi anziché oggetti comportamentali composti insieme in un oggetto più grande.
  3. Entità come nient'altro che un ID rigoroso associato a una raccolta di componenti.

Alla fine ho lasciato quella società e ho iniziato a lavorare su un ECS come indy (ancora lavorando su di esso mentre svuoto i miei risparmi), ed è stato di gran lunga il sistema più semplice da gestire.

Quello che ho notato con l'approccio ECS è che ha risolto i problemi con i quali stavo ancora lottando. Soprattutto per me, mi sembrava di gestire "città" di dimensioni sane anziché piccoli villaggi con interazioni complesse. Non era difficile da mantenere come una "megalopoli" monolitica, troppo grande nella sua popolazione per essere efficacemente gestita, ma non era così caotico come un mondo pieno di piccoli villaggi che interagiscono tra loro dove solo pensare alle rotte commerciali in tra loro formava un grafico da incubo. ECS ha distillato tutta la complessità verso "sistemi" ingombranti, come un sistema di rendering, una "città" di dimensioni sane ma non una "megalopoli sovrappopolata".

I componenti che diventano dati non elaborati all'inizio mi sono sembrati davvero strani , in quanto infrangono persino il principio base che nasconde le informazioni di OOP. È stato una specie di sfida uno dei più grandi valori che mi è piaciuto di OOP, che era la sua capacità di mantenere invarianti che richiedevano incapsulamento e nascondimento delle informazioni. Ma ha iniziato a diventare una non preoccupazione in quanto è diventato rapidamente evidente cosa stesse accadendo con solo una dozzina di sistemi così ampi che trasformavano quei dati invece che tale logica venisse dispersa tra centinaia e migliaia di sottotipi implementando una combinazione di interfacce. Tendo a pensarlo come se fosse ancora in stile OOP, tranne che sparsi dove i sistemi forniscono la funzionalità e l'implementazione che accedono ai dati, i componenti forniscono i dati e le entità forniscono componenti.

È diventato ancora più facile , in modo controintuitivo, ragionare sugli effetti collaterali causati dal sistema quando c'erano solo una manciata di sistemi ingombranti che trasformavano i dati in grandi passaggi. Il sistema è diventato molto più "piatto", le mie pile di chiamate sono diventate più basse che mai per ogni thread. Potrei pensare al sistema a quel livello di sorvegliante e non incappare in strane sorprese.

Allo stesso modo, ha reso semplici anche le aree critiche per le prestazioni rispetto all'eliminazione di tali query. Da quando l'idea di "Sistema" è diventata molto formalizzata, un sistema potrebbe abbonarsi ai componenti a cui era interessato e ricevere solo un elenco di entità memorizzate nella cache che soddisfano tali criteri. Ogni individuo non ha dovuto gestire l'ottimizzazione della memorizzazione nella cache, ma è diventato centralizzato in un unico posto.

Alcuni professionisti:

  • Sembra solo risolvere quasi tutti i principali problemi architettonici che stavo incontrando nella mia carriera senza mai sentirmi intrappolato in un angolo del design quando ho incontrato esigenze impreviste.

Alcuni contro:

  • Ho ancora difficoltà a avvolgerci la testa a volte, e non è il paradigma più maturo o consolidato anche nel settore dei giochi, in cui le persone discutono esattamente su cosa significhi e su come fare le cose. Non è sicuramente qualcosa che avrei potuto fare con l'ex team con cui ho lavorato, che consisteva in membri profondamente legati alla mentalità in stile COM o alla mentalità in stile C degli anni '80 della base di codice originale. Dove a volte mi confondo è come modellare le relazioni in stile grafico tra i componenti, ma ho sempre trovato una soluzione che non si è rivelata orribile in seguito in cui posso semplicemente fare in modo che un componente dipenda da un altro ("questo movimento il componente dipende da questo come genitore e il sistema utilizzerà la memoizzazione per evitare ripetutamente di fare gli stessi calcoli di movimento ricorsivo ", ad es.)
  • L'ABI è ancora difficile, ma finora mi sarei persino azzardato a dire che è più semplice del semplice approccio all'interfaccia. È un cambiamento di mentalità: la stabilità dei dati diventa l'unico obiettivo per ABI, piuttosto che la stabilità dell'interfaccia, e in qualche modo è più facile ottenere la stabilità dei dati rispetto alla stabilità dell'interfaccia (es: nessuna tentazione di cambiare una funzione solo perché ha bisogno di un nuovo parametro. Questo genere di cose accade all'interno di implementazioni di sistema grossolane che non infrangono l'ABI).

inserisci qui la descrizione dell'immagine

Tuttavia, è ragionevole creare anche applicazioni utilizzando l'architettura Component-Entity-System comune nei motori di gioco?

Quindi, direi assolutamente "sì", con il mio esempio personale di VFX che è un candidato forte. Ma è ancora abbastanza simile alle esigenze del gioco.

Non l'ho messo in pratica in aree più remote completamente distaccate dalle preoccupazioni dei motori di gioco (VFX è abbastanza simile), ma mi sembra che molte più aree siano buone candidate per un approccio ECS. Forse anche un sistema di interfaccia grafica sarebbe adatto a uno, ma uso ancora un approccio OOP più lì (ma senza eredità profonda a differenza di Qt, ad esempio).

È un territorio ampiamente inesplorato, ma mi sembra adatto ogni volta che le tue entità possono essere composte da una ricca combinazione di "tratti" (ed esattamente quale combinazione di tratti che forniscono essendo sempre soggetta a cambiamento), e dove hai una manciata di generalizzati sistemi che elaborano entità che presentano i tratti necessari.

In questi casi diventa un'alternativa molto pratica a qualsiasi scenario in cui potresti essere tentato di usare qualcosa come l'eredità multipla o un'emulazione del concetto (mixin, ad esempio) solo per produrre centinaia o più combo in una gerarchia di eredità profonda o centinaia di combo di classi in una gerarchia piatta che implementa una combinazione specifica di interfacce, ma in cui i sistemi sono pochi in numero (decine, ad esempio).

In questi casi, la complessità della base di codice inizia a sembrare più proporzionale al numero di sistemi anziché al numero di combinazioni di tipi, poiché ogni tipo è ora solo un'entità che compone componenti che non sono altro che dati grezzi. I sistemi GUI si adattano naturalmente a questo tipo di specifiche in cui potrebbero avere centinaia di possibili tipi di widget combinati da altri tipi di base o interfacce, ma solo una manciata di sistemi per elaborarli (sistema di layout, sistema di rendering, ecc.). Se un sistema GUI usasse ECS, sarebbe probabilmente molto più facile ragionare sulla correttezza del sistema quando tutte le funzionalità sono fornite da una manciata di questi sistemi invece di centinaia di diversi tipi di oggetti con interfacce ereditate o classi base. Se un sistema di interfaccia grafica utilizzava ECS, i widget non avrebbero funzionalità, solo dati. Solo la manciata di sistemi che elaborano entità widget avrebbe funzionalità. Il modo in cui gli eventi scavalcabili per un widget verrebbero gestiti è al di là di me, ma solo sulla base della mia esperienza limitata finora, non ho trovato un caso in cui quel tipo di logica non potesse essere trasferito centralmente a un determinato sistema in un modo che, in col senno di poi, ha prodotto una soluzione molto più elegante che mi sarei mai aspettato.

Mi piacerebbe vederlo impiegato in più campi, in quanto è stato un salvavita nel mio. Naturalmente è inadatto se il tuo design non si scompone in questo modo, dalle entità che aggregano componenti ai sistemi grossolani che elaborano quei componenti, ma se si adattano naturalmente a questo tipo di modello, è la cosa più meravigliosa che abbia mai incontrato .


1) Cosa ha fatto il tuo esempio di programma VFX dal punto di vista di un utente? 2) A quale progetto ECS stai lavorando ora? ♥ Grazie per aver scritto questo! ♥
cucciolo

1
Spiegazione molto approfondita, grazie. Sento che sto arrivando a molte delle stesse conclusioni che sei riguardo a come ECS sia al di là dei giochi; nel mio caso GUI particolarmente complesse. All'inizio sembra davvero strano andare così contro il grano di ciò che di solito viene fatto (le gerarchie di eredità profonde sono particolarmente importanti nei framework dell'interfaccia utente), ma è incoraggiante vedere gli altri che trovano questo approccio più efficace.
Danny Yaroslavski,

1
Grazie per questo fantastico ... articolo! Per una GUI basata su componenti, consiglierei di guardare UGUI di Unity3d. È incredibilmente flessibile ed estensibile rispetto a quelli basati sull'eredità come CocoaTouch.
Ivan Mir,

16

L'architettura Component-Entity-System per i motori di gioco funziona per i giochi a causa della natura del software di gioco e delle sue caratteristiche uniche e requisiti di qualità. Ad esempio, le entità forniscono un mezzo uniforme per affrontare e lavorare con le cose nel gioco, che possono essere drasticamente differenti nel loro scopo e nel loro uso, ma devono essere rese, aggiornate o serializzate / deserializzate dal sistema in modo uniforme. Incorporando un modello di componente in questa architettura, si consente loro di mantenere una struttura di base semplice, aggiungendo al contempo più caratteristiche e funzionalità secondo necessità, con accoppiamento di codice basso. Esistono diversi sistemi software che potrebbero trarre vantaggio dalle caratteristiche di questo progetto, come applicazioni CAD, codec A / V,

TL; DR - I modelli di progettazione funzionano bene solo quando il dominio problematico è sufficientemente adatto alle caratteristiche e agli svantaggi che impongono alla progettazione.


8

Se il dominio problematico è adatto, sicuramente.

Il mio lavoro attuale riguarda un'app che deve supportare una varietà di funzionalità in base a una serie di fattori di runtime. L'uso di entità basate su componenti per disaccoppiare tutte queste capacità e consentire estensibilità e testabilità in isolamento è stato idilliaco per noi.

modifica: il mio lavoro consiste nel fornire connettività all'hardware proprietario (in C #). A seconda del fattore di forma dell'hardware, del firmware installato, del livello di servizio acquistato dal cliente, ecc., È necessario fornire diversi livelli di funzionalità al dispositivo. Anche alcune funzionalità che hanno la stessa interfaccia hanno implementazioni diverse a seconda della versione del dispositivo.

Le basi di codice precedenti qui avevano interfacce molto ampie con molte non implementate. Alcuni hanno avuto molte interfacce sottili che sono state poi composte staticamente in una classe bestiale. Alcuni hanno semplicemente usato string -> dizionari dizionari per modellarlo. (abbiamo molti dipartimenti che tutti pensano di poterlo fare meglio)

Tutti questi hanno le loro carenze. Le interfacce ampie sono un dolore e mezzo da deridere / testare efficacemente. Aggiungere nuove funzionalità significa cambiare l'interfaccia pubblica (e tutte le implementazioni esistenti). Molte interfacce sottili hanno portato a un consumo molto brutto di codice, ma da quando abbiamo finito per passare un grosso oggetto grasso, i test sono ancora stati sottoposti. Inoltre le interfacce sottili non gestivano bene le loro dipendenze. I dizionari di stringa hanno i soliti problemi di analisi ed esistenza e anche buchi infernali di prestazioni, leggibilità e manutenibilità.

Ciò che stiamo usando ora è un'entità molto sottile che ha i suoi componenti scoperti e composti in base alle informazioni di runtime. Le dipendenze vengono eseguite in modo dichiarativo e si risolvono automaticamente dal framework dei componenti principali. I componenti stessi possono essere testati in modo isolato poiché lavorano direttamente con le loro dipendenze e i problemi con dipendenze mancanti si trovano presto - e in una posizione piuttosto che al primo utilizzo della dipendenza. Nuovi componenti (o test) possono essere inseriti e nessun codice esistente ne è influenzato. I consumatori chiedono all'entità un'interfaccia per il componente, quindi siamo liberi di andare in giro con le varie implementazioni (e come le implementazioni sono mappate ai dati di runtime) con relativa libertà.

Per una situazione come questa in cui la composizione dell'oggetto e le sue interfacce possono includere alcuni sottogruppi (molto vari) di componenti comuni, funziona molto bene.


1
Supponendo che ti sia permesso, puoi fornire maggiori dettagli sul tuo lavoro attuale? Sono curioso di sapere in che modo il CES è stato idilliaco per quello che stai costruendo.
Andrew De Andrade,

C'è qualche articolo, articolo o blog sulla tua esperienza? inoltre, vorrei avere maggiori dettagli tecnici a riguardo :)
user1778770 del

@ user1778770 - non disponibile pubblicamente, no. Che tipo di domande hai avuto?
Telastyn,

Bene, iniziamo con qualcosa di semplice, il tuo concetto copre l'intero stack di applicazioni (ad es. Dal business al frontend)? o solo un singolo strato di un singolo caso d'uso?
user1778770,

@ user1778770 - nella mia implementazione, entità / componenti esistono in un livello. Entità diverse possono esistere in layer diversi, ma spesso non sono 1: 1 (altrimenti i layer non forniscono alcun vantaggio).
Telastyn,
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.