Cuando varias clases necesitan acceder a los mismos datos, ¿dónde deben declararse los datos?


39

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?


1
Los miembros globales se consideran 'feos' pero son rápidos y hacen que el desarrollo sea más fácil, si es un juego pequeño, no hay problema (en mi humilde opinión). También podría crear una clase externa que maneje la lógica ( por qué las torres necesitan estos vectores) y tenga acceso a todos los vectores.
Jonathan Connell

-1 si esto está relacionado con la programación del juego, entonces comer pizza también lo está. Obtenga algunos buenos libros de diseño de software
Maik Semder

9
@Maik: ¿Cómo no está relacionado el diseño de software con la programación de juegos? El hecho de que también se aplique a otros campos de programación no lo hace fuera de tema.
BlueRaja - Danny Pflughoeft

Las listas de patrones de diseño de software de @BlueRaja se adaptan mejor a SO, para eso es para lo que está allí después de todo. GD.SE es para programación de juegos, no para diseño de software
Maik Semder

Respuestas:


53

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:

  • Las variables globales . Estos son los más fáciles de implementar, pero el peor diseño. Si usa demasiadas variables globales, rápidamente se encontrará escribiendo módulos que dependen demasiado entre sí ( acoplamiento fuerte ), lo que hace que el flujo de la lógica sea muy difícil de seguir. Las variables globales no son multihilo amigable. Las variables globales hacen que el seguimiento de la vida útil de los objetos sea más difícil y desordenan el espacio de nombres. Sin embargo, son la opción más eficaz, por lo que hay momentos en los que pueden y deben usarse, pero úselos con precaución.
  • 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í:

    class MyClass
    {
    private:
        static MyClass* _instance;
        MyClass() {} //private constructor
    
    public:
        static MyClass* getInstance();
        void method();
    };
    
    ...
    
    MyClass* MyClass::_instance = NULL;
    MyClass* MyClass::getInstance()
    {
        if(_instance == NULL)
            _instance = new MyClass(); //Not thread-safe version
        return _instance;
    
        //Note that _instance is *never* deleted - 
        //it exists for the entire lifetime of the program!
    }
  • 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.

    //Example of dependency injection
    class Tower
    {
    private:
        MissileCreationService* _missileCreator;
        CreepLocatorService* _creepLocator;
    public:
        Tower(MissileCreationService*, CreepLocatorService*);
    }
    
    //In order to create a tower, the creating-class must also have instances of
    // MissileCreationService and CreepLocatorService; thus, if we want to 
    // add a new service to the Tower constructor, we must add it to the
    // constructor of every class which creates a Tower as well!
    //This is not a problem in languages like C# and Java, where you can use
    // a framework to create an instance and inject automatically.

    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 ServiceLocatorclase, 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.


Los comentarios no son para discusión extendida; Esta conversación se ha movido al chat .
Josh

Una de las mejores y más claras respuestas que he leído. Bien hecho. Sin embargo, pensé que siempre se suponía que un servicio debía ser compartido.
Nikos

5

Yo personalmente usaría polimorfismo aquí. ¿Por qué tener un missilevector, un towervector y un creepvector ... cuando todos llaman a la misma función? update? ¿Por qué no tener un vector de punteros a alguna clase base Entityo GameObject?

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 towerpuede enviar un mensaje al map(al que tiene acceso, ¿quizás una referencia a su propietario?) Que golpeó a creep, y mapluego le dice creepque 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í.


1
Su sugerencia sobre el polimorfismo no es realmente relevante. Los tengo almacenados en vectores separados para que pueda iterar sobre cada tipo individualmente, como en el código de dibujo (donde querré dibujar ciertos objetos primero) o en el código de colisión.
Juicy

Para mis propósitos, el mapa posee las entidades, ya que el mapa aquí es análogo a 'nivel'. Consideraré tu idea sobre los mensajes, gracias.
Juicy

1
En un juego, el rendimiento es importante. Entonces, los vectores de los mismos tiempos de objeto tienen una mejor localidad de referencia. Además, los objetos polimórficos con punteros virtuales tienen un rendimiento horrible porque no pueden integrarse en el ciclo de actualización.
Zan Lynx

0

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 .

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.