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.
Você perguntou como os jogos comerciais fazem isso. ;)
fonte
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.
fonte
long long int
s ou semelhante, num sistema ECS puro.)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:
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
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.
fonte
entity_b->takeDamage();
)Por que não ter uma fila de mensagens global, algo como:
Com:
E no final do ciclo do jogo / manipulação de eventos:
Eu acho que esse é o padrão de comando. E
Execute()
é um virtual puroEvent
, no qual derivadas definem e fazem coisas. Então aqui:fonte
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.
fonte
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.
fonte
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.
fonte
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:
Processamento imediato através de uma mensagem:
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.
Enfileirando a mensagem
Novamente, isso é injusto. O jogador A deve receber os pontos de vida no mesmo turno / ciclo / tick!
fonte
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.