Componentes do jogo, gerenciadores de jogo e propriedades do objeto

15

Estou tentando entender o design de entidades com base em componentes.

Meu primeiro passo foi criar vários componentes que poderiam ser adicionados a um objeto. Para cada tipo de componente, eu tinha um gerente, que chamaria a função de atualização de cada componente, passando coisas como estado do teclado etc., conforme necessário.

A próxima coisa que fiz foi remover o objeto e apenas ter cada componente com um ID. Portanto, um objeto é definido por componentes com os mesmos IDs.

Agora, estou pensando que não preciso de um gerente para todos os meus componentes, por exemplo, tenho um SizeComponent, que apenas possui uma Sizepropriedade). Como resultado, SizeComponentele não possui um método de atualização e o método de atualização do gerente não faz nada.

Meu primeiro pensamento foi ter uma ObjectPropertyclasse que os componentes pudessem consultar, em vez de tê-los como propriedades dos componentes. Portanto, um objeto teria um número de ObjectPropertyeObjectComponent . Os componentes teriam lógica de atualização que consulta propriedades do objeto. O gerente gerenciaria a chamada do método de atualização do componente.

Isso me parece um excesso de engenharia, mas acho que não consigo me livrar dos componentes, porque preciso de uma maneira de os gerentes saberem quais objetos precisam de qual lógica de componente para executar (caso contrário, eu apenas removeria o componente completamente e envie sua lógica de atualização para o gerenciador).

  1. É isso (ter ObjectProperty, ObjectComponente ComponentManagerclasses) over-engenharia?
  2. O que seria uma boa alternativa?
George Duckett
fonte
11
Você tem a idéia certa ao tentar aprender o modelo de componente, mas precisa entender melhor o que ele precisa fazer - e a única maneira de fazer isso é [principalmente] concluir um jogo sem usá-lo. Eu acho que fazer um SizeComponenté um exagero - você pode assumir que a maioria dos objetos tem um tamanho - são coisas como renderização, IA e física onde o modelo de componente é usado; O tamanho sempre se comportará da mesma maneira, para que você possa compartilhar esse código.
Jonathan Dickinson
@ JonathanDickinson, @ Den: Eu acho, então meu problema é onde armazeno propriedades comuns. Por exemplo, um objeto como uma posição, que é usado por a RenderingComponente a PhysicsComponent. Estou pensando demais na decisão de onde colocar a propriedade? Devo apenas colocá-lo em qualquer um, então ter a outra consulta um objeto para o componente que tem a propriedade necessária?
George Duckett
Meu comentário anterior e o processo de pensamento por trás disso é o que está me levando a ter uma classe separada para uma propriedade (ou grupo de propriedades relacionadas, talvez) que os componentes possam consultar.
George Duckett
11
Eu realmente gosto dessa idéia - pode valer a pena tentar; mas ter um objeto para descrever cada propriedade individual é realmente caro. Você pode tentar PhysicalStateInstance(um por objeto) ao lado de um GravityPhysicsShared(um por jogo); no entanto, sou tentado a dizer que isso está se aventurando nos domínios da euforia dos arquitetos, não se projete em um buraco (exatamente o que eu fiz com meu primeiro sistema de componentes). BEIJO.
Jonathan Dickinson

Respostas:

6

A resposta simples para sua primeira pergunta é Sim, você acabou de projetar o design. O 'Até onde eu vou quebrar as coisas?' Essa pergunta é muito comum quando a próxima etapa é executada e o objeto central (geralmente chamado de Entidade) é removido.

Quando você divide os objetos em um nível tão detalhado que tenha tamanho por conta própria, o design foi longe demais. Um valor de dados por si só não é um componente. É um tipo de dados incorporado e geralmente pode ser chamado exatamente como você começou a chamá-los, uma propriedade. Uma propriedade não é um componente, mas um componente contém propriedades.

Então, aqui estão algumas diretrizes que eu tento seguir quando desenvolvo em um sistema de componentes:

  • Não tem colher.
    • Este é o passo que você já deu para se livrar do objeto central. Isso remove todo o debate sobre o que entra no objeto Entity e o que entra em um componente, pois agora tudo o que você tem são os componentes.
  • Componentes não são estruturas
    • Se você dividir algo em que apenas contém dados, ele não será mais um componente, será apenas uma estrutura de dados.
    • Um componente deve conter toda a funcionalidade necessária para realizar uma tarefa muito específica de uma maneira específica.
    • A interface IRenderable fornece a solução genérica para exibir visualmente qualquer coisa no jogo. CRenderableSprite e CRenderableModel é uma implementação de componente dessa interface que fornece os detalhes para renderizar em 2D e 3D, respectivamente.
    • IUseable é a interface para algo com o qual um jogador pode interagir. CUseableItem seria o componente que dispara a arma ativa ou bebe a poção selecionada, enquanto CUseableTrigger pode ser o local em que um jogador entra em uma torre ou joga uma alavanca para soltar a ponte levadiça.

