Que projetos existem para um sistema de entidade baseado em componentes que seja fácil de usar, mas ainda flexível?


23

Estou interessado no sistema de entidades baseado em componentes há um tempo e li inúmeros artigos sobre ele (Os jogos Insomiac , o bastante padrão Evolve Your Hierarchy , a T-Machine , Chronoclast ... só para citar alguns).

Todos eles parecem ter uma estrutura externa de algo como:

Entity e = Entity.Create();
e.AddComponent(RenderComponent, ...);
//do lots of stuff
e.GetComponent<PositionComponent>(...).SetPos(4, 5, 6);

E se você trouxer a ideia de dados compartilhados (este é o melhor design que eu já vi até agora, em termos de não duplicar dados em todos os lugares)

e.GetProperty<string>("Name").Value = "blah";

Sim, isso é muito eficiente. No entanto, não é exatamente o mais fácil de ler ou escrever; parece muito desajeitado e trabalhando contra você.

Pessoalmente, gostaria de fazer algo como:

e.SetPosition(4, 5, 6);
e.Name = "Blah";

Embora, é claro, a única maneira de chegar a esse tipo de design esteja de volta ao tipo de hierarquia Entidade-> NPC-> Enemy-> FlyingEnemy-> FlyingEnemyWithAHatOn que esse design tenta evitar.

Alguém viu um design para esse tipo de sistema de componentes que ainda é flexível, mas mantém um nível de facilidade de uso? E, nesse caso, consegue contornar o (provavelmente o problema mais difícil) o armazenamento de dados de uma maneira boa?

Que projetos existem para um sistema de entidade baseado em componentes que seja fácil de usar, mas ainda flexível?

Respostas:


13

Uma das coisas que o Unity faz é fornecer alguns acessadores auxiliares no objeto do jogo pai para fornecer um acesso mais amigável aos componentes comuns.

Por exemplo, você pode ter sua posição armazenada em um componente Transform. Usando seu exemplo, você teria que escrever algo como

e.GetComponent<Transform>().position = new Vector3( whatever );

Mas no Unity, isso é simplificado para

e.transform.position = ....;

Onde transformliteralmente é apenas um método auxiliar simples na GameObjectclasse base ( Entityclasse no seu caso) que faz

Transform transform
{ 
    get { return this.GetComponent<Transform>(); }
}

O Unity também faz algumas outras coisas, como definir uma propriedade "Nome" no próprio objeto do jogo, em vez de em seus componentes filhos.

Pessoalmente, não gosto da ideia do seu design de dados compartilhados por nome. O acesso às propriedades pelo nome, em vez de por uma variável, e o usuário também precisa saber de que tipo é realmente parece propenso a erros. O que o Unity faz é que eles usam métodos semelhantes à GameObject transformpropriedade dentro das Componentclasses para acessar os componentes irmãos. Portanto, qualquer componente arbitrário que você escreve pode simplesmente fazer o seguinte:

var myPos = this.transform.position;

Para acessar a posição. Onde essa transformpropriedade faz algo parecido com isto

Transform transform
{
    get { return this.gameObject.GetComponent<Transform>(); }
}

Claro, é um pouco mais detalhado do que apenas dizer e.position = whatever, mas você se acostuma e não parece tão desagradável quanto as propriedades genéricas. E sim, você teria que fazer isso da maneira indireta para os componentes do cliente, mas a idéia é que todos os componentes comuns do "mecanismo" (renderizadores, coletores, fontes de áudio, transformações etc.) tenham acessadores fáceis.


Percebo por que o sistema de dados compartilhados por nome pode ser um problema, mas de que outra forma eu poderia evitar o problema de vários componentes que precisam de dados (ou seja, em qual armazenar algo)? Apenas tomando muito cuidado na fase de design?
The Duck Comunista

Além disso, em termos de 'ter o usuário também precisa saber que tipo é', eu não poderia simplesmente convertê-lo em property.GetType () no método get ()?
The Duck Comunista

