2012-02-18 5 views
6

Esto es para un proyecto de juego pequeño con SDL en MinGW/Windows.Decisiones de herencia/interfaz para el motor de física

estoy trabajando en un motor de física, y mi idea era tener un Physics::Object, que todos los objetos físicos deben derivar de y se registra a sí mismo con una clase mundial Physics::System (que es un patrón monostate) para que el usuario no lo hace necesita rastrear qué objetos están incluidos en los cálculos de física y solo necesita llamar a una función como Physics::System::PerformTimestepCalculation(double dt).

Esto funciona bien, e incluso lo implementé usando una única clase derivada Physics::Circle, que es un círculo 2d. Estaba bastante contento con la detección de colisión predictiva, aunque todavía tengo que optimizarla.

De todos modos, me encontré con problema cuando comencé a agregar otras primitivas para incluir en el cálculo, p. línea. El Physics::System::PerformTimestepCalculation(double dt) se llenó de llamadas a Object::GetID() o funciones similares (puede evitar el dynamic_cast <>), pero me siento sucio.

Hice un poco de reading y me di cuenta de que mis elementos de mi jerarquía no son sustituibles (es decir, la colisión entre dos círculos es muy diferente entre la colisión de dos líneas).

Me gusta la forma en que mi Physics::Objects "autoregistro" con la clase System para que automáticamente se incluyan en los cálculos, y realmente no quiero perder esto.

Debe haber algunas otras rutas de diseño sensatas. ¿Cómo puedo rediseñar mejor las cosas para que los objetos no sustituibles no se interpongan en el camino?

Editar FYI: Al final me han separado de la entidad y la forma propiedades, similar a cómo se describe en la respuesta aceptada, y similares a un modelo de componente de sistema de entidad. Significa que todavía tengo la lógica de yuk de "¿es esto un círculo o una línea, y es una línea o un círculo?", Pero ya no estoy pretendiendo que el polimorfismo me ayuda aquí. ¡También significa que utilizo algún tipo de fábrica y puedo tener múltiples mundos de cálculo sucediendo a la vez!

+1

