¿Almacenar todos los objetos del juego en una sola lista es un diseño aceptable?

24

Por cada juego que hice, acabo colocando todos mis objetos de juego (balas, autos, jugadores) en una lista de matriz única, que recorro para dibujar y actualizar. El código de actualización para cada entidad se almacena dentro de su clase.

Me he estado preguntando, ¿es esta la forma correcta de hacer las cosas o es una forma más rápida y eficiente?

Derek
fuente
44
Noté que dijo "lista de matriz", suponiendo que esto sea .Net, en su lugar, debería actualizar a una Lista <T>. Es casi idéntico, excepto que una Lista <T> es de tipo fuerte y debido a eso, no tendrá que escribir cast cada vez que acceda a un elemento. Aquí está el documento: msdn.microsoft.com/en-us/library/6sh2ey19.aspx
John McDonald

Respuestas:

26

No hay una manera correcta. Lo que está describiendo funciona, y presumiblemente es lo suficientemente rápido como para que no importe, ya que parece estar preguntando porque le preocupa el diseño y no porque le haya causado problemas de rendimiento. Entonces es una manera correcta, claro.

Ciertamente, podría hacerlo de manera diferente, por ejemplo, manteniendo una lista homogénea para cada tipo de objeto o asegurándose de que todo en su lista heterogénea esté agrupado de manera tal que haya mejorado la coherencia para el código que está en caché o para permitir actualizaciones paralelas. Es difícil decir si vería una mejora apreciable del rendimiento al hacer esto sin conocer el alcance y la escala de sus juegos.

También podría factorizar aún más las responsabilidades de sus entidades (parece que las entidades se actualizan y se representan a sí mismas en este momento) para seguir mejor el Principio de responsabilidad única. Esto puede aumentar la capacidad de mantenimiento y la flexibilidad de sus interfaces individuales al desacoplarlas entre sí. Pero, una vez más, es difícil para mí saber si o no verías un beneficio del trabajo adicional que implicaría saber lo que sé de tus juegos (que básicamente no es nada).

Recomiendo no estresarse demasiado al respecto y tener en cuenta que podría ser un área potencial de mejora si alguna vez nota problemas de rendimiento o de mantenimiento.

Josh
fuente
16

Como han dicho otros, si es lo suficientemente rápido, entonces es lo suficientemente rápido. Si se pregunta si hay una mejor manera, la pregunta que debe hacerse es

¿Me encuentro iterando a través de todos los objetos pero solo operando en un subconjunto de ellos?

Si lo hace, eso es una indicación de que es posible que desee tener varias listas que contengan solo referencias relevantes. Por ejemplo, si está recorriendo cada objeto del juego para renderizarlos pero solo se necesita renderizar un subconjunto de objetos, puede valer la pena mantener una lista Renderizable separada solo para aquellos objetos que necesitan renderizarse.

Esto reduciría la cantidad de objetos que recorre y omite las comprobaciones innecesarias porque ya sabe que todos los objetos de la lista deben procesarse.

Tendría que perfilarlo para ver si está obteniendo una gran ganancia de rendimiento.

Miguel
fuente
Estoy de acuerdo con esto, generalmente tengo subconjuntos de mi lista para los objetos que necesitan actualizarse en cada cuadro, y he estado considerando hacer lo mismo para los objetos que se procesan. Sin embargo, cuando esas propiedades pueden cambiar sobre la marcha, administrar todas las listas puede ser complejo. Cuándo y el objeto pasa de ser renderizable a no renderizable, o actualizable a no actualizable, y ahora los agrega o elimina de varias listas a la vez.
Nic Foster
8

Depende casi por completo de tus necesidades específicas de juego . Puede funcionar perfectamente para un juego simple, pero falla en un sistema más complejo. Si está funcionando para tu juego, no te preocupes por eso o trata de diseñarlo en exceso hasta que surja la necesidad.

En cuanto a por qué el enfoque simple puede fallar en algunas situaciones, es difícil resumirlo en una sola publicación, ya que las posibilidades son infinitas y dependen completamente de los juegos en sí. Otras respuestas han mencionado, por ejemplo, que es posible que desee agrupar objetos que comparten responsabilidades en una lista separada.

