Stratos: Punto de Encuentro de Desarrolladores

¡Bienvenido a Stratos!

Acceder

Foros





"Engine" con "GO-component system", y jerarquia de transformaciones

Iniciado por TrOnTxU, 29 de Mayo de 2009, 01:22:26 AM

« anterior - próximo »

TrOnTxU

Hola, llevaré sin postear, .... ufff , (más o menos), pero weno, esperemos que alguien se moleste en echarle una ojeada a esto  8)

Pues como no, mi post es para consultar opiniones y buscar alternativas.
El tema es que me he vuelto loco y me he puesto a programar un "engine" (por llamarlo de alguna manera) para XNA en C#.

Cuando llegamos al tema de manejo de GameObjects, me decanto por el planteamiento de Chris Stoy en el Game Programming Gems6, el llamado "Game Object Component System", donde cada objeto de juego esta compuesto por una serie de componentes individuales, que tienen una función especifica y que permiten "separar" el código de los gameObjects en clases más pequeñas, y , bla, bla, bla ...

No sé si alguien a probado el Unity, pero la verdad es que a mi me parece una muy buena implementación de un motor multi-plataforma, sencillo y a la vez potente y versatil. (tampoco me he probado todos los motores del mundo, asi que puede que lo que diga lo tengan más de uno, y asi lo creo yo aunque no lo puedo demostrar).
Por lo que he podido observar los gameObjects estan formados por componentes, y además son scriptables (SIIIII  :D), ... vamos, hoy en dia todo el mundo diria: lo típico. Pero hay una cosa que me gusta en especial manera y es el Transform.
Desde el nivel de script en unity Transform es una clase linkada (siempre, no como un componente que se puede añadir o quitar) a un GameObject. Con ella puedes modificar la posición, rotación y escala (si vamos, lo típico) de un objeto.
Pero con la peculiaridad de que la transformación tiene jerarquia, hijos, un padre y un root de escena, lo que le da un cierto aspecto de nodo de un scene graph, pero solo con transformaciones.

Lo que veo bueno en el Transform de Unity es una cosa que he visto en muy pocos motores, y es que puedes modificar la posicion/rotacion/escala de un objeto, tanto local, como global directamente. Me explico:
- si un objeto esta en la posicion (0,0,10) y tiene un hijo con posicion local (0,0,0) el hijo ocupará la posición "real" (0,0,10).
- si modificamos la posicion del padre a (0,0,8) el hijo se moverá a la posción (0,0,8)
- si modificamos la posición LOCAL del hijo a (0,4,0) su posicion en GLOBAL será (0,4,8).
- pero si modificamos la posicion GLOBAL (o de mundo) del hijo a (0,4,0) su posicion será (0,4,0) y su posicion local se recalculará respecto a la posicion del padre.

A alguien le puede parecer una chorrada, pero para mi es super-importante que quede tan "sencillo" el trabajar con coordenas locales y de mundo, ya que facilita el código de lógica de juego, tanto a nivel de implementación como de comprensión/depuración.

Asi que me he puesto a programar lo mismo. Lo he probado y mola  :o Pero claro, lo he echo de manera rápida y sin optimizar.

Mi primera aproximación personal al tema ha sido crearme mi Manager de Entidades (GameObjects pero si es mi motor yo lo llamo como quiero  ;) )la Entidad en si (con la lista de componentes y tal) y el Transform.

El transform almacena posicion/rotacion y una escala proporcional (o sea un solo float) local, y otros tantos para global. Asi es rapido obtener la pos/rot/escala y establecerla, ... en teoria, porque tambien tenemos que actualizar las coordenadas locales/globales dependiendo del tipo de parametro que cambiemos.
Tambien he acabado por incluir las matrices local y global en la clase, y las mantengo actualizadas, con lo que la lectura de los parametros de la transformacion(usease pos/rot/escala) sique siendo directo, pero establecer nuevos parametros no lo es, ya que hemos de actualizar varias cosas: parametros locales o globales y sus respectivas matrices.
Además de esto, si optamos por mantener las transformaciones siempre actualizadas (en vez de "a la antigua usanza" que era recorrer el grafo de escena al dibujar e ir apilando/desapilando las transformaciones), cada vez que actualizamos un nodo hemos de actualizar también los hijos.

