Eu tenho um jogo de defesa de torre 2D básico em C ++.
Cada mapa é uma classe separada que herda do GameState. O mapa delega a lógica e o código de desenho para cada objeto no jogo e define dados como o caminho do mapa. No pseudo-código, a seção lógica pode se parecer com isso:
update():
for each creep in creeps:
creep.update()
for each tower in towers:
tower.update()
for each missile in missiles:
missile.update()
Os objetos (rastejamentos, torres e mísseis) são armazenados em vetor de ponteiros. As torres devem ter acesso ao vetor de fluência e ao vetor de mísseis para criar novos mísseis e identificar alvos.
A questão é: onde declaro os vetores? Eles devem ser membros da classe Map e passados como argumentos para a função tower.update ()? Ou declarado globalmente? Ou existem outras soluções que me faltam completamente?
Quando várias classes precisam acessar os mesmos dados, onde os dados devem ser declarados?
fonte
Respostas:
Quando você precisa de uma única instância de uma classe em todo o seu programa, chamamos essa classe de serviço . Existem vários métodos padrão de implementação de serviços em programas:
Singletons . Cerca de 10 a 15 anos atrás, os singletons eram o grande padrão de design a se conhecer. No entanto, hoje em dia eles são menosprezados. Eles são muito mais fáceis de multiencadear, mas você deve limitar seu uso a um encadeamento por vez, o que nem sempre é o que você deseja. O rastreamento das vidas úteis é tão difícil quanto às variáveis globais.
Uma classe singleton típica será mais ou menos assim:
Injeção de Dependência (DI) . Isso significa apenas passar o serviço como um parâmetro construtor. Um serviço já deve existir para passá-lo para uma classe; portanto, não há como dois serviços confiarem um no outro; em 98% dos casos, é isso que você deseja (e para os outros 2%, você sempre pode criar um
setWhatever()
método e passar o serviço posteriormente) . Por esse motivo, o DI não tem os mesmos problemas de acoplamento que as outras opções. Ele pode ser usado com multithreading, porque cada encadeamento pode simplesmente ter sua própria instância de cada serviço (e compartilhar apenas aqueles absolutamente necessários). Também torna o código testável por unidade, se você se importa com isso.O problema com a injeção de dependência é que ela ocupa mais memória; agora toda instância de uma classe precisa de referências para todos os serviços que ela usará. Além disso, fica chato de usar quando você tem muitos serviços; existem estruturas que atenuam esse problema em outras linguagens, mas, devido à falta de reflexão do C ++, as estruturas DI no C ++ tendem a ser ainda mais trabalhosas do que apenas manualmente.
Vejo esta página (na documentação do Ninject, uma estrutura C # DI) para outro exemplo.
A injeção de dependência é a solução usual para esse problema e é a resposta que você verá mais altamente votada para perguntas como essa no StackOverflow.com. DI é um tipo de Inversão de Controle (IoC).
Localizador de serviço . Basicamente, apenas uma classe que possui uma instância de todos os serviços. Você pode fazer isso usando reflexão ou simplesmente adicionar uma nova instância a cada vez que desejar criar um novo serviço. Você ainda tem o mesmo problema de antes - Como as classes acessam este localizador? - que pode ser resolvido de qualquer uma das maneiras acima, mas agora você só precisa fazer isso para sua
ServiceLocator
classe, e não para dezenas de serviços. Esse método também é testável por unidade, se você se importa com esse tipo de coisa.Localizadores de serviço são outra forma de Inversão de controle (IoC). Geralmente, estruturas que executam injeção automática de dependência também terão um localizador de serviço.
XNA (estrutura de programação de jogos em C # da Microsoft) inclui um localizador de serviço; Para saber mais, consulte esta resposta .
By the way, IMHO as torres não devem saber sobre os arrepios. A menos que você esteja planejando simplesmente fazer um loop na lista de rastejamentos para cada torre, provavelmente desejará implementar algum particionamento de espaço não trivial ; e esse tipo de lógica não pertence à classe das torres.
fonte
Eu pessoalmente usaria polimorfismo aqui. Por que ter um
missile
vetor, umtower
vetor e umcreep
vetor ... quando todos chamam a mesma função;update
? Por que não ter um vetor de ponteiros para alguma classe baseEntity
ouGameObject
?Acho que uma boa maneira de projetar é pensar 'isso faz sentido em termos de propriedade'? Obviamente, uma torre possui uma maneira de se atualizar, mas um mapa possui todos os objetos nela? Se você for global, você está dizendo que nada é o dono das torres e se arrasta? Global é geralmente uma solução ruim - promove maus padrões de design, mas é muito mais fácil trabalhar com isso. Considere ponderar "eu quero terminar isso?" e 'quero algo que possa reutilizar'?
Uma maneira de contornar isso é alguma forma de sistema de mensagens. O
tower
pode enviar uma mensagem para omap
(ao qual ele tem acesso, talvez uma referência ao seu proprietário?) Que ele bateu em umcreep
e, emmap
seguida, informacreep
que foi atingido. Isso é muito limpo e segrega dados.Outra maneira é apenas pesquisar no próprio mapa o que deseja. No entanto, pode haver problemas com a ordem de atualização aqui.
fonte
Este é um caso em que a estrita programação orientada a objetos (OOP) é interrompida.
De acordo com os princípios da OOP, você deve agrupar dados com comportamento relacionado usando classes. Mas você tem um comportamento (segmentação) que precisa de dados não relacionados entre si (torres e rastejamentos). Nesta situação, muitos programadores tentarão associar o comportamento a parte dos dados de que ele precisa (por exemplo, torres lidam com direcionamento, mas não sabem sobre rastejamentos), mas há outra opção: não agrupe o comportamento com os dados.
Em vez de tornar o comportamento de mira um método da classe de torre, torne-o uma função livre que aceite torres e fluências como argumentos. Isso pode exigir a divulgação de mais membros deixados nas classes torre e fluência, e tudo bem. A ocultação de dados é útil, mas é um meio, não um fim em si, e você não deve ser escravo disso. Além disso, os membros privados não são a única maneira de controlar o acesso aos dados - se os dados não são passados para uma função e não são globais, estão efetivamente ocultos nessa função. Se o uso dessa técnica permitir evitar dados globais, você poderá realmente melhorar o encapsulamento.
Um exemplo extremo dessa abordagem é a arquitetura do sistema da entidade .
fonte