Portanto, com a orientação de componentes não serem estruturas, o SizeComponent foi quebrado demais. Ele contém apenas dados e o que define o tamanho de algo pode variar. Por exemplo, em um componente de renderização, pode ser um escalar 1d ou um vetor 2 / 3d. Em um componente de física, pode ser o volume delimitador do objeto. Em um item de inventário, pode ser a quantidade de espaço que ocupa em uma grade 2D.

Tente desenhar uma boa linha entre teoria e praticidade.

Espero que isto ajude.

James
fonte
Não vamos esquecer que em algumas plataformas, chamar uma função a partir de uma interface é mais longo do que chamar um de um pai de classe (uma vez que a sua resposta incluiu menções de interfaces e classes)
ADB
É bom lembrar, mas eu estava tentando permanecer independente da linguagem e apenas usá-las nos termos gerais de design.
James
"Se você dividir algo em que apenas contém dados, ele não será mais um componente, será apenas uma estrutura de dados". -- Por quê? "Componente" é uma palavra tão genérica que também pode significar estrutura de dados.
Paul Manta
@PaulManta Sim, é um termo genérico, mas o ponto principal desta pergunta e resposta é onde traçar a linha. Minha resposta, como você citou, é apenas minha sugestão para uma regra prática fazer exatamente isso. Como sempre, defendo que nunca deixe que a teoria ou as considerações de design sejam o que impulsiona o desenvolvimento, que visa ajudá-lo.
James
11
@ James Essa foi uma discussão interessante. :) chat.stackexchange.com/rooms/2175 Minha maior reclamação se a sua implementação é que os componentes sabem muito sobre o que os outros componentes estão interessados. Gostaria de continuar a discussão no futuro.
Paul Manta
14

Você já aceitou uma resposta, mas aqui está minha punhalada na CBS. Descobri que uma Componentclasse genérica tem algumas limitações, então fui com um design descrito pela Radical Entertainment na GDC 2009, que sugeriu a separação de componentes em Attributese Behaviors. (" Teoria e prática da arquitetura de componentes de objetos de jogo ", Marcin Chady)

Explico minhas decisões de design em um documento de duas páginas. Vou apenas postar o link, já que é muito longo para colar tudo aqui. Atualmente, ele cobre apenas os componentes lógicos (não os componentes de renderização e física), mas deve fornecer uma idéia do que tentei:

http://www.pdf-archive.com/2012/01/08/entity-component-system/preview/page/1

Aqui está um trecho do documento:

Atributos e comportamentos em resumo

Attributesgerenciar uma categoria de dados e qualquer lógica que eles tenham é de escopo limitado. Por exemplo, Healthpode garantir que seu valor atual nunca seja maior que seu valor máximo e pode até notificar outros componentes quando o valor atual cair abaixo de um determinado nível crítico, mas não contém nenhuma lógica mais complexa. Attributessão dependentes de nenhum outro Attributesou Behaviors.

Behaviorscontrolar como a entidade reage aos eventos do jogo, toma decisões e altera os valores Attributesconforme necessário. Behaviorsdependem de alguns dos Attributes, mas não podem interagir diretamente um com o outro - apenas reagem à forma como os Attributes’valores são alterados pelo outro Behaviorse aos eventos para os quais são enviados.


Edit: E aqui está um diagrama de relacionamento que mostra como os componentes se comunicam:

Diagrama de comunicação entre atributos e comportamentos

Um detalhe da implementação: o nível da entidade EventManageré criado apenas se for usado. A Entityclasse apenas armazena um ponteiro para um EventManagerque é inicializado apenas se algum componente o solicitar.


Edit: Em outra pergunta, dei uma resposta semelhante a esta. Você pode encontrá-lo aqui para uma, talvez, melhor explicação do sistema:
/gamedev//a/23759/6188

Paul Manta
fonte
2

