Diseño basado en componentes: manejo de la interacción de objetos

9

No estoy seguro de cómo exactamente los objetos hacen cosas a otros objetos en un diseño basado en componentes.

Digamos que tengo una Objclase. Hago:

Obj obj;
obj.add(new Position());
obj.add(new Physics());

¿Cómo podría entonces tener otro objeto no solo mover la pelota sino también aplicar esa física? No busco detalles de implementación, sino de manera abstracta cómo se comunican los objetos. En un diseño basado en una entidad, es posible que solo tenga:

obj1.emitForceOn(obj2,5.0,0.0,0.0);

Cualquier artículo o explicación para comprender mejor un diseño basado en componentes y cómo hacer cosas básicas sería realmente útil.

jmasterx
fuente

Respuestas:

10

Eso generalmente se hace mediante mensajes. Puede encontrar muchos detalles en otras preguntas en este sitio, como aquí o allá .

Para responder a su ejemplo específico, un camino a seguir es definir una pequeña Messageclase que sus objetos puedan procesar, por ejemplo:

struct Message
{
    Message(const Objt& sender, const std::string& msg)
        : m_sender(&sender)
        , m_msg(msg) {}
    const Obj* m_sender;
    std::string m_msg;
};

void Obj::Process(const Message& msg)
{
    for (int i=0; i<m_components.size(); ++i)
    {
        // let components do some stuff with msg
        m_components[i].Process(msg);
    }
}

De esta manera no estás "contaminando" tu Objinterfaz de clase con métodos relacionados con componentes. Algunos componentes pueden elegir procesar el mensaje, algunos simplemente pueden ignorarlo.

Puede comenzar llamando a este método directamente desde otro objeto:

Message msg(obj1, "EmitForce(5.0,0.0,0.0)");
obj2.ProcessMessage(msg);

En este caso, obj2's Physicselegirá el mensaje y realizará el procesamiento que sea necesario. Cuando termine, ya sea:

  • Envíe un mensaje "SetPosition" a sí mismo, que el Positioncomponente elegirá;
  • O acceda directamente al Positioncomponente para modificaciones (bastante incorrecto para un diseño basado en componentes puros, ya que no puede suponer que cada objeto tiene un Positioncomponente, pero el Positioncomponente podría ser un requisito Physics).

En general, es una buena idea retrasar el procesamiento real del mensaje a la actualización del siguiente componente. Procesarlo de inmediato podría significar enviar mensajes a otros componentes de otros objetos, por lo que enviar solo un mensaje podría significar rápidamente una pila de espagueti inextricable.

Probablemente tendrá que ir a un sistema más avanzado más adelante: colas de mensajes asíncronos, envío de mensajes a un grupo de objetos, registro / anulación de registro por componente, etc.

La Messageclase puede ser un contenedor genérico para una cadena simple como se muestra arriba, pero el procesamiento de cadenas en tiempo de ejecución no es realmente eficiente. Puede buscar un contenedor de valores genéricos: cadenas, enteros, flotantes ... Con un nombre o mejor aún, un ID, para distinguir diferentes tipos de mensajes. O también puede derivar una clase base para satisfacer necesidades específicas. En su caso, podría imaginar un EmitForceMessagederivado del Messagevector de fuerza deseado y agregarlo, pero tenga cuidado con el costo de tiempo de ejecución de RTTI si lo hace.

Laurent Couvidou
fuente
3
No me preocuparía la "no pureza" de acceder directamente a los componentes. Los componentes se utilizan para satisfacer necesidades funcionales y de diseño, no académicas. Desea comprobar que existe un componente (p. Ej., Comprobar el valor de retorno no es nulo para la llamada de obtención de componente).
Sean Middleditch
Siempre lo pensé como dijiste por última vez, usando RTTI, pero muchas personas han dicho tantas cosas malas sobre RTTI
jmasterx
@SeanMiddleditch Claro, lo haría de esta manera, solo mencionando que para dejar en claro que siempre debe verificar dos veces lo que está haciendo al acceder a otros componentes de la misma entidad.
Laurent Couvidou
@Milo El RTTI implementado por el compilador y dynamic_cast puede convertirse en un cuello de botella, pero no me preocuparé por ahora. Todavía puede optimizar esto más adelante si se convierte en un problema. Los identificadores de clase basados ​​en CRC funcionan como un encanto.
Laurent Couvidou
´template <typename T> uint32_t class_id () {static uint32_t v; return (uint32_t) & v; } ´ - no se necesita RTTI.
arul
3

Lo que hice para resolver un problema similar al que muestra es agregar algunos controladores de componentes específicos y agregar algún tipo de sistema de resolución de eventos.

Entonces, en el caso de su objeto "Física", cuando se inicializa, se agregaría a un administrador central de objetos de Física. En el ciclo del juego, este tipo de gerentes tienen su propio paso de actualización, por lo que cuando este PhysicsManager se actualiza, calcula todas las interacciones físicas y las agrega a una cola de eventos.

Después de producir todos sus eventos, puede resolver su cola de eventos simplemente verificando lo que sucedió y tomando acciones según corresponda, en su caso, debería haber un evento que diga que los objetos A y B interactuaron de alguna manera, por lo que llama a su método emitForceOn.

Ventajas de este método:

  • Conceptualmente, es realmente sencillo de seguir.
  • Le da espacio para optimizaciones específicas, como usar cuádruples o lo que necesite.
  • Termina siendo realmente "plug and play". Los objetos con física no interactúan con objetos no físicos porque no existen para el administrador.

Contras:

  • Termina con muchas referencias moviéndose, por lo que puede ser un poco complicado limpiar todo correctamente si no tiene cuidado (desde su componente hasta el propietario del componente, desde el gerente hasta el componente, desde el evento hasta los participantes, etc. )
  • Debe poner especial atención en el orden en que resuelve todo. Supongo que no es su caso, pero me enfrenté a más de un ciclo infinito en el que un evento creó otro evento y lo estaba agregando directamente a la cola de eventos.

Espero que esto ayude.

PD: Si alguien tiene una forma más limpia / mejor de resolver esto, realmente me gustaría escucharlo.

Carlos
fuente
1
obj->Message( "Physics.EmitForce 0.0 1.1 2.2" );
// and some variations such as...
obj->Message( "Physics.EmitForce", "0.0 1.1 2.2" );
obj->Message( "Physics", "EmitForce", "0.0 1.1 2.2" );

Algunas cosas a tener en cuenta en este diseño:

  • El nombre del componente es el primer parámetro, esto es para evitar que el código trabaje demasiado en el mensaje, no podemos saber qué componentes podría activar un mensaje, y no queremos que todos mastiquen un mensaje con una falla del 90% tasa que se convierte en muchas ramas innecesarias y strcmp 's.
  • El nombre del mensaje es el segundo parámetro.
  • El primer punto (en # 1 y # 2) no es necesario, es solo para facilitar la lectura (para personas, no para computadoras).
  • Es compatible con sscanf, iostream, you-name-it. Sin azúcar sintáctico que no hace nada para simplificar el procesamiento del mensaje.
  • Un parámetro de cadena: pasar los tipos nativos no es más barato en términos de requisitos de memoria porque debe admitir un número desconocido de parámetros de tipo relativamente desconocido.
serpiente5
fuente