Como a comunicação da entidade funciona?

115

Eu tenho dois casos de usuário:

  1. Como entity_Aenviaria uma take-damagemensagem para entity_B?
  2. Como entity_Aconsultaria entity_Bo HP?

Aqui está o que eu encontrei até agora:

  • Fila de mensagens
    1. entity_Acria uma take-damagemensagem e a lança na entity_Bfila de mensagens.
    2. entity_Acria uma query-hpmensagem e a publica entity_B. entity_Bem troca, cria uma response-hpmensagem e a publica entity_A.
  • Publicar / Assinar
    1. entity_Bassina take-damagemensagens (possivelmente com alguma filtragem preventiva, para que apenas as mensagens relevantes sejam entregues). entity_Aproduz take-damagemensagem que faz referência entity_B.
    2. entity_Aassina update-hpmensagens (possivelmente filtradas). Cada quadro entity_Btransmite update-hpmensagens.
  • Sinal / Slots
    1. ???
    2. entity_Aconecta uma update-hpranhura para entity_B's update-hpde sinal.

Existe algo melhor? Tenho um entendimento correto de como esses esquemas de comunicação se vinculariam ao sistema de entidades de um mecanismo de jogo?

deft_code
fonte

Respostas:

67

Boa pergunta! Antes de chegar às perguntas específicas que você fez, direi: não subestime o poder da simplicidade. Tenpn está certo. Lembre-se de que tudo o que você está tentando fazer com essas abordagens é encontrar uma maneira elegante de adiar uma chamada de função ou desacoplar o chamador do chamado. Posso recomendar corotinas como uma maneira surpreendentemente intuitiva de aliviar alguns desses problemas, mas isso é um pouco fora de tópico. Às vezes, é melhor chamar a função e viver com o fato de que a entidade A é acoplada diretamente à entidade B. Veja YAGNI.

Dito isto, eu usei e fiquei satisfeito com o modelo de sinal / slot combinado com a simples passagem de mensagens. Usei-o em C ++ e Lua para um título de iPhone bastante bem-sucedido que tinha um cronograma muito apertado.

Para o caso do sinal / slot, se eu quiser que a entidade A faça algo em resposta a algo que a entidade B fez (por exemplo, destranque uma porta quando algo morre), posso ter a entidade A se inscrever diretamente no evento de morte da entidade B. Ou, possivelmente, a entidade A se inscreveria em cada um de um grupo de entidades, incrementaria um contador em cada evento disparado e abriria a porta após a morte de N deles. Além disso, "grupo de entidades" e "N deles" normalmente seriam definidos pelo designer nos dados do nível. (Além disso, essa é uma área em que as corotinas podem realmente brilhar, por exemplo, WaitForMultiple ("Dying", entA, entB, entC); door.Unlock ();)

Mas isso pode se tornar complicado quando se trata de reações fortemente associadas ao código C ++ ou a eventos inerentemente efêmeros do jogo: causar dano, recarregar armas, depurar, feedback de IA baseado em localização orientado pelo jogador. É aqui que a passagem de mensagens pode preencher as lacunas. Essencialmente, tudo se resume a algo como "diga a todas as entidades nesta área que sofram danos em 3 segundos" ou "sempre que você completar a física para descobrir quem eu atirei, diga a eles para executar esta função de script". É difícil descobrir como fazer isso bem usando publicação / assinatura ou sinal / slot.

Isso pode facilmente ser um exagero (versus o exemplo da tenpn). Também pode ser um inchaço ineficiente se você tiver muita ação. Mas, apesar de suas desvantagens, essa abordagem de "mensagens e eventos" combina muito bem com o código do jogo com script (por exemplo, em Lua). O código de script pode definir e reagir a suas próprias mensagens e eventos sem que o código C ++ seja cuidadoso. E o código de script pode facilmente enviar mensagens que acionam o código C ++, como alterar níveis, tocar sons ou até mesmo deixar uma arma definir quanto dano a mensagem TakeDamage causa. Isso me salvou uma tonelada de tempo, porque eu não estava tendo que constantemente brincar com luabind. E isso me permitiu manter todo o meu código luabind em um só lugar, porque não havia muito dele. Quando acoplado corretamente,

Além disso, minha experiência com o caso de uso nº 2 é que é melhor lidar com isso como um evento na outra direção. Em vez de perguntar qual é a saúde da entidade, dispare um evento / envie uma mensagem sempre que a saúde fizer uma alteração significativa.

