Tengo un juego básico de defensa de torre 2D en C ++.
Cada mapa es una clase separada que hereda de GameState. El mapa delega la lógica y el código de dibujo a cada objeto en el juego y establece datos como la ruta del mapa. En pseudocódigo, la sección lógica podría verse así:
update():
for each creep in creeps:
creep.update()
for each tower in towers:
tower.update()
for each missile in missiles:
missile.update()
Los objetos (pelos de punta, torres y misiles) se almacenan en vectores de punteros. Las torres deben tener acceso al vector de creeps y al vector de misiles para crear nuevos misiles e identificar objetivos.
La pregunta es: ¿dónde declaro los vectores? ¿Deberían ser miembros de la clase Map y pasarse como argumentos a la función tower.update ()? O declarado a nivel mundial? ¿O hay otras soluciones que me faltan por completo?
Cuando varias clases necesitan acceder a los mismos datos, ¿dónde deben declararse los datos?
fuente
Respuestas:
Cuando necesita una sola instancia de una clase en todo su programa, llamamos a esa clase un servicio . Existen varios métodos estándar para implementar servicios en programas:
Singletons . Hace unos 10-15 años, los singletons eran el gran patrón de diseño para conocer. Sin embargo, hoy en día son menospreciados. Son mucho más fáciles de subprocesos múltiples, pero debe limitar su uso a un subproceso a la vez, que no siempre es lo que desea. El seguimiento de vidas es tan difícil como con las variables globales.
Una clase de singleton típica se verá así:
Inyección de dependencia (DI) . Esto solo significa pasar el servicio como un parámetro constructor. Un servicio ya debe existir para pasarlo a una clase, por lo que no hay forma de que dos servicios dependan el uno del otro; en el 98% de los casos, esto es lo que desea (y para el otro 2%, siempre puede crear un
setWhatever()
método y pasar el servicio más adelante) . Debido a esto, DI no tiene los mismos problemas de acoplamiento que las otras opciones. Se puede usar con subprocesos múltiples, porque cada subproceso puede simplemente tener su propia instancia de cada servicio (y compartir solo aquellos que absolutamente necesita). También hace que el código sea comprobable por unidad, si te importa eso.El problema con la inyección de dependencia es que ocupa más memoria; ahora cada instancia de una clase necesita referencias a cada servicio que usará. Además, se vuelve molesto usarlo cuando tienes demasiados servicios; existen marcos que mitigan este problema en otros lenguajes, pero debido a la falta de reflexión de C ++, los marcos DI en C ++ tienden a ser aún más trabajo que simplemente hacerlo manualmente.
Vea esta página (de la documentación de Ninject, un marco C # DI) para otro ejemplo.
La inyección de dependencia es la solución habitual para este problema, y es la respuesta que verá con más votos a preguntas como esta en StackOverflow.com. DI es un tipo de Inversión de Control (IoC).
Localizador de servicios . Básicamente, solo una clase que contiene una instancia de cada servicio. Puede hacerlo utilizando la reflexión , o simplemente puede agregarle una nueva instancia cada vez que desee crear un nuevo servicio. Todavía tiene el mismo problema que antes: ¿cómo acceden las clases a este localizador? - que puede resolverse de cualquiera de las formas anteriores, pero ahora solo necesita hacerlo para su
ServiceLocator
clase, en lugar de para docenas de servicios. Este método también es comprobable por unidad, si te importa ese tipo de cosas.Los localizadores de servicios son otra forma de inversión de control (IoC). Por lo general, los marcos que realizan la inyección automática de dependencias también tendrán un localizador de servicios.
XNA (marco de programación de juegos C # de Microsoft) incluye un localizador de servicios; para obtener más información al respecto, vea esta respuesta .
Por cierto, en mi humilde opinión las torres no deben saber acerca de los pelos de punta. A menos que esté planeando simplemente recorrer la lista de creeps para cada torre, probablemente querrá implementar una partición de espacio no trivial ; y ese tipo de lógica no pertenece a la clase de torres.
fuente
Yo personalmente usaría polimorfismo aquí. ¿Por qué tener un
missile
vector, untower
vector y uncreep
vector ... cuando todos llaman a la misma función?update
? ¿Por qué no tener un vector de punteros a alguna clase baseEntity
oGameObject
?Creo que una buena manera de diseñar es pensar '¿tiene sentido esto en términos de propiedad'? Obviamente, una torre posee una forma de actualizarse, pero ¿un mapa posee todos los objetos que contiene? Si optas por lo global, ¿estás diciendo que nada posee las torres y los pelos de punta? Generalmente, Global es una mala solución: promueve malos patrones de diseño, pero es mucho más fácil trabajar con él. Considere sopesar '¿quiero terminar esto?' y '¿quiero algo que pueda reutilizar'?
Una forma de evitar esto es alguna forma de sistema de mensajería. El
tower
puede enviar un mensaje almap
(al que tiene acceso, ¿quizás una referencia a su propietario?) Que golpeó acreep
, ymap
luego le dicecreep
que ha sido golpeado. Esto es muy limpio y segrega datos.Otra forma es simplemente buscar en el mapa lo que quiere. Sin embargo, puede haber problemas con el orden de actualización aquí.
fuente
Este es un caso en el que la programación estricta orientada a objetos (OOP) se rompe.
De acuerdo con los principios de OOP, debe agrupar datos con comportamiento relacionado utilizando clases. Pero tiene un comportamiento (segmentación) que necesita datos que no están relacionados entre sí (torres y pelos de punta). En esta situación, muchos programadores intentarán asociar el comportamiento con parte de los datos que necesita (p. Ej., Las torres manejan la orientación, pero no conocen los creeps), pero hay otra opción: no agrupar el comportamiento con los datos.
En lugar de hacer que el comportamiento de orientación sea un método de la clase torre, conviértalo en una función libre que acepte torres y escalofríos como argumentos. Esto podría requerir hacer públicos a más miembros que quedan en las clases de torre y creep, y eso está bien. La ocultación de datos es útil, pero es un medio, no un fin en sí mismo, y no debes ser esclavo de ella. Además, los miembros privados no son la única forma de controlar el acceso a los datos: si los datos no se transfieren a una función y no son globales, están efectivamente ocultos de esa función. Si el uso de esta técnica le permite evitar datos globales, en realidad podría estar mejorando la encapsulación.
Un ejemplo extremo de este enfoque es la arquitectura del sistema de entidades .
fuente