Sto implementando una variante del sistema di entità che ha:
Una classe Entity che è poco più di un ID che lega i componenti insieme
Un gruppo di classi di componenti che non hanno "logica dei componenti", solo dati
Un gruppo di classi di sistema (aka "sottosistemi", "gestori"). Questi eseguono tutta l'elaborazione logica dell'entità. Nella maggior parte dei casi di base, i sistemi ripetono semplicemente attraverso un elenco di entità a cui sono interessati e fanno un'azione su ciascuna di esse
Un oggetto classe MessageChannel condiviso da tutti i sistemi di gioco. Ogni sistema può iscriversi a un tipo specifico di messaggi da ascoltare e può anche utilizzare il canale per trasmettere messaggi ad altri sistemi
La variante iniziale della gestione dei messaggi di sistema era qualcosa del genere:
- Esegui un aggiornamento su ciascun sistema di gioco in sequenza
Se un sistema fa qualcosa a un componente e quell'azione potrebbe essere di interesse per altri sistemi, il sistema invia un messaggio appropriato (ad esempio, un sistema chiama
messageChannel.Broadcast(new EntityMovedMessage(entity, oldPosition, newPosition))
ogni volta che un'entità viene spostata)
Ogni sistema che si è abbonato al messaggio specifico ottiene il suo metodo di gestione dei messaggi chiamato
Se un sistema gestisce un evento e la logica di elaborazione degli eventi richiede la trasmissione di un altro messaggio, il messaggio viene trasmesso immediatamente e viene chiamata un'altra catena di metodi di elaborazione dei messaggi
Questa variante è andata bene fino a quando non ho iniziato a ottimizzare il sistema di rilevamento delle collisioni (stava diventando molto lento all'aumentare del numero di entità). Inizialmente avrebbe semplicemente iterato ogni coppia di entità usando un semplice algoritmo di forza bruta. Quindi ho aggiunto un "indice spaziale" che ha una griglia di celle che memorizza entità che si trovano all'interno dell'area di una cella specifica, permettendo così di fare controlli solo su entità nelle celle vicine.
Ogni volta che un'entità si muove, il sistema di collisione verifica se l'entità si scontra con qualcosa nella nuova posizione. In tal caso, viene rilevata una collisione. E se entrambe le entità in collisione sono "oggetti fisici" (entrambi hanno il componente RigidBody e sono pensati per allontanarsi a vicenda in modo da non occupare lo stesso spazio), un sistema di separazione del corpo rigido dedicato chiede al sistema di movimento di spostare le entità in alcuni posizioni specifiche che li separerebbero. Questo a sua volta fa sì che il sistema di movimento invii messaggi di notifica sulle posizioni delle entità modificate. Il sistema di rilevamento delle collisioni deve reagire perché deve aggiornare il suo indice spaziale.
In alcuni casi causa un problema perché i contenuti della cella (un generico Elenco di oggetti in C #) vengono modificati mentre vengono ripetuti, causando così un'eccezione da parte dell'iteratore.
Quindi ... come posso evitare che il sistema di collisione venga interrotto mentre controlla le collisioni?
Ovviamente potrei aggiungere una logica "intelligente" / "difficile" che assicuri che i contenuti della cella vengano ripetuti correttamente, ma penso che il problema non risieda nel sistema di collisione stesso (ho avuto anche problemi simili in altri sistemi), ma il modo in cui i messaggi vengono gestiti mentre viaggiano da un sistema all'altro. Ciò di cui ho bisogno è un modo per assicurarmi che uno specifico metodo di gestione degli eventi riesca a funzionare senza interruzioni.
Cosa ho provato:
- Code di messaggi in arrivo . Ogni volta che un sistema trasmette un messaggio, il messaggio viene aggiunto alle code dei sistemi interessati. Questi messaggi vengono elaborati quando viene chiamato un aggiornamento di sistema per ogni frame. Il problema : se un sistema A aggiunge un messaggio alla coda B del sistema, funziona bene se il sistema B deve essere aggiornato successivamente al sistema A (nello stesso frame di gioco); in caso contrario, il messaggio elabora il frame di gioco successivo (non desiderabile per alcuni sistemi)
- Code di messaggi in uscita . Mentre un sistema gestisce un evento, tutti i messaggi che trasmette vengono aggiunti alla coda dei messaggi in uscita. I messaggi non devono attendere l'elaborazione di un aggiornamento del sistema: vengono gestiti "subito" dopo che il gestore dei messaggi iniziale ha terminato il suo funzionamento. Se la gestione dei messaggi provoca la trasmissione di altri messaggi, anche questi vengono aggiunti a una coda in uscita, quindi tutti i messaggi vengono gestiti nello stesso frame. Il problema: se il sistema di durata dell'entità (ho implementato la gestione della durata dell'entità con un sistema) crea un'entità, ne informa alcuni sistemi A e B. Mentre il sistema A elabora il messaggio, provoca una catena di messaggi che alla fine causano la distruzione dell'entità creata (ad esempio, un'entità proiettile è stata creata proprio dove si scontra con qualche ostacolo, che provoca l'autodistruzione del proiettile). Durante la risoluzione della catena di messaggi, il sistema B non riceve il messaggio di creazione dell'entità. Quindi, se anche il sistema B è interessato al messaggio di distruzione dell'entità, lo ottiene e solo dopo che la "catena" ha terminato di risolversi, ottiene il messaggio iniziale di creazione dell'entità. Questo fa sì che il messaggio di distruzione venga ignorato, il messaggio di creazione sia "accettato",
MODIFICA - RISPOSTE ALLE DOMANDE, COMMENTI:
- Chi modifica il contenuto della cella mentre il sistema di collisione scorre su di essi?
Mentre il sistema di collisione sta eseguendo controlli di collisione su alcune entità e sui vicini, una collisione potrebbe essere rilevata e il sistema di entità invierà un messaggio che verrà immediatamente reagito da altri sistemi. La reazione al messaggio potrebbe causare la creazione e la gestione immediata di altri messaggi. Quindi un altro sistema potrebbe creare un messaggio che il sistema di collisione dovrebbe quindi elaborare immediatamente (ad esempio, un'entità spostata in modo che il sistema di collisione debba aggiornare il suo indice spaziale), anche se i precedenti controlli di collisione non erano ancora terminati.
- Non riesci a lavorare con una coda di messaggi in uscita globale?
Di recente ho provato una singola coda globale. Causa nuovi problemi. Problema: sposto un'entità serbatoio in un'entità muro (il serbatoio è controllato con la tastiera). Quindi decido di cambiare direzione del serbatoio. Per separare il serbatoio e il muro da ciascun telaio, il sistema CollidingRigidBodySeparationSystem sposta il serbatoio lontano dal muro con la minima quantità possibile. La direzione di separazione dovrebbe essere opposta alla direzione di movimento del serbatoio (quando inizia il disegno del gioco, il serbatoio dovrebbe apparire come se non si fosse mai mosso nel muro). Ma la direzione diventa opposta alla NUOVA direzione, spostando così il serbatoio su un lato diverso del muro rispetto a com'era inizialmente. Perché si verifica il problema: ecco come vengono gestiti i messaggi ora (codice semplificato):
public void Update(int deltaTime)
{
m_messageQueue.Enqueue(new TimePassedMessage(deltaTime));
while (m_messageQueue.Count > 0)
{
Message message = m_messageQueue.Dequeue();
this.Broadcast(message);
}
}
private void Broadcast(Message message)
{
if (m_messageListenersByMessageType.ContainsKey(message.GetType()))
{
// NOTE: all IMessageListener objects here are systems.
List<IMessageListener> messageListeners = m_messageListenersByMessageType[message.GetType()];
foreach (IMessageListener listener in messageListeners)
{
listener.ReceiveMessage(message);
}
}
}
Il codice scorre in questo modo (supponiamo che non sia il primo frame di gioco):
- I sistemi iniziano l'elaborazione di TimePassedMessage
- InputHandingSystem converte i tasti premuti in azioni entità (in questo caso, una freccia sinistra si trasforma in azione MoveWest). L'azione dell'entità è memorizzata nel componente ActionExecutor
- ActionExecutionSystem , in risposta all'azione dell'entità, aggiunge un MovementDirectionChangeRequestedMessage alla fine della coda dei messaggi
- MovementSystem sposta la posizione dell'entità in base ai dati del componente Velocity e aggiunge il messaggio PositionChangedMessage alla fine della coda. Il movimento viene eseguito utilizzando la direzione / velocità del movimento del fotogramma precedente (diciamo a nord)
- I sistemi interrompono l'elaborazione di TimePassedMessage
- I sistemi iniziano l'elaborazione di MovementDirectionChangeRequestedMessage
- MovementSystem modifica la velocità dell'entità / direzione del movimento come richiesto
- I sistemi interrompono l'elaborazione di MovementDirectionChangeRequestedMessage
- I sistemi iniziano l'elaborazione di PositionChangedMessage
- CollisionDetectionSystem rileva che, poiché un'entità si spostava, si imbatteva in un'altra entità (il carro armato andava all'interno di un muro). Aggiunge un CollisionOccuredMessage alla coda
- I sistemi interrompono l'elaborazione di PositionChangedMessage
- I sistemi iniziano l'elaborazione di CollisionOccuredMessage
- CollidingRigidBodySeparationSystem reagisce alla collisione separando serbatoio e parete. Poiché il muro è statico, viene spostato solo il serbatoio. La direzione di movimento dei serbatoi viene utilizzata come indicatore della provenienza del serbatoio. È spostato in una direzione opposta
ERRORE: Quando il serbatoio spostava questo frame, si spostava usando la direzione del movimento dal frame precedente, ma quando veniva separato, veniva usata la direzione del movimento da QUESTO frame, anche se era già diverso. Non è così che dovrebbe funzionare!
Per prevenire questo errore, la vecchia direzione del movimento deve essere salvata da qualche parte. Potrei aggiungerlo a qualche componente solo per correggere questo specifico bug, ma questo caso non indica un modo fondamentalmente sbagliato di gestire i messaggi? Perché il sistema di separazione dovrebbe preoccuparsi di quale direzione di movimento utilizza? Come posso risolvere questo problema con eleganza?
- Potresti voler leggere gamadu.com/artemis per vedere cosa hanno fatto con Aspects, da cui parte alcuni dei problemi che stai riscontrando.
In realtà, conosco Artemis da un po 'di tempo ormai. Ho studiato il suo codice sorgente, letto i forum, ecc. Ma ho visto "Aspetti" menzionati solo in alcuni punti e, per quanto ne capisco, in pratica significano "Sistemi". Ma non riesco a vedere come Artemis side risolva alcuni dei miei problemi. Non usa nemmeno i messaggi.
- Vedi anche: "Comunicazione entità: coda messaggi vs Pubblica / Iscriviti vs Segnale / Slot"
Ho già letto tutte le domande su gamedev.stackexchange relative ai sistemi di entità. Questo non sembra discutere dei problemi che sto affrontando. Mi sto perdendo qualcosa?
- Gestire i due casi in modo diverso, l'aggiornamento della griglia non deve fare affidamento sui messaggi di movimento poiché fa parte del sistema di collisione
Non sono sicuro di cosa intendi. Le implementazioni precedenti di CollisionDetectionSystem avrebbero semplicemente verificato la presenza di collisioni su un aggiornamento (quando veniva gestito un TimePassedMessage), ma dovevo minimizzare i controlli quanto potevo a causa delle prestazioni. Quindi sono passato al controllo delle collisioni quando un'entità si muove (la maggior parte delle entità nel mio gioco sono statiche).