Como uso corretamente singletons na programação do mecanismo C ++?

16

Eu sei que singletons são ruins, meu antigo mecanismo de jogo usava um objeto 'Game' singleton que lida com tudo, desde manter todos os dados até o loop do jogo real. Agora estou fazendo um novo.

O problema é que, para desenhar algo no SFML, você usa window.draw(sprite)onde window é um sf::RenderWindow. Existem 2 opções que vejo aqui:

  1. Faça um objeto de jogo único que todas as entidades do jogo recuperem (o que eu usei antes)
  2. Faça disso o construtor de entidades: Entity(x, y, window, view, ...etc)(isso é ridículo e irritante)

Qual seria a maneira correta de fazer isso, mantendo o construtor de uma Entidade apenas x e y?

Eu poderia tentar acompanhar tudo o que faço no loop principal do jogo e desenhar manualmente o sprite no loop do jogo, mas isso também parece confuso e também quero controle total absoluto sobre uma função de draw inteira da entidade.

Acumulador
fonte
1
Você pode passar a janela como um argumento da função 'render'.
dari
25
Singletons não são ruins! eles podem ser úteis e às vezes necessários (é claro que é discutível).
ExOfDe 3/15/15
3
Sinta-se livre para substituir singletons por globais simples. Não há sentido em criar recursos globalmente necessários "sob demanda", não há sentido em distribuí-los. No entanto, para entidades, você pode usar uma classe "level" para armazenar certas coisas que são relevantes para todas elas.
usar o seguinte código
Declaro minha janela e outras dependências em meu main e, em seguida, tenho ponteiros em minhas outras classes.
precisa saber é
1
@JAB Facilmente corrigido com inicialização manual de main (). A inicialização lenta faz com que isso aconteça em um momento desconhecido, o que não é uma boa ideia para os sistemas principais.
precisa saber é o seguinte

Respostas:

3

Armazene apenas os dados necessários para renderizar o sprite dentro de cada entidade, recupere-o da entidade e passe-o para a janela para renderização. Não há necessidade de armazenar nenhuma janela ou visualizar dados dentro de entidades.

Você pode ter uma classe Game ou Engine de nível superior que contém uma classe Level (contém todas as entidades atualmente sendo usadas) e uma classe Renderer (contém a janela, a visualização e qualquer outra coisa para renderizar).

Portanto, o loop de atualização do jogo em sua classe de nível superior pode ser semelhante a:

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());
}
Assistente azul
fonte
3
Não há nada ideal em um singleton. Por que tornar públicas as implementações públicas quando você não precisa? Por que escrever em Logger::getInstance().Log(...)vez de apenas Log(...)? Por que inicializar a classe aleatoriamente quando perguntado se você pode fazê-lo manualmente apenas uma vez? Uma função global que faz referência a globais estáticos é muito mais simples de criar e usar.
precisa saber é o seguinte
@ snake5 Justificar singletons no Stack Exchange é como simpatizar com Hitler.
Willy Goat
30

A abordagem simples é criar o que antes era Singleton<T>global T. Os Globals também têm problemas, mas eles não representam um monte de código extra de trabalho e clichê para impor uma restrição trivial. Essa é basicamente a única solução que não envolve (potencialmente) tocar no construtor da entidade.

A abordagem mais difícil, mas possivelmente melhor, é passar suas dependências para onde você precisar . Sim, isso pode envolver a passagem de Window *para um monte de objetos (como sua entidade) de uma maneira que parece grosseira. O fato de parecer grosseiro deve lhe dizer uma coisa: seu design pode ser grosseiro.

A razão pela qual isso é mais difícil (além de envolver mais digitação) é que isso geralmente leva à refatoração de suas interfaces, para que o que você "precisa" passar seja necessário por menos classes no nível da folha. Isso faz com que muita feiura seja inerente ao passar seu renderizador para que tudo desapareça e também melhora a manutenção geral do seu código, reduzindo a quantidade de dependências e acoplamentos, cuja extensão você ficou muito óbvia ao considerar as dependências como parâmetros. . Quando as dependências eram singletons ou globais, era menos óbvio a interconexão de seus sistemas.

Mas é potencialmente uma grande empresa. Fazer isso em um sistema após o fato pode ser absolutamente doloroso. Pode ser muito mais pragmático para você simplesmente deixar seu sistema em paz, com o singleton, por enquanto (especialmente se você estiver tentando realmente lançar um jogo que, de outra forma, funciona muito bem; os jogadores geralmente não vão se importar se você tiver um singleton ou quatro lá).

Se você quiser tentar fazer isso com o design existente, talvez seja necessário postar muito mais detalhes sobre sua implementação atual, pois não há realmente uma lista de verificação geral para fazer essas alterações. Ou venha discuti-lo no chat .

