Estou trabalhando em um jogo 2D onde você pode mover para cima, baixo, esquerda e direita. Eu tenho essencialmente dois objetos de lógica de jogo:
- Jogador: Tem uma posição em relação ao mundo
- Mundo: desenha o mapa e o jogador
Até agora, o mundo depende do jogador (ou seja, tem uma referência a ele), precisando de sua posição para descobrir onde desenhar o personagem do jogador e qual parte do mapa desenhar.
Agora, quero adicionar a detecção de colisão para tornar impossível o jogador se mover através das paredes.
A maneira mais simples de pensar é pedir ao jogador que pergunte ao mundo se o movimento pretendido é possível. Mas isso introduziria uma dependência circular entre o Player e o World (ou seja, cada um mantém uma referência ao outro), o que parece evitar. A única maneira de inventar é fazer o mundo mover o jogador , mas acho isso pouco intuitivo.
Qual é minha melhor opção? Ou evitar uma dependência circular não vale a pena?
Respostas:
O mundo não deve se desenhar; o renderizador deve desenhar o mundo. O jogador não deve se desenhar; o renderizador deve desenhar o jogador em relação ao mundo.
O jogador deve perguntar ao mundo sobre detecção de colisão; ou talvez as colisões devam ser tratadas por uma classe separada que verifique a detecção de colisões não apenas contra o mundo estático, mas também contra outros atores.
Eu acho que o mundo provavelmente não deveria estar ciente do jogador; deve ser um primitivo de baixo nível, não um objeto divino. O Player provavelmente precisará invocar alguns métodos do Mundo, talvez indiretamente (detecção de colisão ou verificação de objetos interativos, etc.).
fonte
Renderer
é necessário algum tipo, mas isso não significa que a lógica de como cada coisa é renderizada é tratada peloRenderer
, cada coisa que precisa ser desenhada provavelmente deve herdar de uma interface comum comoIDrawable
ouIRenderable
(ou interface equivalente em qualquer idioma que você esteja usando). O mundo poderia serRenderer
, suponho, mas isso parece estar ultrapassando sua responsabilidade, especialmente se já fosse umIRenderable
.Aqui está como um mecanismo de renderização típico lida com essas coisas:
Há uma distinção fundamental entre onde um objeto está no espaço e como ele é desenhado.
Desenhando um objeto
Você normalmente tem uma classe Renderer que faz isso. Simplesmente pega um objeto (Modelo) e desenha na tela. Pode ter métodos como drawSprite (Sprite), drawLine (..), drawModel (Model), o que você quiser. É um renderizador, portanto, ele deve fazer todas essas coisas. Ele também usa qualquer API existente abaixo, para que você possa ter, por exemplo, um renderizador que usa OpenGL e outro que usa DirectX. Se você deseja portar seu jogo para outra plataforma, basta escrever um novo renderizador e usá-lo. É tão fácil.
Movendo um objeto
Cada objeto é anexado a algo que gostamos de chamar de SceneNode . Você consegue isso através da composição. Um SceneNode contém um objeto. É isso aí. O que é um SceneNode? É uma classe simples que contém todas as transformações (posição, rotação, escala) de um objeto (geralmente relativas a outro SceneNode) junto com o objeto real.
Gerenciando os objetos
Como os SceneNodes são gerenciados? Através de um SceneManager . Esta classe cria e acompanha todos os SceneNode da sua cena. Você pode solicitar um SceneNode específico (geralmente identificado por um nome de sequência como "Player" ou "Table") ou uma lista de todos os nós.
Desenhando o mundo
Isso deve estar bem óbvio agora. Basta percorrer todos os SceneNode da cena e fazer com que o Renderer o desenhe no lugar certo. Você pode desenhá-lo no lugar certo, solicitando que o renderizador armazene as transformações de um objeto antes de renderizá-lo.
Detecção de colisão
Isso nem sempre é trivial. Geralmente, você pode consultar a cena sobre qual objeto está em um determinado ponto no espaço ou quais objetos um raio cruzará. Dessa forma, você pode criar um raio do seu jogador na direção do movimento e perguntar ao gerente de cena qual é o primeiro objeto que o raio cruza. Você pode então escolher mover o jogador para a nova posição, movê-lo em uma quantidade menor (para aproximá-lo do objeto em colisão) ou não movê-lo. Certifique-se de que essas consultas sejam tratadas por classes separadas. Eles devem solicitar ao SceneManager uma lista de SceneNodes, mas é outra tarefa determinar se esse SceneNode cobre um ponto no espaço ou se cruza com um raio. Lembre-se de que o SceneManager apenas cria e armazena nós.
Então, qual é o jogador e o que é o mundo?
O Player pode ser uma classe que contém um SceneNode, que por sua vez contém o modelo a ser renderizado. Você move o player alterando a posição do nó da cena. O mundo é simplesmente uma instância do SceneManager. Ele contém todos os objetos (por meio de SceneNodes). Você lida com a detecção de colisão fazendo consultas no estado atual da cena.
Isso está longe de ser uma descrição completa ou precisa do que acontece na maioria dos mecanismos, mas deve ajudar a entender os fundamentos e por que é importante respeitar os princípios de POO sublinhados pelo SOLID . Não se resigne à idéia de que é muito difícil reestruturar seu código ou que isso realmente não vai ajudá-lo. Você ganhará muito mais no futuro projetando cuidadosamente seu código.
fonte
Por que você quer evitar isso? Dependências circulares devem ser evitadas se você deseja criar uma classe reutilizável. Mas o jogador não é uma classe que precisa ser reutilizável. Você gostaria de usar o Player sem um mundo? Provavelmente não.
Lembre-se de que as classes nada mais são do que coleções de funcionalidades. A questão é como dividir a funcionalidade. Faça o que for necessário. Se você precisar de uma decadência circular, que assim seja. (O mesmo vale para todos os recursos de OOP, a propósito. Codifique as coisas de uma maneira que sirva a um propósito, não siga apenas paradigmas às cegas.)
Editar
Ok, para responder à pergunta: você pode evitar que o jogador precise conhecer o mundo para verificações de colisão usando retornos de chamada:
O tipo de física que você descreveu na pergunta pode ser tratado pelo mundo se você expor a velocidade das entidades:
No entanto, note que você provavelmente precisará de uma dependência do mundo, mais cedo ou mais tarde, ou seja, sempre que precisar da funcionalidade do mundo: deseja saber onde está o inimigo mais próximo? Você quer saber a que distância está a próxima borda? Dependência é.
fonte
render(World)
. O debate é sobre se todo o código deve estar dentro de uma classe ou se o código deve ser dividido em unidades lógicas e funcionais, que são mais fáceis de manter, estender e gerenciar. BTW, boa sorte em reutilizar esses gerenciadores de componentes, mecanismos de física e gerenciadores de entrada, todos inteligentemente indiferenciados e completamente acoplados.Seu design atual parece ir contra o primeiro princípio do design SOLID .
Esse primeiro princípio, chamado de "princípio de responsabilidade única", geralmente é uma boa diretriz a seguir, a fim de não criar objetos monolíticos e práticos, que sempre prejudicarão seu design.
Para concretizar, seu
World
objeto é responsável por atualizar e manter o estado do jogo e por desenhar tudo.E se o seu código de renderização mudar / precisar mudar? Por que você deveria atualizar as duas classes que realmente não têm nada a ver com a renderização? Como o Liosan já disse, você deve ter um
Renderer
.Agora, para responder sua pergunta real ...
Existem muitas maneiras de fazer isso, e essa é apenas uma maneira de dissociar:
Object
s na qual o jogador está localizado, mas não depende da classe do jogador (use herança para conseguir isso).InputManager
.Renderer
desenha todos os objetos.fonte
health
que somente esta instânciaPlayer
possui).O jogador deve perguntar ao mundo sobre coisas como detecção de colisão. A maneira de evitar a dependência circular é não ter o mundo dependente do Player. O mundo precisa saber para onde está se desenhando: você provavelmente quer isso mais distante, talvez com uma referência a um objeto Camera que, por sua vez, possa conter uma referência a alguma entidade a ser rastreada.
O que você deseja evitar em termos de referências circulares não é tanto manter referências um ao outro, mas sim se referir um ao outro explicitamente no código.
fonte
Sempre que dois tipos diferentes de objetos podem se perguntar. Eles dependerão um do outro, pois precisam manter uma referência ao outro para chamar seus métodos.
Você pode evitar a dependência circular fazendo o Mundo perguntar ao Jogador, mas o Jogador não pode perguntar ao Mundo, ou vice-versa. Desta forma, o mundo tem referências aos jogadores, mas os jogadores não precisam de referência ao mundo. Ou vice-versa. Mas isso não resolverá o problema, porque o Mundo precisaria perguntar aos jogadores se eles têm algo a perguntar e dizer na próxima ligação ...
Portanto, você não pode realmente solucionar esse "problema" e acho que não há necessidade de se preocupar com isso. Mantenha o design estúpido e simples o máximo que puder.
fonte
Retirando os detalhes sobre jogador e mundo, você tem um caso simples de não querer introduzir uma dependência circular entre dois objetos (que dependendo do seu idioma pode nem importar, veja o link no comentário do Fuhrmanator). Existem pelo menos duas soluções estruturais muito simples que se aplicariam a este e a problemas semelhantes:
1) Introduza o padrão singleton em sua classe mundial . Isso permitirá que o jogador (e qualquer outro objeto) encontre facilmente o objeto mundial sem pesquisas caras ou links permanentemente retidos. A essência desse padrão é apenas que a classe tem uma referência estática à única instância dessa classe, que é definida na instanciação do objeto e limpa na exclusão.
Dependendo da sua linguagem de desenvolvimento e da complexidade que você deseja, você pode facilmente implementá-la como uma superclasse ou interface e reutilizá-la para muitas classes principais das quais você não espera ter mais de uma no seu projeto.
2) Se o idioma em que você está desenvolvendo o suportar (muitos o fazem), use uma referência fraca . Esta é uma referência que não afeta coisas como coleta de lixo. É útil exatamente nesses casos, mas não faça suposições sobre se o objeto que você está referenciando fraco ainda existe.
No seu caso particular, o (s) seu (s) jogador (es) pode (m) conter uma referência fraca ao mundo. O benefício disso (como no singleton) é que você não precisa procurar o objeto mundial de alguma forma em cada quadro ou ter uma referência permanente que dificultará os processos afetados por referências circulares, como coleta de lixo.
fonte
Como os outros disseram, acho que seu
World
está fazendo uma coisa demais: ele está tentando ambos contêm o jogoMap
(que deve ser uma entidade distinta) e ser umRenderer
ao mesmo tempo.Portanto, crie um novo objeto (chamado
GameMap
, possivelmente) e armazene os dados no nível do mapa. Escreva funções nele que interagem com o mapa atual.Então você também precisa de um
Renderer
objeto. Você pode fazer desseRenderer
objeto a coisa que contémGameMap
ePlayer
(assim comoEnemies
) e também os desenha.fonte
Você pode evitar dependências circulares não adicionando as variáveis como membros. Use uma função estática CurrentWorld () para o player ou algo parecido. Porém, não invente uma interface diferente daquela implementada no World, isso é completamente desnecessário.
Também é possível destruir a referência antes / durante a destruição do objeto player para efetivamente parar os problemas causados por referências circulares.
fonte