Acho difícil encontrar uma maneira de organizar os objetos do jogo para que sejam polimórficos, mas ao mesmo tempo não polimórficos.
Aqui está um exemplo: supondo que queremos todos os nossos objetos update()
e draw()
. Para fazer isso, precisamos definir uma classe base GameObject
que tenha esses dois métodos puros virtuais e permita que o polimorfismo entre em ação:
class World {
private:
std::vector<GameObject*> objects;
public:
// ...
update() {
for (auto& o : objects) o->update();
for (auto& o : objects) o->draw(window);
}
};
O método update deve cuidar de qualquer estado que o objeto de classe específico precise atualizar. O fato é que cada objeto precisa conhecer o mundo ao seu redor. Por exemplo:
- Uma mina precisa saber se alguém está colidindo com ela
- Um soldado deve saber se o soldado de outra equipe está próximo
- Um zumbi deve saber onde está o cérebro mais próximo, dentro de um raio.
Para interações passivas (como a primeira), eu estava pensando que a detecção de colisão poderia delegar o que fazer em casos específicos de colisões no próprio objeto com a on_collide(GameObject*)
.
A maioria das outras informações (como os outros dois exemplos) poderia ser consultada apenas pelo mundo do jogo passado para o update
método. Agora o mundo não distingue objetos com base em seu tipo (ele armazena todos os objetos em um único contêiner polimórfico); portanto, o que de fato ele retornará com um ideal world.entities_in(center, radius)
é um contêiner de GameObject*
. Mas é claro que o soldado não quer atacar outros soldados de sua equipe e um zumbi não se importa com outros zumbis. Então, precisamos distinguir o comportamento. Uma solução pode ser a seguinte:
void TeamASoldier::update(const World& world) {
auto list = world.entities_in(position, eye_sight);
for (const auto& e : list)
if (auto enemy = dynamic_cast<TeamBSoldier*>(e))
// shoot towards enemy
}
void Zombie::update(const World& world) {
auto list = world.entities_in(position, eye_sight);
for (const auto& e : list)
if (auto enemy = dynamic_cast<Human*>(e))
// go and eat brain
}
mas é claro que o número de dynamic_cast<>
por quadro pode ser terrivelmente alto, e todos sabemos o quão lento dynamic_cast
pode ser. O mesmo problema também se aplica ao on_collide(GameObject*)
delegado que discutimos anteriormente.
Então, qual é a maneira ideal de organizar o código para que os objetos possam conhecer outros objetos e poder ignorá-los ou executar ações com base em seu tipo?
fonte
dynamic_cast<Human*>
, implemente algo como abool GameObject::IsHuman()
, que retornafalse
por padrão, mas é substituído para retornartrue
naHuman
classe.IsA
substituições personalizadas provou ser apenas marginalmente melhor do que a transmissão dinâmica na prática para mim. A melhor coisa a fazer é que o usuário tenha, sempre que possível, listas de dados classificadas em vez de iterar cegamente por todo o conjunto de entidades.TeamASoldier
eTeamBSoldier
é realmente idêntica - disparada contra qualquer pessoa do outro time. Tudo o que precisa de outras entidades é umGetTeam()
método mais específico e, pelo exemplo de congusbongus, que pode ser abstraído ainda mais em umIsEnemyOf(this)
tipo de interface. O código não precisa se preocupar com classificações taxonômicas de soldados, zumbis, jogadores, etc. Concentre-se na interação, não nos tipos.Respostas:
Em vez de implementar a tomada de decisão de cada entidade em si mesma, você pode alternativamente optar pelo padrão do controlador. Você teria classes de controlador central que estão cientes de todos os objetos (que são importantes para eles) e controlam seu comportamento.
Um MovementController lidaria com o movimento de todos os objetos que podem se mover (faça a localização da rota, atualize as posições com base nos vetores de movimento atuais).
Um MineBehaviorController verifica todas as minas e todos os soldados e ordena que uma mina exploda quando um soldado se aproxima demais.
Um ZombieBehaviorController verificaria todos os zumbis e soldados nas proximidades, escolheria o melhor alvo para cada zumbi e ordenaria que ele se movesse para lá e o atacasse (o movimento em si é tratado pelo MovementController).
Um SoldierBehaviorController analisaria toda a situação e, em seguida, apresentaria instruções táticas para todos os soldados (você se muda para lá, dispara, cura o cara ...). A execução real desses comandos de nível superior também seria realizada por controladores de nível inferior. Quando você se esforça, pode tornar a IA capaz de tomar decisões cooperativas bastante inteligentes.
fonte
std::map
entidades são apenas IDs e, em seguida, precisamos criar algum tipo de sistema de tipos (talvez com um componente de tag, porque o renderizador precisará saber o que desenhar); e se não quisermos fazer isso, precisaremos de um componente de desenho: mas ele precisa que o componente de posição saiba onde ser desenhado, então criamos dependências entre os componentes que resolvemos com um sistema de mensagens super complexo. É isso que você está sugerindo?Primeiro, tente implementar recursos para que os objetos permaneçam independentes um do outro, sempre que possível. Especialmente, você deseja fazer isso para multiencadeamento. No seu primeiro exemplo de código, o conjunto de todos os objetos pode ser dividido em conjuntos correspondentes ao número de núcleos da CPU e atualizado com muita eficiência.
Mas, como você disse, a interação com outros objetos é necessária para alguns recursos. Isso significa que o estado de todos os objetos deve ser sincronizado em alguns pontos. Em outras palavras, seu aplicativo deve aguardar que todas as tarefas paralelas sejam concluídas primeiro e depois aplicar cálculos que envolvam interação. É bom reduzir o número desses pontos de sincronização, pois eles sempre implicam que alguns encadeamentos devem esperar que outros terminem.
Portanto, sugiro armazenar em buffer essas informações sobre os objetos necessários de dentro de outros objetos. Dado esse buffer global, você pode atualizar todos os seus objetos independentemente um do outro, mas apenas dependentes deles mesmos e do buffer global, que é mais rápido e fácil de manter. Em um intervalo de tempo fixo, digamos, após cada quadro, atualize o buffer com o estado atual dos objetos.
Então, o que você faz uma vez por quadro é: 1. armazena globalmente o estado dos objetos atuais; 2. atualiza todos os objetos baseados em si mesmos e no buffer;
fonte
Use um sistema baseado em componentes, no qual você possui um GameObject barebones que contém 1 ou mais componentes que definem seu comportamento.
Por exemplo, digamos que algum objeto deva se mover para a esquerda e direita o tempo todo (uma plataforma), você pode criar esse componente e anexá-lo a um GameObject.
Agora, digamos que um objeto de jogo deva girar lentamente o tempo todo, você pode criar um componente separado que faça exatamente isso e anexá-lo ao GameObject.
E se você quiser ter uma plataforma móvel que também gire, em uma hierarquia de classe tradicional que se torna difícil de fazer sem duplicar o código.
A beleza desse sistema é que, em vez de ter uma classe Rotatable ou MovingPlatform, você anexa esses dois componentes ao GameObject e agora possui um MovingPlatform que roda automaticamente.
Todos os componentes têm uma propriedade, 'requiredUpdate', que, enquanto verdadeiro, o GameObject chamará o método 'update' no referido componente. Por exemplo, suponha que você tenha um componente Arrastável, esse componente ao passar o mouse para baixo (se estiver sobre o GameObject) pode definir 'requireUpdate' como true e, em seguida, na configuração do mouse para false. Permitindo que ele siga o mouse apenas quando o mouse estiver pressionado.
Um dos desenvolvedores do Tony Hawk Pro Skater tem o artigo escrito e vale a pena ler: http://cowboyprogramming.com/2007/01/05/evolve-your-heirachy/
fonte
Favorecer a composição sobre a herança.
Meu conselho mais forte, além disso, seria: não se deixe levar pela mentalidade de "quero que isso seja extremamente flexível". A flexibilidade é ótima, mas lembre-se de que, em algum nível, em qualquer sistema finito como um jogo, existem partes atômicas que são usadas para construir o todo. De uma maneira ou de outra, seu processamento depende desses tipos atômicos predefinidos. Em outras palavras, atender a "qualquer" tipo de dados (se possível) não ajudaria a longo prazo, se você não tiver código para processá-los. Fundamentalmente, todo código deve analisar / processar dados com base em especificações conhecidas ... o que significa um conjunto predefinido de tipos. Qual o tamanho desse conjunto? Você decide.
Este artigo oferece uma visão do princípio da composição sobre a herança no desenvolvimento de jogos por meio de uma arquitetura de componente de entidade robusta e com bom desempenho.
Ao criar entidades a partir de subconjuntos (diferentes) de algum superconjunto de componentes predefinidos, você oferece aos seus IAs maneiras concretas e fragmentadas de compreender o mundo e os atores ao seu redor, lendo os estados desses componentes.
fonte
Pessoalmente, recomendo manter a função draw fora da própria classe Object. Eu até recomendo manter a localização / coordenadas dos objetos fora do próprio objeto.
Esse método draw () vai lidar com a API de renderização de baixo nível do OpenGL, OpenGL ES, Direct3D, sua camada de quebra automática nessas APIs ou uma API de mecanismos. Pode ser que você tenha que trocar entre eles (se você quisesse dar suporte ao OpenGL + OpenGL ES + Direct3D, por exemplo.
Esse GameObject deve conter apenas as informações básicas sobre sua aparência visual, como uma malha ou talvez um pacote maior, incluindo entradas de sombreador, estado de animação e assim por diante.
Você também vai querer um pipeline gráfico flexível. O que acontece se você deseja solicitar objetos com base na distância da câmera. Ou seu tipo de material. O que acontece se você deseja desenhar um objeto 'selecionado' com uma cor diferente. E se, em vez de realmente renderizar tanto quanto você chamar uma função de desenho em um objeto, ele a colocar em uma lista de comandos de ações a serem executadas pela renderização (pode ser necessário para a segmentação). Você pode fazer esse tipo de coisa com o outro sistema, mas é uma PITA.
O que eu recomendo é que, em vez de desenhar diretamente, você vincule todos os objetos que deseja a outra estrutura de dados. Essa ligação realmente precisa apenas ter uma referência ao local dos objetos e às informações de renderização.
Seus níveis / partes / áreas / mapas / hubs / mundo inteiro / qualquer que seja um índice espacial, ele contém os objetos e os retorna com base em consultas de coordenadas e pode ser uma lista simples ou algo como um Octree. Também poderia ser um invólucro para algo implementado por um mecanismo de física de terceiros como um cenário de física. Ele permite que você faça coisas como "Consultar todos os objetos que estão à vista da câmera com alguma área extra ao seu redor" ou para jogos mais simples, nos quais você pode simplesmente renderizar tudo e pegar a lista inteira.
Os índices espaciais não precisam conter as informações de posicionamento reais. Eles trabalham armazenando objetos em estruturas de árvores em relação à localização de outros objetos. Eles podem ser considerados como um tipo de cache com perdas, permitindo uma pesquisa rápida de um objeto com base em sua posição. Não há necessidade real de duplicar suas coordenadas X, Y, Z reais. Dito isto, você poderia se quisesse manter
Na verdade, seus objetos de jogo nem precisam conter suas próprias informações de localização. Por exemplo, um objeto que não foi colocado em um nível não deve ter coordenadas x, y, z, isso não faz sentido. Você pode conter isso no índice especial. Se você precisar procurar as coordenadas do objeto com base em sua referência real, desejará ter uma ligação entre o objeto e o gráfico de cena (os gráficos de cena são para retornar objetos com base em coordenadas, mas são lentos em retornar coordenadas com base em objetos) .
Quando você adiciona um objeto a um nível. Ele fará o seguinte:
1) Crie uma estrutura de localização:
Isso também pode ser uma referência a um objeto em mecanismos de física de terceiros. Ou pode ser uma coordenada de deslocamento com uma referência a outro local (para uma câmera de rastreamento ou um objeto ou exemplo anexado). Com o polimorfismo, pode ser dependendo se é um objeto estático ou dinâmico. Mantendo aqui uma referência ao índice espacial, quando as coordenadas são atualizadas, o índice espacial também pode ser.
Se você estiver preocupado com a alocação dinâmica de memória, use um conjunto de memórias.
2) Uma ligação / ligação entre seu objeto, sua localização e o gráfico da cena.
3) A ligação é adicionada ao índice espacial dentro do nível no ponto apropriado.
Quando você está se preparando para renderizar.
1) Pegue a câmera (será apenas outro objeto, exceto que sua localização rastreará o personagem dos jogadores e seu renderizador terá uma referência especial a ela, de fato, é tudo o que realmente precisa).
2) Obtenha o SpacialBinding da câmera.
3) Obtenha o índice espacial da ligação.
4) Consulte os objetos que são (possivelmente) visíveis para a câmera.
5A) Você precisa ter as informações visuais processadas. Texturas carregadas na GPU e assim por diante. É melhor fazer isso com antecedência (como na carga nivelada), mas talvez possa ser feito em tempo de execução (para um mundo aberto, você pode carregar coisas quando estiver próximo de um pedaço, mas ainda assim deve ser feito com antecedência).
5B) Opcionalmente, crie uma árvore de renderização em cache, se você quiser classificar em profundidade / material ou acompanhar objetos próximos que possam estar visíveis posteriormente. Caso contrário, você pode simplesmente consultar o índice espacial sempre que ele depender de seus requisitos de jogo / desempenho.
Seu renderizador provavelmente precisará de um objeto RenderBinding que se vincule entre o Object, as coordenadas
Então, quando você renderizar, basta executar a lista.
Eu usei as referências acima, mas elas podem ser ponteiros inteligentes, ponteiros brutos, identificadores de objetos e assim por diante.
EDITAR:
Quanto a tornar as coisas "conscientes" uma da outra. Isso é detecção de colisão. Provavelmente seria implementado no Octree. Você precisaria fornecer algum retorno de chamada em seu objeto principal. Esse material é melhor manipulado por um mecanismo de física adequado, como o Bullet. Nesse caso, substitua Octree por PhysicsScene e Position por um link para algo como CollisionMesh.getPosition ().
fonte