Realmente depende das propriedades que você precisa e de onde precisa. A quantidade de memória que você terá e o poder / tipo de processamento que você usará. Eu já vi e tentei fazer o seguinte:

  • As propriedades usadas por vários componentes, mas modificadas apenas por um, são armazenadas nesse componente. Shape é um bom exemplo em um jogo em que o sistema de IA, o sistema de física e o sistema de renderização precisam acessar a forma de base, é uma propriedade pesada e deve permanecer apenas em um lugar, se possível.
  • Às vezes, propriedades como a posição precisam ser duplicadas. Por exemplo, se você executar vários sistemas em paralelo, evite espreitar os sistemas e sim sincronize a posição (copie do componente principal ou sincronize através de deltas ou com uma passagem de colisão, se necessário).
  • As propriedades originárias de controles ou "intenções" de IA podem ser armazenadas em um sistema dedicado, pois podem ser aplicadas a outros sistemas sem serem visíveis de fora.
  • Propriedades simples podem se tornar complicadas. Às vezes, sua posição exigirá um sistema dedicado se você precisar compartilhar muitos dados (posição, orientação, delta do quadro, movimento atual total do delta, movimento delta do quadro atual e do quadro anterior, rotação ...). Nesse caso, você terá que ir com o sistema e acessar os dados mais recentes do componente dedicado e talvez seja necessário alterá-los através de acumuladores (deltas).
  • Às vezes, suas propriedades podem ser armazenadas em uma matriz bruta (duplo *) e seus componentes simplesmente terão ponteiros para as matrizes que contêm as diferentes propriedades. O exemplo mais óbvio é quando você precisa de cálculos paralelos massivos (CUDA, OpenCL). Portanto, ter um sistema para gerenciar adequadamente os ponteiros pode vir em poucos.

Esses princípios têm suas limitações. É claro que você precisará enviar a geometria para o renderizador, mas provavelmente não desejará recuperá-la a partir daí. A geometria principal será armazenada no mecanismo de física no caso em que ocorrerem deformações e sincronizadas com o renderizador (de tempos em tempos, dependendo da distância dos objetos). Então, de certa forma, você a duplicará de qualquer maneira.

Não há sistemas perfeitos. E alguns jogos terão melhor desempenho com um sistema mais simples, enquanto outros exigirão sincronizações mais complexas entre os sistemas.

Primeiramente, verifique se todas as propriedades podem ser acessadas de maneira simples a partir de seus componentes, para que você possa alterar a maneira como as armazena de forma transparente depois de começar a ajustar seus sistemas.

Não há vergonha em copiar algumas propriedades. Se alguns componentes precisam reter uma cópia local, às vezes é mais eficiente copiar e sincronizar, em vez de acessar um valor "externo"

Além disso, a sincronização não precisa acontecer em todos os quadros. Alguns componentes podem ser sincronizados com menos frequência do que outros. O componente Render geralmente é um bom exemplo. Os que não interagem com os jogadores podem ser sincronizados com menos frequência, assim como os que estão longe. As pessoas distantes e fora do campo da câmera podem ser sincronizadas com menos frequência.


Quando se trata do seu componente de tamanho, provavelmente ele pode ser incluído no componente de posição:

  • nem todas as entidades com um tamanho têm um componente de física, áreas por exemplo, portanto, agrupá-lo com a física não é do seu interesse.
  • tamanho provavelmente não importa sem uma posição
  • todos os objetos com uma posição provavelmente terão um tamanho (que pode ser usado para scripts, física, IA, renderização ...).
  • o tamanho provavelmente não é atualizado a cada ciclo, mas a posição pode ser.

Em vez do tamanho, você pode até usar um modificador de tamanho, ele pode ser mais prático.

Quanto ao armazenamento de todas as propriedades em um sistema genérico de armazenamento de propriedades ... Não sei se você está indo na direção certa ... Concentre-se nas propriedades que são essenciais para o seu jogo e crie componentes que agrupem o maior número possível de propriedades relacionadas. Desde que você abstraia o acesso a essas propriedades corretamente (por meio de getters nos componentes que precisam delas, por exemplo), você poderá movê-las, copiá-las e sincronizá-las posteriormente, sem interromper muito a lógica.

Coiote
fonte
2
Entre +1 ou -1 comigo, porque meu representante atual é 666 desde o dia 9 de novembro ... É assustador.
Coyote
1

Se os componentes puderem ser adicionados arbitrariamente às entidades, você precisará de uma maneira de consultar se um determinado componente existe em uma entidade e obter uma referência a ele. Portanto, você pode percorrer uma lista de objetos derivados de ObjectComponent até encontrar o desejado e retorná-lo. Mas você retornaria um objeto do tipo correto.

Em C ++ ou C #, isso geralmente significa que você teria um método de modelo na entidade como T GetComponent<T>() . E uma vez que você tenha essa referência, você sabe exatamente quais dados de membros eles mantêm; basta acessá-los diretamente.

Em algo como Lua ou Python, você não necessariamente tem um tipo explícito desse objeto e provavelmente também não se importa. Mas, novamente, você pode simplesmente acessar a variável membro e manipular qualquer exceção que surgir da tentativa de acessar algo que não existe.

A consulta de propriedades de objetos parece explicitamente duplicar o trabalho que o idioma pode fazer por você, em tempo de compilação para idiomas de tipo estaticamente ou em tempo de execução para idiomas de tipo dinâmico.

