Como conectar em rede este sistema de entidades?

33

Eu projetei um sistema de entidades para um FPS. Basicamente, funciona assim:

Temos um objeto "mundo", chamado GameWorld. Isso contém uma matriz de GameObject, bem como uma matriz do ComponentManager.

GameObject contém uma matriz de Component. Ele também fornece um mecanismo de evento que é realmente simples. Os próprios componentes podem enviar um evento para a entidade, que é transmitido para todos os componentes.

Component é basicamente algo que dá ao GameObject certas propriedades e, como o GameObject é na verdade apenas um contêiner, tudo o que tem a ver com um objeto do jogo acontece nos componentes. Exemplos incluem ViewComponent, PhysicsComponent e LogicComponent. Se a comunicação entre eles for necessária, isso poderá ser feito através do uso de eventos.

ComponentManager apenas uma interface como Component e, para cada classe Component, geralmente deve haver uma classe ComponentManager. Esses gerenciadores de componentes são responsáveis ​​por criar os componentes e inicializá-los com propriedades lidas a partir de algo como um arquivo XML.

O ComponentManager também cuida de atualizações em massa de componentes, como o PhysicsComponent, onde usarei uma biblioteca externa (que faz tudo no mundo de uma só vez).

Para configurabilidade, usarei uma fábrica para as entidades que lerão um arquivo XML ou um script, criarão os componentes especificados no arquivo (que também adiciona uma referência a ele no gerenciador de componentes certo para atualizações em massa) e injete-os em um objeto GameObject.

Agora vem o meu problema: vou tentar usar isso para jogos multiplayer. Não tenho ideia de como abordar isso.

Primeiro: quais entidades os clientes devem ter desde o início? Eu deveria começar explicando como um mecanismo para um jogador determinaria quais entidades criar.

No editor de níveis, você pode criar "pincéis" e "entidades". Escovas são para coisas como paredes, pisos e tetos, formas basicamente simples. As entidades são o GameObject de que falei. Ao criar entidades no editor de níveis, você pode especificar propriedades para cada um de seus componentes. Essas propriedades são passadas diretamente para algo como um construtor no script da entidade.

Quando você salva o nível para o mecanismo carregar, ele é decomposto em uma lista de entidades e suas propriedades associadas. Os pincéis são convertidos em uma entidade "worldspawn".

Quando você carrega esse nível, apenas instancia todas as entidades. Parece simples, não é?

Agora, para conectar em rede as entidades, encontro vários problemas. Primeiro, quais entidades devem existir no cliente desde o início? Supondo que o servidor e o cliente possuam o arquivo de nível, o cliente também pode instanciar todas as entidades no nível, mesmo se elas existirem apenas para fins de regras do jogo no servidor.

Outra possibilidade é que o cliente instancia uma entidade assim que o servidor envia informações sobre ela, e isso significa que o cliente terá apenas entidades de que precisa.

Outra questão é como enviar as informações. Eu acho que o servidor poderia usar a compactação delta, o que significa que ele só envia novas informações quando algo muda, em vez de enviar um instantâneo para o cliente em cada quadro. Embora isso signifique que o servidor deve acompanhar o que cada cliente sabe no momento.

E, finalmente, como a rede deve ser injetada no mecanismo? Estou pensando em um componente, NetworkComponent, que é injetado em todas as entidades que deveriam estar em rede. Mas como o componente de rede deve saber quais variáveis ​​devem ser acessadas e como acessá-las e, finalmente, como o componente de rede correspondente no cliente deve saber como alterar as variáveis ​​de rede?

Estou tendo um grande problema para abordar isso. Eu realmente aprecio se você me ajudou no caminho. Estou aberto a dicas sobre como melhorar o design do sistema de componentes, portanto, não tenha medo de sugerir isso.

Carter
fonte

Respostas:

13

Esta é uma maldita fera (perdão) de uma pergunta com muitos detalhes +1 lá. Definitivamente, o suficiente para ajudar as pessoas que se deparam com isso.

