Un sistema Entity-Component non è terribile per il disaccoppiamento / nascondere le informazioni?


11

Il titolo è intenzionalmente iperbolico e potrebbe essere solo la mia inesperienza con il modello, ma ecco il mio ragionamento:

Il modo "normale" o probabilmente semplice di implementare le entità è implementarle come oggetti e sottoclassare comportamenti comuni. Questo porta al classico problema di "è EvilTreeuna sottoclasse di Treeo Enemy?". Se permettiamo l'ereditarietà multipla, sorge il problema del diamante. Potremmo invece estrapolare la funzionalità combinata Treee Enemyrafforzare la gerarchia che conduce alle classi di Dio, oppure possiamo intenzionalmente tralasciare il comportamento nelle nostre Treee nelle Entityclassi (rendendole interfacce in casi estremi) in modo che EvilTreepossano implementare quella stessa - che porta a duplicazione del codice se mai avremo un SomewhatEvilTree.

I sistemi Entity-Component cercano di risolvere questo problema dividendo Treee Enemyobiettare in diversi componenti - diciamo Position, Healthe AI- e implementano sistemi, come quelli AISystemche cambiano la posizione di un'entità in base alle decisioni dell'IA. Fin qui tutto bene, ma cosa succede se EvilTreepuò prendere un power-up e infliggere danni? Per prima cosa abbiamo bisogno di a CollisionSysteme a DamageSystem(probabilmente abbiamo già questi). La CollisionSystemnecessità di comunicare con DamageSystem: Ogni volta che due cose si scontrano, CollisionSysteminvia un messaggio al DamageSystemaffinché possa sottrarre salute. Anche i danni sono influenzati dai potenziamenti, quindi dobbiamo conservarli da qualche parte. Creiamo un nuovo PowerupComponentche attribuiamo alle entità? Ma poi ilDamageSystemdeve sapere qualcosa di cui preferirebbe non sapere nulla - dopotutto, ci sono anche cose che infliggono danni che non possono raccogliere potenziamenti (ad es Spike. a). Consentiamo PowerupSystemdi modificare un StatComponentche viene utilizzato anche per calcoli di danno simili a questa risposta ? Ma ora due sistemi accedono agli stessi dati. Man mano che il nostro gioco diventa più complesso, diventerebbe un grafico delle dipendenze immateriali in cui i componenti sono condivisi tra molti sistemi. A quel punto possiamo semplicemente usare le variabili statiche globali e sbarazzarci di tutto il boilerplate.

C'è un modo efficace per risolvere questo? Un'idea che avevo era quella di lasciare che i componenti avessero determinate funzioni, ad esempio dare StatComponent attack()che restituisce un intero per impostazione predefinita ma può essere composto quando si verifica un accensione:

attack = getAttack compose powerupBy(20) compose powerdownBy(40)

Questo non risolve il problema che attackdeve essere salvato in un componente a cui accedono più sistemi, ma almeno potrei digitare correttamente le funzioni se ho un linguaggio che lo supporta sufficientemente:

// In StatComponent
type Strength = PrePowerup | PostPowerup
type Damage = Int
type PrePowerup = Int
type PostPowerup = Int
attack: Strength = getAttack //default value, can be changed by systems
getAttack: PrePowerup

// these functions can be defined in other components or in PowerupSystems
powerupBy: Strength -> PostPowerup
powerdownBy: Strength -> PostPowerup
subtractArmor: Strength -> Damage

// in DamageSystem
dealDamage: Damage -> () = attack compose subtractArmor compose hurtSomeEntity

In questo modo garantisco almeno il corretto ordinamento delle varie funzioni aggiunte dai sistemi. Ad ogni modo, sembra che mi sto avvicinando rapidamente alla programmazione reattiva funzionale qui, quindi mi chiedo se non avrei dovuto usarlo dall'inizio (ho appena esaminato FRP, quindi potrei sbagliarmi qui). Vedo che ECS è un miglioramento rispetto a complesse gerarchie di classi, ma non sono convinto che sia l'ideale.

