Uso de una arquitectura de sistema de entidad con paralelismo basado en tareas

9

Antecedentes

He estado trabajando en la creación de un motor de juegos multiproceso en mi tiempo libre y actualmente estoy tratando de decidir la mejor manera de trabajar un sistema de entidades en lo que ya he creado. Hasta ahora, he usado este artículo de Intel como punto de partida para mi motor. Hasta ahora, he implementado el ciclo de juego normal usando tareas, y ahora estoy avanzando para incorporar algunos de los sistemas y / o sistemas de entidades. He usado algo similar a Artemis en el pasado, pero el paralelismo me está desanimando.

El artículo de Intel parece abogar por tener múltiples copias de los datos de la entidad, y que se realicen cambios a cada entidad que se distribuye internamente al final de una actualización completa. Esto significa que el renderizado siempre estará un fotograma atrás, pero parece un compromiso aceptable dados los beneficios de rendimiento que se deben obtener. Sin embargo, cuando se trata de un sistema de entidades como Artemis, tener cada entidad duplicada para cada sistema significa que cada componente también necesitará duplicarse. Esto es factible, pero para mí parece que usaría mucha memoria. Las partes del documento de Intel que discuten esto son 2.2 y 3.2.2 principalmente. He buscado un poco para ver si puedo encontrar buenas referencias para integrar las arquitecturas que busco, pero aún no he podido encontrar nada útil.

Nota: Estoy usando C ++ 11 para este proyecto, pero imagino que la mayoría de lo que pido debería ser bastante independiente del lenguaje.

Solución posible

Tenga un EntityManager global que se use para crear y administrar Entidades y Atributos de Entidad. Permita el acceso de lectura a ellos solo durante la fase de actualización y almacene todos los cambios en una cola por hilo. Una vez que se completan todas las tareas, las colas se combinan y se aplican los cambios en cada una. Esto podría tener problemas con múltiples escrituras en los mismos campos, pero estoy seguro de que podría haber un sistema de prioridad o marca de tiempo para solucionarlo. Esto parece un buen enfoque para mí porque los sistemas pueden ser notificados de los cambios en las entidades de forma bastante natural durante la etapa de distribución de cambios.

Pregunta

Estoy buscando comentarios sobre mi solución para ver si tiene sentido. No mentiré y afirmaré que soy un experto en subprocesos múltiples, y lo estoy haciendo en gran medida por práctica. Puedo prever algunos problemas complicados que surgen de mi solución en la que múltiples sistemas leen / escriben múltiples valores. La cola de cambios que mencioné también podría ser difícil de formatear de manera que cualquier cambio posible pudiera comunicarse fácilmente cuando no estoy trabajando con POD.

Cualquier comentario / consejo sería muy apreciado! ¡Gracias!

Enlaces

Ross Hays
fuente

Respuestas:

12

Fork-Join

No necesita copias separadas de los componentes. Simplemente use un modelo de unión de horquilla, que se menciona (extremadamente mal) en ese artículo de Intel.

En un ECS, efectivamente tiene un bucle similar a:

while in game:
  for each system:
    for each component in system:
      update component

Cambie esto a algo como:

while in game:
  for each system:
    divide components into groups
    for each group:
      start thread (
        for each component in group:
          update component
      )
    wait for all threads to finish

La parte difícil es el bit "dividir componentes en grupos". Para los gráficos casi no hay necesidad de compartir datos, por lo que es simple (divida los objetos renderizables de manera uniforme por el número de subprocesos de trabajo disponibles). Para la física y la IA, desea encontrar "islas" lógicas de objetos que no interactúen y juntarlas. Cuanta menos interacción entre componentes, mejor.

Para la interacción que debe existir, los mensajes retrasados ​​funcionan mejor. Si el objeto A necesita decirle al objeto B que sufra daños, A puede colocar un mensaje en un grupo por subproceso. Cuando se unen los subprocesos, los grupos se concatenan en un solo grupo. Si bien no está directamente relacionado con el subproceso, vea la serie de eventos de artículos de los desarrolladores de BitSquid (de hecho, lea todo el blog; no estoy de acuerdo con todo lo que hay allí, pero es un recurso fantástico).

Tenga en cuenta que "fork-join" no significa usar fork()(lo que crea procesos, no hilos), ni implica que realmente debe unir los hilos. Simplemente significa que toma una sola tarea, la divide en partes más pequeñas para que las maneje su grupo de subprocesos de trabajo y luego espera a que se procesen todas las parcelas.

Proxies

Este enfoque se puede usar solo o en combinación con el método de unión en horquilla para que la necesidad de una separación estricta sea menos importante.

Puede ser más amigable con los hilos interactivos utilizando un enfoque simple de dos capas. Tener entidades "autorizadas" y entidades "proxy". Las entidades autorizadas solo pueden modificarse desde un único hilo que es el claro propietario de la entidad autorizada. Las entidades proxy no pueden modificarse, solo leerse. En un punto de sincronización en el ciclo del juego, propague todos los cambios de las entidades autorizadas a los servidores proxy correspondientes.

Reemplace "entidades" con "componentes" según corresponda. La esencia es que necesita como máximo dos copias de cualquier objeto, y hay puntos claros de "sincronización" en su bucle de juego cuando puede copiar de uno a otro en la mayoría de los diseños de motores de juego roscados.

Puede expandir proxies para permitir que se sigan utilizando (un subconjunto de) métodos / mensajes simplemente enviando todos estos elementos a una cola que se entrega al objeto autorizado en el siguiente marco.

Tenga en cuenta que el enfoque proxy es un diseño fantástico para tener en un nivel superior, ya que hace que el soporte de red sea muy fácil.

Sean Middleditch
fuente
Había leído algunas cosas sobre la bifurcación que mencionaste antes y tenía la impresión de que si bien te permite utilizar cierto paralelismo, hay situaciones en las que algunos hilos de trabajo pueden estar esperando que un grupo termine. Idealmente, estoy tratando de evitar esa situación. La idea de proxy es interesante y se parece un poco a lo que estaba trabajando. Una entidad tiene EntityAttributes y esos son envoltorios para los valores realmente almacenados por la entidad. Entonces, los valores se pueden leer en cualquier momento, pero solo se establecen en ciertos momentos y pueden contener un valor proxy en el atributo, ¿correcto?
Ross Hays
1
Existe una buena posibilidad de que al tratar de evitar la espera, pase tanto tiempo analizando el gráfico de dependencia que pierda tiempo en general.
Patrick Hughes
@roflha: sí, podrías poner los proxies en el nivel EntityAttribute. O haga una entidad separada con un segundo conjunto de atributos. O simplemente abandone el concepto de atributos por completo y use un diseño de componentes menos granular.
Sean Middleditch
@SeanMiddleditch Cuando digo atributo, me refiero esencialmente a los componentes que creo. Los atributos no son solo valores individuales como flotantes y cadenas si así es como lo hice sonar. Más bien son clases que contienen información específica como un PositionAttribute. Si componente es el nombre aceptado para eso, entonces quizás debería cambiar. ¿Pero recomendaría el proxy en el nivel de entidad en lugar del nivel de componente / atributo?
Ross Hays
1
Recomiendo lo que encuentre más fácil de implementar. Solo recuerda que el punto sería poder consultar proxies sin tomar bloqueos, sin usar ningún atómico y sin puntos muertos.
Sean Middleditch