Como ventaja, al dibujar ya tenemos todas las matrices de mundo actualizadas, y solo hemos de recorrer la lista de objetos "visibles" (podemos hacernos una lista con las entidades que tienen el componente IRender, por ejemplo) y aplicar esa matriz al shader antes de comenzar a dibujar.

Por ejemplo, el código de una propiedad como la posicion global seria algo asi:

/// <summary>
/// Escala local
/// </summary>
public float LocalScale
{
    get { return localScale; }
    set
    {
        localScale = value;
        _updateValues(UpdateValuesMode.GLOBAL_COORDS);
    }
}


Esto es: si pido la escala local simplemente me devuelve la variable correspondiente, pero cuando hago un "set" de ese valor, primero le pone el nuevo valor a la variable y seguidamente actualiza las coordenadas globales, para que sean coherentes con el cambio.
Ahora el código de _updateValues:
...
if (((int)mode & (int)UpdateValuesMode.GLOBAL_COORDS) != 0)
{
    localMatrix =
        Matrix.CreateFromQuaternion(localRotation) *
        Matrix.CreateScale(localScale) *
        Matrix.CreateTranslation(localPosition);

    Matrix parentWorldMatrix = (Parent == null) ? Matrix.Identity : Parent.worldMatrix;
    worldMatrix = localMatrix * parentWorldMatrix;
    Vector3 outScale;
    worldMatrix.Decompose(out outScale, out globalRotation, out globalPosition);
    globalScale = outScale.X;
}
...
foreach (XutiTransform childTransform in this.childs)
{
    childTransform._updateFromParent();
}
...

Antes que nada, SE QUE NO SE HAN DE UTILIZAR SOBRECARGAS DE OPERADORES PORQUE EL COMPILER NO LO OPTIMIZA, es simplemente para que el código quede más legible, y os prometo que los sustituiré por las funciones correspondientes con paso por referencia.
Explicando lo que hace el código, por si alguien se pierde, es crear la matriz de transformacion local a partir de los parametros (pos/rot/escala) locales. Seguidamente se multiplica por la matriz del padre para obtener la matriz de mundo (la transformacion final del objeto antes de dibujar).
En tercer lugar se extraen los parametros globales. Y por ultimo llamamos a _updateFromParent en todos los hijos, funcion que detallo aqui:
private void _updateFromParent()
{
    Matrix parentWorldMatrix = (Parent == null)?Matrix.Identity:Parent.worldMatrix;
    worldMatrix = localMatrix * parentWorldMatrix;
    Vector3 outScale;
    worldMatrix.Decompose(out outScale, out globalRotation, out globalPosition);
    globalScale = outScale.X;

    foreach (XutiTransform childTransform in this.childs)
    {
        childTransform._updateFromParent();
    }
}

Que de echo es muy parecida a la parte de actualizar coordenadas globales, pero sin tanto código, para que sea mas ligera y actualizar una larga serie de hijos sea lo menos costoso posible.
El tema de modificar las coordenadas gobales es un poco mas complicado porque actualizar los valores locales se vuelve un poco más complicado, basicamente porque hay que obtener la inversa de la matriz global del padre, algo asi:
this.worldMatrix =
    Matrix.CreateFromQuaternion(globalRotation) *
    Matrix.CreateScale(globalScale) *
    Matrix.CreateTranslation(globalPosition);

localMatrix = worldMatrix * Matrix.Invert(Parent.worldMatrix);
Vector3 outScale;
localMatrix.Decompose(out outScale, out localRotation, out localPosition);
localScale = outScale.X;


Bueno, pues después de este tostón mis preguntas:

1) ¿Creeis que es una buena solucion mantener tantos valores actualizados? (La verdad es que modificar la transformacion de un objeto es sencillo a nivel de programacion, pero costoso a nivel de computación, eso sin contar la cantidad de memoria que consume una transformación)
2) ¿Se os ocurre alguna alternativa para mejorar el rendimiento y que la clase siga cumpliendo los requisitos?

Bueno, espero que alguien pueda comentarme sus opiniones, y podamos intercambiar experiencias sobre este tema, que me parece bastante fundamental a la hora de diseñar un "motor".

Pues nada, un saludo, y gracias por leerte esto. XD
Adew!
Vicent: Linked-In  ***  ¡¡Ya tengo blog!!

