Jogos multijogador. Parte 2: predição e reconciliação

> April 26, 2021 |  Categorias:  Multiplayer   Multijogador  

No artigo anterior vimos como lidar com cheaters utilizando um modelo conhecido como arquitetura cliente-servidor. Também analisamos as limitações desse modelo, advindas das limitações existentes em nossas redes de internet.

Predição no cliente

Na implementação descrita anteriormente, o cliente não passa de um terminal burro. Ou seja, apenas envia os comandos do jogador ao servidor e fica aguardando que ele diga o que deve ser desenhado na tela. Dependendo da velocidade com que essa informação viaja, o jogo pode se tornar altamente irresponsivo.

Uma maneira de resolver esse problema é dotar o cliente de certa medida de inteligência, permitindo que ele saiba tanto quanto o servidor a respeito das regras do jogo que lhe são pertinentes – isto é, deve saber sobre a solidez dos objetos, fricção, gravidade e a lógica que produz o movimento com base nessas e tantas outras variáveis disponíveis ao desenvolvedor. Ao invés de apenas enviar comandos, o cliente utiliza de seu conhecimento para aplicá-los imediatamente, fazendo com que o personagem controlado pelo jogador mova-se a localização desejada antes de receber a confirmação do servidor.

Assumindo que a simulação seja determinística, ou seja, que os comandos do usuário produzam os mesmos efeitos localmente e no servidor, teremos uma previsão precisa dos eventos que ainda não aconteceram no servidor. Esse procedimento, conhecido como predição no cliente, ou client-side prediction, elimina os efeitos do lag da percepção do usuário, pois o jogo agora possui autonomia para decidir os resultados dos comandos do jogador. Além disso, caso estivermos lidando com um cheater, a autoridade do servidor ainda se preserva – o hacker pode manipular comandos e variáveis para desenhar o que quiser em sua tela, mas isso não afetaria o que os outros jogadores veem, pois o servidor sabe diferenciar comandos válidos de comandos inválidos.

Problemas de sincronização

O que acontece quando o jogador e o servidor discordam? Isto é, quando o resultado das previsões do cliente não se confirmam nas mensagens do servidor? Se a simulação executada pelo cliente não for precisa o suficiente, ou se as condições da rede forem adversas, lenta e certamente a posição do jogador desenhada na tela do cliente irá se distanciar das coordenadas registradas pelo servidor, criando um problema de sincronização que indubitavelmente afetará a integridade da partida.

A título de exemplo, imagine a seguinte situação: você está jogando Call of Duty e acaba de eliminar um inimigo; Você tem pouca vida restando e avista novos alvos se aproximando e então decide entrar em uma das construções por perto para se recuperar. Você já está em segurança, recarregando sua arma e, de repente, sem qualquer sinal de inimigos por perto, a tela perde a cor – você foi eliminado. O que houve? Há uma dessincronia entre você e o servidor, as predições de seu jogo não refletiram a realidade. O cliente desenhou seu personagem no lugar errado. Para seus oponentes, você está parado em campo aberto. Um alvo fácil. Para evitar essas situações desagradáveis, utilizamos uma técnica conhecida como reconciliação com o servidor, ou server reconciliation.

Reconciliação com o servidor

A reconciliação com o servidor é um conceito simples: o cliente deve sempre obedecer o servidor e corrigir suas predições com base nas informações recebidas. Sendo assim, quando o servidor envia o resultado de um comando executado pelo usuário, o cliente é obrigado a aceitar essa atualização. Isso, no entanto, cria uma situação estranha.

Devido ao tempo que as mensagens entre cliente e servidor levam para ser trocadas, a mensagem do servidor está, obrigatoriamente, desatualizada. As correções, quando recebidas pelo cliente, mostram o passado do servidor. Por exemplo, imagine que o comando enviado pelo jogador leva 100 milissegundos para chegar ao servidor. Chegando lá, o servidor faz uns cálculos com base na informação recebida e manda os resultados para o cliente, demorando mais uns 100 milissegundos para que o usuário receba a confirmação de seu movimento, fazendo com que o procedimento todo leve 200 milissegundos.

Durante esses 200 milissegundos, o jogador continuou movendo-se para frente, confiando nas predições de seu cliente. O cliente, ao receber essa informação, seguindo o princípio da reconciliação com servidor, é obrigado reverter a posição do jogador. Isto é, o personagem é imediatamente colocado na posição enviada pelo servidor em sua última atualização. Isso faz com que o usuário seja atirado ao passado, desfazendo por completo as predições executadas no cliente.

Certamente essa é uma situação bastante desconcertante para o usuário, que assistirá a um jogo condenado a indecisão, constantemente alternando entre predição e autoridade do servidor, indo para frente e para trás, para frente e para trás…

Uma solução

Felizmente, esse é um problema fácil de resolver. Basta que o cliente mantenha um histórico dos comandos enviados ao servidor. Esses comandos devem ser sempre numerados. O primeiro comando enviado é o número 1, o segundo é o número 2 e assim por diante.

A título de exemplo, vamos assumir que o jogador deseja se mover do ponto inicial x = 10 para o ponto x = 12. Digamos que esse movimento, de acordo com uma lógica definida no jogo, precisa de 2 comandos para ser executado. De modo que o comando 1 leve o personagem para o ponto x = 11 e o comando 2 leve-o até o ponto x = 12.

O cliente executa o comando número 1, faz o envio ao servidor e o armazena em seu histórico local.

