Quando várias classes precisam acessar os mesmos dados, onde os dados devem ser declarados?

39

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?

Suculento
fonte
1
Os membros globais são considerados "feios", mas são rápidos e facilitam o desenvolvimento, se é um jogo pequeno, isso não é problema (IMHO). Você também pode criar uma classe externa que lida com a lógica ( por que as torres precisam desses vetores) e tem acesso a todos os vetores.
Jonathan Connell
-1 se isso estiver relacionado à programação de jogos, também comer pizza também. Agarrar-se alguns livros de design bom software
Maik Semder
9
@Maik: Como o design de software não está relacionado à programação de jogos? Só porque também se aplica a outros campos da programação não o torna fora de tópico.
BlueRaja - Danny Pflughoeft
As listas de padrões de design de software do @BlueRaja são mais adequadas para SO, é para isso que serve. GD.SE é para programação de jogos, não de projeto de software
Maik Semder

Respostas:

53

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:

  • Variáveis ​​globais . Estes são os mais fáceis de implementar, mas o pior design. Se você usar muitas variáveis ​​globais, rapidamente se encontrará escrevendo módulos que dependem um do outro ( acoplamento forte ), dificultando o acompanhamento do fluxo da lógica. Variáveis ​​globais não são compatíveis com multithreading. Variáveis ​​globais dificultam o rastreamento do tempo de vida dos objetos e desorganizam o espaço para nome. Eles são, no entanto, a opção com melhor desempenho; portanto, há momentos em que eles podem e devem ser usados, mas use-os de forma avulsa.
  • 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:

    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!
    }
  • 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.

    //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.

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

BlueRaja - Danny Pflughoeft
fonte
Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo .
Josh
Uma das melhores e mais claras respostas que já li. Bem feito. Eu pensei que um serviço sempre deveria ser compartilhado.
Nikos
5

Eu pessoalmente usaria polimorfismo aqui. Por que ter um missilevetor, um towervetor e um creepvetor ... quando todos chamam a mesma função; update? Por que não ter um vetor de ponteiros para alguma classe base Entityou GameObject?

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 towerpode enviar uma mensagem para o map(ao qual ele tem acesso, talvez uma referência ao seu proprietário?) Que ele bateu em um creepe, em mapseguida, informa creepque 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.

O Pato Comunista
fonte
1
Sua sugestão sobre polimorfismo não é realmente relevante. Eu os tenho armazenados em vetores separados para que eu possa iterar sobre cada tipo individualmente, como no código de desenho (onde desejarei que certos objetos sejam desenhados primeiro) ou no código de colisão.
Juicy
Para meus propósitos, o mapa possui as entidades, pois o mapa aqui é análogo a 'level'. Vou considerar sua idéia sobre mensagens, obrigado.
Juicy
1
Em um jogo, o desempenho é importante. Portanto, vetores dos mesmos tempos de objeto têm melhor localidade de referência. Além disso, objetos polimórficos com ponteiros virtuais têm um desempenho terrível porque não podem ser incorporados no loop de atualização.
Zan Lynx
0

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 .

Steve S
fonte