1
Que tipo de dados globais (no escopo da entidade) você está enviando para esses repositórios de dados compartilhados? Qualquer tipo de dado do objeto do jogo deve ser facilmente acessível através das classes "engine" (transformação, coletores, renderizadores, etc.). Se você estiver fazendo alterações na lógica do jogo com base nesses "dados compartilhados", é muito mais seguro ter uma classe e garantir que o componente esteja nesse objeto do jogo do que apenas pedir cegamente para ver se existe alguma variável nomeada. Então, sim, você deve lidar com isso na fase de design. Você sempre pode criar um componente que apenas armazena dados, se realmente precisar.
Tetrad

@Tetrad - Você pode elaborar brevemente como o método GetTransponent <Transform> proposto funcionaria? Ele passaria por todos os componentes do GameObject e retornaria o primeiro componente do tipo T? Ou algo mais está acontecendo lá?
Michael

@ Michael que iria funcionar. Você pode apenas fazer parte do chamador armazenar em cache localmente como uma melhoria de desempenho.
Tétrada

3

Eu sugeriria que algum tipo de classe de interface para seus objetos de entidade seria bom. Ele poderia executar o tratamento de erros com a verificação para garantir que uma entidade contenha também um componente do valor apropriado em um local, para que você não precise fazer isso em todos os lugares em que acessar os valores. Como alternativa, na maioria dos projetos que já fiz com um sistema baseado em componentes, trato diretamente dos componentes, solicitando, por exemplo, seu componente posicional e depois acessando / atualizando diretamente as propriedades desse componente.

Muito básica em um nível alto, a classe absorve a entidade em questão e fornece uma interface fácil de usar para os componentes subjacentes da seguinte maneira:

public class EntityInterface
{
    private Entity entity { get; set };
    public EntityInterface(Entity e)
    {
        entity = e;
    }

    public Vector3 Position
    {
        get
        {
            return entity.GetProperty<Vector3>("Position");
        }
        set
        {
            entity.GetProperty<Vector3>("Position") = value;
        }
    }
}

Se eu presumir que precisaria lidar com NoPositionComponentException ou algo assim, qual é a vantagem disso em colocar tudo isso na classe Entity?
The Duck comunista

Sendo inseguro sobre o que sua classe de entidade realmente contém, não posso dizer que há uma vantagem ou não. Eu, pessoalmente, advogo sistemas de componentes onde não existe uma entidade básica, por exemplo, simplesmente para evitar a questão de 'Bem, o que pertence a ela'. No entanto, consideraria uma classe de interface como essa um bom argumento para ela existir.
James

Posso ver como isso é mais fácil (não preciso consultar a posição por meio de GetProperty), mas não vejo a vantagem dessa maneira em vez de colocá-la na própria classe Entity.
The Duck comunista

2

No Python, você pode interceptar a parte 'setPosition' e.SetPosition(4,5,6)via declarar uma __getattr__função na Entidade. Essa função pode percorrer os componentes e encontrar o método ou propriedade apropriado e retornar isso, para que a chamada ou atribuição da função vá para o local certo. Talvez o C # tenha um sistema semelhante, mas pode não ser possível devido ao fato de ser digitado estaticamente - presumivelmente não pode ser compilado a e.setPositionmenos que e tenha definidoPosition na interface em algum lugar.

Você também pode fazer com que todas as suas entidades implementem as interfaces relevantes para todos os componentes que você pode adicionar a eles, com métodos de stub que geram uma exceção. Então, quando você chama addComponent, você apenas redireciona as funções da entidade da interface desse componente para o componente. Um pouco complicado embora.

Mas, talvez, mais fácil seria sobrecarregar o operador [] na sua classe Entity para procurar os componentes ligados para a presença de uma propriedade válida e retorno que, por isso pode ser atribuído a assim: e["Name"] = "blah". Cada componente precisará implementar seu próprio GetPropertyByName e a Entidade chama cada um por vez até encontrar o componente responsável pela propriedade em questão.


Eu tinha pensado em usar indexadores .. isso é realmente bom. Vou esperar um pouco para ver o que mais aparece, mas estou voltando-me para uma mistura disso, o GetComponent () usual e algumas propriedades como @ James sugeridas para componentes comuns.
The Duck Comunista