Ese es uno de los cambios más comunes que podrías estar haciendo dependiendo del diseño de tu juego. Aprovecho la oportunidad para describir algunos otros ejemplos (más complejos), pero recuerde que todavía hay muchas otras posibles razones y soluciones.

Para empezar, señalaré que actualizar los objetos de su juego y representarlos puede tener diferentes requisitos en algunos juegos y, por lo tanto, deben manejarse por separado. Algunos posibles problemas con la actualización y representación de objetos del juego:

Actualización de objetos del juego

Aquí hay un extracto de Game Engine Architecture que recomendaría leer:

En presencia de dependencias entre objetos, la técnica de actualizaciones por fases descrita anteriormente debe ajustarse un poco. Esto se debe a que las dependencias entre objetos pueden generar reglas conflictivas que rigen el orden de actualización.

En otras palabras, en algunos juegos es posible que los objetos dependan unos de otros y requieran un orden específico de actualización. En este escenario, es posible que deba diseñar una estructura más compleja que una lista para almacenar objetos y sus interdependencias.

ingrese la descripción de la imagen aquí

Representación de objetos del juego

En algunos juegos, las escenas y los objetos crean una jerarquía de nodos padre-hijo y deben representarse en el orden correcto y con transformaciones en relación con sus padres. En esos casos, es posible que necesite una estructura más compleja como un gráfico de escena (una estructura de árbol) en lugar de una sola lista:

ingrese la descripción de la imagen aquí

Otras razones podrían ser, por ejemplo, el uso de una estructura de datos de particionamiento espacial para organizar sus objetos de una manera que mejore la eficiencia de realizar el sacrificio de vista de frustum.

David Gouveia
fuente
4

La respuesta es "sí, esto está bien". Cuando trabajé en grandes simulaciones de hierro donde el rendimiento no era negociable, me sorprendió ver todo en una gran matriz global (GRANDE y GRANDE). Más tarde trabajé en un sistema OO y pude comparar los dos. Aunque era súper feo, la versión de "una matriz para gobernarlos a todos" en C simple y simple de un solo subproceso compilada en depuración fue varias veces más rápida que el sistema OO multiproceso "bonito" compilado -O2 en C ++. Creo que un poco tuvo que ver con la excelente localidad de caché (tanto en espacio como en tiempo) en la versión big-fat-array.

Si desea rendimiento, una matriz C global grande y gruesa será el límite superior (por experiencia).

luego
fuente
2

Básicamente, lo que quiere hacer es crear listas según las necesite. Si vuelve a dibujar los objetos del terreno de una sola vez, una lista de ellos podría ser útil. Pero para que esto valga la pena, necesitaría decenas de miles de ellos, y muchos 10,000 de cosas que no son terreno. De lo contrario, examinar su lista de todo y usar un "si" para extraer objetos del terreno es lo suficientemente rápido.

Siempre he necesitado una lista maestra, independientemente de cuántas otras listas haya tenido. Por lo general, lo convierto en un mapa, una tabla con clave o un diccionario, de algún tipo para que pueda acceder aleatoriamente a elementos individuales. Pero una lista simple es mucho más simple; Si no accede aleatoriamente a los objetos del juego, no necesita el Mapa. Y la búsqueda secuencial ocasional a través de unos pocos miles de entradas para encontrar la correcta no tomará mucho tiempo. Entonces, diría que, hagas lo que hagas, planea quedarte con la lista maestra. Considere usar un Mapa si se hace grande. (Aunque si puedes usar índices en lugar de claves, puedes quedarte con matrices y tener algo mucho mejor que un Mapa. ​​Siempre terminé con grandes espacios entre los números de índice, así que tuve que renunciar).

Una palabra sobre las matrices: son increíblemente rápidas. Pero cuando se levantan alrededor de 100,000 elementos, comienzan a darles cabida a los recolectores de basura. Si puede asignar el espacio una vez y no tocarlo después, está bien. Pero si está expandiendo continuamente la matriz, está asignando y liberando continuamente grandes cantidades de memoria y es probable que su juego se cuelgue durante segundos e incluso falle con un error de memoria.

