2010-08-06 14 views
13

Ha habido one more question en lo que es el diseño orientado a datos, y hay uno article al que a menudo se hace referencia (y lo he leído como 5 o 6 veces). Entiendo el concepto general de esto, especialmente cuando se trata, por ejemplo, de modelos 3D, donde le gustaría mantener todos los vértices juntos, y no contaminar sus rostros con normales, etc.Diseño orientado a los datos en la práctica?

Sin embargo, tengo un dificultades para visualizar cómo el diseño orientado a datos podría funcionar para cualquier cosa, pero los casos más triviales (3d modelos, partículas, BSP-árboles, y así sucesivamente). ¿Hay algún buen ejemplo que realmente abrace el diseño orientado a datos y muestra cómo esto podría funcionar en la práctica? Puedo abrir grandes bases de código si es necesario.

Lo que me interesa especialmente es el mantra "donde hay uno, hay muchos", que realmente no puedo conectar con el resto aquí. Sí, siempre hay más de un enemigo, sin embargo, aún necesita actualizar a cada enemigo individualmente, porque no se están moviendo de la misma manera ahora ¿o sí? Lo mismo va para el 'balls'-ejemplo, en la respuesta aceptada a la pregunta anterior (que en realidad pregunté esto en un comentario a la respuesta, pero no he recibido una respuesta todavía). ¿Es simplemente que el renderizado solo necesitará las posiciones, y no las velocidades, mientras que la simulación del juego necesitará ambas cosas, pero no los materiales? ¿O me estoy perdiendo algo? Quizás ya lo he entendido y es un concepto mucho más sencillo de lo que pensaba.

Cualquier punteros sería muy apreciada!

Respuesta

39

Muy bien, entonces me doy cuenta de que esta pregunta es bastante antigua, pero sigo pensando que vale la pena responder, porque desafortunadamente el Sr. Le Blanc, y lo digo con todo respeto, tiene un montón de DOD mal. De hecho, la frase "Después de que hayas hecho tu diseño, te haces la siguiente pregunta: ¿cómo puedo organizar todos los datos que he diseñado en una sola gota grande?" es tan opuesto a lo que DOD intenta hacer que es casi una parodia, aunque el resto de la respuesta va más al grano. Sin embargo, sin falta de respeto, el Sr. Le Blanc es obviamente un miembro muy entendido y servicial de esta comunidad.

Entonces, ¿de qué se trata el DOD? Obviamente, se trata de rendimiento, pero no es solo eso. También se trata de un código bien diseñado que sea legible, fácil de entender e incluso reutilizable. Ahora el diseño orientado a objetos tiene que ver con el diseño de código y datos para encajar en "objetos" virtuales encapsulados. Cada objeto es una entidad separada con variables para las propiedades que el objeto puede tener y métodos para actuar sobre sí mismo u otros objetos en el mundo. La ventaja del diseño OO es que es fácil modelar mentalmente su código en objetos porque el mundo entero (real) que nos rodea parece funcionar de la misma manera. Objetos con propiedades que pueden interactuar entre sí.

Ahora el problema es que la CPU en su computadora funciona de una manera completamente diferente. Funciona mejor cuando dejas que haga las mismas cosas una y otra vez. ¿Porqué es eso? Por una pequeña cosa llamada caché. Acceder a la RAM en una computadora moderna puede tomar 100 o 200 ciclos de CPU (¡y la CPU tiene que esperar todo ese tiempo!), Que es demasiado larga. Entonces, hay una pequeña porción de memoria en la CPU a la que se puede acceder muy rápidamente, la memoria caché. El problema es que solo hay unas pocas tapas MB. Así que cada vez que necesita datos que no estaban en la memoria caché, aún necesita recorrer el largo camino hasta la memoria RAM. No es solo así para los datos, lo mismo ocurre con el código. Intentar ejecutar una función que no está en la caché de instrucciones causará una pérdida mientras el código se carga desde la RAM.

