Estou implementando uma variante do sistema de entidades que possui:
Uma classe Entity que é pouco mais que um ID que liga componentes
Um monte de classes de componentes que não têm "lógica de componente", apenas dados
Um monte de classes de sistema (aka "subsistemas", "gerentes"). Eles fazem todo o processamento lógico da entidade. Na maioria dos casos básicos, os sistemas iteram através de uma lista de entidades nas quais estão interessados e executam uma ação em cada uma delas.
Um objeto de classe MessageChannel que é compartilhado por todos os sistemas de jogo. Cada sistema pode se inscrever em tipos específicos de mensagens para ouvir e também pode usar o canal para transmitir mensagens para outros sistemas.
A variante inicial do tratamento de mensagens do sistema era algo como isto:
- Execute uma atualização em cada sistema de jogo sequencialmente
Se um sistema faz algo com um componente e essa ação pode ser interessante para outros sistemas, o sistema envia uma mensagem apropriada (por exemplo, um sistema chama
messageChannel.Broadcast(new EntityMovedMessage(entity, oldPosition, newPosition))
sempre que uma entidade é movida)
Cada sistema que se inscreveu na mensagem específica obtém seu método de tratamento de mensagens chamado
Se um sistema estiver manipulando um evento e a lógica de processamento de eventos exigir que outra mensagem seja transmitida, a mensagem será transmitida imediatamente e outra cadeia de métodos de processamento de mensagens será chamada
Essa variante estava OK até eu começar a otimizar o sistema de detecção de colisões (estava ficando muito lento à medida que o número de entidades aumentava). A princípio, ele iterava cada par de entidades usando um algoritmo simples de força bruta. Em seguida, adicionei um "índice espacial" que possui uma grade de células que armazena entidades que estão dentro da área de uma célula específica, permitindo assim fazer verificações apenas nas entidades nas células vizinhas.
Toda vez que uma entidade se move, o sistema de colisão verifica se a entidade está colidindo com algo na nova posição. Se for, uma colisão é detectada. E se ambas as entidades que colidem são "objetos físicos" (ambas têm o componente RigidBody e se destinam a se afastar para não ocupar o mesmo espaço), um sistema rígido de separação de corpos exige que o sistema de movimento mova as entidades para alguma parte posições específicas que os separariam. Isso, por sua vez, faz com que o sistema de movimentação envie mensagens notificando sobre as posições alteradas da entidade. O sistema de detecção de colisões deve reagir porque precisa atualizar seu índice espacial.
Em alguns casos, isso causa um problema porque o conteúdo da célula (uma lista genérica de objetos de Entidade em C #) é modificado enquanto eles estão sendo repetidos, causando uma exceção a ser lançada pelo iterador.
Então ... como posso impedir que o sistema de colisão seja interrompido enquanto ele verifica colisões?
É claro que eu poderia adicionar uma lógica "inteligente" / "complicada" que garante que o conteúdo da célula seja iterado corretamente, mas acho que o problema não está no próprio sistema de colisão (eu também tive problemas semelhantes em outros sistemas), mas na maneira como as mensagens são tratadas à medida que passam de um sistema para outro. O que eu preciso é de alguma maneira de garantir que um método específico de manipulação de eventos seja executado sem interrupções.
O que eu tentei:
- Filas de mensagens recebidas . Toda vez que um sistema transmite uma mensagem, ela é adicionada às filas de mensagens dos sistemas interessados nela. Essas mensagens são processadas quando uma atualização do sistema é chamada de cada quadro. O problema : se um sistema A adicionar uma mensagem à fila B do sistema, funcionará bem se o sistema B for atualizado mais tarde do que o sistema A (no mesmo quadro de jogo); caso contrário, fará com que a mensagem processe o próximo quadro de jogo (não desejável para alguns sistemas)
- Filas de mensagens enviadas . Enquanto um sistema está manipulando um evento, todas as mensagens que ele transmite são adicionadas à fila de mensagens enviadas. As mensagens não precisam esperar que uma atualização do sistema seja processada: elas são tratadas "imediatamente" depois que o manipulador de mensagens inicial terminar seu trabalho. Se o manuseio das mensagens fizer com que outras mensagens sejam transmitidas, elas também serão adicionadas a uma fila de saída, para que todas as mensagens sejam tratadas no mesmo quadro. O problema: se o sistema de tempo de vida da entidade (implementei o gerenciamento de tempo de vida da entidade com um sistema) cria uma entidade, ele notifica alguns sistemas A e B sobre ele. Enquanto o sistema A processa a mensagem, ele causa uma cadeia de mensagens que eventualmente causam a destruição da entidade criada (por exemplo, uma entidade de bala foi criada exatamente onde colide com algum obstáculo, o que faz com que a bala se autodestrua). Enquanto a cadeia de mensagens está sendo resolvida, o sistema B não recebe a mensagem de criação da entidade. Portanto, se o sistema B também estiver interessado na mensagem de destruição da entidade, ele será recebido e somente após a conclusão da "cadeia", ele receberá a mensagem inicial de criação da entidade. Isso faz com que a mensagem de destruição seja ignorada, a mensagem de criação seja "aceita",
EDITAR - RESPOSTAS A PERGUNTAS, COMENTÁRIOS:
- Quem modifica o conteúdo da célula enquanto o sistema de colisão itera sobre eles?
Enquanto o sistema de colisão estiver realizando verificações de colisão em alguma entidade e em seus vizinhos, uma colisão pode ser detectada e o sistema da entidade enviará uma mensagem que será reagida imediatamente por outros sistemas. A reação à mensagem pode fazer com que outras mensagens sejam criadas e também tratadas imediatamente. Portanto, algum outro sistema pode criar uma mensagem que o sistema de colisão precisaria processar imediatamente (por exemplo, uma entidade movida para que o sistema de colisão precise atualizar seu índice espacial), mesmo que as verificações anteriores de colisão ainda não tenham sido concluídas.
- Você não pode trabalhar com uma fila de mensagens de saída global?
Eu tentei uma única fila global recentemente. Isso causa novos problemas. Problema: movo uma entidade de tanque para uma entidade de parede (o tanque é controlado com o teclado). Então eu decido mudar a direção do tanque. Para separar o tanque e a parede de cada quadro, o CollidingRigidBodySeparationSystem afasta o tanque da parede pela menor quantidade possível. A direção de separação deve ser a oposta à direção de movimento do tanque (quando o desenho do jogo começa, o tanque deve parecer que nunca se moveu contra a parede). Mas a direção se torna oposta à direção NEW, movendo o tanque para um lado diferente da parede do que era inicialmente. Por que o problema ocorre: É assim que as mensagens são tratadas agora (código simplificado):
public void Update(int deltaTime)
{
m_messageQueue.Enqueue(new TimePassedMessage(deltaTime));
while (m_messageQueue.Count > 0)
{
Message message = m_messageQueue.Dequeue();
this.Broadcast(message);
}
}
private void Broadcast(Message message)
{
if (m_messageListenersByMessageType.ContainsKey(message.GetType()))
{
// NOTE: all IMessageListener objects here are systems.
List<IMessageListener> messageListeners = m_messageListenersByMessageType[message.GetType()];
foreach (IMessageListener listener in messageListeners)
{
listener.ReceiveMessage(message);
}
}
}
O código flui assim (vamos supor que não seja o primeiro quadro do jogo):
- Os sistemas iniciam o processamento do TimePassedMessage
- InputHandingSystem converte pressionamentos de tecla em ação da entidade (nesse caso, uma seta esquerda se transforma em ação MoveWest). A ação da entidade é armazenada no componente ActionExecutor
- ActionExecutionSystem , em reação à ação da entidade, adiciona um MovementDirectionChangeRequestedMessage ao final da fila de mensagens
- MovementSystem move a posição da entidade com base nos dados do componente Velocity e adiciona a mensagem PositionChangedMessage ao final da fila. O movimento é feito usando a direção / velocidade do quadro anterior (digamos norte)
- Os sistemas param de processar TimePassedMessage
- Os sistemas iniciam o processamento de MovementDirectionChangeRequestedMessage
- MovementSystem altera a velocidade / direção do movimento da entidade, conforme solicitado
- Os sistemas param de processar MovementDirectionChangeRequestedMessage
- Os sistemas iniciam o processamento de PositionChangedMessage
- O CollisionDetectionSystem detecta que, porque uma entidade se moveu, ela se deparou com outra entidade (o tanque foi dentro de uma parede). Ele adiciona um CollisionOccuredMessage à fila
- Os sistemas param de processar PositionChangedMessage
- Os sistemas começam a processar o CollisionOccuredMessage
- O CollidingRigidBodySeparationSystem reage à colisão separando o tanque e a parede. Como a parede é estática, apenas o tanque é movido. A direção de movimento dos tanques é usada como um indicador de onde o tanque veio. É deslocado na direção oposta
ERRO: Quando o tanque moveu esse quadro, ele se moveu usando a direção do movimento do quadro anterior, mas quando estava sendo separado, a direção do movimento do quadro ESTE foi usada, mesmo que já fosse diferente. Não é assim que deve funcionar!
Para evitar esse bug, a antiga direção do movimento precisa ser salva em algum lugar. Eu poderia adicioná-lo a algum componente apenas para corrigir esse bug específico, mas esse caso não indica uma maneira fundamentalmente errada de lidar com mensagens? Por que o sistema de separação se importa com a direção do movimento que usa? Como posso resolver esse problema com elegância?
- Você pode ler gamadu.com/artemis para ver o que eles fizeram com o Aspects, que lado soluciona alguns dos problemas que você está vendo.
Na verdade, eu já conheço Artemis há um bom tempo. Investiguei o código fonte, li os fóruns, etc. Mas já vi "Aspectos" sendo mencionados apenas em alguns lugares e, até onde eu entendi, eles basicamente significam "Sistemas". Mas não vejo como o lado de Artemis resolve alguns dos meus problemas. Ele nem usa mensagens.
- Consulte também: "Comunicação da entidade: Fila de mensagens vs Publicar / Assinar vs Sinal / Slots"
Eu já li todas as perguntas sobre gamedev.stackexchange sobre sistemas de entidades. Este parece não discutir os problemas que estou enfrentando. Estou esquecendo de algo?
- Lide com os dois casos de maneira diferente, a atualização da grade não precisa depender das mensagens de movimento, pois faz parte do sistema de colisão
Não sei bem o que você quer dizer. Implementações mais antigas do CollisionDetectionSystem apenas checavam colisões em uma atualização (quando um TimePassedMessage era tratado), mas eu precisava minimizar as verificações o máximo possível devido ao desempenho. Então, mudei para a verificação de colisão quando uma entidade se move (a maioria das entidades no meu jogo é estática).