¿Tan rápido y más eficiente? Seguro. Un mapa para acceso aleatorio. Para listas realmente grandes, una lista vinculada superará a una matriz si no necesita acceso aleatorio. Múltiples mapas / listas para organizar los objetos de varias maneras, según los necesite. Podría realizar varios subprocesos de la actualización.

Pero todo es mucho trabajo. Ese conjunto único es rápido y simple. Puede agregar otras listas y cosas solo cuando sea necesario, y probablemente no las necesitará. Solo ten en cuenta qué puede salir mal si tu juego se hace grande. Es posible que desee tener una versión de prueba con 10 veces más objetos que en la versión real para darle una idea de cuándo se enfrentará a problemas. (Solo haga esto si su juego se está haciendo grande). (Y no intente el subproceso múltiple sin pensarlo mucho. Todo lo demás es fácil de comenzar y puede hacer todo lo que quiera o lo que quiera y retroceda en cualquier momento. Con los subprocesos múltiples, pagará un alto precio por entrar, las cuotas para quedarse son altas y es difícil volver a salir).

En otras palabras, sigue haciendo lo que estás haciendo. Su límite más grande es probablemente su propio tiempo, y está haciendo el mejor uso posible de eso.

RalphChapin
fuente
Realmente no creo que una lista vinculada supere a una matriz en cualquier tamaño, a menos que a menudo agregue o elimine cosas del medio.
Zan Lynx
@ZanLynx: Y aun así. Creo que las nuevas optimizaciones de tiempo de ejecución ayudan mucho a las matrices, no es que lo necesitaran. Pero si asigna y libera grandes fragmentos de memoria de forma repetida y rápida, como puede suceder con grandes matrices, puede atar el recolector de basura en nudos. (La cantidad total de memoria también es un factor aquí. El problema es una función del tamaño de los bloques, la frecuencia de liberar y asignar, y la memoria disponible.) La falla ocurre repentinamente, con el programa bloqueado o bloqueado. Por lo general, hay mucha memoria libre; el GC simplemente no puede usarlo. Las listas vinculadas evitan este problema.
RalphChapin
Por otro lado, las listas vinculadas tienen una probabilidad significativamente mayor de perder la memoria caché en la iteración debido al hecho de que los nodos no son contiguos en la memoria. No es tan fácil de cortar. Puede aliviar los problemas de reasignación al no hacerlo (asignando por adelantado si es posible, porque a menudo lo es) y puede aliviar los problemas de coherencia de caché en una lista vinculada al agrupar la asignación de los nodos (aunque todavía tiene el costo de seguimiento de la referencia , es relativamente trivial).
Josh
Sí, pero incluso en una matriz, ¿no solo estás almacenando punteros en los objetos? (A menos que sea una matriz de corta duración para un sistema de partículas o algo así). Si es así, todavía habrá muchos errores de caché.
Todd Lehman el
1

He usado un método algo similar para juegos simples. Almacenar todo en una matriz es muy útil cuando se cumplen las siguientes condiciones:

  1. Tienes un número pequeño o un máximo conocido de objetos de juego
  2. Usted favorece la simplicidad sobre el rendimiento (es decir: las estructuras de datos más optimizadas, como los árboles, no valen la pena)

En cualquier caso, implementé esta solución como una matriz de tamaño fijo que contiene una gameObject_ptrestructura. Esta estructura contenía un puntero al objeto del juego en sí y un valor booleano que representaba si el objeto estaba "vivo" o no.

El propósito del booleano es acelerar las operaciones de creación y eliminación. En lugar de destruir los objetos del juego cuando se eliminan de la simulación, simplemente establecería la bandera "viva" false.

Al realizar cálculos en objetos, el código recorrerá la matriz, realizando cálculos en objetos marcados como "vivos", y solo se representan los objetos marcados como "vivos".