Volver a la programación OO. Los objetos son grandes, pero la mayoría de las funciones solo necesitan una pequeña parte de esos datos, por lo que estamos desperdiciando el caché cargando datos innecesarios. Los métodos llaman a otros métodos que llaman a otros métodos, agotando su caché de instrucciones. Aún así, a menudo hacemos muchas de las mismas cosas una y otra vez. Tomemos una bala de un juego, por ejemplo. En una implementación ingenua, cada bala podría ser un objeto separado. Puede haber una clase de administrador de bala. Llama a la primera función de actualización de bala. Actualiza la posición 3D usando la dirección/velocidad. Esto hace que muchos otros datos del objeto se carguen en la memoria caché. A continuación, llamamos a la clase World Manager para verificar una colisión con otros objetos. Esto carga muchas otras cosas en la memoria caché, tal vez incluso hace que el código de la clase original de administrador de viñetas se elimine de la memoria caché de instrucciones. Ahora volvemos a la actualización de bala, no hubo colisión, así que volvemos al administrador de bala. Es posible que necesite cargar algún código de nuevo. A continuación, actualización bala # 2. Esto carga una gran cantidad de datos en el caché, llamadas al mundo ... etc. Por lo tanto, en esta situación hipotética, tenemos 2 puestos para cargar código y digamos 2 puestos para cargar datos. Eso es al menos 400 ciclos desperdiciados, por 1 viñeta, y no hemos tenido balas que golpeen a otra cosa en cuenta. Ahora una CPU funciona a 3+ GHz por lo que no vamos a notar una sola bala, pero ¿y si tenemos 100 balas? O incluso más?

Así que este es el lugar donde hay una historia. Sí, hay algunos casos en los que solo tienes acceso a objetos, clases de administrador, acceso a archivos, etc. Pero, con mayor frecuencia, hay muchos casos similares. El diseño orientado a objetos ingenuo o incluso no ingenuo provocará muchos problemas. Así que ingrese el diseño orientado a los datos. La clave del DOD es modelar su código alrededor de sus datos, y no al revés como con OO-design. Esto comienza en las primeras etapas del diseño. Primero no diseña su código OO y luego lo optimiza. Comienza por enumerar y examinar sus datos y pensar cómo quiere modificarlos (voy a llegar a un ejemplo práctico en un momento).Una vez que sepa cómo su código va a modificar los datos, puede diseñarlos de manera que sean lo más eficientes posible para procesarlos. Ahora puede pensar que esto solo puede llevar a una sopa horrible de código y datos en todas partes, pero ese solo es el caso si lo diseña mal (el diseño incorrecto es igual de fácil con la programación OO). Si lo diseña bien, el código y los datos se pueden diseñar cuidadosamente alrededor de una funcionalidad específica, lo que permite un código muy legible e incluso muy reutilizable.

Volvamos a nuestras viñetas. En lugar de crear una clase para cada viñeta, solo conservamos el administrador de viñetas. Cada bala tiene una posición y una velocidad. La posición de cada bala debe actualizarse. Cada bala debe tener un control de colisión y todas las balas que han golpeado algo deben tomar alguna acción en consecuencia. Entonces, al echar un vistazo a esta descripción, puedo diseñar todo este sistema de una manera mucho mejor. Pongamos las posiciones de todas las viñetas en una matriz/vector. Pongamos la velocidad de todas las balas en una matriz/vector. Ahora comencemos iterando entre esas dos matrices y actualizando cada valor de posición con su velocidad correspondiente. Ahora, todos los datos cargados en el caché de datos son datos que vamos a usar. Incluso podemos poner un comando de precarga inteligente para precargar previamente algunos datos de la matriz, de modo que los datos estén en caché cuando lleguemos a ellos. A continuación, comprobación de colisión. No voy a entrar en detalles aquí, pero pueden imaginarse cómo la actualización de todas las viñetas puede ayudar. También tenga en cuenta que si hay una colisión, no vamos a llamar a una nueva función o hacer algo. Solo mantenemos un vector con todas las viñetas que tuvieron colisión y cuando se realiza la verificación de colisión, podemos actualizarlas una tras otra. ¿Ve cómo pasamos de tener acceso a la memoria a casi ninguno al disponer nuestros datos de manera diferente? ¿Notó también cómo nuestro código y nuestros datos, aunque ya no están diseñados de manera OO, todavía son fáciles de entender y fáciles de reutilizar?

Para volver al "donde hay uno, hay muchos". Al diseñar el código OO, piensas en un objeto, el prototipo/clase. Una bala tiene una velocidad, una bala tiene una posición, una bala moverá cada cuadro por su velocidad, una bala puede golpear algo, etc. Cuando pienses en eso, pensarás en una clase, con velocidad, posición y función de actualización que mueve la bala y comprueba la colisión. Sin embargo, cuando tiene varios objetos, debe pensar en todos ellos. Las balas tienen posiciones, velocidad. Algunas balas pueden tener colisión. ¿Ves que ya no estamos pensando en un objeto individual? Estamos pensando en todos ellos y estamos diseñando código de manera muy diferente ahora.

Espero que esto ayude a responder su segunda parte de la pregunta. No se trata de si necesitas actualizar a cada enemigo o no, se trata de la forma más eficiente de actualizarlos. Y aunque diseñar solo a tus enemigos usando DOD puede no ayudar a obtener mucho rendimiento, diseñando todo el juego alrededor de estos principios (¡solo cuando corresponda!) Puede generar una gran cantidad de mejoras en el rendimiento.