Kylotan
fonte
Eu entendo sobre a obtenção de componentes fortemente tipados de uma entidade (usando genéricos ou algo semelhante), minha pergunta é mais sobre onde essas propriedades devem ir, particularmente onde uma propriedade é usada por vários componentes e não se pode dizer que um único componente é o proprietário. Veja meu terceiro e quarto comentário sobre a questão.
precisa
Basta escolher um existente, se for o caso, ou fatorar a propriedade em um novo componente, se não for o caso. Por exemplo, o Unity tem um componente 'Transform' que é apenas a posição e, se algo mais precisar alterar a posição do objeto, eles o fazem através desse componente.
Kylotan
1

"Acho que meu problema é onde armazeno propriedades comuns. Por exemplo, um objeto como uma posição, usada por um RenderingComponent e um PhysicsComponent. Estou pensando demais na decisão de onde colocar a propriedade? em qualquer um deles, a outra consulta tem um objeto para o componente que possui a propriedade necessária? "

O fato é que RenderingComponent usa a posição, mas o PhysicsComponent fornece . Você só precisa de uma maneira de informar a cada componente do usuário qual provedor usar. Idealmente, de uma maneira agnóstica, caso contrário, haverá uma dependência.

"... minha pergunta é mais sobre onde essas propriedades devem ir, particularmente onde uma propriedade é usada por vários componentes e não se pode dizer que nenhum componente é o proprietário. Veja meu terceiro e quarto comentários sobre a questão."

Não existe uma regra comum. Depende da propriedade específica (veja acima).

Faça um jogo com uma arquitetura feia, mas baseada em componentes e depois refatorá-la.

Den
fonte
Acho que não entendo direito o PhysicsComponentque fazer então. Eu vejo isso como gerenciar a simulação do objeto dentro de um ambiente físico, o que me leva a essa confusão: nem todas as coisas que precisam ser renderizadas precisarão ser simuladas; portanto, parece errado adicionar PhysicsComponentquando adiciono RenderingComponentporque contém uma posição que RenderingComponentusa. Eu podia me ver facilmente terminando com uma rede de componentes interconectados, o que significa que tudo / a maioria precisa ser adicionado a cada entidade.
precisa
Eu tive uma situação semelhante na verdade :). Eu tenho um PhysicsBodyComponent e um SimpleSpatialComponent. Ambos fornecem posição, ângulo e tamanho. Mas o primeiro participa da simulação de física e possui propriedades relevantes adicionais, e o segundo apenas mantém esses dados espaciais. Se você possui seu próprio mecanismo físico, pode até herdar o primeiro do último.
Den
"Eu podia me ver facilmente terminando com uma rede de componentes interconectados, o que significa que tudo / a maioria precisa ser adicionado a cada entidade." Isso porque você não tem um protótipo de jogo real. Estamos falando de alguns componentes básicos aqui. Não é à toa que eles serão usados ​​em todos os lugares.
Den
1

Seu intestino está dizendo a você que ter a ThingProperty, ThingComponente ThingManagerpara cada Thingtipo de componente um pouco de exagero. Eu acho que está certo.

Mas, você precisa de alguma maneira de acompanhar os componentes relacionados em termos de quais sistemas os utilizam, a que entidade eles pertencem etc.

TransformPropertyvai ser bem comum. Mas quem está encarregado disso, o sistema de renderização? O sistema de física? O sistema de som? Por que um Transformcomponente precisaria se atualizar?

A solução é remover qualquer tipo de código de suas propriedades fora de getters, setters e inicializadores. Componentes são dados, usados ​​pelos sistemas no jogo para executar várias tarefas como renderização, IA, reprodução de som, movimento etc.

Leia sobre Artemis: http://piemaster.net/2011/07/entity-component-artemis/

Observe o código e você verá que ele se baseia em sistemas que declaram suas dependências como listas de ComponentTypes. Você escreve cada uma de suas Systemclasses e no método construtor / init declara de quais tipos esse sistema depende.

Durante a configuração de níveis ou outros enfeites, você cria suas entidades e adiciona componentes a elas. Depois, você solicita que a entidade reporte à Artemis, e a Artemis descobre, com base na composição dessa entidade, quais sistemas estariam interessados ​​em saber sobre essa entidade.

Então, durante a fase de atualização do seu loop, você Systemterá agora uma lista de quais entidades atualizar. Agora você pode ter granularidade dos componentes para que possa conceber sistemas de loucos, entidades construir fora de um ModelComponent, TransformComponent, FliesLikeSupermanComponent, e SocketInfoComponent, e fazer algo estranho como fazer um disco voador que voa entre clientes conectados a um jogo multiplayer. Ok, talvez não, mas a ideia é que isso mantenha as coisas dissociadas e flexíveis.

Artemis não é perfeito, e os exemplos no site são um pouco básicos, mas a separação de código e dados é poderosa. Também é bom para o seu cache, se você fizer certo. Artemis provavelmente não faz bem nessa frente, mas é bom aprender.

michael.bartnett
fonte