Em termos de interfaces, btw, acabei com três classes para implementar tudo isso: EventHost, EventClient e MessageClient. Os EventHosts criam slots, os EventClients se inscrevem / se conectam a eles e os MessageClients associam um delegado a uma mensagem. Observe que o destino delegado de um MessageClient não precisa necessariamente ser o mesmo objeto que possui a associação. Em outras palavras, os MessageClients podem existir apenas para encaminhar mensagens para outros objetos. FWIW, a metáfora host / cliente é meio inapropriada. Source / Sink podem ser melhores conceitos.

Desculpe, eu meio que divaguei lá. É a minha primeira resposta :) Espero que faça sentido.

BRaffle
fonte
Obrigado pela resposta. Ótimas idéias. A razão pela qual eu acabei de projetar a passagem de mensagens é por causa de Lua. Eu gostaria de poder criar novas armas sem o novo código C ++. Então, seus pensamentos responderam a algumas das minhas perguntas não feitas.
Deft_code 25/07/10
Quanto às corotinas, eu também acredito muito nas corotinas, mas nunca brinco com elas em C ++. Eu tinha uma vaga esperança de usar corotinas no código lua para lidar com o bloqueio de chamadas (por exemplo, esperar pela morte). Valeu a pena o esforço? Receio estar cego pelo meu intenso desejo por corotinas em c ++.
Deft_code 25/07/10
Por fim, qual foi o jogo para iphone? Posso obter mais informações sobre o sistema de entidades que você usou?
Deft_code 25/07/10
2
O sistema de entidades estava principalmente em C ++. Portanto, havia, por exemplo, uma classe Imp que lidava com o comportamento do Imp. Lua pode alterar os parâmetros do Imp no spawn ou via mensagem. O objetivo com Lua era ajustar-se a um cronograma apertado, e a depuração do código Lua é muito demorada. Usamos Lua para scripts de níveis (quais entidades vão para onde, eventos que acontecem quando você pressiona os gatilhos). Então, em Lua, diríamos coisas como SpawnEnt ("Imp"), em que Imp é uma associação de fábrica registrada manualmente. Sempre geraria um pool global de entidades. Agradável e simples. Usamos muitos smart_ptr e fraco_ptr.
BRaffle
1
Então BananaRaffle: Você diria que este é um resumo preciso de sua resposta: "Todas as três soluções que você postou têm seus usos, assim como outras. Não procure a solução perfeita, use apenas o que você precisa, onde faz sentido . "
Ipsquiggle 8/09/10
76
// in entity_a's code:
entity_b->takeDamage();

Você perguntou como os jogos comerciais fazem isso. ;)

tenpn
fonte
8
Um voto negativo? Sério, é assim que é feito normalmente! Os sistemas de entidades são ótimos, mas não ajudam a atingir os marcos iniciais.
Dezpn
Faço jogos em Flash profissionalmente, e é assim que faço. Você chama enemy.damage (10) e, em seguida, procura todas as informações necessárias de public getters.
Iain
7
Isso é sério, como os motores de jogos comerciais fazem isso. Ele não está brincando. Target.NotifyTakeDamage (DamageType, DamageAmount, DamageDealer, etc.) geralmente é como funciona.
AA Grapsas
3
Os jogos comerciais também escrevem incorretamente o "dano"? :-P
Ricket
15
Sim, eles causam erros de digitação incorreta, entre outras coisas. :)
LearnCocos2D
17

Uma resposta mais séria:

Já vi quadros usados ​​muito. Versões simples nada mais são do que estruturas que são atualizadas com coisas como o HP de uma entidade, que essas entidades podem consultar.

Seus quadros-negros podem ser a visão mundial desta entidade (pergunte ao quadro-negro de B qual é o seu HP) ou a visão de mundo de uma entidade (A consulta seu quadro-negro para ver qual é o alvo do HP de A).

Se você atualizar apenas os quadros-negros em um ponto de sincronização no quadro, poderá lê-los posteriormente em qualquer encadeamento, facilitando a implementação de multithreading.

Lousas mais avançadas podem ser mais como tabelas de hash, mapeando seqüências de caracteres para valores. Isso é mais sustentável, mas obviamente tem um custo de tempo de execução.

Tradicionalmente, um quadro-negro é apenas uma comunicação unidirecional - não lidaria com a perda de danos.

