¿Estoy en el camino correcto con esta arquitectura de componentes?

9

Recientemente he decidido renovar la arquitectura de mi juego para deshacerme de las jerarquías de clase profundas y reemplazarlas con componentes configurables. La primera jerarquía que estoy reemplazando es la jerarquía de elementos y me gustaría saber si estoy en el camino correcto.

Anteriormente, tenía una jerarquía que se parecía a esto:

Item -> Equipment -> Weapon
                  -> Armor
                  -> Accessory
     -> SyntehsisItem
     -> BattleUseItem -> HealingItem
                      -> ThrowingItem -> ThrowsAsAttackItem

No hace falta decir que estaba empezando a complicarse y no era una solución fácil para los artículos que necesitaban ser de varios tipos (es decir, algunos equipos se usan en la síntesis de artículos, algunos equipos son arrojables, etc.)

Luego intenté refactorizar y colocar la funcionalidad en la clase de elemento base. Pero luego me di cuenta de que el artículo tenía muchos datos no utilizados / superfluos. Ahora estoy tratando de hacer un componente como arquitectura, al menos para mis artículos antes de intentar hacerlo con mis otras clases de juego.

Esto es lo que estoy pensando actualmente para la configuración del componente:

Tengo una clase de elemento base que tiene ranuras para varios componentes (es decir, una ranura de componente de equipo, una ranura de componente de curación, etc., así como un mapa para componentes arbitrarios) así que algo como esto:

class Item
{
    //Basic item properties (name, ID, etc.) excluded
    EquipmentComponent* equipmentComponent;
    HealingComponent* healingComponent;
    SynthesisComponent* synthesisComponent;
    ThrowComponent* throwComponent;
    boost::unordered_map<std::string, std::pair<bool, ItemComponent*> > AdditionalComponents;
} 

Todos los componentes del elemento heredarían de una clase ItemComponent base, y cada tipo de Componente es responsable de decirle al motor cómo implementar esa funcionalidad. es decir, el componente de curación le dice a los mecánicos de batalla cómo consumir el elemento como elemento de curación, mientras que el componente de lanzamiento le dice al motor de batalla cómo tratar el elemento como un elemento arrojable.

El mapa se utiliza para almacenar componentes arbitrarios que no son componentes de elementos centrales. Lo estoy emparejando con un bool para indicar si el Item Container debe administrar el ItemComponent o si está siendo administrado por una fuente externa.

Mi idea aquí era que definiera los componentes principales utilizados por mi motor de juego desde el principio, y mi fábrica de elementos asignaría los componentes que el elemento realmente tiene, de lo contrario son nulos. El mapa contendría componentes arbitrarios que generalmente serían agregados / consumidos por archivos de secuencias de comandos.

Mi pregunta es, ¿es este un buen diseño? Si no, ¿cómo se puede mejorar? Pensé en agrupar todos los componentes en el mapa, pero el uso de la indexación de cadenas parecía innecesario para los componentes principales del elemento

usuario127817
fuente

Respuestas:

8

Parece un primer paso muy razonable.

Está optando por una combinación de generalidad (el mapa de "componentes adicionales") y el rendimiento de búsqueda (los miembros codificados), que puede ser un poco de optimización previa: su punto con respecto a la ineficiencia general de las cadenas la búsqueda está bien hecha, pero puede aliviar eso eligiendo indexar los componentes por algo más rápido de hash. Un enfoque podría ser dar a cada tipo de componente un ID de tipo único (esencialmente está implementando RTTI personalizado liviano ) e índice basado en eso.

De todos modos, le advierto que exponga una API pública para el objeto Item que le permita solicitar cualquier componente, los codificados y los adicionales, de manera uniforme. Esto facilitaría cambiar la representación subyacente o el equilibrio de los componentes codificados / no codificados sin tener que refactorizar todos los clientes de componentes de elementos.

También puede considerar proporcionar versiones "ficticias" sin operación de cada uno de los componentes codificados y asegurarse de que siempre estén asignados; luego puede usar miembros de referencia en lugar de punteros, y nunca necesitará verificar si hay un puntero NULO antes de interactuar con una de las clases de componentes codificados. Aún incurrirá en el costo del despacho dinámico para interactuar con los miembros de ese componente, pero eso ocurriría incluso con los miembros del puntero. Esto es más un problema de limpieza del código porque el impacto en el rendimiento será insignificante con toda probabilidad.

No creo que sea una buena idea tener dos tipos diferentes de ámbitos de por vida (en otras palabras, no creo que el bool que tienes en el mapa de componentes adicionales sea una gran idea). Complica el sistema e implica que la destrucción y la liberación de recursos no serán terriblemente deterministas. La API para sus componentes sería mucho más clara si optara por una estrategia de administración de por vida u otra: la entidad administra la vida útil del componente o el subsistema que se da cuenta de los componentes (prefiero el último porque se combina mejor con el componente externo) enfoque, que discutiremos a continuación).

El gran inconveniente que veo con su enfoque es que está agrupando todos los componentes en el objeto "entidad", que en realidad no siempre es el mejor diseño. De mi respuesta relacionada a otra pregunta basada en componentes:

Su enfoque de usar un gran mapa de componentes y una llamada a update () en el objeto del juego es bastante subóptimo (y un escollo común para los primeros que construyen este tipo de sistemas). La coherencia de caché es muy pobre durante la actualización y no le permite aprovechar la concurrencia y la tendencia hacia el proceso de SIMD de grandes lotes de datos o comportamiento a la vez. A menudo es mejor usar un diseño en el que el objeto del juego no actualice sus componentes, sino que el subsistema responsable del componente en sí los actualice todos a la vez.

Básicamente, está adoptando el mismo enfoque almacenando los componentes en la entidad del elemento (que es, nuevamente, un primer paso completamente aceptable). Lo que puede descubrir es que la mayor parte del acceso a los componentes que le preocupa el rendimiento es solo actualizarlos, y si elige utilizar un enfoque más externo para la organización de componentes, donde los componentes se mantienen en un caché coherente , la estructura de datos eficiente (para su dominio) por un subsistema que comprende sus necesidades al máximo, puede lograr un rendimiento de actualización mucho mejor y más paralelo.

Pero señalo esto solo como algo a considerar como una dirección futura: ciertamente no querrá exagerar en diseñar esto en exceso; puede hacer una transición gradual a través de una refactorización constante o puede descubrir que su implementación actual satisface sus necesidades perfectamente y no hay necesidad de repetirla.

Comunidad
fuente
1
+1 por sugerir deshacerse del objeto Item. Si bien será más trabajo por adelantado, terminará produciendo un mejor sistema de componentes.
James
Tenía algunas preguntas más, no estoy seguro de si debería comenzar un nuevo tema, así que voy a intentarlo aquí primero: para mi clase de elemento, no hay métodos que llamaré evert frame (o incluso cerrar). Para mis subsistemas gráficos, seguiré su consejo y mantendré todos los objetos para actualizar bajo el sistema. La otra pregunta que tuve es cómo manejo las comprobaciones de componentes. Por ejemplo, quiero ver si puedo usar un elemento como X, así que, naturalmente, comprobaría si el elemento tiene el componente necesario para realizar X. ¿Es esta la forma correcta de hacerlo? Gracias de nuevo por la respuesta
user127817