Como posso ter objetos interagindo e se comunicando sem forçar uma hierarquia?

9

Espero que essas divagações tornem minha pergunta clara - eu entenderia totalmente se elas não o fizerem, então, deixe-me saber se é esse o caso e tentarei me esclarecer.

Conheça o BoxPong , um jogo muito simples que criei para me familiarizar com o desenvolvimento de jogos orientados a objetos. Arraste a caixa para controlar a bola e colecionar objetos amarelos.
Fazer o BoxPong me ajudou a formular, entre outras coisas, uma pergunta fundamental: como posso ter objetos que interajam sem precisar "pertencer" um ao outro? Em outras palavras, existe uma maneira de os objetos não serem hierárquicos, mas coexistirem? (Entrarei em mais detalhes abaixo.)

Suspeito que o problema da coexistência de objetos seja comum, então espero que exista uma maneira estabelecida de resolvê-lo. Não quero reinventar a roda quadrada, então acho que a resposta ideal que estou procurando é "aqui está um padrão de design que é comumente usado para resolver seu tipo de problema".

Especialmente em jogos simples como o BoxPong, é claro que há, ou deveria haver, um punhado de objetos coexistindo no mesmo nível. Há uma caixa, há uma bola, há um colecionável. Tudo o que posso expressar em linguagens orientadas a objetos, embora - ou ao que parece - sejam estritas relações HAS-A . Isso é feito através de variáveis-membro. Não posso simplesmente começar balle deixar fazer o que é necessário; preciso que permaneça permanentemente em outro objeto. Eu configurá-lo para que o objeto principal do jogo tem uma caixa, ea caixa por sua vez, tem uma bola, e tem um contador de pontuação. Cada objeto também possui umupdate()método, que calcula posição, direção, etc., e seguirei de maneira semelhante: chamo o método de atualização do objeto principal do jogo, que chama os métodos de atualização de todos os seus filhos, e eles, por sua vez, chamam os métodos de atualização de todos os seus filhos. Esta é a única maneira que eu vejo para criar um jogo orientado a objetos, mas acho que não é o caminho ideal. Afinal, eu não pensaria exatamente na bola como pertencendo à caixa, mas como estando no mesmo nível e interagindo com ela. Suponho que isso possa ser alcançado transformando todos os objetos do jogo em variáveis-membro do objeto principal do jogo, mas não vejo isso resolvendo nada. Quero dizer ... deixando de lado a desordem óbvia, como haveria uma maneira de a bola e a caixa se conhecerem , isto é, interagir?

Há também a questão dos objetos que precisam passar informações entre si. Tenho muita experiência em escrever código para o SNES, onde você tem acesso a praticamente toda a RAM o tempo todo. Digamos que você esteja criando um inimigo personalizado para o Super Mario World e que ele remova todas as moedas de Mario, depois armazene zero para endereçar $ 0DBF, sem problemas. Não há limitações dizendo que os inimigos não podem acessar o status do jogador. Eu acho que fui mimado por essa liberdade, porque com C ++ e coisas semelhantes, muitas vezes me pego imaginando como tornar um valor acessível a alguns outros objetos (ou mesmo globais).
Usando o exemplo do BoxPong, e se eu quisesse que a bola batesse nas bordas da tela? widthe heightsão propriedades da Gameclasse,ballpara ter acesso a eles. Eu poderia passar esses tipos de valores (por meio de construtores ou dos métodos onde eles são necessários), mas isso só me causa más práticas.

Acho que meu principal problema é que preciso que os objetos se conheçam, mas a única maneira de fazer isso é uma hierarquia estrita, o que é feio e impraticável.

Eu ouvi falar de "classes de amigos" em C ++ e meio que sei como elas funcionam, mas se elas são a solução definitiva, então como é que eu não vejo friendpalavras-chave espalhadas por todo projeto C ++ e como é que as conceito não existe em todas as línguas OOP? (O mesmo vale para ponteiros de função, dos quais aprendi recentemente.)

Agradecemos antecipadamente por respostas de qualquer tipo - e, novamente, se houver uma parte que não faça sentido para você, entre em contato.

vvye
fonte
2
Grande parte da indústria de jogos mudou-se para a arquitetura do sistema de componentes de entidades e suas variações. É uma mentalidade diferente das abordagens tradicionais de OO, mas funciona bem e faz sentido quando o conceito entra em cena. O Unity o usa. Na verdade, o Unity usa apenas a parte Entity-Component, mas é baseado no ECS.
Dunk
O problema de permitir que as classes colaborem entre si sem conhecimento um do outro é resolvido pelo padrão de design do Mediador. Você já olhou?
Fuhrmanator

Respostas:

13