tenpn
fonte
Eu nunca tinha ouvido falar do modelo de quadro-negro antes.
Deft_code 22/07/10
Eles também são bons para reduzir dependências, da mesma forma que uma fila de eventos ou modelo de publicação / assinatura.
22410 dezpn
2
Essa é também a "definição" canônica de como um sistema E / C / S "ideal" deve "funcionar". Os componentes formam a lousa; os sistemas são o código que atua sobre ele. (Entidades, é claro, são apenas long long ints ou semelhante, num sistema ECS puro.)
BRPocock
6

Estudei um pouco esse problema e vi uma boa solução.

Basicamente, é tudo sobre subsistemas. É semelhante à idéia do quadro-negro mencionada por tenpn.

As entidades são feitas de componentes, mas são apenas bolsas de propriedade. Nenhum comportamento é implementado nas próprias entidades.

Digamos que as entidades tenham um componente de Saúde e um componente de Dano.

Então você tem algum MessageManager e três subsistemas: ActionSystem, DamageSystem, HealthSystem. A certa altura, o ActionSystem faz seus cálculos no mundo do jogo e gera um evento:

HIT, source=entity_A target=entity_B power=5

Este evento é publicado no MessageManager. Agora, em um determinado momento, o MessageManager percorre as mensagens pendentes e descobre que o DamageSystem se inscreveu nas mensagens HIT. Agora, o MessageManager entrega a mensagem HIT ao DamageSystem. O DamageSystem percorre sua lista de entidades que possuem componente de Dano, calcula os pontos de dano dependendo da potência de impacto ou de algum outro estado de ambas as entidades etc. e publica eventos

DAMAGE, source=entity_A target=entity_B amount=7

O HealthSystem se inscreveu nas mensagens DAMAGE e agora, quando o MessageManager publica a mensagem DAMAGE no HealthSystem, o HealthSystem tem acesso às entidades entity_A e entity_B com seus componentes Health, então novamente o HealthSystem pode fazer seus cálculos (e talvez publicar o evento correspondente para o MessageManager).

Nesse mecanismo de jogo, o formato das mensagens é o único acoplamento entre todos os componentes e subsistemas. Os subsistemas e entidades são completamente independentes e desconhecem um ao outro.

Não sei se algum mecanismo de jogo real implementou ou não essa ideia, mas parece bastante sólido e limpo e espero que algum dia eu o implemente no meu mecanismo de jogo de nível amador.

JustAMartin
fonte
Esta é uma resposta muito melhor do que a resposta aceita pela IMO. Dissociado, sustentável e extensível (e também não um desastre de acoplamento como a resposta piada entity_b->takeDamage();)
Danny Yaroslavski
4

Por que não ter uma fila de mensagens global, algo como:

messageQueue.push_back(shared_ptr<Event>(new DamageEvent(entityB, 10, entityA)));

Com:

DamageEvent(Entity* toDamage, uint amount, Entity* damageDealer);

E no final do ciclo do jogo / manipulação de eventos:

while(!messageQueue.empty())
{
    Event e = messageQueue.front();
    messageQueue.pop_front();
    e.Execute();
}

Eu acho que esse é o padrão de comando. E Execute()é um virtual puro Event, no qual derivadas definem e fazem coisas. Então aqui:

DamageEvent::Execute() 
{
    toDamage->takeDamage(amount); // Or of course, you could now have entityA get points, or a recognition of damage, or anything.
}
O Pato Comunista
fonte
3

Se o seu jogo for para um jogador, basta usar o método de objetos de destino (como sugerido pela tenpn).

Se você é (ou deseja oferecer suporte) a vários jogadores (multicliente para ser exato), use uma fila de comandos.

  • Quando A causa dano a B no cliente 1, basta colocar na fila o evento de dano.
  • Sincronize as filas de comandos pela rede
  • Manipule os comandos na fila de ambos os lados.
Andreas
fonte
2
Se você é sério em evitar trapacear, A não prejudica B no cliente. O cliente que possui A envia um comando "ataque B" ao servidor, que faz exatamente o que a tenpn disse; o servidor sincroniza esse estado com todos os clientes relevantes.
@ Joe: Sim, se houver um servidor que seja um ponto válido a considerar, mas às vezes não há problema em confiar no cliente (por exemplo, em um console) para evitar uma carga pesada do servidor.
Andreas
2