Eu só queria adicionar meus 2 centavos por não enviar dados de física !! Sinceramente, não posso enfatizar isso o suficiente. Mesmo que você o tenha otimizado até o momento, você pode praticamente enviar 40 esferas que oscilam com microcolisão e pode atingir a velocidade máxima em uma sala trêmula que nem diminui a taxa de quadros. Estou me referindo à execução de sua "compactação delta / codificação", também conhecida como diferenciação de dados que você discutiu. É bem parecido com o que eu ia abordar.

Reckoning morto versus diferenciação de dados: eles são diferentes o suficiente e realmente não ocupam os mesmos métodos, o que significa que você pode implementar ambos para aumentar ainda mais a otimização! Nota: Eu não os usei juntos, mas trabalhei com os dois.

Codificação delta ou diferenciação de dados: o servidor carrega dados sobre o que os clientes sabem e envia apenas as diferenças entre os dados antigos e o que deve ser alterado para o cliente. por exemplo, pseudo-> em um exemplo, você pode enviar os dados "315 435 222 3546 33" quando os dados já estiverem "310 435 210 4000 40". Alguns são apenas ligeiramente alterados e nenhum é alterado! Em vez disso, você enviaria (no delta) "5 0 12 -454 -7", que é consideravelmente mais curto.

Exemplos melhores podem ser algo que muda muito mais do que, por exemplo, digamos que eu tenha uma lista vinculada com 45 objetos vinculados agora. Quero matar 30 deles, então faço isso e depois envio a todos quais são os novos dados de pacote, o que diminuiria a velocidade do servidor se ele ainda não estivesse construído para fazer coisas como essa, e aconteceu porque estava tentando para se corrigir, por exemplo. Na codificação delta, você simplesmente colocaria (pseudo) "list.kill 30 at 5" e removeria 30 objetos da lista após o 5º e autenticaria os dados, mas em cada cliente e não no servidor.

Prós: (Só consigo pensar em um de cada agora)

  1. Velocidade: Obviamente, no meu último exemplo que descrevi. Seria uma diferença muito maior que o exemplo anterior. Em geral, não posso dizer honestamente, por experiência própria, quais seriam as mais comuns, pois trabalho muito mais com o acerto de contas morto

Contras:

  1. Se você estiver atualizando seu sistema e quiser adicionar mais alguns dados que devem ser editados pelo delta, será necessário criar novas funções para alterar esses dados! (por exemplo, como anterior "list.kill 30 às 5" Oh merda, preciso de um método de desfazer adicionado ao cliente! "list.kill undo")

Cálculo morto: Simplificando, aqui está uma analogia. Estou escrevendo um mapa para alguém sobre como chegar a um local e incluindo apenas os pontos para onde ir, em geral, porque é bom o suficiente (pare na construção, vire à esquerda). O mapa de outra pessoa inclui os nomes das ruas e também quantos graus para virar à esquerda, isso é necessário? (Não...)

O acerto de contas morto é o local em que cada cliente possui um algoritmo constante por cliente. Os dados são praticamente alterados dizendo quais dados precisam ser alterados e como fazê-lo. O cliente altera os dados por conta própria. Um exemplo é que, se eu tenho um personagem que não é meu jogador, mas está sendo movido por outra pessoa que brinca comigo, não devo atualizar os dados a cada quadro, porque muitos dados são consistentes!

Digamos que meu personagem esteja se movendo em alguma direção, muitos servidores enviarão dados para os clientes dizendo (quase por quadro) onde o jogador está e que está se movendo (por motivos de animação). São tantos dados desnecessários! Por que diabos eu preciso atualizar todos os quadros, onde a unidade está e qual direção está voltada E se está movendo? Simplificando: eu não. Você atualiza os clientes somente quando a direção muda, quando o verbo muda (isMoving = true?) E qual é o objeto! Cada cliente moverá o objeto de acordo.