Em geral, acontece muito mal se objetos do mesmo nível se conhecerem. Quando os objetos se conhecem, eles são amarrados ou acoplados um ao outro. Isso os torna difíceis de mudar, difíceis de testar, difíceis de manter.

Funciona muito melhor se houver algum objeto "acima" que conheça os dois e possa definir as interações entre eles. O objeto que conhece os dois pares pode conectá-los por injeção de dependência ou por eventos ou por passagem de mensagem (ou qualquer outro mecanismo de desacoplamento). Sim, isso leva a uma hierarquia artificial, mas é muito melhor do que a bagunça de espaguete que você recebe quando as coisas simplesmente interagem à vontade. Isso é apenas mais importante em C ++, pois você precisa de algo para possuir a vida útil dos objetos também.

Então, resumindo, você pode fazer isso apenas tendo objetos lado a lado em todos os lugares ligados pelo acesso ad hoc, mas é uma má idéia. A hierarquia fornece ordem e propriedade clara. O principal a lembrar é que os objetos no código não são necessariamente objetos na vida real (ou mesmo no jogo). Se os objetos no jogo não formarem uma boa hierarquia, uma abstração diferente pode ser melhor.

Telastyn
fonte
2

Usando o exemplo do BoxPong, e se eu quisesse que a bola batesse nas bordas da tela? width e height são propriedades da classe Game, e eu precisaria da bola para ter acesso a elas.

Não!

Eu acho que o principal problema que você está enfrentando é que você está interpretando "Programação Orientada a Objetos" um pouco literalmente demais. No POO, um objeto não representa uma "coisa", mas uma "idéia" que significa que "Bola", "Jogo", "Física", "Matemática", "Data" etc. Todos são objetos válidos. Também não há requisito para os objetos "saberem" de nada. Por exemplo, Date.Now().getTommorrow()pergunte ao computador que dia é hoje, aplique regras de datas misteriosas para descobrir a data de amanhã e devolva-a ao chamador. O Dateobjeto não sabe de mais nada, apenas precisa solicitar informações do sistema, conforme necessário. Além disso, Math.SquareRoot(number)não precisa saber nada além da lógica de como calcular uma raiz quadrada.

Então, no seu exemplo que citei, a "bola" não deve saber nada sobre a "caixa". Caixas e bolas são idéias completamente diferentes e não têm o direito de conversar. Mas um mecanismo de física sabe o que é um Box and Ball (ou pelo menos o ThreeDShape), e sabe onde eles estão e o que deveria estar acontecendo com eles. Portanto, se a bola encolher porque está fria, o Mecanismo de Física diria a ela que ela é menor agora.

É como construir um carro. Um chip de computador não sabe nada sobre um motor de carro, mas um carro pode usar um chip de computador para controlar um motor. A idéia simples de usar coisas pequenas e simples juntas para criar uma coisa um pouco maior e mais complexa, que é reutilizável como componente de outras partes mais complexas.

E no seu exemplo de Mario, e se você estiver em uma sala de desafio onde tocar um inimigo não drena as moedas de Marios, mas apenas o ejeta da sala? É fora do espaço de idéias de Mario ou do inimigo que Mario deve perder moedas ao tocar em um inimigo (de fato, se Mario tiver uma estrela de invulnerabilidade, ele mata o inimigo). Portanto, qualquer objeto (domínio / idéia) responsável pelo que acontece quando mario toca um inimigo é o único que precisa saber sobre um deles e deve fazer o que quer com qualquer um deles (na medida em que esse objeto permita mudanças externas dirigidas )

Além disso, com suas declarações sobre objetos que chamam filhos Update(), isso é extremamente propenso a erros, e se Updatefor chamado várias vezes por quadro de pais diferentes? (mesmo se você perceber isso, isso é tempo de CPU desperdiçado que pode retardar o seu jogo). Todos devem tocar apenas o que precisam, quando precisam. Se você estiver usando o Update (), deverá usar algum tipo de padrão de assinatura para garantir que todas as atualizações sejam chamadas uma vez por quadro (se isso não for tratado para você como no Unity)

Aprender a definir suas idéias de domínio em blocos claros, isolados, bem definidos e fáceis de usar será o fator mais importante para a capacidade de utilizar o OOP.

Tezra
fonte
1

Conheça o BoxPong, um jogo muito simples que criei para me familiarizar com o desenvolvimento de jogos orientados a objetos.

Fazer o BoxPong me ajudou a formular, entre outras coisas, uma pergunta fundamental: como posso ter objetos que interajam sem precisar "pertencer" um ao outro?

Tenho muita experiência em escrever código para o SNES, onde você tem acesso a praticamente toda a RAM o tempo todo. Digamos que você esteja criando um inimigo personalizado para o Super Mario World e que ele remova todas as moedas de Mario, depois armazene zero para endereçar $ 0DBF, sem problemas.

