Estoy implementando una variante de sistema de entidad que tiene:
Una clase de entidad que es poco más que una ID que une componentes
Un grupo de clases de componentes que no tienen "lógica de componentes", solo datos
Un grupo de clases de sistema (también conocido como "subsistemas", "administradores"). Estos hacen todo el procesamiento lógico de la entidad. En la mayoría de los casos básicos, los sistemas simplemente iteran a través de una lista de entidades que les interesan y realizan una acción sobre cada una de ellas.
Un objeto de clase MessageChannel que es compartido por todos los sistemas de juego. Cada sistema puede suscribirse a un tipo específico de mensajes para escuchar y también puede usar el canal para transmitir mensajes a otros sistemas
La variante inicial del manejo de mensajes del sistema era algo como esto:
- Ejecute una actualización en cada sistema de juego secuencialmente
Si un sistema hace algo a un componente y esa acción podría ser de interés para otros sistemas, el sistema envía un mensaje apropiado (por ejemplo, un sistema llama
messageChannel.Broadcast(new EntityMovedMessage(entity, oldPosition, newPosition))
cada vez que se mueve una entidad)
Cada sistema que se suscribió al mensaje específico obtiene su método de manejo de mensajes llamado
Si un sistema maneja un evento y la lógica de procesamiento de eventos requiere que se transmita otro mensaje, el mensaje se transmite de inmediato y se llama a otra cadena de métodos de procesamiento de mensajes
Esta variante estuvo bien hasta que comencé a optimizar el sistema de detección de colisión (se estaba volviendo muy lento a medida que aumentaba el número de entidades). Al principio, solo iteraría cada par de entidades utilizando un algoritmo de fuerza bruta simple. Luego agregué un "índice espacial" que tiene una cuadrícula de celdas que almacena entidades que están dentro del área de una celda específica, lo que permite hacer verificaciones solo en entidades en celdas vecinas.
Cada vez que una entidad se mueve, el sistema de colisión verifica si la entidad está colisionando con algo en la nueva posición. Si es así, se detecta una colisión. Y si ambas entidades en colisión son "objetos físicos" (ambas tienen un componente RigidBody y están destinadas a alejarse entre sí para no ocupar el mismo espacio), un sistema de separación de cuerpo rígido dedicado le pide al sistema de movimiento que mueva las entidades a algunos posiciones específicas que los separarían. Esto a su vez hace que el sistema de movimiento envíe mensajes notificando sobre las posiciones cambiadas de la entidad. El sistema de detección de colisión está destinado a reaccionar porque necesita actualizar su índice espacial.
En algunos casos, causa un problema porque el contenido de la celda (una lista genérica de objetos de entidad en C #) se modifica mientras se repite, lo que provoca que el iterador arroje una excepción.
Entonces ... ¿cómo puedo evitar que el sistema de colisión se interrumpa mientras verifica si hay colisiones?
Por supuesto, podría agregar una lógica "inteligente" / "complicada" que garantice que el contenido de la celda se repita correctamente, pero creo que el problema no radica en el sistema de colisión en sí (también tuve problemas similares en otros sistemas), sino en la forma los mensajes se manejan a medida que viajan de un sistema a otro. Lo que necesito es alguna forma de asegurarme de que un método de manejo de eventos específico haga su trabajo sin interrupciones.
Lo que he intentado:
- Colas de mensajes entrantes . Cada vez que un sistema transmite un mensaje, el mensaje se agrega a las colas de mensajes de los sistemas que están interesados en él. Estos mensajes se procesan cuando una actualización del sistema se llama cada trama. El problema : si un sistema A agrega un mensaje a la cola B del sistema, funciona bien si el sistema B debe actualizarse más tarde que el sistema A (en el mismo marco del juego); de lo contrario, hace que el mensaje procese el siguiente marco del juego (no es deseable para algunos sistemas)
- Colas de mensajes salientes . Mientras un sistema maneja un evento, cualquier mensaje que transmita se agrega a la cola de mensajes salientes. Los mensajes no necesitan esperar a que se procese una actualización del sistema: se manejan "de inmediato" una vez que el manejador de mensajes inicial ha finalizado su trabajo. Si el manejo de los mensajes hace que se transmitan otros mensajes, también se agregan a una cola saliente, por lo que todos los mensajes se manejan en el mismo marco. El problema: si el sistema de vida útil de la entidad (implementé la gestión de vida útil de la entidad con un sistema) crea una entidad, notifica algunos sistemas A y B al respecto. Mientras el sistema A procesa el mensaje, provoca una cadena de mensajes que eventualmente hace que la entidad creada sea destruida (por ejemplo, se creó una entidad de bala justo donde choca con algún obstáculo, lo que hace que la bala se autodestruya). Mientras se resuelve la cadena de mensajes, el sistema B no recibe el mensaje de creación de la entidad. Entonces, si el sistema B también está interesado en el mensaje de destrucción de la entidad, lo obtiene, y solo después de que la "cadena" termina de resolverse, obtiene el mensaje inicial de creación de la entidad. Esto hace que el mensaje de destrucción sea ignorado, el mensaje de creación sea "aceptado",
EDITAR - RESPUESTAS A PREGUNTAS, COMENTARIOS:
- ¿Quién modifica el contenido de la celda mientras el sistema de colisión itera sobre ellos?
Mientras el sistema de colisión está realizando comprobaciones de colisión en alguna entidad y sus vecinos, puede detectarse una colisión y el sistema de la entidad enviará un mensaje que será reaccionado de inmediato por otros sistemas. La reacción al mensaje puede hacer que se creen otros mensajes y también se manejen de inmediato. Por lo tanto, algún otro sistema podría crear un mensaje de que el sistema de colisión tendría que procesarse de inmediato (por ejemplo, una entidad se movió por lo que el sistema de colisión necesita actualizar su índice espacial), a pesar de que las verificaciones de colisión anteriores aún no se habían terminado.
- ¿No puedes trabajar con una cola de mensajes salientes global?
Intenté una sola cola global recientemente. Causa nuevos problemas. Problema: muevo una entidad de tanque a una entidad de pared (el tanque se controla con el teclado). Entonces decido cambiar la dirección del tanque. Para separar el tanque y la pared de cada marco, el sistema CollidingRigidBodySeparationSystem aleja el tanque de la pared en la menor cantidad posible. La dirección de separación debe ser opuesta a la dirección de movimiento del tanque (cuando comienza el sorteo del juego, el tanque debe verse como si nunca se hubiera movido hacia la pared). Pero la dirección se vuelve opuesta a la NUEVA dirección, moviendo el tanque a un lado diferente de la pared de lo que era inicialmente. Por qué ocurre el problema: así es como se manejan los mensajes ahora (código simplificado):
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);
}
}
}
El código fluye así (supongamos que no es el primer fotograma del juego):
- Los sistemas comienzan a procesar TimePassedMessage
- InputHandingSystem convierte las pulsaciones de teclas en acción de entidad (en este caso, una flecha izquierda se convierte en acción MoveWest). La acción de la entidad se almacena en el componente ActionExecutor
- ActionExecutionSystem , en reacción a la acción de la entidad, agrega un MotionDirectionChangeRequestedMessage al final de la cola de mensajes
- MovementSystem mueve la posición de la entidad en función de los datos del componente Velocity y agrega el mensaje PositionChangedMessage al final de la cola. El movimiento se realiza utilizando la dirección / velocidad del movimiento del cuadro anterior (digamos norte)
- Los sistemas dejan de procesar TimePassedMessage
- Los sistemas comienzan a procesar MotionDirectionChangeRequestedMessage
- Movimiento El sistema cambia la velocidad de la entidad / dirección del movimiento según lo solicitado
- Los sistemas dejan de procesar MotionDirectionChangeRequestedMessage
- Los sistemas comienzan a procesar PositionChangedMessage
- CollisionDetectionSystem detecta que debido a que una entidad se movió, se topó con otra entidad (el tanque entró dentro de una pared). Agrega un mensaje CollisionOccuredMessage a la cola
- Los sistemas dejan de procesar PositionChangedMessage
- Los sistemas comienzan a procesar CollisionOccuredMessage
- CollidingRigidBodySeparationSystem reacciona a la colisión separando el tanque y la pared. Como la pared es estática, solo se mueve el tanque. La dirección de movimiento de los tanques se usa como un indicador de dónde vino el tanque. Está desplazado en una dirección opuesta
ERROR: Cuando el tanque movió este cuadro, se movió usando la dirección de movimiento del cuadro anterior, pero cuando se estaba separando, se usó la dirección de movimiento de ESTE cuadro, aunque ya era diferente. ¡No es así como debería funcionar!
Para evitar este error, la antigua dirección de movimiento debe guardarse en algún lugar. Podría agregarlo a algún componente solo para corregir este error específico, pero ¿este caso no indica alguna forma fundamentalmente incorrecta de manejar los mensajes? ¿Por qué debería importarle al sistema de separación qué dirección de movimiento utiliza? ¿Cómo puedo resolver este problema con elegancia?
- Es posible que desee leer gamadu.com/artemis para ver qué hicieron con Aspects, qué lado explica algunos de los problemas que está viendo.
En realidad, he estado familiarizado con Artemis desde hace bastante tiempo. Investigué su código fuente, leí los foros, etc. Pero he visto que se mencionan "Aspectos" solo en unos pocos lugares y, por lo que entiendo, básicamente significan "Sistemas". Pero no puedo ver cómo Artemis evita algunos de mis problemas. Ni siquiera usa mensajes.
- Consulte también: "Comunicación de la entidad: Cola de mensajes vs Publicar / Suscribir vs Señal / Ranuras"
Ya leí todas las preguntas de gamedev.stackexchange sobre los sistemas de entidades. Este no parece discutir los problemas que estoy enfrentando. ¿Me estoy perdiendo de algo?
- Maneje los dos casos de manera diferente, la actualización de la cuadrícula no necesita depender de los mensajes de movimiento, ya que es parte del sistema de colisión
No estoy seguro de lo que quieres decir. Las implementaciones anteriores de CollisionDetectionSystem solo verificaban las colisiones en una actualización (cuando se manejaba un TimePassedMessage), pero tenía que minimizar las verificaciones tanto como podía debido al rendimiento. Así que cambié a la verificación de colisiones cuando una entidad se mueve (la mayoría de las entidades en mi juego son estáticas).