¿Cómo uso correctamente los singletons en la programación del motor C ++?

16

Sé que los singletons son malos, mi viejo motor de juego usaba un objeto singleton 'Game' que maneja todo, desde mantener todos los datos hasta el bucle real del juego. Ahora estoy haciendo uno nuevo.

El problema es que, para dibujar algo en SFML, se usa window.draw(sprite)donde window es an sf::RenderWindow. Hay 2 opciones que veo aquí:

  1. Crea un objeto Singleton Game que cada entidad del juego recupere (lo que usé antes)
  2. Haga de este el constructor de entidades: Entity(x, y, window, view, ...etc)(esto es simplemente ridículo y molesto)

¿Cuál sería la forma correcta de hacer esto mientras se mantiene el constructor de una entidad solo para x e y?

Podría intentar hacer un seguimiento de todo lo que hago en el bucle principal del juego, y dibujar manualmente su sprite en el bucle del juego, pero eso también parece desordenado, y también quiero un control total absoluto sobre una función de dibujo completa para la entidad.

Acumulador
fuente
1
Puede pasar la ventana como argumento de la función 'render'.
dari
25
¡Los singletons no son malos! pueden ser útiles y a veces necesarios (por supuesto, es discutible).
ExOfDe
3
Siéntase libre de reemplazar singletons con globals simples No tiene sentido crear recursos requeridos a nivel mundial "a pedido", no tiene sentido pasarlos. Sin embargo, para las entidades, puede usar una clase de "nivel" para mantener ciertas cosas que son relevantes para todas ellas.
serpiente5
Declaro mi ventana y otras dependencias en mi main, y luego tengo punteros en mis otras clases.
KaareZ
1
@JAB Fácilmente arreglado con inicialización manual desde main (). La inicialización diferida hace que suceda en un momento desconocido, lo cual no es una buena idea para los sistemas centrales.
serpiente5

Respuestas:

3

Solo almacene los datos necesarios para representar el sprite dentro de cada entidad, luego recupérelos de la entidad y páselos a la ventana para su representación. No es necesario almacenar ninguna ventana o ver datos dentro de las entidades.

Podría tener una clase de Juego o Motor de nivel superior que tenga un Nivel de clase (tiene todas las entidades que se utilizan actualmente), y un Procesador de clase (contiene la ventana, ver y cualquier otra cosa para la representación).

Entonces el ciclo de actualización del juego en tu clase de nivel superior podría verse así:

EntityList entities = mCurrentLevel.getEntities();
for(auto& i : entities){
  // Run game logic...
  i->update(...);
}
// Render all the entities
for(auto& i : entities){
  mRenderer->draw(i->getSprite());
}
Mago azul
fuente
3
No hay nada ideal en un singleton. ¿Por qué hacer públicos los aspectos internos de implementación cuando no es necesario? ¿Por qué escribir en Logger::getInstance().Log(...)lugar de solo Log(...)? ¿Por qué inicializar la clase al azar cuando se le pregunta si puede hacerlo manualmente solo una vez? Una función global que hace referencia a globales estáticos es mucho más simple de crear y usar.
serpiente5
@ snake5 Justificar singletons en Stack Exchange es como simpatizar con Hitler.
Willy Goat
30

El enfoque simple es simplemente hacer lo que solía ser Singleton<T> global en su Tlugar. Los globales también tienen problemas, pero no representan un montón de trabajo extra y código repetitivo para imponer una restricción trivial. Esta es básicamente la única solución que no implicará (potencialmente) tocar el constructor de la entidad.

El enfoque más difícil, pero posiblemente mejor, es pasar sus dependencias a donde las necesita . Sí, esto podría implicar pasar Window *a un grupo de objetos (como su entidad) de una manera que parezca asquerosa. El hecho de que parezca asqueroso debería decirle algo: su diseño puede ser asqueroso.

La razón por la que esto es más difícil (más allá de involucrar más tipeo) es que esto a menudo conduce a la refactorización de sus interfaces para que lo que "necesita" pasar sea necesario por menos clases de nivel de hoja. Esto hace que gran parte de la fealdad inherente al pasar su renderizador a todo desaparezca, y también mejora la capacidad de mantenimiento general de su código al reducir la cantidad de dependencias y el acoplamiento, la medida en que lo hizo muy obvio al tomar las dependencias como parámetros . Cuando las dependencias eran simples o globales, era menos obvio cuán interconectados estaban sus sistemas.

Pero es potencialmente un importante empresa . Hacerlo en un sistema después del hecho puede ser francamente doloroso. Puede ser mucho más pragmático para ti simplemente dejar tu sistema solo, con el singleton, por ahora (especialmente si estás intentando enviar un juego que de lo contrario funciona bien; a los jugadores generalmente no les importará si tienes un singleton o cuatro allí).

Si desea intentar hacer esto con su diseño existente, es posible que deba publicar muchos más detalles sobre su implementación actual, ya que en realidad no hay una lista de verificación general para realizar estos cambios. O ven a discutirlo en el chat .

