Il titolo è intenzionalmente iperbolico e potrebbe essere solo la mia inesperienza con lo schema, ma ecco il mio ragionamento:
Il modo "normale" o verosimilmente diretto di implementare le entità è implementarle come oggetti e sottoclasse comportamenti comuni. Questo porta al classico problema di "è un EvilTree
una sottoclasse di Tree
o Enemy
?". Se permettiamo l'ereditarietà multipla, sorge il problema dei diamanti. Potremmo invece tirare la funzionalità combinata di Tree
e Enemy
più in alto nella gerarchia che porta alle classi di Dio, oppure possiamo intenzionalmente omettere comportamenti nelle nostre classi Tree
e Entity
(rendendole interfacce nel caso estremo ) in modo che il EvilTree
possa implementare quello stesso, il che porta alla duplicazione del codice se mai abbiamo un SomewhatEvilTree
.
I sistemi Entity-Component cercano di risolvere questo problema dividendo l'oggetto Tree
e Enemy
in diversi componenti - diciamo Position
, Health
e AI
- e implementano sistemi, come AISystem
che modifica la posizione di una Entità secondo le decisioni dell'IA. Fin qui tutto bene, ma cosa succede se EvilTree
può raccogliere un potenziamento e infliggere danno? Per prima cosa abbiamo bisogno di un CollisionSystem
e un DamageSystem
(probabilmente ne abbiamo già). CollisionSystem
deve comunicare con DamageSystem
: Ogni volta che due cose si scontrano, CollisionSystem
invia un messaggio a DamageSystem
in modo da poter sottrarre la salute. Anche il danno è influenzato dai potenziamenti quindi dobbiamo memorizzarlo da qualche parte. Creiamo un nuovo PowerupComponent
che attribuiamo alle entità? Ma poi DamageSystem
deve sapere qualcosa di cui preferirebbe non sapere nulla - dopo tutto, ci sono anche cose che infliggono danno che non possono raccogliere potenziamenti (ad esempio un Spike
). Consentiamo a PowerupSystem
di modificare un StatComponent
utilizzato anche per calcoli di danni simili a questa risposta ? Ma ora due sistemi accedono agli stessi dati. Man mano che il nostro gioco diventa più complesso, diventerebbe un grafico di dipendenza immateriale 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 es. dai il StatComponent
attack()
che restituisce un intero di default ma che può essere composto quando si verifica un powerup:
attack = getAttack compose powerupBy(20) compose powerdownBy(40)
Questo non risolve il problema che attack
deve essere salvato in un componente a cui si accede da più sistemi, ma almeno potrei digitare correttamente le funzioni se ho una lingua che la supporti 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 stia rapidamente avvicinando alla programmazione reattiva funzionale, quindi mi chiedo se non avrei dovuto usarlo all'inizio (ho appena guardato in FRP, quindi potrei sbagliarmi qui). Vedo che ECS è un miglioramento rispetto alle gerarchie di classi complesse, 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? Il FRP è solo strettamente più adatto a questo problema? Questi problemi derivano solo dalla complessità intrinseca di ciò che sto cercando di programmare; Ad esempio, il FRP avrebbe problemi simili?