Otra optimización simple es organizar la matriz por tipo de objeto. Todas las viñetas, por ejemplo, podrían vivir en las últimas n celdas de la matriz. Luego, al almacenar un int con el valor del índice o un puntero al elemento de matriz, se puede hacer referencia a tipos específicos a un costo reducido.

No hace falta decir que esta solución no es buena para juegos con grandes cantidades de datos, ya que la matriz será inaceptablemente grande. Tampoco es adecuado para juegos con restricciones de memoria que prohíben que los objetos no utilizados ocupen memoria. La conclusión es que si tiene un límite superior conocido en la cantidad de objetos del juego que tendrá en un momento dado, no hay nada de malo en almacenarlos en una matriz estática.

¡Esta es mi primera publicación! ¡Espero que responda tu pregunta!

blz
fuente
¡Primer comentario! ¡Hurra!
Tili
0

Es aceptable, aunque inusual. Términos como "más rápido" y "más eficiente" son relativos; normalmente organizaría los objetos del juego en categorías separadas (una lista para las viñetas, una lista para los enemigos, etc.) para que la programación funcione más organizada (y, por lo tanto, más eficiente), pero no es como si la computadora recorriera todos los objetos más rápido .

jhocking
fuente
2
Es cierto en términos de la mayoría de los juegos XNA, pero organizar tus objetos puede hacer que iterar a través de ellos sea más rápido: es más probable que los objetos del mismo tipo lleguen al caché de instrucciones y datos de tu CPU. Los errores de caché son caros, especialmente en consolas. En la Xbox 360, por ejemplo, una pérdida de caché L2 le costará ~ 600 ciclos. Eso deja mucho rendimiento sobre la mesa. Este es un lugar donde el código administrado tiene una ventaja sobre el código nativo: los objetos asignados muy juntos en el tiempo también estarán cerca en la memoria, y la situación mejora con la recolección de basura (compactación).
Programador de juegos crónicos el
0

Dices matriz única y objetos.

Entonces (sin su código para mirar), supongo que todos están relacionados con 'uberGameObject'.

Entonces quizás dos problemas.

1) Puede tener un objeto 'dios': hace toneladas de cosas, no segmentadas realmente por lo que hace o es.

2) ¿Recorre esta lista y emite para escribir? o desempaquetando cada uno. Esto tiene una gran penalización de rendimiento.

considere dividir diferentes tipos (viñetas, tipos malos, etc.) en sus propias listas fuertemente tipadas.

List<BadGuyObj> BadGuys = new List<BadGuyObj>();
List<BulletsObj> Bullets = new List<BulletObj>();

 //Game 
OnUpdate(gameticks gt)
{  foreach (BulletObj thisbullet in Bullets) {thisbullet.Update();}  // much quicker.
Dr9
fuente
No es necesario realizar la conversión de tipos si hay algún tipo de clase base común o interfaz que tenga todos los métodos que se llamarán en el ciclo de actualización (por ejemplo update()).
bummzack
0

Puedo proponer un compromiso entre la lista única general y las listas separadas para cada tipo de objeto.

Mantenga referencia a un objeto en múltiples listas. Estas listas están definidas por comportamientos básicos. Por ejemplo, MarineSoldierse hará referencia en el objeto PhysicalObjList, GraphicalObjList, UserControlObjList, HumanObjList. Entonces, cuando procesas la física, iteras PhysicalObjList, cuando el proceso es dañino por el veneno HumanObjList, si el soldado muere, retíralo HumanObjListpero sigue PhysicalObjList. Para que este mecanismo funcione, debe organizar la inserción / eliminación automática de objetos cuando se crea / elimina.

Ventajas del enfoque:

  • organización tipificada de objetos (no AllObjectsListcon conversión al actualizar)
  • las listas se basan en lógicas, no en clases de objetos
  • sin problemas con objetos con habilidades combinadas ( MegaAirHumanUnderwaterObjectse insertarían en las listas correspondientes en lugar de crear una nueva lista para su tipo)

PD: En cuanto a la actualización automática, encontrará problemas cuando interactúan varios objetos y cada uno de ellos depende de otros. Para resolver esto, necesitamos afectar el objeto externamente, no dentro de la actualización automática.

Brigadir
fuente