Ao receber o comando número 1, o servidor faz os cálculos e envia uma resposta dizendo “esses aqui são resultados do comando de número 1”. No momento em que essa resposta chegar ao cliente, certamente o cliente já haverá enviado, previsto e guardado em seu histórico os resultados do comando número 2, fazendo com que o jogador enxergue na tela seu personagem desenhado na posição x = 12, conforme seu desejo.

Neste momento, aplicamos a reconciliação, revertendo a posição do jogador conforme a última atualização recebida. O jogador volta a posição x = 11. O cliente recorre a seu histórico – ele sabe que o jogador executou o comando número 2. Utilizando-se dessa informação, ele rapidamente executa novamente o comando armazenado, somando-o as coordenadas recebidas, fazendo com que o personagem mais uma vez se encontre na posição x = 12.

Por fim, sabendo que não precisará mais do comando número 1 em seu histórico, já que o mesmo já foi aceito pelo servidor, o cliente deleta-o de seu histórico.

Aproveitando-se da velocidade dos atuais processadores, o desenvolvedor é capaz programar o computador para realizar esse procedimento em altíssima velocidade, de modo que a percepção do jogador não seja capaz de identificar o ocorrido, sustentando a ilusão de que o movimento ocorreu em apenas uma única e fluída etapa.

Essa abordagem garante que o servidor mantenha sua autoridade sobre o estado do jogo. Levando em conta que na maior parte do tempo a simulação local é perfeitamente igual à do servidor, raramente as correções recebidas pelo cliente serão bruscas a ponto de gerar um efeito perceptível. A autoridade do servidor só se faz visível quando as condições de rede são altamente desfavoráveis, criando um efeito já conhecido por apreciadores de jogos multijogador, onde o personagem parece ser teleportado, isto é, transferido instantaneamente de um ponto ao outro do ambiente virtual. Se o jogador estiver trapaceando, também sofrerá o mesmo efeito, tornado clientes modificados totalmente inviáveis para a modalidade de cheats que permitem ao jogador ganhar vantagens na movimentação de seu personagem.

Compensando o lag

O sistema discutido fala somente em movimentação, mas a mesma técnica pode ser aplicada em muitas outras situações. O cliente pode, a desejo do usuário, prever o ataque de uma espada contra um oponente, fazendo o sangue do inimigo jorrar, mas apenas atualizando seus hitpoints após permissão explícita do servidor. Vale notar, no entanto, que as técnicas aqui apresentadas possuem limitações que devem ser observadas com cuidado pelos desenvolvedores.

É importante que o desenvolvedor esteja sempre consciente dos efeitos causados pelas limitações de velocidade das redes de internet. É papel dos programadores utilizarem-se de artimanhas para ocultar problemas inerentes a mecânicas comuns a jogos criados para um ambiente multijogador. A esse conceito dá-se o nome de compensação de lag. Se o golpe do jogador tem chances de matar o inimigo, é melhor esperar o servidor dizer que ele morreu antes de matá-lo – afinal, não queremos oponentes ressuscitando entre um golpe e outro. Faça o personagem executar uma animação exagerada, mas não jogue o no chão antes do servidor dizer que seus pontos de vida chegaram em 0.

O mesmo vale para mecânicas que podem salvar um personagem da morte certa. Se eu vejo a espada de um oponente vindo em minha direção, sei que meu escudo pode me proteger e logo pressiono um botão para executar essa ação. No entanto, o comando não chega instantaneamente no servidor. Utilizando de uma implementação ingênua, ao pressionar do botão o escudo é instantaneamente posicionado entre a lâmina e o personagem; o jogador acredita que acabou de realizar uma incrível defesa, mas a arma simplesmente passa pelo escudo, como se ele não estivesse lá, levando o personagem a morte. Ocorre que o servidor ainda não sabe que o usuário tentou se defender – a intenção do usuário foi mais rápida que a velocidade da rede.

Há várias maneiras de lidar com essa situação. Uma técnica popularizada por jogos de estratégia como Warcraft e Age of Empires é utilizar de animações de antecipação ao movimento. Nestes jogos, o movimento dos personagens é controlado pelos cliques do mouse. Quando o usuário clica no local para onde deseja se mover, o jogo mostra uma animação do local do cursor – via de regra uma seta animada – dando a impressão ao jogador de que algo está acontecendo. O movimento do personagem, no entanto, só é realizado de fato quando chega a mensagem do servidor enviando-o para seu destino.

Voltando ao exemplo do escudo, quando o jogador deseja defender-se de um ataque, devemos ao pressionar do botão de executar uma animação onde o personagem apenas inicia o movimento de defesa, mas não o completa. O escudo move-se rapidamente em direção a lâmina do oponente, mas ainda sim a impressão é de que a morte é certa. Na fração de segundo em que isso ocorre, o jogador se enche de dúvidas sobre seu destino – felizmente, a resposta do servidor chega. O cliente acelera a animação e a espada se choca com o escudo. Voam faíscas e o som de metal batendo contra o metal enche os ouvidos do usuário. Uma vitoriosa em bem-sucedida defesa. Lag devidamente compensado.

Conclusão

No próximo artigo vamos falar a respeito da interpolação de entidades, uma técnica capaz de fazer com que o movimento dos oponentes pareça ser altamente fluído, mesmo quando a rede está bem lenta e as atualizações do servidor demoram a chegar.