Talvez esteja faltando o que está sendo dito aqui, mas isso parece mais um substituto para e.GetProperty <type> ("NameOfProperty"). Qualquer coisa com e ["NameOfProperty"].
James

@ James É um invólucro para ele (muito parecido com o seu design) .. exceto que é mais dinâmico - ele faz isso automaticamente e de forma mais limpa que o GetPropertymétodo .. Apenas a desvantagem é a manipulação e comparação de strings. Mas esse é um ponto discutível.
The Duck Comunista

Ah, meu mal-entendido lá. Eu assumi que o GetProperty fazia parte da entidade (e faria o que foi descrito acima) e não o método C #.
James

2

Para expandir a resposta de Kylotan, se você estiver usando o C # 4.0, você pode digitar estaticamente uma variável para ser dinâmica .

Se você herdar de System.Dynamic.DynamicObject, poderá substituir TryGetMember e TrySetMember (entre os vários métodos virtuais) para interceptar nomes de componentes e retornar o componente solicitado.

dynamic entity = EntityRegistry.Entities[1000];
entity.MyComponent.MyValue = 100;

Escrevi um pouco sobre sistemas de entidades ao longo dos anos, mas presumo que não posso competir com os gigantes. Algumas notas do ES (um pouco desatualizadas e não refletem necessariamente meu entendimento atual dos sistemas de entidades, mas vale a pena ler, imho).


Obrigado por isso, ajudará :) A maneira dinâmica não teria o mesmo tipo de efeito que apenas transmitir para property.GetType ()?
The Duck comunista

Em algum momento, haverá uma conversão, já que o TryGetMember possui um objectparâmetro out - mas tudo isso será resolvido no tempo de execução. Eu acho que é uma maneira glorificada de manter uma interface fluente sobre ie entity.TryGetComponent (compName, componente out). Eu não tenho certeza se entendi a pergunta, no entanto :)
Raine

A questão é que eu poderia usar tipos dinâmicos em qualquer lugar ou (AFAICS) return (property.GetType ()) property.
The Duck Comunista

1

Eu vejo muito interesse aqui no ES em C #. Confira meu porto da excelente implementação do ES Artemis:

https://github.com/thelinuxlich/artemis_CSharp

Além disso, um exemplo de jogo para você começar a trabalhar (usando o XNA 4): https://github.com/thelinuxlich/starwarrior_CSharp

Sugestões são bem-vindas!


Certamente parece bom. Pessoalmente, não sou fã da idéia de tantos EntityProcessingSystems - no código, como isso funciona em termos de uso?
The Duck comunista

Todo sistema é um aspecto que processa seus componentes dependentes. Veja isto para ter uma idéia: github.com/thelinuxlich/starwarrior_CSharp/tree/master/…
thelinuxlich

0

Decidi usar o excelente sistema de reflexão do C #. Baseei-o no sistema de componentes gerais, além de uma mistura das respostas aqui.

Os componentes são adicionados com muita facilidade:

Entity e = new Entity(); //No templates/archetypes yet.
e.AddComponent(new HealthComponent(100));

E pode ser consultado por nome, tipo ou (se comuns, como Saúde e Posição) por propriedades:

e["Health"] //This, unfortunately, is a bit slower since it requires a dict search
e.GetComponent<HealthComponent>()
e.Health

E removido:

e.RemoveComponent<HealthComponent>();

Os dados são, novamente, acessados ​​normalmente por propriedades:

e.MaxHP

Qual é o lado do componente armazenado; menos sobrecarga se não tiver HP:

public int? MaxHP
{
get
{

    return this.Health.MaxHP; //Error handling returns null if health isn't here. Excluded for clarity
}
set
{
this.Health.MaxHP = value;
}

A grande quantidade de digitação é auxiliada pelo Intellisense no VS, mas na maioria das vezes há pouca digitação - o que eu estava procurando.

Nos bastidores, estou armazenando um Dictionary<Type, Component>e Componentssubstituindo o ToStringmétodo de verificação de nome. Meu design original usava principalmente strings para nomes e outras coisas, mas se tornou um porco para se trabalhar (olha, eu tenho que lançar em todos os lugares também a falta de propriedades de fácil acesso).

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.