Estoy trabajando en un juego 2D en el que puedes moverte hacia arriba, hacia abajo, hacia la izquierda y hacia la derecha. Tengo esencialmente dos objetos lógicos del juego:
- Jugador: tiene una posición relativa al mundo
- Mundo: dibuja el mapa y el jugador
Hasta ahora, el mundo depende del jugador (es decir, tiene una referencia a él), necesitando su posición para determinar dónde dibujar el personaje del jugador y qué parte del mapa dibujar.
Ahora quiero agregar detección de colisión para que sea imposible que el jugador se mueva a través de las paredes.
La forma más simple en la que puedo pensar es que el jugador le pregunte al mundo si el movimiento previsto es posible. Pero eso introduciría una dependencia circular entre Player y World (es decir, cada uno tiene una referencia al otro), lo que parece evitar. La única forma en que se me ocurrió es hacer que el Mundo moviera al Jugador , pero eso me parece poco intuitivo.
¿Cuál es mi mejor opción? ¿O no vale la pena evitar una dependencia circular?
Respuestas:
El mundo no debe dibujarse a sí mismo; el Renderer debería dibujar el mundo. El jugador no debe dibujarse solo; el Renderer debe dibujar al jugador en relación con el mundo.
El jugador debe preguntarle al mundo sobre la detección de colisiones; o tal vez las colisiones deberían ser manejadas por una clase separada que verificaría la detección de colisiones no solo contra el mundo estático sino también contra otros actores.
Creo que el mundo probablemente no debería estar al tanto del jugador; debería ser un primitivo de bajo nivel, no un objeto divino. El jugador probablemente necesitará invocar algunos métodos del mundo, tal vez indirectamente (detección de colisión o comprobación de objetos interactivos, etc.).
fuente
Renderer
es necesario un tipo de, pero eso no significa que la lógica de cómo se procesa cada cosa es manejada porRenderer
, cada cosa que debe dibujarse probablemente debe heredar de una interfaz común comoIDrawable
oIRenderable
(o equivalente de interfaz en cualquier idioma que esté usando). El mundo podría ser elRenderer
, supongo, pero eso parece que estaría sobrepasando su responsabilidad, especialmente si ya era unIRenderable
sí mismo.Así es como un motor de renderizado típico maneja estas cosas:
Hay una distinción fundamental entre dónde está un objeto en el espacio y cómo se dibuja el objeto.
Dibujar un objeto
Por lo general, tiene una clase Renderer que hace esto. Simplemente toma un objeto (Modelo) y dibuja en la pantalla. Puede tener métodos como drawSprite (Sprite), drawLine (..), drawModel (Model), lo que sea que necesite. Es un Renderer, así que se supone que debe hacer todas estas cosas. También usa cualquier API que tenga debajo, por lo que puede tener, por ejemplo, un procesador que use OpenGL y uno que use DirectX. Si desea portar su juego a otra plataforma, simplemente escriba un nuevo renderizador y use ese. Es fácil.
Mover un objeto
Cada objeto está unido a algo a lo que nos referimos como SceneNode . Lo logras a través de la composición. Un SceneNode contiene un objeto. Eso es. ¿Qué es un SceneNode? Es una clase simple que contiene todas las transformaciones (posición, rotación, escala) de un objeto (generalmente en relación con otro SceneNode) junto con el objeto real.
Administrar los objetos
¿Cómo se gestionan los SceneNodes? A través de un SceneManager . Esta clase crea y realiza un seguimiento de cada SceneNode en su escena. Puede pedirle un SceneNode específico (generalmente identificado por un nombre de cadena como "Player" o "Table") o una lista de todos los nodos.
Dibujando el mundo
Esto debería ser bastante obvio por ahora. Simplemente camine por cada SceneNode en la escena y haga que el Renderer lo dibuje en el lugar correcto. Puede dibujarlo en el lugar correcto haciendo que el procesador almacene las transformaciones de un objeto antes de representarlo.
Detección de colisiones
Esto no siempre es trivial. Por lo general, puede consultar la escena sobre qué objeto está en un determinado punto del espacio o qué objetos se intersectarán con un rayo. De esta manera, puede crear un rayo desde su reproductor en la dirección del movimiento y preguntarle al administrador de la escena cuál es el primer objeto que se cruza con el rayo. Luego puede elegir mover al jugador a la nueva posición, moverlo en una cantidad menor (para colocarlo al lado del objeto que colisiona) o no moverlo en absoluto. Asegúrese de que estas consultas sean manejadas por clases separadas. Deberían pedirle al SceneManager una lista de SceneNodes, pero es otra tarea determinar si ese SceneNode cubre un punto en el espacio o se cruza con un rayo. Recuerde que el SceneManager solo crea y almacena nodos.
Entonces, ¿qué es el jugador y qué es el mundo?
El jugador podría ser una clase que contiene un SceneNode, que a su vez contiene el modelo que se representará. Mueves al jugador cambiando la posición del nodo de escena. El mundo es simplemente una instancia del SceneManager. Contiene todos los objetos (a través de SceneNodes). Maneja la detección de colisiones haciendo consultas sobre el estado actual de la escena.
Esto está lejos de ser una descripción completa o precisa de lo que sucede dentro de la mayoría de los motores, pero debería ayudarlo a comprender los fundamentos y por qué es importante respetar los principios OOP subrayados por SOLID . No se resigne a la idea de que es demasiado difícil reestructurar su código o que realmente no lo ayudará. Ganará mucho más en el futuro diseñando cuidadosamente su código.
fuente
¿Por qué querrías evitar eso? Deben evitarse las dependencias circulares si desea crear una clase reutilizable. Pero el jugador no es una clase que deba ser reutilizable. ¿Alguna vez querrías usar el reproductor sin un mundo? Probablemente no.
Recuerde que las clases no son más que colecciones de funcionalidad. La pregunta es cómo se divide la funcionalidad. Haz lo que necesites hacer. Si necesita una decadencia circular, que así sea. (Lo mismo ocurre con cualquier característica de OOP por cierto. Codifique las cosas de una manera que tenga un propósito, no solo siga los paradigmas a ciegas).
Edite
Bien, para responder la pregunta: puede evitar que el Jugador necesite conocer el mundo para las comprobaciones de colisión mediante devoluciones de llamada:
El mundo puede manejar el tipo de física que ha descrito en la pregunta si expone la velocidad de las entidades:
Sin embargo, tenga en cuenta que probablemente necesitará una dependencia del mundo tarde o temprano, es decir, cuando necesite la funcionalidad del mundo: ¿quiere saber dónde está el enemigo más cercano? ¿Quieres saber qué tan lejos está la próxima repisa? Dependencia es.
fuente
render(World)
. El debate gira en torno a si todo el código debe estar concentrado dentro de una clase, o si el código debe dividirse en unidades lógicas y funcionales, que luego son más fáciles de mantener, ampliar y administrar. Por cierto, buena suerte reutilizando esos gerentes de componentes, motores de física y gerentes de entrada, todos inteligentemente indiferenciados y completamente acoplados.Su diseño actual parece ir en contra del primer principio del diseño SÓLIDO .
Este primer principio, llamado el "principio de responsabilidad única", generalmente es una buena guía a seguir para no crear objetos monolíticos de hacer todo que siempre dañarán su diseño.
Para concretar, su
World
objeto es responsable tanto de actualizar y mantener el estado del juego como de dibujar todo.¿Qué pasa si su código de renderizado cambia / tiene que cambiar? ¿Por qué debería tener que actualizar ambas clases que en realidad no tienen nada que ver con el renderizado? Como Liosan ya ha dicho, deberías tener un
Renderer
.Ahora, para responder a su pregunta real ...
Hay muchas formas de hacer esto, y esta es solo una forma de desacoplar:
Object
s en la que se encuentra el jugador, pero no depende de la clase de jugador (use la herencia para lograr esto).InputManager
.Renderer
dibuja todos los objetos.fuente
health
que solo tiene esta instanciaPlayer
).El jugador debe preguntarle al mundo sobre cosas como la detección de colisiones. La forma de evitar la dependencia circular es no hacer que el mundo tenga una dependencia del jugador. El mundo necesita saber dónde está dibujando: probablemente desee abstraerlo más lejos, tal vez con una referencia a un objeto de cámara que a su vez puede contener una referencia a alguna entidad para rastrear.
Lo que desea evitar en términos de referencias circulares no es tanto mantener referencias entre sí, sino más bien referirse explícitamente en el código.
fuente
Siempre que dos tipos diferentes de objetos puedan preguntarse entre sí. Dependerán unos de otros, ya que necesitan mantener una referencia al otro para llamar a sus métodos.
Puede evitar la dependencia circular haciendo que el Mundo le pregunte al Jugador, pero el Jugador no puede preguntarle al Mundo, o viceversa. De esta manera, el mundo tiene referencias a los jugadores, pero los jugadores no necesitan referencias al mundo. O viceversa. Pero esto no resolverá el problema, porque el mundo necesitaría preguntar a los jugadores si tienen algo que preguntar, y decirles en la próxima llamada ...
Por lo tanto, no puede evitar este "problema" y creo que no hay necesidad de preocuparse por eso. Mantenga el diseño estúpido simple todo el tiempo que pueda.
fuente
Excluyendo los detalles sobre el jugador y el mundo, tienes un caso simple de no querer introducir una dependencia circular entre dos objetos (que dependiendo de tu idioma puede no importar, mira el enlace en el comentario de Fuhrmanator). Existen al menos dos soluciones estructurales muy simples que se aplicarían a este y otros problemas similares:
1) Introduce el patrón singleton en tu clase mundial . Esto permitirá que el jugador (y cualquier otro objeto) encuentre fácilmente el objeto mundial sin búsquedas costosas o enlaces permanentes. La esencia de este patrón es que la clase tiene una referencia estática a la única instancia de esa clase, que se establece en la instanciación del objeto y se borra en su eliminación.
Dependiendo de su lenguaje de desarrollo y la complejidad que desee, podría implementarlo fácilmente como una superclase o interfaz y reutilizarlo para muchas clases principales de las que no espera tener más de una en su proyecto.
2) Si el lenguaje en el que se está desarrollando lo admite (muchos lo hacen), use una referencia débil . Esta es una referencia que no afecta cosas como la recolección de basura. Es útil en estos casos exactamente, solo asegúrese de no hacer suposiciones sobre si el objeto al que hace referencia débil todavía existe.
En su caso particular, su (s) Jugador (s) podrían tener una referencia débil al mundo. El beneficio de esto (como con el singleton) es que no necesita buscar el objeto mundial de alguna manera en cada cuadro, o tener una referencia permanente que obstaculice los procesos afectados por referencias circulares como la recolección de basura.
fuente
Como los otros han dicho, creo que su
World
está haciendo una cosa demasiados: se trata de tanto contener el juegoMap
(que debe ser una entidad distinta) y sea unaRenderer
a la vez.Por lo tanto, cree un nuevo objeto (llamado
GameMap
, posiblemente) y almacene los datos de nivel de mapa en él. Escriba funciones en él que interactúen con el mapa actual.Entonces también necesitas un
Renderer
objeto. Usted podría hacer que esteRenderer
objeto lo que tanto contieneGameMap
yPlayer
(así comoEnemies
), y también los atrae.fuente
Puede evitar dependencias circulares al no agregar las variables como miembros. Use una función estática CurrentWorld () para el jugador o algo así. Sin embargo, no invente una interfaz diferente de la implementada en World, esto es completamente innecesario.
También es posible destruir la referencia antes / mientras se destruye el objeto jugador para detener efectivamente los problemas causados por referencias circulares.
fuente