Eu diria: Não use nenhum, contanto que você não precise explicitamente de feedback instantâneo do dano.

A entidade / componente / responsável pelo dano deve enviar os eventos a uma fila de eventos local ou a um sistema em um nível igual que contenha eventos de dano.

Deve haver um sistema de sobreposição com acesso às duas entidades que solicita os eventos da entidade a e os passa para a entidade b. Ao não criar um sistema geral de eventos que qualquer coisa possa usar de qualquer lugar para passar um evento para qualquer coisa a qualquer momento, você cria um fluxo de dados explícito que sempre torna o código mais fácil de depurar, mais fácil de medir o desempenho, mais fácil de entender e ler e frequentemente leva a um sistema mais bem projetado em geral.

Simon
fonte
1

Apenas faça a ligação. Não faça request-hp seguido por query-hp - se você seguir esse modelo, terá um mundo de mágoas.

Você também pode querer dar uma olhada nas Mono Continuações. Eu acho que seria ideal para os NPCs.

James Bellinger
fonte
1

Então, o que acontece se tivermos os jogadores A e B tentando se acertar no mesmo ciclo de atualização ()? Suponha que a Atualização () do jogador A ocorra antes da Atualização () do jogador B no Ciclo 1 (ou marque, ou o que você chamar). Há dois cenários em que posso pensar:

  1. Processamento imediato através de uma mensagem:

    • o jogador A.Update () vê que o jogador deseja acertar B, o jogador B recebe uma mensagem notificando o dano.
    • o jogador B.HandleMessage () atualiza os pontos de vida do jogador B (ele morre)
    • o jogador B.Update () vê o jogador B morto. ele não pode atacar o jogador A

Isso é injusto, os jogadores A e B devem bater um no outro, o jogador B morreu antes de bater em A apenas porque essa entidade / objeto de jogo foi atualizado () mais tarde.

  1. Enfileirando a mensagem

    • O jogador A.Update () vê que o jogador deseja acertar B, o jogador B recebe uma mensagem notificando o dano e o armazena em uma fila
    • O jogador A.Update () verifica sua fila, está vazia
    • o jogador B.Update () verifica primeiro os movimentos, para que o jogador B envie uma mensagem ao jogador A com danos também
    • O jogador B.Update () também lida com as mensagens na fila, processa os danos do jogador A
    • Novo ciclo (2): O jogador A quer beber uma poção de vida, para que o jogador A.Update () seja chamado e a jogada seja processada
    • O jogador A.Update () verifica a fila de mensagens e processa os danos do jogador B

Novamente, isso é injusto. O jogador A deve receber os pontos de vida no mesmo turno / ciclo / tick!


fonte
4
Você não está realmente respondendo à pergunta, mas acho que sua resposta seria uma excelente pergunta. Por que não ir em frente e perguntar como resolver uma priorização "injusta"?
bummzack
Duvido que a maioria dos jogos se importe com essa injustiça, porque é atualizada com tanta frequência que raramente é um problema. Uma solução simples é alternar entre iterar para frente e para trás na lista de entidades durante a atualização.
Kylotan
Eu uso 2 chamadas, então chamo Update () para todas as entidades, depois do loop eu itero novamente e chamo algo como pEntity->Flush( pMessages );. Quando entity_A gera um novo evento, ele não é lido por entity_B nesse quadro (ele também tem a chance de tomar a poção), então ambos recebem dano e depois processam a mensagem da cura da poção que seria a última na fila . O jogador B ainda morre de qualquer maneira, pois a mensagem da poção é a última da fila: P, mas pode ser útil para outros tipos de mensagens, como limpar ponteiros para entidades mortas.
Pablo Ariel
Eu acho que nos níveis de quadros, a maioria das implementações de jogos é simplesmente injusta. como Kylotan disse.
v.oddou
Este problema é incrivelmente fácil de resolver. Basta aplicar o dano um ao outro nos manipuladores de mensagens ou o que for. Você definitivamente não deve sinalizar o jogador como morto dentro do manipulador de mensagens. Em "Update ()" você simplesmente faz "if (hp <= 0) die ();" (no início de "Update ()", por exemplo). Dessa forma, ambos podem se matar ao mesmo tempo. Além disso: muitas vezes você não danifica o jogador diretamente, mas através de algum objeto intermediário, como uma bala.
Tara20