Estou no caminho certo com essa arquitetura de componentes?

9

Decidi recentemente reformular minha arquitetura de jogo para livrar-me de hierarquias profundas de classe e substituí-las por componentes configuráveis. A primeira hierarquia que estou substituindo é a hierarquia de itens e gostaria de alguns conselhos para saber se estou no caminho certo.

Anteriormente, eu tinha uma hierarquia que era algo assim:

Item -> Equipment -> Weapon
                  -> Armor
                  -> Accessory
     -> SyntehsisItem
     -> BattleUseItem -> HealingItem
                      -> ThrowingItem -> ThrowsAsAttackItem

Escusado será dizer que estava começando a ficar confuso e não foi uma solução fácil para itens que precisavam ser de vários tipos (ou seja, alguns equipamentos são usados ​​na síntese de itens, outros são jogáveis ​​etc.)

Tentei refatorar e colocar a funcionalidade na classe de item base. Mas então eu estava notando que o Item tinha muitos dados não utilizados / supérfluos. Agora estou tentando fazer um componente como arquitetura, pelo menos para meus itens, antes de tentar fazê-lo em minhas outras classes de jogos.

Aqui está o que estou pensando atualmente para a configuração do componente:

Eu tenho uma classe de item base que possui slots para vários componentes (ou seja, um slot de componente de equipamento, um slot de componente de recuperação, etc., bem como um mapa para componentes arbitrários), algo assim:

class Item
{
    //Basic item properties (name, ID, etc.) excluded
    EquipmentComponent* equipmentComponent;
    HealingComponent* healingComponent;
    SynthesisComponent* synthesisComponent;
    ThrowComponent* throwComponent;
    boost::unordered_map<std::string, std::pair<bool, ItemComponent*> > AdditionalComponents;
} 

Todos os componentes do item herdariam de uma classe ItemComponent base e cada tipo de componente é responsável por informar ao mecanismo como implementar essa funcionalidade. ou seja, o HealingComponent diz à mecânica da batalha como consumir o item como um item de cura, enquanto o ThrowComponent diz ao mecanismo de batalha como tratar o item como um item jogável.

O mapa é usado para armazenar componentes arbitrários que não são componentes principais do item. Estou associando-o a um bool para indicar se o Contêiner de Itens deve gerenciar o ItemComponent ou se está sendo gerenciado por uma fonte externa.

Minha idéia aqui foi que eu defina os componentes principais usados ​​pelo meu mecanismo de jogo, e minha fábrica de itens atribuiria os componentes que o item realmente possui, caso contrário, eles serão nulos. O mapa conteria componentes arbitrários que geralmente seriam adicionados / consumidos por arquivos de script.

Minha pergunta é: esse é um bom design? Caso contrário, como pode ser melhorado? Eu considerei agrupar todos os componentes no mapa, mas o uso de indexação de strings parecia desnecessário para os componentes principais do item

user127817
fonte

Respostas:

8

Parece um primeiro passo bastante razoável.

Você está optando por uma combinação de generalidade (o mapa de "componentes adicionais") e desempenho de pesquisa (os membros codificados), o que pode ser um pouco de pré-otimização - o seu ponto a respeito da ineficiência geral da cadeia de caracteres baseada em strings a pesquisa é bem-feita, mas você pode aliviá-la escolhendo indexar componentes por algo mais rápido que o hash. Uma abordagem pode ser fornecer a cada tipo de componente um ID de tipo exclusivo (essencialmente você está implementando RTTI personalizado leve ) e indexar com base nisso.

Independentemente disso, aconselho você a expor uma API pública para o objeto Item que permita solicitar qualquer componente - os codificados permanentemente e os adicionais - de maneira uniforme. Isso tornaria mais fácil alterar a representação ou o equilíbrio subjacente dos componentes codificados / não codificados, sem precisar refatorar todos os clientes do componente do item.

Você também pode considerar fornecer versões no-op "fictícias" de cada um dos componentes codificados e garantir que eles sejam sempre atribuídos - você pode usar membros de referência em vez de ponteiros e nunca precisará procurar um ponteiro NULL antes de interagir com uma das classes de componentes codificados. Você ainda arcará com o custo do envio dinâmico para interagir com os membros desse componente, mas isso ocorreria mesmo com os membros do ponteiro. Esse é mais um problema de limpeza de código, pois o impacto no desempenho será desprezível com toda a probabilidade.

Não acho que seja uma boa idéia ter dois tipos diferentes de escopos de vida útil (em outras palavras, não acho que o bool que você possui no mapa de componentes adicionais seja uma ótima idéia). Isso complica o sistema e implica que a destruição e a liberação de recursos não serão terrivelmente determinísticas. A API para seus componentes seria muito mais clara se você optar por uma estratégia de gerenciamento vitalícia ou outra - a entidade gerencia a vida útil do componente ou o subsistema que realiza os componentes (eu prefiro o último porque combina melhor com o componente externo abordagem, que discutirei a seguir).

A grande desvantagem que vejo com a sua abordagem é que você está agrupando todos os componentes no objeto "entidade", que nem sempre é o melhor design. Da minha resposta relacionada a outra pergunta baseada em componente:

Sua abordagem de usar um grande mapa de componentes e uma chamada de update () no objeto do jogo é bastante subótima (e uma armadilha comum para quem primeiro constrói esse tipo de sistema). Isso reduz a coerência do cache durante a atualização e não permite que você aproveite a simultaneidade e a tendência para o processo no estilo SIMD de grandes lotes de dados ou comportamento de uma só vez. Geralmente é melhor usar um design em que o objeto do jogo não atualize seus componentes, mas o subsistema responsável pelo próprio componente os atualiza todos de uma vez.

Você está basicamente adotando a mesma abordagem armazenando os componentes na entidade do item (que é, novamente, uma primeira etapa totalmente aceitável). O que você pode descobrir é que a maior parte do acesso aos componentes dos quais você está preocupado com o desempenho é apenas para atualizá-los e se você optar por usar uma abordagem mais externa à organização dos componentes, onde os componentes serão mantidos em um cache coerente , estrutura de dados eficiente (para seu domínio) por um subsistema que mais entende suas necessidades, é possível obter um desempenho de atualização muito melhor e mais paralelizável.

Mas aponto isso apenas como algo a considerar como uma direção futura - você certamente não quer exagerar na engenharia; você pode fazer uma transição gradual por meio de refatoração constante ou pode descobrir que sua implementação atual atende perfeitamente às suas necessidades e não há necessidade de iterar.

Comunidade
fonte
11
+1 por sugerir se livrar do objeto Item. Embora seja mais trabalhoso, acabará produzindo um sistema de componentes melhor.
James
Eu tinha mais algumas perguntas, sem saber se deveria começar um novo tópico, então vou tentar aqui primeiro: Para a minha classe de itens, não há métodos que chamarei de quadro evert (ou até perto). Para os meus subsistemas gráficos, vou seguir seu conselho e manter todos os objetos para atualizar no sistema. A outra pergunta que tive foi como lidar com verificações de componentes? Como, por exemplo, quero ver se posso usar um item como X, então naturalmente verificaria se o item tem o componente necessário para executar o X. Essa é a maneira correta de fazer isso? Obrigado novamente para a resposta
user127817