Pessoalmente, essa é uma tática de bom senso. É algo que eu pensava que era inteligente ao inventar há muito tempo, que acabou sendo usado o tempo todo.

Respostas

Para ser franco, leia o post de James e leia o que eu disse sobre os dados. Sim, você definitivamente deve usar a codificação delta, mas pense também em usar o cálculo morto.

Pessoalmente, eu instanciaria os dados no cliente, quando eles recebiam informações sobre ele do servidor (algo que você sugeriu).

Somente objetos que podem mudar devem ser anotados como editáveis, certo? Eu gosto da sua ideia de incluir que um objeto deve ter dados de rede, através do seu sistema de componentes e entidades! É inteligente e deve funcionar muito bem. Mas você nunca deve fornecer pincéis (ou dados absolutamente consistentes) a qualquer método de rede. Eles não precisam disso, pois é algo que nem pode mudar (cliente para cliente).

Se for algo parecido com uma porta, eu daria dados de rede, mas apenas um booleano, aberto ou não, então obviamente que tipo de objeto é. O cliente deve saber como alterá-lo, por exemplo, é aberto, fechado, cada cliente recebe que todos devem fechá-lo, para que você altere os dados booleanos e animar a porta para ser fechada.

Quanto à forma como ele deve saber quais variáveis ​​devem ser colocadas em rede, eu posso ter um componente que é realmente um objeto SUB e fornecer componentes que você gostaria de colocar em rede. Outra idéia é não apenas ter, AddComponent("whatever")mas também AddNetComponent("and what have you")porque parece mais inteligente pessoalmente.

Joshua Hedges
fonte
Esta é uma resposta ridiculamente longa! Sinto muito por isso. Como eu pretendia fornecer apenas uma pequena quantidade de conhecimento e depois meus 2 centavos sobre algumas coisas. Então, eu entendo que muito disso pode ser um pouco desnecessário de se notar.
Joshua Hedges
3

Iria escrever um comentário, mas decidiu que isso poderia ser informação suficiente para uma resposta.

Primeiro, marque +1 em uma pergunta tão bem escrita, com muitos detalhes para julgar a resposta.

Para o carregamento de dados, o cliente carregaria o mundo a partir do arquivo mundial. Se suas entidades tiverem IDs provenientes do arquivo de dados, eu também os carregaria por padrão, para que seu sistema de rede possa se referir a eles para saber de quais objetos ele está falando. Todos que carregam os mesmos dados iniciais devem significar que todos têm os mesmos IDs para esses objetos.

Em segundo lugar, não crie um componente NetworkComponent, pois isso não faria nada além de replicar dados em outros componentes existentes (física, animação e similares são algumas coisas comuns a serem enviadas). Para usar sua própria nomeação, convém criar um NetworkComponentManager. Isso seria um pouco diferente do outro relacionamento Component para ComponentManager que você tem, mas isso pode ser instanciado quando você inicia um jogo em rede e tem qualquer tipo de componente que possua um aspecto de rede que forneça seus dados ao gerente para que possam ser empacotados e enviá-lo. É aqui que sua funcionalidade Salvar / Carregar pode ser usada se você tiver algum tipo de mecanismo de serialização / desserialização que também possa ser usado para empacotar dados para, conforme mencionado,

Dada a sua pergunta e o nível de informações, acho que não preciso entrar em muito mais detalhes, mas se algo não estiver claro, poste um comentário e atualizarei a resposta para resolver isso.

Espero que isto ajude.

James
fonte
Então, o que você está dizendo é que os componentes que devem ser conectados em rede devem implementar algum tipo de interface como essa ?: void SetNetworkedVariable (nome da string, valor NetworkedVariable); NetworkedVariable GetNetworkedVariable (nome da string); Onde NetworkedVariable é usado para fins de interpolação e outros itens de rede. Eu não sei como identificar quais componentes que implementam isso. Eu poderia usar a identificação do tipo de tempo de execução, mas isso me parece feio.
Carter