java - programacion - El patrón compuesto/sistema de entidad y OOP tradicional
composite example (4)
Estoy trabajando en un pequeño juego escrito en Java (pero la pregunta es independiente del idioma). Como quería explorar varios patrones de diseño, me colgué en el sistema Composite pattern / Entity (que originalmente leí aquí y aquí ) como una alternativa a la típica herencia jerárquica profunda.
Ahora, después de escribir varios miles de líneas de código, estoy un poco confundido. Creo que entiendo el patrón y me gusta usarlo. Creo que es genial y Starbucks-ish, pero parece que el beneficio que proporciona es de corta duración y (lo que más me molesta) depende en gran medida de su granularidad.
Aquí hay una imagen del segundo artículo anterior:
Me encanta la forma en que los objetos (entidades del juego, o como quieras llamarlos) tienen un conjunto mínimo de componentes y la idea inferida es que podrías escribir código que se parece a algo como:
BaseEntity Alien = new BaseEntity();
BaseEntity Player = new BaseEntity();
Alien.addComponent(new Position(), new Movement(), new Render(), new Script(), new Target());
Player.addComponent(new Position(), new Movement(), new Render(), new Script(), new Physics());
... lo que sería realmente bueno ... pero en REALIDAD, el código termina pareciéndose a algo así
BaseEntity Alien = new BaseEntity();
BaseEntity Player = new BaseEntity();
Alien.addComponent(new Position(), new AlienAIMovement(), new RenderAlien(), new ScriptAlien(), new Target());
Player.addComponent(new Position(), new KeyboardInputMovement(), new RenderPlayer(), new ScriptPlayer(), new PhysicsPlayer());
Parece que termino teniendo algunos componentes muy especializados que se componen de componentes menores. Muchas veces, tengo que hacer algunos componentes que tienen dependencias de otros componentes. Después de todo, ¿cómo puedes renderizar si no tienes una posición? No solo eso, sino la forma en que terminas representando un jugador contra un extraterrestre vs. una granada puede ser fundamentalmente diferente. No puede tener UN componente que dicte los tres, a menos que haga un componente muy grande (en cuyo caso ... ¿por qué está usando el patrón compuesto de todos modos?)
Para dar otro ejemplo de la vida real. Tengo personajes en mi juego que pueden equipar varios equipos. Cuando se equipa una pieza de equipo, se cambian algunas estadísticas y se muestran visualmente. Así es como se ve mi código en este momento:
billy.addControllers(new Movement(), new Position(), new CharacterAnimationRender(), new KeyboardCharacterInput());
billy.get(CharacterAnimationRender.class).setBody(BODY.NORMAL_BODY);
billy.get(CharacterAnimationRender.class).setFace(FACE.BLUSH_FACE);
billy.get(CharacterAnimationRender.class).setHair(HAIR.RED_HAIR);
billy.get(CharacterAnimationRender.class).setDress(DRESS.DRAGON_PLATE_ARMOR);
La clase anterior CharacterAnimationRender.class
solo afecta a lo que se visualiza VISUALMENTE. Entonces claramente necesito hacer otro Componente que maneje las estadísticas de los cambios. Sin embargo, ¿por qué debería hacer algo como esto?
billy.addControllers(new CharacterStatistics());
billy.get(CharacterAnimationRender.class).setBody(BODY.NORMAL_BODY);
billy.get(CharacterStatistics.class).setBodyStats(BODY_STATS.NORMAL_BODY);
¿Cuándo puedo crear un controlador / componente CharacterGearStuff
que maneje TANTO la distribución de las estadísticas como el cambio visual?
En pocas palabras, no estoy seguro de cómo se supone que esto ayudará a la productividad, ya que, a menos que desee manejar todo manualmente, aún debe crear "metacomponentes" que dependan de más de 2 componentes (y modificar / modificar de forma cruzada todos sus subcomponentes - devolviéndonos a OOP). O tal vez estoy pensando en eso completamente mal. ¿Soy yo?
Creo que estás usando un enfoque equivocado aquí. Se supone que los patrones deben ser adoptados para satisfacer sus necesidades, no al revés. Lo simple siempre es mejor que lo complejo, y si sientes que algo no funciona bien, esto significa que debes darte unos pasos atrás y quizás comenzar desde el principio.
Para mí, esto tiene un olor a código ya:
BaseEntity Alien = new BaseEntity();
Alien.addComponent(new Position(), new AlienAIMovement(), new RenderAlien(), new ScriptAlien(), new Target());
Esperaría que el código orientado a objetos para que se vea algo como esto:
Alien alien = new AlienBuilder()
.withPosition(10, 248)
.withTargetStrategy(TargetStrategy.CLOSEST)
.build();
//somewhere in the main loop
Renderer renderer = getRenderer();
renderer.render(alien);
Cuando utiliza clases genéricas para todas sus entidades, tendrá una API muy genérica y difícil de usar para manejar sus objetos.
Además, se siente mal tener cosas como Posición, Movimiento y Renderer bajo el mismo paraguas Componente. La posición no es un componente, es un atributo. El movimiento es un comportamiento, y Renderer es algo que no está relacionado con su modelo de dominio en absoluto, es parte del subsistema de gráficos. El componente podría ser una rueda para un automóvil, partes del cuerpo y pistolas para un extraterrestre.
El desarrollo del juego es algo muy sofisticado y es muy difícil hacerlo bien desde el primer intento. Reescribe tu código desde cero, aprende de tus errores y siente lo que estás haciendo, no solo intenta adaptar un patrón de algún artículo. Y si quieres mejorar en los patrones, debes probar algo más aparte del desarrollo del juego. Escribe un editor de texto simple, por ejemplo.
David,
primero gracias por tu pregunta perfecta.
Entiendo tu problema y creo que no usaste el patrón correctamente. Por favor, lea este artículo: http://en.wikipedia.org/wiki/Composite_pattern
Si, por ejemplo, no puede implementar el movimiento de clase general y necesita AlienAIMovement y KeyboardMovement, probablemente debería utilizar el patrón Visitor. Pero antes de comenzar la refacturación de miles de líneas de código, compruebe si puede hacer lo siguiente.
¿Es una oportunidad para escribir Class Movement que acepta parámetros de tipo BaseEntity? ¿Probablemente la diferencia entre todas las implementaciones de Movimiento es solo un parámetro, una bandera o más? En este caso, su código se verá así:
Alien.addComponent(new Position(), new Movement(Alien), new Render(Alien), new Script(Alien), new Target());
Creo que no es tan malo.
Si no es posible, intente crear instancias usando fábrica, entonces
Alien.addComponent(f.createPosition(), f.createMovement(Alien), f.createRender(Alien), f.createRenderScript(Alien), f.createTarget());
Espero que mis sugerencias me ayuden.
Ents parece estar diseñado para hacer exactamente lo que quieres. Si aún quieres tu propia biblioteca, al menos podrías aprender de su diseño.
Todas las respuestas anteriores parecen torpes y crean toneladas de objetos innecesarios, IMO.
Parece que ha entendido mal el patrón del componente.
Los componentes son solo datos, no hay código. Si tiene código en su componente, ya no es un componente, es algo más complejo.
Entonces, por ejemplo, debería poder compartir trivialmente su CharacterAnimationRender y CharacterStatistics, por ejemplo:
CharacterStats { int BODY }
CharacterGameStats { ...not sure what data you have that affects gameplay, but NOT the rendering... }
CharacterVisualDetails { int FACE, int HAIR }
... pero no es necesario que estos sean conscientes de la existencia de los demás. En el momento en que hablas de "dependencias" entre componentes, sospecho que te has perdido. ¿Cómo puede una estructura de ints "depender de" otra estructura de ints? Ellos no pueden. Son solo pedazos de datos.
...
Volviendo a sus preocupaciones al principio, termina con:
Alien.addComponent(new Position(), new AlienAIMovement(), new RenderAlien(), new ScriptAlien(), new Target());
Player.addComponent(new Position(), new KeyboardInputMovement(), new RenderPlayer(), new ScriptPlayer(), new PhysicsPlayer());
...Eso es perfecto. ASUMIENDO que ha escrito esos componentes correctamente, ha dividido los datos en pequeños fragmentos que son fáciles de leer / depurar / editar / codificar.
Sin embargo, esto es hacer conjeturas porque no has especificado lo que hay dentro de esos componentes ... ej. AlienAIMovement - ¿Qué hay en eso? Normalmente, esperaría que tengas un "AIMovement ()" y luego lo edites para convertirlo en una versión de Alien, por ejemplo, cambiar algunos indicadores internos en ese componente para indicar que está usando las funciones "Alien" en tu sistema AI.