Estou trabalhando em um jogo 2D isométrico com multiplayer de escala moderada, aproximadamente 20 a 30 jogadores conectados ao mesmo tempo a um servidor persistente. Eu tive algumas dificuldades para implementar uma boa previsão de movimento.
Física / Movimento
O jogo não tem uma implementação física verdadeira, mas usa os princípios básicos para implementar o movimento. Em vez de pesquisar continuamente as entradas, as alterações de estado (por exemplo, eventos de mouse / down / up / move) são usadas para alterar o estado da entidade do personagem que o jogador está controlando. A direção do jogador (ie / nordeste) é combinada com uma velocidade constante e transformada em um verdadeiro vetor 3D - a velocidade da entidade.
No loop principal do jogo, "Update" é chamado antes de "Draw". A lógica de atualização aciona uma "tarefa de atualização física" que rastreia todas as entidades com velocidade diferente de zero e usa uma integração muito básica para alterar a posição das entidades. Por exemplo: entity.Position + = entity.Velocity.Scale (ElapsedTime.Seconds) (onde "Seconds" é um valor de ponto flutuante, mas a mesma abordagem funcionaria para valores inteiros em milissegundos).
O ponto principal é que nenhuma interpolação é usada para o movimento - o mecanismo de física rudimentar não tem conceito de "estado anterior" ou "estado atual", apenas posição e velocidade.
Mudança de estado e pacotes de atualização
Quando a velocidade da entidade do personagem que o jogador está controlando muda, um pacote "move avatar" é enviado ao servidor contendo o tipo de ação da entidade (stand, walk, run), direção (nordeste) e posição atual. Isso é diferente de como os jogos em primeira pessoa 3D funcionam. Em um jogo em 3D, a velocidade (direção) pode mudar de quadro para quadro conforme o jogador se move. Enviar todas as alterações de estado transmitiria efetivamente um pacote por quadro, o que seria muito caro. Em vez disso, os jogos 3D parecem ignorar as alterações de estado e enviar pacotes de "atualização de estado" em um intervalo fixo - digamos, a cada 80-150ms.
Como as atualizações de velocidade e direção ocorrem com muito menos frequência no meu jogo, posso enviar todas as alterações de estado. Embora todas as simulações de física ocorram na mesma velocidade e sejam determinísticas, a latência ainda é um problema. Por esse motivo, envio pacotes de atualização de posição de rotina (semelhantes a um jogo em 3D), mas com muito menos frequência - agora a cada 250ms, mas suspeito que com uma boa previsão, posso facilmente aumentá-lo para 500ms. O maior problema é que agora eu me desviei da norma - todas as outras documentações, guias e amostras online enviam atualizações de rotina e interpolam entre os dois estados. Parece incompatível com minha arquitetura e preciso criar um algoritmo de previsão de movimento melhor que seja mais próximo de uma arquitetura (muito básica) de "física em rede".
O servidor recebe o pacote e determina a velocidade do jogador a partir do seu tipo de movimento, com base em um script (o jogador pode executar? Obtenha a velocidade de execução do jogador). Uma vez que tenha a velocidade, combina-a com a direção para obter um vetor - a velocidade da entidade. Ocorre alguma detecção de fraude e validação básica, e a entidade no lado do servidor é atualizada com a velocidade, direção e posição atuais. A otimização básica também é executada para impedir que os jogadores inundem o servidor com solicitações de movimentação.
Após atualizar sua própria entidade, o servidor transmite um pacote "atualização da posição do avatar" para todos os outros jogadores dentro do alcance. O pacote de atualização de posição é usado para atualizar as simulações de física do lado do cliente (estado mundial) dos clientes remotos e executar a previsão e a compensação de atraso.
Previsão e compensação de atraso
Como mencionado acima, os clientes têm autoridade para sua própria posição. Exceto em casos de trapaça ou anomalias, o avatar do cliente nunca será reposicionado pelo servidor. Nenhuma extrapolação ("mover agora e corrigir depois") é necessária para o avatar do cliente - o que o jogador vê está correto. No entanto, é necessário algum tipo de extrapolação ou interpolação para todas as entidades remotas que estão se movendo. Algum tipo de previsão e / ou compensação de atraso é claramente necessário no mecanismo de simulação / física local do cliente.
Problemas
Eu tenho lutado com vários algoritmos e tenho várias perguntas e problemas:
Devo extrapolar, interpolar ou ambos? Meu "pressentimento" é que eu deveria estar usando pura extrapolação com base na velocidade. A mudança de estado é recebida pelo cliente, o cliente calcula uma velocidade "prevista" que compensa o atraso e o sistema físico regular faz o resto. No entanto, parece contrário a todos os outros exemplos de código e artigos - todos parecem armazenar vários estados e executar interpolação sem um mecanismo de física.
Quando um pacote chega, tentei interpolar a posição do pacote com a velocidade do pacote por um período de tempo fixo (por exemplo, 200 ms). Então, tomo a diferença entre a posição interpolada e a atual posição de "erro" para calcular um novo vetor e colocá-lo na entidade em vez da velocidade que foi enviada. No entanto, a suposição é de que outro pacote chegará nesse intervalo de tempo e é incrivelmente difícil "adivinhar" quando o próximo pacote chegará - especialmente porque nem todos eles chegam em intervalos fixos (ou seja, alterações de estado). O conceito é fundamentalmente falho ou está correto, mas precisa de algumas correções / ajustes?
O que acontece quando um player remoto para? Eu posso parar imediatamente a entidade, mas ela será posicionada no local "errado" até que ela se mova novamente. Se eu estimar um vetor ou tentar interpolar, tenho um problema porque não armazeno o estado anterior - o mecanismo de física não tem como dizer "você precisa parar depois de atingir a posição X". Ele simplesmente entende uma velocidade, nada mais complexo. Estou relutante em adicionar as informações de "estado de movimento de pacotes" às entidades ou ao mecanismo de física, uma vez que viola os princípios básicos de design e sangra o código de rede no restante do mecanismo de jogo.
O que deve acontecer quando as entidades colidem? Existem três cenários: o jogador controlador colide localmente, duas entidades colidem no servidor durante uma atualização de posição ou uma atualização remota de entidade colide no cliente local. Em todos os casos, não tenho certeza de como lidar com a colisão - além de trapacear, os dois estados estão "corretos", mas em períodos diferentes. No caso de uma entidade remota, não faz sentido desenhá-lo caminhando através de uma parede, então eu executo a detecção de colisão no cliente local e faço com que ele "pare". Com base no ponto 2 acima, eu poderia calcular um "vetor corrigido" que tenta continuamente mover a entidade "através da parede", que nunca será bem-sucedida - o avatar remoto fica preso até que o erro fique muito alto e "se encaixe" posição. Como os jogos resolvem isso?