Vicente

Es un poco tarde para leerme todo eso (mañana me lo leo con más calma). Pero un apuntes: el compilador del framework 3.5 SP 1 sí optimiza las sobrecargas de operadores :)

http://blogs.msdn.com/vancem/archive/2008/05/12/what-s-coming-in-net-runtime-performance-in-version-v3-5-sp1.aspx

Un saludo!

Vicente

TrOnTxU

Gracias Vicente!!!
:o

Creia haberlo oido en tu charla del CDV, pero no estaba seguro :-[

Me voy a ahorrar varios cambios de lineas   ^_^
Vicent: Linked-In  ***  ¡¡Ya tengo blog!!

wereoffs

Yo también imlementé un motor basándome en el mismo artículo. La idea de diseño no está mal, pero no me convence en lo referente a las dependencias (acoplamiento) que pueden tener los diferentes gameobjectcomponent. Yo no guardo la matriz de transformacion en el mismo gameobject, sinó que lo he implementado en los nodos del scenegraph. Por lo de guardar la posicion transformada, tengo diferentes nodos de scenegraphimplementados que heretan de una interficie que incluye los métodos par apoder consultar la posición global, una implementación calcula la posición global del nodo cuando se llama, mientras que otra guarda la posición ya calculada en memoria.

Vaya, en definitiva, que usando estos patroens se tiene una buena cohesion en los objetos, pero demasiado acomlpamiento :/.

He escrito el mensaje demasiado rápido pq tengo que irme y no tengo tiempo xD Espero que algo se pueda entender :P

un saludo! :)

TrOnTxU

Hola, gracias por contestar :)

CitarYo no guardo la matriz de transformacion en el mismo gameobject, sinó que lo he implementado en los nodos del scenegraph.
Asi es como lo tengo en la versión actual del "motor" (cada vez más risa esta palabra XD ) que corre ahora mi juego de puzzles en la xbox.
Pero realmente mi idea era sustituir el scenegraph "tradicional" por el entity manager y la transformación de los nodos del scenegraph por la clase esta llamada Transform que va asociada a cada entidad.

Si que tienes razón en cuanto al acoplamiento (aunque creo que tampoco tiene porque crearse mucha dependencia entre los componenetes, solo la que tu quieras darle, haciendo más componentes y pequeños o menos y grandes), de todas formas tampoco domino la "visión académica" del software, asi que si pudieras explicar los incovenientes que le ves seguramente me ahorrarias algún que otro susto :D
Por otro lado, como muy bien apuntas, la cohesión es de puta madre. Lo que para mi se traduce al final en una programación de gameplay mucho más sencilla y distribuida, además de por esa forma de hacer el "in-drive", que explican en el mismo articulo.
Además una de las cosas que me propuse conseguir era poder distribuir más adelante las tareas del motor entre los procesadores de la xbox, y la verdad es que me cuesta mucho pensar concurrentemente. Pero encontré unos ppt de una presentacion de la gdc2008 (Getting more from multicore, de Ian Lewis noseque de XNA Microsoft) con una explicación de tres tecnicas de ejecucion multiproceso (BSP, CSP y Task Pool), y la verdad es que intuyo más sencillo poder intentar hacer multi-thread en el motor con este tipo de patrón. (También lo he comparado con varios articulos de gamasutra sobre el mismo tema: engines con multi-threading)
Aunque, como he dicho, no tengo mucha idea del tema, y de momento tampoco estoy diseñandolo todo especifico para dividirlo en hilos.
¿Alguien con algo más de experiencia en multi-threading puede darme su opinión, plis? Gracias :)

Aunque, basicamente, mi pregunta anterior era más sobre el tema de la clase Transform que habia detallado, si la veiais útil (por la simplificación de la interfaz para acceder las coordenadas), o demasiado infeciente en algún sentido, si creeis que se podia mejorar, que opinais de mantener actualizada una matriz de mundo constantemente, etc.

Pues nada, gracias de nuevo por contestar :)
Dewwww!
Vicent: Linked-In  ***  ¡¡Ya tengo blog!!






Stratos es un servicio gratuito, cuyos costes se cubren en parte con la publicidad.
Por favor, desactiva el bloqueador de anuncios en esta web para ayudar a que siga adelante.
Muchísimas gracias.