Pelo que você postou, acho que um grande passo na direção "sem singleton" seria evitar a necessidade de suas entidades terem acesso à janela ou exibição. Isso sugere que eles se desenham, e você não precisa que as entidades se desenhem . Você pode adotar uma metodologia em que as entidades contenham apenas as informações que permitiriameles devem ser desenhados por algum sistema externo (que possui as referências de janela e exibição). A entidade apenas expõe sua posição e o sprite que deve usar (ou algum tipo de referência ao referido sprite, se você deseja armazenar em cache os sprites reais no próprio renderizador para evitar instâncias duplicadas). O renderizador é simplesmente instruído a desenhar uma lista específica de entidades, através da qual ele percorre, lê os dados e usa seu objeto de janela mantido internamente para chamar drawcom o sprite procurado pela entidade.

Comunidade
fonte
3
Não estou familiarizado com C ++, mas não existem estruturas de injeção de dependência confortáveis ​​para essa linguagem?
bgusach
1
Eu não descreveria nenhum deles como "confortável" e não os considero particularmente úteis em geral, mas outros podem ter uma experiência diferente com eles, por isso é um bom ponto para trazê-los à tona.
1
O método que ele descreve como fazê-lo para que as entidades não se identifiquem, mas mantêm as informações e um único sistema lida com o desenho de todas as entidades é muito usado nos mecanismos de jogo mais populares atualmente.
Patrick W. McMahon
1
+1 em "O fato de parecer grosseiro deve dizer uma coisa: seu design pode ser grosseiro".
precisa saber é o seguinte
+1 por fornecer o caso ideal e a resposta pragmática.
6

Herdar de sf :: RenderWindow

Na verdade, o SFML incentiva você a herdar de suas classes.

class GameWindow: public sf::RenderWindow{};

A partir daqui, você cria funções de desenho de membro para entidades de desenho.

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

Agora você pode fazer isso:

GameWindow window;
Entity entity;

window.draw(entity);

Você pode dar um passo adiante ainda mais se suas Entidades tiverem seus próprios sprites exclusivos, tornando a Entidade herdada de sf :: Sprite.

class Entity: public sf::Sprite{};

Agora sf::RenderWindowpode apenas desenhar entidades, e entidades agora têm funções como setTexture()esetColor() . A Entidade pode até usar a posição do sprite como sua própria posição, permitindo que você use a setPosition()função para mover a Entidade E seu sprite.


No final , é muito bom se você tiver:

window.draw(game);

Abaixo estão alguns exemplos de implementações rápidas

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);
 }
};

OU

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

Você evita singletons no desenvolvimento de jogos da mesma maneira que evita-os em todos os outros tipos de desenvolvimento de software: você passa as dependências .

Com isso fora do caminho, você pode optar por passar as dependências diretamente como tipos simples (como int,Window* , etc) ou você pode optar por passá-las em um ou mais personalizado invólucro tipos (comoEntityInitializationOptions ).

A primeira maneira pode ser irritante (como você descobriu), enquanto a última permite que você passe tudo em um objeto e modifique os campos (e até especialize o tipo de opções) sem sair do lugar e alterar todos os construtores de entidades. Eu acho que o último caminho é melhor.

TC
fonte
3

Singletons não são ruins. Em vez disso, são fáceis de abusar. Por outro lado, os globais são ainda mais fáceis de abusar e têm muito mais problemas.

A única razão válida para substituir um singleton por um global é pacificar os inimigos religiosos de singleton.

O problema é ter um design que inclua classes das quais apenas uma única instância global exista e que precise ser acessível de qualquer lugar. Isso se desfaz assim que você acaba tendo várias instâncias do singleton, por exemplo, em um jogo ao implementar a tela dividida ou em um aplicativo corporativo suficientemente grande quando você percebe que um único criador de logs nem sempre é uma ótima idéia. .

Resumindo, se você realmente tem uma classe em que possui uma única instância global que não pode ser razoavelmente transmitida por referência , o singleton costuma ser uma das melhores soluções em um conjunto de soluções abaixo do ideal.

Peter - Unban Robert Harvey
fonte
1
Sou um odiador religioso e não considero uma solução global também. : S
Dan Pantry
1

Injetar dependências. Um benefício de fazer isso agora é que você pode criar vários tipos dessas dependências por meio de uma fábrica. Infelizmente, arrancar singletons de uma classe que os usa é como puxar um gato pelas patas traseiras pelo tapete. Mas se você os injetar, poderá trocar as implementações, talvez em tempo real.

RenderSystem(IWindow* window);

Agora você pode injetar vários tipos de janelas. Isso permite que você escreva testes no RenderSystem com vários tipos de janelas, para que você possa ver como o RenderSystem irá quebrar ou executar. Isso não é possível, ou mais difícil, se você usar singletons diretamente dentro do "RenderSystem".

Agora é mais testável, modular e também é dissociado de uma implementação específica. Depende apenas de uma interface, não de uma implementação concreta.

Todd
fonte