En la primera parte de la pregunta, hay otros ejemplos de DOD. Lo siento, pero no tengo tanto allí. Sin embargo, hay un muy buen ejemplo, me encontré con esto hace algún tiempo, una serie sobre diseño orientado a datos de un árbol de comportamiento por Bjoern Knafla: http://bjoernknafla.com/data-oriented-behavior-tree-overview Es probable que desee comenzar en el primero de la serie de 4, los enlaces están en el artículo en sí. Espero que esto ayude, a pesar de la vieja pregunta. O tal vez algún otro usuario de SO encuentre esta pregunta y use esta respuesta.

+0

El enlace parece estar roto, pero supongo que es el que publicó en [altdevblogaday] (http://www.altdevblogaday.com/2011/07/09/data-oriented-behavior-tree-overview/) también. Gracias por una muy buena respuesta! – falstro

+2

Gran explicación. Hay una cosa que no obtuve. Tener un vector para cada propiedad (posición, movimiento, ...) ¿cómo están conectados? Imagine que una bala colisiona, ¿cómo sé qué elemento debo eliminar de todos los vectores? – danijar

+2

Podrían estar conectados por una identificación. Entonces, en lugar de un puntero a un objeto de viñeta, tiene una identificación que la clase BulletManager puede traducir al índice correcto. Digamos que quieres destruir/eliminar una viñeta con id # 9001, llamar a la función de eliminación de BulletManager con esa identificación, BulletManager busca # 9001 en su índice y encuentra que apunta al índice de matriz 42. Entonces puede, en todos sus datos arrays (en nuestro caso posición y velocidad) reemplazan los datos en el índice 42 con los datos en el último índice, actualicen la tabla de id para reflejar que el ID que apuntaba al último índice apunta ahora al índice 42 y el # 9001 se ha ido – Mart

1

leí la pregunta que está vinculada y el artículo.

He leído un libro sobre el tema del diseño basado en datos.

estoy más o menos en el mismo barco que tú.

La manera en que entiendo el artículo de Noel es que diseñas tu juego de la forma típica orientada a objetos. Tienes clases y métodos que funcionan en las clases.

Después de haber hecho su diseño, usted se pregunta la siguiente pregunta:

¿Cómo puedo organizar todos los datos que he diseñado en una enorme burbuja?

Piénselo en términos de escribir todo su diseño como un método funcional, con muchos métodos subordinados. Me recuerda los masivos programas de Cobol de 500,000 líneas de mi juventud.

Ahora, probablemente no escriba todo el juego como un método funcional enorme. Realmente, en el artículo, Noel está hablando sobre la parte de renderización de un juego. Piense en ello como un motor de juego (el único método funcional enorme) y el código para controlar el motor del juego (el código OOP).

Lo que me interesa especialmente es el mantra "donde hay uno, hay muchos", que realmente no puedo conectar con el resto aquí. Sí, siempre hay más de un enemigo, sin embargo, aún necesita actualizar a cada enemigo individualmente, porque no se están moviendo de la misma manera ahora ¿o sí?

Estás pensando en términos de objetos. Intenta pensar en términos de funcionalidad.

Cada actualización enemigo es una iteración de un bucle.

Lo que es importante es que los datos del enemigo estén estructurados para estar en una ubicación de memoria, en lugar de diseminarse en instancias de objetos enemigos.

+0

Es curioso cómo hacer una pregunta sobre el tema lo hace más claro, ¿eh? Gracias por su respuesta. Con respecto al "Estás pensando en términos de objetos", veo que hemos llegado a la misma conclusión aquí (ve más abajo en mi pregunta, solo reformúlalo para usar la posición del enemigo, y así sucesivamente), donde el punto importante es que la función solo debe ocuparse de los datos que necesita, como posición enemiga, orientación y otra información relevante para la actualización del juego, mientras que no necesitará (por ejemplo) detalles específicos como el modelo de objetos, materiales, secuencia de animación , huesos, etc. +1 – falstro

+0

@roe: puede colocar solo los datos de representación en el motor de representación o puede poner todos los datos en el motor de representación. La función puede hacer referencia a los datos en el motor de renderizado. Depende de la cantidad de datos que tu juego necesite. –

+0

Al comienzo de esta respuesta, usted se refiere al "diseño impulsado por datos", por lo que yo entiendo, esto es algo completamente distinto al diseño orientado a datos *. ¿Un error? De lo contrario, ¿podría explicar la relevancia del primero? – JBentley

Cuestiones relacionadas