C'è una soluzione intorno a questo? C'è una funzionalità / modello che mi manca per disaccoppiare ECS in modo più pulito? FRP è semplicemente più adatto a questo problema? Questi problemi nascono solo dalla complessità intrinseca di ciò che sto cercando di programmare; cioè FRP avrebbe problemi simili?



Mi manca molto il blog di Eric (di quando parlava di C #).
OldFart

Risposte:


21

ECS rovina completamente il nascondimento dei dati. Questo è un compromesso del modello.

ECS è eccellente nel disaccoppiamento. Un buon ECS consente a un sistema di spostamento di dichiarare di funzionare su qualsiasi entità che abbia una componente velocità e posizione, senza preoccuparsi di quali tipi di entità esistono o di quali altri sistemi accedono a questi componenti. Questo è almeno equivalente nel disaccoppiare il potere ad avere oggetti di gioco che implementano determinate interfacce.

Due sistemi che accedono agli stessi componenti sono una caratteristica, non un problema. È pienamente previsto e non accoppia i sistemi in alcun modo. È vero che i sistemi avranno un grafico delle dipendenze implicito, ma tali dipendenze sono inerenti al mondo modellato. Dire che il sistema di danno non dovrebbe avere la dipendenza implicita dal sistema di powerup è affermare che i power-up non influenzano il danno e che probabilmente è sbagliato. Tuttavia, mentre esiste la dipendenza, i sistemi non sono accoppiati : puoi rimuovere il sistema di accensione dal gioco senza influire sul sistema di danno, perché la comunicazione è avvenuta attraverso il componente stat ed era completamente implicita.

La risoluzione di queste dipendenze e dei sistemi di ordinazione può essere eseguita in un'unica posizione centrale, in modo analogo a come funziona la risoluzione delle dipendenze in un sistema DI. Sì, un gioco complesso avrà un grafico complesso di sistemi, ma questa complessità è inerente e almeno è contenuta.


7

Non c'è quasi modo di aggirare il fatto che un sistema deve accedere a più componenti. Affinché qualcosa come un VelocitySystem funzioni, probabilmente avrebbe bisogno di accedere a un VelocityComponent e PositionComponent. Nel frattempo anche RenderingSystem deve accedere a questi dati. Indipendentemente da ciò che fai, ad un certo punto il sistema di rendering deve sapere dove eseguire il rendering dell'oggetto e VelocitySystem deve sapere dove spostare l'oggetto.

Ciò di cui hai bisogno è l' esplicitazione delle dipendenze. Ogni sistema deve essere esplicito su quali dati leggerà e su quali dati scriverà. Quando un sistema vuole recuperare un particolare componente, deve essere in grado di farlo solo esplicitamente . Nella sua forma più semplice, ha semplicemente i componenti per ogni tipo richiesto (ad es. Il RenderSystem ha bisogno di RenderComponents e PositionComponents) come argomenti e restituisce qualunque cosa sia cambiata (ad es. Solo RenderComponents).

In questo modo garantisco almeno il corretto ordinamento delle varie funzioni aggiunte dai sistemi

Puoi avere un ordine in un tale design. Nulla dice che per ECS i tuoi sistemi debbano essere indipendenti dall'ordine o cose del genere.

FRP è semplicemente più adatto a questo problema? Questi problemi nascono solo dalla complessità intrinseca di ciò che sto cercando di programmare; cioè FRP avrebbe problemi simili?

L'utilizzo di questa progettazione del sistema di componenti entità e FRP non si escludono a vicenda. In effetti, i sistemi possono essere visti come nient'altro che non avere uno stato, semplicemente eseguendo trasformazioni di dati (i componenti).

FRP non risolverebbe il problema di dover utilizzare le informazioni richieste per eseguire alcune operazioni.

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.