Design baseado em componentes: manipulação de interação de objetos

9

Não tenho certeza de como exatamente os objetos fazem as coisas com outros objetos em um design baseado em componentes.

Diga que eu tenho uma Objaula. Eu faço:

Obj obj;
obj.add(new Position());
obj.add(new Physics());

Como eu poderia então ter outro objeto, não apenas mover a bola, mas aplicar a física. Não estou procurando detalhes de implementação, mas de maneira abstrata como os objetos se comunicam. Em um design baseado em entidade, você pode apenas ter:

obj1.emitForceOn(obj2,5.0,0.0,0.0);

Qualquer artigo ou explicação para entender melhor um projeto orientado a componentes e como fazer coisas básicas seria realmente útil.

jmasterx
fonte

Respostas:

10

Isso geralmente é feito usando mensagens. Você pode encontrar muitos detalhes em outras perguntas neste site, como aqui ou ali .

Para responder seu exemplo específico, um caminho a seguir é definir uma Messageclasse pequena que seus objetos possam processar, por exemplo:

struct Message
{
    Message(const Objt& sender, const std::string& msg)
        : m_sender(&sender)
        , m_msg(msg) {}
    const Obj* m_sender;
    std::string m_msg;
};

void Obj::Process(const Message& msg)
{
    for (int i=0; i<m_components.size(); ++i)
    {
        // let components do some stuff with msg
        m_components[i].Process(msg);
    }
}

Dessa forma, você não está "poluindo" sua Objinterface de classe com métodos relacionados a componentes. Alguns componentes podem optar por processar a mensagem, outros podem ignorá-la.

Você pode começar chamando esse método diretamente de outro objeto:

Message msg(obj1, "EmitForce(5.0,0.0,0.0)");
obj2.ProcessMessage(msg);

Nesse caso, obj2o usuário Physicsselecionará a mensagem e fará o processamento necessário. Quando terminar, ele irá:

  • Envie uma mensagem "SetPosition" para si mesmo, informando que o Positioncomponente será escolhido;
  • Ou acesse diretamente o Positioncomponente para modificações (bastante errado para um design baseado em componente puro, pois você não pode assumir que todo objeto tem um Positioncomponente, mas o Positioncomponente pode ser um requisito Physics).

Geralmente, é uma boa idéia adiar o processamento real da mensagem para a próxima atualização do componente. Processá-lo imediatamente pode significar enviar mensagens para outros componentes de outros objetos; portanto, enviar apenas uma mensagem pode significar rapidamente uma pilha de espaguete inextricável.

Você provavelmente terá que optar por um sistema mais avançado posteriormente: filas de mensagens assíncronas, envio de mensagens para um grupo de objetos, registro / cancelamento de registro por componente de mensagens etc.

A Messageclasse pode ser um contêiner genérico para uma sequência simples, como mostrado acima, mas o processamento de sequências em tempo de execução não é realmente eficiente. Você pode optar por um contêiner de valores genéricos: cadeias, números inteiros, flutuantes ... Com um nome ou, melhor ainda, um ID, para distinguir diferentes tipos de mensagens. Ou você também pode derivar uma classe base para atender às necessidades específicas. No seu caso, você pode imaginar um EmitForceMessageque deriva Messagee adiciona o vetor de força desejado - mas cuidado com o custo de tempo de execução do RTTI, se você fizer isso.

Laurent Couvidou
fonte
3
Eu não me preocuparia com a "não pureza" de acessar componentes diretamente. Os componentes são usados ​​para atender às necessidades funcionais e de design, e não à academia. Você deseja verificar se um componente existe (por exemplo, verificar o valor de retorno não é nulo para a chamada do componente get).
Sean Middleditch
Eu sempre pensei nisso como você disse passado, usando RTTI mas muitas pessoas têm dito tantas coisas ruins sobre RTTI
jmasterx
@SeanMiddleditch Claro, eu faria dessa maneira, apenas mencionando isso para deixar claro que você sempre deve verificar duas vezes o que está fazendo ao acessar outros componentes da mesma entidade.
Laurent Couvidou
@Milo O RTTI implementado pelo compilador e o seu dynamic_cast podem se tornar um gargalo, mas não me preocupo com isso por enquanto. Você ainda pode otimizar isso mais tarde, se isso se tornar um problema. Os identificadores de classe baseados em CRC funcionam como um encanto.
Laurent Couvidou
Modelo <nome do tipo T> uint32_t class_id () {estático uint32_t v; retornar (uint32_t) & v; } ´ - não é necessário RTTI.
Arul 28/08
3

O que eu fiz para resolver um problema semelhante ao que você mostra é adicionar alguns manipuladores de componentes específicos e algum tipo de sistema de resolução de eventos.

Portanto, no caso do seu objeto "Física", quando ele é inicializado, ele se adiciona a um gerente central de objetos de Física. No ciclo do jogo, esses tipos de gerentes têm sua própria etapa de atualização; portanto, quando este PhysicsManager é atualizado, calcula todas as interações da física e as adiciona a uma fila de eventos.

Depois de produzir todos os seus eventos, você pode resolver sua fila de eventos simplesmente verificando o que aconteceu e realizando ações de acordo. No seu caso, deve haver um evento dizendo que os objetos A e B interagiram de alguma forma, para que você chame seu método emitForceOn.

Prós deste método:

  • Conceitualmente, é realmente simples de seguir.
  • Oferece espaço para otimizações específicas, como o uso de quadtress ou o que você precisar.
  • Isso acaba sendo realmente "plug and play". Objetos com física não interagem com objetos não-físicos porque não existem para o gerente.

Contras:

  • Você acaba tendo muitas referências, por isso pode ficar um pouco confuso limpar tudo corretamente, se você não tomar cuidado (do componente ao proprietário do componente, do gerente ao componente, do evento aos participantes, etc.) )
  • Você precisa colocar um pensamento especial na ordem em que resolve tudo. Acho que não é o seu caso, mas enfrentei mais de um loop infinito em que um evento criou outro evento e eu estava apenas adicionando-o diretamente à fila de eventos.

Eu espero que isso ajude.

PS: Se alguém tiver uma maneira mais limpa / melhor de resolver isso, eu realmente gostaria de ouvir.

Carlos
fonte
1
obj->Message( "Physics.EmitForce 0.0 1.1 2.2" );
// and some variations such as...
obj->Message( "Physics.EmitForce", "0.0 1.1 2.2" );
obj->Message( "Physics", "EmitForce", "0.0 1.1 2.2" );

Algumas coisas a serem observadas neste design:

  • O nome do componente é o primeiro parâmetro - para evitar o trabalho excessivo de código na mensagem - não sabemos quais componentes uma mensagem pode acionar - e não queremos que todos eles cheguem a uma mensagem com 90% de falha taxa que se converte em muitos ramos desnecessários e strcmp 's.
  • O nome da mensagem é o segundo parâmetro.
  • O primeiro ponto (nos itens 1 e 2) não é necessário, é apenas para facilitar a leitura (para pessoas, não para computadores).
  • É compatível com sscanf, iostream, you-name-it. Não há açúcar sintático que não faça nada para simplificar o processamento da mensagem.
  • Um parâmetro de cadeia: passar os tipos nativos não é mais barato em termos de requisitos de memória, porque você precisa oferecer suporte a um número desconhecido de parâmetros de tipo relativamente desconhecido.
snake5
fonte