Você parece estar perdendo o objetivo da programação orientada a objetos.

A Programação Orientada a Objetos trata de gerenciar dependências, invertendo seletivamente determinadas dependências-chave em sua arquitetura, para que você possa impedir rigidez, fragilidade e não reutilização.

O que é dependência? Dependência é confiar em outra coisa. Quando você armazena zero para endereçar $ 0DBF, conta com o fato de que esse endereço é onde as moedas de Mario estão localizadas e que as moedas são representadas como um número inteiro. Seu código de inimigo personalizado depende do código que implementa Mario e suas moedas. Se você alterar o local onde Mario armazena suas moedas na memória, atualize manualmente todo o código referente à localização da memória.

O código orientado a objetos tem tudo a ver com tornar seu código dependente de abstrações e não de detalhes. Então, ao invés de

class Mario
{
    public:
        int coins;
}

você escreveria

class Mario
{
    public:
        void LoseCoins();

    private:
        int coins;
}

Agora, se você quiser alterar a maneira como Mario armazena suas moedas de uma int para uma longa ou dupla, ou armazena-as na rede ou em um banco de dados ou inicia outro processo longo, faça a alteração em um único local: o A classe Mario e todos os outros códigos continuam funcionando sem alterações.

Portanto, quando você pergunta

como posso ter objetos que interajam sem precisar "pertencer" um ao outro?

você está realmente perguntando:

como posso ter um código que depende diretamente um do outro sem abstrações?

que não é programação orientada a objetos.

Eu sugiro que você comece lendo tudo aqui: http://objectmentor.com/omSolutions/oops_what.html e depois pesquise no youtube tudo por Robert Martin e assista a tudo.

Minhas respostas derivam dele, e algumas delas são citadas diretamente dele.

Mark Murfin
fonte
Obrigado pela resposta (e a página que você vinculou; parece interessante). Na verdade, eu sei sobre abstração e reutilização, mas acho que não coloquei isso muito bem na minha resposta. No entanto, a partir do código de exemplo que você forneceu, posso ilustrar melhor meu ponto agora! Você está basicamente dizendo que o objeto inimigo não deve mario.coins = 0;, mas mario.loseCoins();, o que é bom e verdadeiro - mas meu argumento é: como o inimigo pode ter acesso ao marioobjeto de qualquer maneira? marioser uma variável membro de enemynão me parece correto.
vvye
Bem, a resposta simples é passar Mario como argumento para uma função no Inimigo. Você pode ter uma função como marioNearby () ou attackMario () que levaria um Mario como argumento. Portanto, sempre que a lógica por trás da interação entre um Inimigo e um Mario for ativada, você chamaria enemy.marioNearby (mario) que chamaria mario.loseCoins (); Mais tarde, você pode decidir que há uma classe de inimigos que fazem com que Mario perca apenas uma moeda ou até ganhe moedas. Agora você tem um lugar para fazer essa alteração que não causa alterações de efeito colateral em outro código.
Mark Murfin
Ao passar Mario para um inimigo, você os acoplou. Mario e Inimigo não deveriam saber que o outro é uma coisa. É por isso que criamos objetos de ordem superior para gerenciar como acoplar os objetos simples.
Tezra 25/01/19
@Tezra Mas então, esses objetos de ordem superior não são reutilizáveis? Parece que esses objetos agem como funções, eles existem apenas para ser o procedimento que exibem.
Steve Chamaillard
@SteveChamaillard Todo programa terá pelo menos um pouco de lógica específica que não faz sentido em nenhum outro programa, mas a idéia é manter essa lógica isolada em algumas classes de alta ordem. Se você tem mario, inimigo e classe de nível, pode reutilizar mario e inimigo em outros jogos. Se você amarrar o inimigo e o mario diretamente um ao outro, qualquer jogo que precise de um também terá que puxar o outro.
Tezra 26/01/19
0

Você pode ativar o acoplamento flexível aplicando o padrão do mediador. A implementação requer que você tenha uma referência a um componente mediador que conheça todos os componentes receptores.

Ele percebe o tipo de "mestre do jogo, por favor, deixe isso e aquilo acontecer".

Um padrão generalizado é o padrão de publicação-assinatura. É apropriado se o mediador não deve conter muita lógica. Caso contrário, use um mediador artesanal que saiba onde encaminhar todas as chamadas e talvez até modificá-las.

Existem variantes síncronas e assíncronas geralmente denominadas barramento de eventos, barramento de mensagens ou fila de mensagens. Procure-os para determinar se eles são apropriados no seu caso particular.

Hero Wanders
fonte