Sugeriría comenzar leyendo las 3 grandes mentiras de Mike Acton, porque violas dos de ellas. Lo digo en serio, esto cambiará la forma en que diseñas tu código: http://cellperformance.beyond3d.com/articles/2008/03/three-big-lies.html
Entonces, ¿qué violas?
Mentira # 3: el código es más importante que los datos
Hablas de la inyección de dependencia, que puede ser útil en algunos (y solo en algunos) casos, pero siempre debe sonar una gran alarma si la usas, ¡especialmente en el desarrollo de juegos! ¿Por qué? Porque es una abstracción a menudo innecesaria. Y las abstracciones en los lugares equivocados son horribles. Entonces tienes un juego. El juego tiene gestores para diferentes componentes. Los componentes están todos definidos. Así que haga una clase en algún lugar de su código de bucle principal del juego que "tenga" los gerentes. Me gusta:
private CollissionManager _collissionManager;
private BulletManager _bulletManager;
Dele algunas funciones getter para obtener cada clase de administrador (getBulletManager ()). Tal vez esta clase en sí misma sea Singleton o sea accesible desde una (de todos modos, probablemente tengas un Singleton Game central). No hay nada de malo con datos y comportamientos codificados bien definidos.
No cree un ManagerManager que le permita registrar Managers con una clave, que otras clases que desean utilizar el manager pueden recuperar con esa clave. Es un gran sistema y muy flexible, pero aquí se habla de un juego. Sabes exactamente qué sistemas hay en el juego. ¿Por qué pretender que no lo haces? Porque este es un sistema para las personas que piensan que el código es más importante que los datos. Dirán "El código es flexible, los datos simplemente lo llenan". Pero el código es solo información. El sistema que describí es mucho más fácil, más confiable, más fácil de mantener y mucho más flexible (por ejemplo, si el comportamiento de un gerente difiere del de otros gerentes, solo tiene que cambiar algunas líneas en lugar de reelaborar todo el sistema)
Mentira # 2: el código debe diseñarse en torno a un modelo del mundo
Entonces tienes una entidad en el mundo del juego. La entidad tiene varios componentes que definen su comportamiento. Entonces, crea una clase Entity con una lista de objetos Component y una función Update () que llama a la función Update () de cada Componente. ¿Derecho?
No :) Eso es diseñar alrededor de un modelo del mundo: tienes viñeta en tu juego, así que agregas una clase Bullet. Luego actualiza cada viñeta y pasa a la siguiente. Esto matará absolutamente su rendimiento y le brinda una base de código retorcida horrible con código duplicado en todas partes y sin estructuración lógica de código similar. (Consulte mi respuesta aquí para obtener una explicación más detallada de por qué el diseño OO tradicional apesta, o busque Diseño orientado a datos)
Echemos un vistazo a la situación sin nuestro sesgo OO. Queremos lo siguiente, ni más ni menos (tenga en cuenta que no hay ningún requisito para hacer una clase para entidad u objeto):
- Tienes un montón de entidades
- Las entidades se componen de una serie de componentes que definen el comportamiento de la entidad.
- Desea actualizar cada componente del juego en cada cuadro, preferiblemente de forma controlada
- Aparte de identificar los componentes como pertenecientes, no hay nada que la entidad misma deba hacer. Es un enlace / ID para un par de componentes.
Y veamos la situación. Su sistema de componentes actualizará el comportamiento de cada objeto en el juego en cada cuadro. Este es definitivamente un sistema crítico de su motor. ¡El rendimiento es importante aquí!
Si está familiarizado con la arquitectura de la computadora o el diseño orientado a datos, sabe cómo se logra el mejor rendimiento: memoria compacta y agrupando la ejecución del código. Si ejecuta fragmentos de código A, B y C como este: ABCABCABC, no obtendrá el mismo rendimiento que cuando lo ejecuta así: AAABBBCCC. Esto no es solo porque las instrucciones y el caché de datos se usarán de manera más eficiente, sino también porque si ejecuta todas las "A" una tras otra, hay mucho espacio para la optimización: eliminar el código duplicado, calcular previamente los datos que utilizan todas las "A", etc.
Entonces, si queremos actualizar todos los componentes, no los hagamos clases / objetos con una función de actualización. No llamemos a esa función de actualización para cada componente en cada entidad. Esa es la solución "ABCABCABC". Agrupemos todas las actualizaciones de componentes idénticos. Entonces podemos actualizar todos los componentes A, seguidos de B, etc. ¿Qué necesitamos para hacer esto?
Primero, necesitamos gerentes de componentes. Para cada tipo de componente en el juego, necesitamos una clase de administrador. Tiene una función de actualización que actualizará todos los componentes de ese tipo. Tiene una función de creación que agregará un nuevo componente de ese tipo y una función de eliminación que destruirá el componente especificado. Puede haber otras funciones auxiliares para obtener y establecer datos específicos de ese componente (por ejemplo: establecer el modelo 3D para Componente del modelo). Tenga en cuenta que el administrador es de alguna manera un cuadro negro para el mundo exterior. No sabemos cómo se almacenan los datos de cada componente. No sabemos cómo se actualiza cada componente. No nos importa, siempre y cuando los componentes se comporten como deberían.
Luego necesitamos una entidad. Podrías hacer de esto una clase, pero eso no es necesario. Una entidad no podría ser más que una ID entera única o una cadena hash (también un número entero). Cuando crea un componente para la Entidad, pasa la ID como argumento al Administrador. Cuando desee eliminar el componente, vuelva a pasar la ID. Puede haber algunas ventajas al agregar un poco más de datos a la Entidad en lugar de simplemente convertirlo en una ID, pero esas solo serán funciones auxiliares porque, como he enumerado en los requisitos, todos los componentes definen el comportamiento de la entidad. Sin embargo, es su motor, así que haga lo que tenga sentido para usted.
Lo que sí necesitamos es un Entity Manager. Esta clase generará ID únicos si usa la solución de solo ID, o puede usarse para crear / administrar objetos Entity. También puede mantener una lista de todas las entidades en el juego si lo necesitas. El Entity Manager podría ser la clase central de su sistema de componentes, almacenando las referencias a todos los ComponentManagers en su juego y llamando a sus funciones de actualización en el orden correcto. De esa manera, todo el bucle del juego tiene que hacer es llamar a EntityManager.update () y todo el sistema está muy bien separado del resto de su motor.
Esa es la vista panorámica, echemos un vistazo a cómo funcionan los gerentes de componentes. Esto es lo que necesitas:
- Crear datos de componentes cuando se llama a create (entityID)
- Eliminar datos de componentes cuando se llama a remove (entityID)
- Actualice todos los datos de componentes (aplicables) cuando se llame a update () (es decir, no todos los componentes necesitan actualizar cada marco)
El último es donde define el comportamiento / lógica de los componentes y depende completamente del tipo de componente que está escribiendo. AnimationComponent actualizará los datos de animación en función del marco en el que se encuentre. DragableComponent actualizará solo un componente que está siendo arrastrado por el mouse. PhysicsComponent actualizará los datos en el sistema de física. Aún así, debido a que actualiza todos los componentes del mismo tipo de una vez, puede hacer algunas optimizaciones que no son posibles cuando cada componente es un objeto separado con una función de actualización que podría llamarse en cualquier momento.
Tenga en cuenta que todavía nunca he pedido la creación de una clase XxxComponent para contener datos de componentes. Eso depende de usted. ¿Te gusta el diseño orientado a datos? Luego, estructura los datos en matrices separadas para cada variable. ¿Te gusta el diseño orientado a objetos? (No lo recomendaría, todavía matará su rendimiento en muchos lugares). Luego cree un objeto XxxComponent que contendrá los datos de cada componente.
Lo mejor de los gerentes es la encapsulación. Ahora la encapsulación es una de las filosofías más horriblemente mal utilizadas en el mundo de la programación. Así es como debe usarse. Solo el administrador sabe qué datos del componente se almacenan, dónde y cómo funciona la lógica de un componente. Hay algunas funciones para obtener / establecer datos, pero eso es todo. Puede reescribir todo el administrador y sus clases subyacentes y, si no cambia la interfaz pública, nadie se da cuenta. ¿Cambió el motor de física? Simplemente reescriba PhysicsComponentManager y listo.
Luego hay una última cosa: comunicación e intercambio de datos entre componentes. Ahora esto es complicado y no existe una solución única para todos. Puede crear funciones get / set en los administradores para permitir, por ejemplo, que el componente de colisión obtenga la posición del componente de posición (es decir, PositionManager.getPosition (entityID)). Podrías usar un sistema de eventos. Podría almacenar algunos datos compartidos en la entidad (la solución más fea en mi opinión). Podría usar (esto se usa a menudo) un sistema de mensajería. ¡O use una combinación de múltiples sistemas! No tengo el tiempo ni la experiencia para entrar en cada uno de estos sistemas, pero google y stackoverflow search son tus amigos.