Qué quiere decir que tienen un problema [doble de despacho] (http://en.wikipedia.org/wiki/Double_dispatch)? Si mal no recuerdo, [Modern C++ Design] (http: //www.amazon.ca/Modern-Design-Generic-Programming-Patterns/dp/0201704315) tiene un capítulo sobre técnicas de implementación, y que podría interesarle. –

Respuesta

5

Los motores de física pública más exitosos disponibles no son muy pesados ​​en los 'patrones' u 'diseño orientado a objetos'.

He aquí un resumen de copia de seguridad de mi, es cierto negrita, la afirmación:

Chipmunk - escrito en C, dicho suficiente.

Box2d - Escrito en C++, y aquí hay algo de polimorfismo. hay una jerarquía de formas (clase base b2Shape) con algunas funciones virtuales. Sin embargo, esa abstracción se filtra como un tamiz, y encontrarás muchos moldes para dividir las clases en todo el código fuente. También hay una jerarquía de 'contactos', que resulta más exitosa, aunque con una única función virtual sería trivial reescribir esto sin polimorfismo (la ardilla utiliza un puntero a la función, creo). b2Body es la clase utilizada para representar cuerpos rígidos, y no es virtual.

Bullet - Escrito en C++, se usa en un montón de juegos. Montones de funciones, toneladas de código (en relación con los otros dos). En realidad, existe una clase base que se extiende a las representaciones de cuerpo rígido y cuerpo blando, pero solo una pequeña parte del código puede hacer uso de ella.La mayor parte de la función virtual de la clase base se relaciona con la serialización (guardar/cargar el estado del motor), de las dos funciones virtuales restantes el cuerpo blando falla al implementar una con un TODO informándonos de que algún truco necesita ser limpiado. No es exactamente un respaldo resonante del polimorfismo en los motores de física.

Esas son muchas palabras, y ni siquiera he comenzado a responder a su pregunta. Todo lo que quiero recalcar es que el polimorfismo no es algo que se aplique de manera efectiva en los motores de física existentes. Y eso probablemente no se deba a que los autores no "obtuvieron" OO.

De todos modos, mi consejo: polimorfismo zanja para su clase de entidad. No va a terminar con 100 tipos diferentes que posiblemente no pueda refactorizar en una fecha posterior, los datos de forma de su motor de física serán bastante homogéneos (polys convexos, cajas, esferas, etc.) y los datos de su entidad probablemente serán aún más homogéneo (probablemente solo cuerpos rígidos para empezar).

Otro error que creo que está cometiendo solo es compatible con un sistema de Física ::. Es útil poder simular cuerpos de forma independiente (por ejemplo, para un juego de dos jugadores), y la forma más sencilla de hacerlo es admitir múltiples sistemas de Física.

Teniendo esto en cuenta, el "patrón" más limpio a seguir sería un patrón de fábrica. Cuando los usuarios quieren crear un cuerpo rígido, que necesitan para contar la Física :: Sistema (que actúa como una fábrica) que lo haga por ellos, por lo que en su Física :: Sistema:

// returning a smart pointer would not be unreasonable, but I'm returning a raw pointer for simplicity: 
rigid_body_t* AddBody(body_params_t const& body_params); 

Y en el código de cliente :

circle_params_t circle(1.f /*radius*/); 
body_params_t b(1.f /*mass*/, &circle /*shape params*/, xform /*system transform*/); 
rigid_body_t* body = physics_system.AddBody(b); 

Anyhoo, kind of a rant. Espero que esto sea útil. Por lo menos, quiero señalarte hacia box2d. Está escrito en un dialecto bastante simple de C++ y los patrones aplicados en él serán relevantes para su motor, ya sea 3D o 2D.

+0

+1 para "nadie más lo hace de esa manera". Creo que eso es útil. Supongo que si siguiera el patrón de fábrica, podría tener listas separadas para cada tipo de objeto ... hmmm. – Bingo

+0

Aceptado como usted habló más, y dio el consejo más útil sobre diseño de software, que es lo que mi pregunta hizo. – Bingo

3

El problema de las jerarquías es que no siempre tienen sentido, y tratar de meter todo en una jerarquía solo resulta en decisiones incómodas y trabajo frustrante en el futuro.

La otra solución que se puede utilizar es etiquetada unión, mejor incorporada por boost::variant.

La idea es crear un objeto que puede contener uno instancia de un tipo determinado (entre una lista preseleccionada) en un momento dado:

typedef boost::variant<Ellipsis, Polygon, Blob> Shape; 

Y entonces usted puede proporcionar la funcionalidad conmutando la lista tipo:

struct AreaComputer: boost::static_visitor<double> { 
    template <typename T> 
    double operator()(T const& e) { return area(a); } 
}; 

void area(Shape const& s) { 
    AreaComputer ac; 
    return boost::apply_visitor(s, ac); 
} 

el funcionamiento es el mismo que un despacho virtual (no demasiado, normalmente), pero se obtiene una mayor flexibilidad:

void func(boost::variant<Ellipsis, Blob> const& eb); 
void bar(boost::variant<Ellipsis, Polygon> const& ep); 
// ... 

Puede proporcionar funciones solo cuando relevante.

Y sobre el tema de las visitas binaria:

struct CollisionComputer: boost::static_visitor<CollisionResult> { 
    CollisionResult operator()(Circle const& left, Circle const& right); 
    CollisionResult operator()(Line const& left, Line const& right); 

    CollisionResult operator()(Circle const& left, Line const& right); 
    CollisionResult operator()(Line const& left, Circle const& right); 
}; 

CollisionResult collide(Shape const& left, Shape const& right) { 
    return boost::apply_visitor(CollisionComputer(), left, right); 
} 
+0

Gracias por su respuesta. No estoy muy familiarizado con los visitantes o las variantes. Creo que tengo el ejemplo del área, pero parece trivial porque podría hacerse fácilmente usando una llamada virtual, ¿no? ¿Puedes expandir la sección de mayor flexibilidad y quizás mostrar algo como 'double TimeOfCollision (Shape * s1, Shape * s2)' para varios tipos de formas? (es decir, círculo + círculo, círculo + línea, línea + línea, etc.) – Bingo

+0

@Bingo: la flexibilidad de la que hablé no está relacionada con la visita (no realmente), sino más bien con el hecho de que puedes empacar los elementos como desees. (según la necesidad), mientras que con la herencia estás atascado si no tienen un padre común y no es tan bueno si su padre común tiene otras clases derivadas que no te importan ... –

Cuestiones relacionadas