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 "è EvilTree
una sottoclasse di Tree
o Enemy
?". Se permettiamo l'ereditarietà multipla, sorge il problema del diamante. Potremmo invece estrapolare la funzionalità combinata Tree
e Enemy
rafforzare la gerarchia che conduce alle classi di Dio, oppure possiamo intenzionalmente tralasciare il comportamento nelle nostre Tree
e nelle Entity
classi (rendendole interfacce in casi estremi) in modo che EvilTree
possano implementare quella stessa - che porta a duplicazione del codice se mai avremo un SomewhatEvilTree
.
I sistemi Entity-Component cercano di risolvere questo problema dividendo Tree
e Enemy
obiettare in diversi componenti - diciamo Position
, Health
e AI
- e implementano sistemi, come quelli AISystem
che cambiano la posizione di un'entità in base alle decisioni dell'IA. Fin qui tutto bene, ma cosa succede se EvilTree
può prendere un power-up e infliggere danni? Per prima cosa abbiamo bisogno di a CollisionSystem
e a DamageSystem
(probabilmente abbiamo già questi). La CollisionSystem
necessità di comunicare con DamageSystem
: Ogni volta che due cose si scontrano, CollisionSystem
invia un messaggio al DamageSystem
affinché possa sottrarre salute. Anche i danni sono influenzati dai potenziamenti, quindi dobbiamo conservarli da qualche parte. Creiamo un nuovo PowerupComponent
che attribuiamo alle entità? Ma poi ilDamageSystem
deve 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 PowerupSystem
di modificare un StatComponent
che 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 attack
deve 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?