Por lo que ha publicado, creo que un gran paso en la dirección "no singleton" sería evitar la necesidad de que sus entidades tengan acceso a la ventana o vista. Sugiere que se dibujan a sí mismos, y no es necesario que las entidades se dibujen a sí mismas . Puede adoptar una metodología donde las entidades solo contienen la información que permitiría mismas para que sean dibujadas por algún sistema externo (que tiene las referencias de ventana y vista). La entidad simplemente expone su posición y el sprite que debe usar (o algún tipo de referencia a dicho sprite, si desea almacenar en caché los sprites reales en el renderizador para evitar tener instancias duplicadas). Simplemente se le dice al renderizador que dibuje una lista particular de entidades, a través de las cuales recorre, lee los datos y usa su objeto de ventana interno para llamar drawcon el sprite buscado para la entidad.

Comunidad
fuente
3
No estoy familiarizado con C ++, pero ¿no hay marcos de inyección de dependencia cómodos para este lenguaje?
bgusach
1
No describiría ninguno de ellos como "cómodo", y no los encuentro particularmente útiles en general, pero otros pueden tener una experiencia diferente con ellos, por lo que es un buen punto mencionarlos.
1
El método que describe como hacer que las entidades no las dibujen a sí mismas sino que contengan la información y que un solo sistema maneje el dibujo de todas las entidades se usa mucho en los motores de juegos más populares hoy en día.
Patrick W. McMahon
1
+1 para "El hecho de que parezca asqueroso debería decirte algo: tu diseño podría ser asqueroso".
Shadow503
+1 por dar tanto el caso ideal como la respuesta pragmática.
6

Heredar de sf :: RenderWindow

SFML realmente lo alienta a heredar de sus clases.

class GameWindow: public sf::RenderWindow{};

Desde aquí, puede crear funciones de dibujo de miembros para entidades de dibujo.

class GameWindow: public sf::RenderWindow{
public:
 void draw(const Entity& entity);
};

Ahora puedes hacer esto:

GameWindow window;
Entity entity;

window.draw(entity);

Incluso puede llevar esto un paso más allá si sus Entidades van a mantener sus propios sprites únicos haciendo que Entity herede de sf :: Sprite.

class Entity: public sf::Sprite{};

Ahora sf::RenderWindowsolo puede dibujar Entidades, y las entidades ahora tienen funciones como setTexture()y setColor(). La Entidad incluso podría usar la posición del sprite como su propia posición, permitiéndole usar la setPosition()función tanto para mover la Entidad Y su sprite.


Al final , es bastante bueno si solo tienes:

window.draw(game);

A continuación se presentan algunas implementaciones de ejemplo rápido

class GameWindow: public sf::RenderWindow{
 sf::Sprite entitySprite; //assuming your Entities don't need unique sprites.
public:
 void draw(const Entity& entity){
  entitySprite.setPosition(entity.getPosition());
  sf::RenderWindow::draw(entitySprite);
 }
};

O

class GameWindow: public sf::RenderWindow{
public:
 void draw(const Entity& entity){
  sf::RenderWindow::draw(entity.getSprite()); //assuming Entities hold their own sprite.
 }
};
Cabra Willy
fuente
3

Evitas los singletons en el desarrollo de juegos de la misma manera que los evitas en cualquier otro tipo de desarrollo de software: pasas las dependencias .

Con eso fuera del camino, se puede elegir para pasar las dependencias directamente como tipos desnudos (como int, Window*, etc.) o se puede optar por pasar en uno o más tipos de encargo envoltorio (como EntityInitializationOptions).

La primera forma puede ser molesta (como descubrió), mientras que la segunda le permitirá pasar todo en un objeto y modificar los campos (e incluso especializar el tipo de opciones) sin tener que cambiar y cambiar cada constructor de entidades. Creo que la última forma es mejor.

TC
fuente
3

Los singletons no son malos. En cambio, son fáciles de abusar. Por otro lado, los globales son aún más fáciles de abusar y tienen muchos más problemas.

La única razón válida para reemplazar un singleton con un global es pacificar a los odiadores religiosos singleton.

El problema es tener un diseño que incluye clases de las cuales solo existe una instancia global y que debe ser accesible desde cualquier lugar. Esto se rompe tan pronto como terminas teniendo múltiples instancias del singleton, por ejemplo, en un juego cuando implementas una pantalla dividida, o en una aplicación empresarial suficientemente grande cuando notas que un solo registrador no siempre es una gran idea después de todo .

En pocas palabras, si realmente tiene una clase donde tiene una única instancia global que no puede pasar razonablemente por referencia , singleton es a menudo una de las mejores soluciones en un conjunto de soluciones subóptimas.

Peter - Unban Robert Harvey
fuente
1
Soy un enemigo de la religión religiosa y tampoco considero una solución global. : S
Dan Pantry
1

Inyectar dependencias. Una ventaja de hacerlo es que ahora puede crear varios tipos de estas dependencias a través de una fábrica. Desafortunadamente, extraer singletons de una clase que los usa es como tirar de un gato por las patas traseras a través de una alfombra. Pero si los inyecta, puede intercambiar implementaciones, tal vez sobre la marcha.

RenderSystem(IWindow* window);

Ahora puede inyectar varios tipos de ventanas. Esto le permite escribir pruebas contra el RenderSystem con varios tipos de ventanas para que pueda ver cómo su RenderSystem se romperá o funcionará. Esto no es posible, o más difícil, si usa singletons directamente dentro de "RenderSystem".

Ahora es más comprobable, modular y también está desacoplado de una implementación específica. Solo depende de una interfaz, no de una implementación concreta.

Todd
fuente