Atirador de rede do navegador no Node.js

O desenvolvimento de jogos multiplayer é complicado por vários motivos: a hospedagem pode ser cara, a estrutura não é óbvia e a implementação é difícil. Neste tutorial, tentarei ajudá-lo a superar a última barreira.

Este artigo é destinado a desenvolvedores que podem criar jogos e conhecem o JavaScript, mas nunca criaram jogos on-line para vários jogadores. Depois de concluir este tutorial, você dominará a implementação dos componentes básicos de rede no seu jogo e poderá desenvolvê-lo em algo mais! Aqui está o que vamos criar:


Você pode jogar o jogo terminado aqui ! Quando você pressiona as teclas W ou "para cima", a nave se aproxima do cursor; quando você clica no mouse, ele dispara. (Se ninguém estiver online, verifique o funcionamento do multiplayer, abra duas janelas do navegador em um computador ou uma delas no telefone). Se você deseja executar o jogo localmente, o código fonte completo está disponível no GitHub .

Ao criar o jogo, usei os recursos gráficos do Pirate Pack da Kenney e da estrutura do jogo Phaser . Neste tutorial, você recebe a função de programador de rede. O ponto de partida será uma versão totalmente funcional do jogo para usuário único, e nossa tarefa será escrever um servidor no Node.js usando o Socket.io para a parte da rede. Para não sobrecarregar o tutorial, vou me concentrar nas partes relacionadas ao multiplayer e pular conceitos relacionados ao Phaser e Node.js.

Você não precisa configurar nada localmente, porque criaremos este jogo completamente no navegador no Glitch.com ! O Glitch é uma ferramenta incrível para criar aplicativos da Web, incluindo back-ends, bancos de dados e muito mais. É ótimo para criação de protótipos, treinamento e colaboração, e ficarei muito feliz em apresentar seus recursos neste tutorial.

Vamos começar.

1. Preparação


Publiquei o rascunho do projeto no Glitch.com .

Dicas de interface: Você pode iniciar a visualização do aplicativo clicando no botão Mostrar (canto superior esquerdo).


A barra lateral vertical à esquerda contém todos os arquivos do aplicativo. Para editar este aplicativo, você deve criar seu "remix". Portanto, criaremos uma cópia dele em nossa conta (ou "fork" no jargão git). Clique no botão Remixar este botão.


Neste ponto, você está editando o aplicativo em uma conta anônima. Para salvar seu trabalho, você pode fazer login (canto superior direito).

Agora, antes de prosseguir, é importante que você se familiarize com o jogo no qual adicionaremos o modo multiplayer. Dê uma olhada no index.html . Ele tem três funções importantes que você precisa conhecer: preload (linha 99), create (linha 115) e GameLoop (linha 142), bem como o objeto do jogador (linha 35).

Se você prefere aprender praticando, entenda o trabalho do jogo concluindo as seguintes tarefas:

  • Aumente o tamanho do mundo (linha 29) - observe que existe um tamanho de mundo separado para o mundo do jogo e um tamanho de janela para a própria tela da página .
  • Torne possível avançar com a ajuda do “espaço” (linha 53).
  • Mude o tipo de navio do jogador (linha 129).
  • Diminua o movimento das conchas (linha 155).

Instale o Socket.io


O Socket.io é uma biblioteca para gerenciar comunicações em tempo real dentro de um navegador usando o WebSockets (em vez de usar protocolos como o UDP, que são usados ​​para criar jogos multiplayer clássicos). Além disso, a biblioteca possui maneiras redundantes de garantir a operação, mesmo quando o WebSockets não é suportado. Ou seja, ela lida com protocolos de mensagens e permite o uso de um sistema conveniente de mensagens com base em eventos.

A primeira coisa que precisamos fazer é instalar o módulo Socket.io. No Glitch, isso pode ser feito acessando o arquivo package.json e, em seguida, inserindo o módulo necessário nas dependências ou clicando em Adicionar pacote e inserindo “socket.io”.


Agora é a hora certa para lidar com os logs do servidor. Clique no botão Logs à esquerda para abrir o log do servidor. Você deve ver que ele instala o Socket.io com todas as suas dependências. É aqui que você precisa procurar todos os erros e a saída do código do servidor.


Agora vamos para o server.js . É aqui que nosso código do servidor está localizado. Até o momento, há apenas algum código básico para servir nosso HTML. Adicione uma linha na parte superior do arquivo para habilitar o Socket.io:

 var io = require('socket.io')(http); //     http 

Agora também precisamos habilitar o Socket.io no cliente, então vamos voltar ao index.html e adicionar as seguintes linhas dentro da tag <head> :

 <!--    Socket.io --> <script src="/socket.io/socket.io.js"></script> 

Nota: O Socket.io processa automaticamente o carregamento da biblioteca do cliente nesse caminho, portanto, esta linha funciona mesmo se não houver diretório /socket.io/ em suas pastas.

Agora o Socket.io está incluído no projeto e está pronto para começar!

2. Reconhecimento e desova de jogadores


Nosso primeiro passo real será aceitar conexões no servidor e criar novos players no cliente.

Aceitando conexões do servidor


Adicione este código à parte inferior do server.js :

 //  Socket.io    io.on('connection', function(socket){ console.log("New client has connected with id:",socket.id); }) 

Por isso, pedimos ao Socket.io para ouvir todos os eventos de connection que ocorrem automaticamente quando um cliente se conecta. A biblioteca cria um novo objeto de socket para cada cliente, em que socket.id é o identificador exclusivo desse cliente.

Para verificar se isso funciona, retorne ao cliente ( index.html ) e adicione esta linha em algum lugar da função de criação :

 var socket = io(); //    'connection'   

Se você iniciar o jogo e observar o log do servidor (clique no botão Logs ), verá que o servidor registrou esse evento de conexão!

Agora, ao conectar um novo jogador, esperamos que ele nos forneça informações sobre seu estado. No nosso caso, precisamos saber pelo menos x , y e ângulo para criá-lo corretamente no ponto certo.

O evento de connection foi um evento embutido acionado pelo Socket.io. Podemos ouvir qualquer evento definido independentemente. Vou nomear meu evento como new-player e espero que o cliente o envie assim que ele se conectar com informações sobre sua posição. Ficará assim:

 //  Socket.io    io.on('connection', function(socket){ console.log("New client has connected with id:",socket.id); socket.on('new-player',function(state_data){ //   new-player    console.log("New player has state:",state_data); }) }) 

Se você executar esse código, até ver qualquer coisa no log do servidor, porque ainda não dissemos ao cliente para gerar esse evento para new-player . Mas vamos fingir por um momento que já fizemos isso e continuar trabalhando no servidor. O que deve acontecer depois de obter a localização de um novo jogador que entra?

Podemos enviar uma mensagem a todos os outros jogadores conectados, para que eles saibam que um novo jogador apareceu. O Socket.io possui uma função conveniente para isso:

 socket.broadcast.emit('create-player',state_data); 

Quando socket.emit chamado socket.emit mensagem é simplesmente passada para esse único cliente. Quando socket.broadcast.emit é chamado socket.broadcast.emit ele é enviado a todos os clientes conectados ao servidor, exceto em cujo soquete essa função foi chamada.

A função io.emit envia uma mensagem para cada cliente conectado ao servidor sem exceções. Em nosso esquema, não precisamos disso, porque se recebermos uma mensagem do servidor nos pedindo para criar nossa própria nave, receberemos uma duplicata do sprite, porque já criamos nossa própria nave quando o jogo começou. Aqui está uma dica útil sobre os vários tipos de recursos de mensagens que usaremos neste tutorial.

O código do servidor agora deve ficar assim:

 //  Socket.io    io.on('connection', function(socket){ console.log("New client has connected with id:",socket.id); socket.on('new-player',function(state_data){ //   new-player    console.log("New player has state:",state_data); socket.broadcast.emit('create-player',state_data); }) }) 

Ou seja, toda vez que um jogador se conectar, esperamos que ele nos envie uma mensagem com informações sobre sua localização, e enviamos esses dados a todos os outros jogadores para que eles possam criar seu sprite.

Criação de cliente


Agora, para concluir esse ciclo, precisamos executar duas ações no cliente:

  1. Gere uma mensagem com os dados da nossa localização após a conexão.
  2. Ouça os eventos create-player e crie um player neste momento.

Para executar a primeira ação após a criação de um player na função de criação (aproximadamente na linha 135), podemos gerar uma mensagem contendo os dados de localização que precisamos enviar:

 socket.emit('new-player',{x:player.sprite.x,y:player.sprite.y,angle:player.sprite.rotation}) 

Não precisamos nos preocupar em serializar os dados que estão sendo enviados. Você pode transferi-los para qualquer tipo de objeto, e o Socket.io o processará para nós.

Antes de prosseguir, teste o código . Deveríamos ver uma mensagem semelhante nos logs do servidor:

 New player has state: { x: 728.8180247836519, y: 261.9979387913289, angle: 0 } 

Agora sabemos que nosso servidor recebe uma notificação sobre a conexão de um novo player e lê corretamente os dados sobre sua localização!

Em seguida, queremos ouvir as solicitações para criar um novo player. Podemos colocar esse código imediatamente após a geração da mensagem, deve ficar assim:

 socket.on('create-player',function(state){ // CreateShip -      ,     CreateShip(1,state.x,state.y,state.angle) }) 

Agora teste o código . Abra duas janelas com o jogo e verifique se ele funciona.

Você deve ver que, após a abertura de dois clientes, o primeiro cliente possui duas naves criadas e o segundo possui apenas uma.

Tarefa: você pode descobrir por que isso aconteceu? Ou como você pode consertar isso? Passo a passo, siga a lógica do cliente / servidor que escrevemos e tente depurá-la.

Espero que você tenha tentado descobrir por conta própria! O seguinte acontece: quando o primeiro jogador se conecta, o servidor envia um evento de create-player para todos os outros jogadores, mas ainda não há jogadores que possam recebê-lo. Depois de conectar o segundo jogador, o servidor envia suas mensagens novamente e o primeiro jogador o recebe e cria o sprite corretamente, enquanto o segundo jogador perdeu a mensagem do primeiro jogador.

Ou seja, o problema é que o segundo jogador se conecta ao jogo mais tarde e ele precisa saber o estado do jogo. Devemos informar a todos os novos jogadores que já existem (assim como outros eventos que ocorreram no mundo) para que eles possam se orientar. Antes de resolvermos esse problema, tenho um breve aviso.

Aviso de Sincronização de Status do Jogo


Existem duas abordagens para implementar a sincronização de todos os players. O primeiro é enviar uma quantidade mínima de informações sobre as alterações que ocorreram na rede. Ou seja, toda vez que um novo jogador estiver conectado, enviaremos a todos os outros jogadores apenas informações sobre esse novo jogador (e enviaremos uma lista de todos os outros jogadores do mundo para esse novo jogador) e, após desconectar, informaremos todos os jogadores que esse jogador em particular foi desconectado.

A segunda abordagem é transmitir todo o estado do jogo. Nesse caso, sempre que você se conecta ou desconecta, enviamos a todos uma lista completa de todos os jogadores.

A primeira abordagem é melhor, pois minimiza a quantidade de informações transmitidas pela rede, mas pode ser muito difícil de implementar e tem a probabilidade de os jogadores ficarem fora de sincronia. O segundo garante que os jogadores estejam sempre sincronizados, mas cada mensagem terá que enviar mais dados.

No nosso caso, em vez de tentar enviar mensagens quando um jogador está conectado para criá-lo e quando desconectado para excluí-lo, bem como quando se move para atualizar sua posição, podemos combinar tudo isso em um evento de update comum. Esse evento de atualização sempre envia as posições de cada jogador para todos os clientes. É isso que o servidor deve fazer. A tarefa do cliente é manter a conformidade do mundo com o estado recebido.

Para implementar esse esquema, farei o seguinte:

  1. Manterei um dicionário de jogadores, cuja chave será o ID deles e o valor serão os dados de sua localização.
  2. Adicione um player a este dicionário quando estiver conectado e envie um evento de atualização.
  3. Remova o player deste dicionário quando estiver desligado e envie um evento de atualização.

Você pode tentar implementar esse sistema você mesmo, porque essas etapas são bastante simples ( minha dica de recurso pode ser útil aqui). Aqui está a aparência da implementação completa:

 //  Socket.io    // 1 -      / var players = {}; io.on('connection', function(socket){ console.log("New client has connected with id:",socket.id); socket.on('new-player',function(state_data){ //   new-player    console.log("New player has state:",state_data); // 2 -      players[socket.id] = state_data; //    io.emit('update-players',players); }) socket.on('disconnect',function(){ // 3-       delete players[socket.id]; //    }) }) 

O lado do cliente é um pouco mais complicado. Por um lado, agora devemos nos preocupar apenas com o evento update-players , mas, por outro lado, devemos considerar criar novas naves se o servidor enviar mais naves do que sabemos, ou excluir se houver muitas delas.

É assim que eu manejo esse evento no cliente:

 //     // : -         other_players = {} socket.on('update-players',function(players_data){ var players_found = {}; //        for(var id in players_data){ //      if(other_players[id] == undefined && id != socket.id){ // ,      var data = players_data[id]; var p = CreateShip(1,data.x,data.y,data.angle); other_players[id] = p; console.log("Created new player at (" + data.x + ", " + data.y + ")"); } players_found[id] = true; //     if(id != socket.id){ other_players[id].x = players_data[id].x; //  ,    ,      other_players[id].y = players_data[id].y; other_players[id].rotation = players_data[id].angle; } } //       for(var id in other_players){ if(!players_found[id]){ other_players[id].destroy(); delete other_players[id]; } } }) 

No lado do cliente, eu armazeno os navios no dicionário other_players , que acabei de definir na parte superior do script (não é mostrado aqui). Como o servidor envia dados do jogador para todos os jogadores, devo adicionar uma verificação para que o cliente não crie um sprite extra para si. (Se você tiver problemas com a estruturação, aqui está o código completo que deve estar em index.html no momento).

Agora teste o código . Você deve poder criar vários clientes e ver o número correto de navios criados nas posições corretas!

3. Sincronização das posições dos navios


Aqui começa uma parte muito interessante. Queremos sincronizar as posições dos navios em todos os clientes. Isso revelará a simplicidade da estrutura que criamos no momento. Já temos um evento de atualização que pode sincronizar os locais de todos os navios. Basta que façamos o seguinte:

  1. Forçar o cliente a gerar uma mensagem cada vez que ele se move para uma nova posição.
  2. Ensine o servidor a ouvir essa mensagem de movimentação e atualize o elemento de dados do player no dicionário do players .
  3. Gere um evento de atualização para todos os clientes.

E isso deve ser o suficiente! Agora é sua vez de tentar implementar isso sozinho.

Se você está completamente confuso e precisa de uma dica, observe o projeto finalizado .

Nota sobre como minimizar os dados transmitidos pela rede


A maneira mais direta de implementá-lo é atualizar as posições de todos os jogadores cada vez que um evento de movimento é recebido de qualquer jogador. É ótimo que os jogadores sempre obtenham as informações mais recentes imediatamente após elas aparecerem, mas o número de mensagens transmitidas pela rede pode aumentar facilmente para centenas por quadro. Imagine que você tem 10 jogadores, cada um dos quais envia uma mensagem de movimento em cada quadro. O servidor deve encaminhá-los de volta para todos os 10 jogadores. Já são 100 mensagens por quadro!

Seria melhor fazer isso: espere até o servidor receber todas as mensagens de todos os jogadores e envie a todos os jogadores uma grande atualização contendo todas as informações. Assim, reduziremos o número de mensagens transmitidas para o número de usuários presentes no jogo (em vez do quadrado desse número). O problema aqui é que todos os usuários terão o mesmo atraso do player com a conexão mais lenta.

Outra solução é enviar as atualizações do servidor a uma frequência constante, independentemente do número de mensagens recebidas do player. Um padrão comum é atualizar o servidor aproximadamente 30 vezes por segundo.

No entanto, ao escolher a estrutura do seu servidor, você deve avaliar o número de mensagens transmitidas em cada quadro nos estágios iniciais do desenvolvimento do jogo.

4. Sincronização de shell


Estamos quase terminando! A última parte séria é a sincronização em uma rede de conchas. Podemos implementá-lo da mesma maneira que os players sincronizados:

  • Cada cliente envia as posições de todas as suas conchas em cada quadro.
  • O servidor os redireciona para cada jogador.

Mas há um problema.

Proteção contra trapaça


Se você redirecionar tudo o que o cliente transmite como as verdadeiras posições dos projéteis, o jogador pode trapacear facilmente, modificando seu cliente e transmitindo dados falsos para você, por exemplo, projéteis se teletransportando para as posições dos navios. Você pode verificar isso facilmente fazendo o download da página da web, alterando o código para JavaScript e abrindo-o novamente. E isso é um problema não apenas para jogos de navegador. No caso geral, nunca podemos confiar nos dados provenientes do usuário.

Para lidar parcialmente com esse problema, tentaremos usar outro esquema:

  • O cliente gera uma mensagem sobre o shell de tiro com sua posição e direção.
  • O servidor simula o movimento das conchas.
  • O servidor atualiza os dados de cada cliente, passando a posição de todos os shells.
  • Os clientes processam conchas nas posições recebidas do servidor.

Assim, o cliente é responsável pela posição do projétil, mas não por sua velocidade e nem por seu movimento adicional. O cliente pode alterar a posição dos shells por si mesmo, mas isso não altera o que os outros clientes veem.

Para implementar esse esquema, adicionaremos a geração de mensagens quando acionadas. Não vou mais criar o próprio sprite, porque sua existência e localização serão inteiramente determinadas pelo servidor. Agora, nosso novo tiro de projétil em index.html ficará assim:

 //   if(game.input.activePointer.leftButton.isDown && !this.shot){ var speed_x = Math.cos(this.sprite.rotation + Math.PI/2) * 20; var speed_y = Math.sin(this.sprite.rotation + Math.PI/2) * 20; /*    ,       ,       var bullet = {}; bullet.speed_x = speed_x; bullet.speed_y = speed_y; bullet.sprite = game.add.sprite(this.sprite.x + bullet.speed_x,this.sprite.y + bullet.speed_y,'bullet'); bullet_array.push(bullet); */ this.shot = true; //  ,     socket.emit('shoot-bullet',{x:this.sprite.x,y:this.sprite.y,angle:this.sprite.rotation,speed_x:speed_x,speed_y:speed_y}) } 

Agora também podemos comentar todo o fragmento de código atualizando os shells no cliente:

 /*     ,         //   for(var i=0;i<bullet_array.length;i++){ var bullet = bullet_array[i]; bullet.sprite.x += bullet.speed_x; bullet.sprite.y += bullet.speed_y; //  ,       if(bullet.sprite.x < -10 || bullet.sprite.x > WORLD_SIZE.w || bullet.sprite.y < -10 || bullet.sprite.y > WORLD_SIZE.h){ bullet.sprite.destroy(); bullet_array.splice(i,1); i--; } } */ 

Finalmente, precisamos que o cliente escute as atualizações do shell. Decidi implementar isso da mesma maneira que com os jogadores, ou seja, o servidor simplesmente envia uma matriz de todas as posições de shell em um evento chamado bullets-update , e o cliente cria ou destrói shells para manter a sincronização. Aqui está o que parece:

 //     socket.on('bullets-update',function(server_bullet_array){ //     ,   for(var i=0;i<server_bullet_array.length;i++){ if(bullet_array[i] == undefined){ bullet_array[i] = game.add.sprite(server_bullet_array[i].x,server_bullet_array[i].y,'bullet'); } else { //      ! bullet_array[i].x = server_bullet_array[i].x; bullet_array[i].y = server_bullet_array[i].y; } } //    ,   for(var i=server_bullet_array.length;i<bullet_array.length;i++){ bullet_array[i].destroy(); bullet_array.splice(i,1); i--; } }) 

E é tudo o que deve estar no cliente. Suponho que você já saiba onde incorporar esses fragmentos de código e como agrupá-los, mas se tiver algum problema, poderá sempre olhar o resultado final .

Agora no server.js, precisamos rastrear e simular shells. Primeiro, criaremos uma matriz para rastrear conchas, semelhante a uma matriz para jogadores:

 var bullet_array = []; //         

Em seguida, ouvimos o evento de tiro com projétil:

 //   shoot-bullet        socket.on('shoot-bullet',function(data){ if(players[socket.id] == undefined) return; var new_bullet = data; data.owner_id = socket.id; //    id  bullet_array.push(new_bullet); }); 

Agora simulamos cascas 60 vezes por segundo:

 //   60       function ServerGameLoop(){ for(var i=0;i<bullet_array.length;i++){ var bullet = bullet_array[i]; bullet.x += bullet.speed_x; bullet.y += bullet.speed_y; // ,       if(bullet.x < -10 || bullet.x > 1000 || bullet.y < -10 || bullet.y > 1000){ bullet_array.splice(i,1); i--; } } } setInterval(ServerGameLoop, 16); 

E o último passo é enviar o evento update em algum lugar dentro desta função (mas definitivamente fora do loop for):

 //  ,    ,    io.emit("bullets-update",bullet_array); 

Agora podemos finalmente testar o jogo! Se tudo der certo, você deverá ver que os shells estão corretamente sincronizados em todos os clientes. O fato de termos implementado isso no servidor nos forçou a trabalhar mais, mas nos deu muito mais controle. Por exemplo, quando recebemos um evento de disparo de projétil, podemos verificar se a velocidade do projétil está dentro de um determinado intervalo e, se não for assim, saberemos que esse jogador está trapaceando.

5. Colisão com conchas


Esta é a última mecânica básica que implementamos. Espero que você já esteja acostumado com o procedimento para planejar sua implementação, primeiro concluindo completamente a implementação do cliente e depois migrando para o servidor (ou vice-versa). Esse método é muito menos suscetível a erros do que saltar quando implementado.

A verificação de colisão é uma mecânica crucial do jogo, por isso queremos que ela seja protegida contra trapaças. Nós o implementamos no servidor da mesma maneira que fizemos com os shells. Precisamos do seguinte:

  • Verifique se o projétil está perto o suficiente de qualquer jogador no servidor.
  • Gere um evento para todos os clientes quando um projétil atingir um jogador.
  • Ensine o cliente a ouvir o evento atingido e fazer o navio piscar quando atingido.

Você pode tentar implementar esta parte você mesmo. Para fazer o navio do jogador piscar ao ser atingido, basta definir seu canal alfa como 0:

 player.sprite.alpha = 0; 

E ele retornará sem problemas à total opacidade (isso é feito na atualização do player). Para outros jogadores, a ação será semelhante, mas você precisará retornar seu canal alfa para um na função de atualização com algo semelhante:

 for(var id in other_players){ if(other_players[id].alpha < 1){ other_players[id].alpha += (1 - other_players[id].alpha) * 0.16; } else { other_players[id].alpha = 1; } } 

A única parte difícil pode ser verificar se o jogador não bate em suas próprias conchas (caso contrário, ele sofrerá dano toda vez que atirar).

Observe que, nesse esquema, mesmo que o cliente tente trapacear e se recuse a aceitar a mensagem de acerto enviada a ele pelo servidor, isso mudará apenas o que vê em sua própria tela. Todos os outros jogadores ainda verão que atingem o jogador.

6. Suavização de movimento


Se você concluiu todas as etapas até este ponto, posso parabenizá-lo. Você acabou de criar um jogo multiplayer funcional! Envie o link para um amigo e veja como a magia do multiplayer online pode reunir jogadores!

O jogo é totalmente funcional, mas nosso trabalho não termina aí. Existem alguns problemas que podem afetar negativamente a jogabilidade, e devemos lidar com eles:

  • Se nem todo mundo tem uma conexão rápida, o movimento dos outros jogadores parece muito contorcido.
  • Os projéteis parecem lentos, porque não são disparados imediatamente. Antes de aparecer na tela do cliente, eles estão aguardando uma mensagem de retorno do servidor.

Podemos resolver o primeiro problema interpolando nossos dados de posição do navio no cliente. Portanto, se não recebermos atualizações com rapidez suficiente, podemos mover o navio sem problemas para o local onde deveria estar, e não apenas teleportá-lo para lá.

Os reservatórios requerem uma solução mais complexa. Queremos que o servidor processe shells para proteger contra trapaças, mas também precisamos de uma reação instantânea: um tiro e um projétil voador. A melhor solução é uma abordagem híbrida. O servidor e o cliente podem simular shells, e o servidor ainda enviará atualizações para as posições dos shells. Se eles estiverem fora de sincronia, assumimos que o servidor está correto e redefinimos a posição do projétil no cliente.

Não implementaremos esse sistema shell neste tutorial, mas é bom saber que esse método existe.

A simples interpolação das posições dos navios é muito simples. Em vez de definir uma posição diretamente no evento de atualização, onde primeiro recebemos novos dados de posição, simplesmente salvamos a posição de destino:

 //     if(id != socket.id){ other_players[id].target_x = players_data[id].x; //  ,    ,     other_players[id].target_y = players_data[id].y; other_players[id].target_rotation = players_data[id].angle; } 

Em seguida, na função de atualização (também no lado do cliente), contornamos todos os outros jogadores e os empurramos para o objetivo:

 //     ,      for(var id in other_players){ var p = other_players[id]; if(p.target_x != undefined){ px += (p.target_x - px) * 0.16; py += (p.target_y - py) * 0.16; //  ,    /  var angle = p.target_rotation; var dir = (angle - p.rotation) / (Math.PI * 2); dir -= Math.round(dir); dir = dir * Math.PI * 2; p.rotation += dir * 0.16; } } 

Assim, o servidor nos envia atualizações 30 vezes por segundo, mas ainda podemos jogar a 60 qps e o jogo ainda parece tranquilo!

Conclusão


Examinamos muitos problemas. Vamos listá-los: aprendemos como transferir mensagens entre o cliente e o servidor, como sincronizar o estado do jogo, transmitindo-o do servidor para todos os jogadores. Essa é a maneira mais fácil de implementar um jogo online para vários jogadores.

Também aprendemos a proteger o jogo da trapaça, simulando suas partes importantes no servidor e informando os clientes sobre os resultados. Quanto menos você confiar no cliente, mais seguro será o jogo.

Por fim, aprendemos a superar atrasos usando a interpolação do cliente. A compensação por atrasos é um tópico sério e é muito importante (alguns jogos com um atraso suficientemente grande tornam-se simplesmente impossíveis de jogar). Interpolar enquanto aguarda a próxima atualização do servidor é apenas uma maneira de reduzir o problema. Outro é prever os próximos quadros com antecedência e corrigi-los ao receber dados reais do servidor, mas, é claro, essa abordagem pode ser muito difícil.

Uma maneira completamente diferente de reduzir o impacto dos atrasos é fazer com que o design do sistema contorne esse problema. A vantagem do giro lento do navio é que ele é uma mecânica única de movimento e que é uma maneira de impedir mudanças bruscas de movimento. Portanto, mesmo com uma conexão lenta, eles ainda não destruirão a jogabilidade. É muito importante considerar o atraso ao desenvolver os elementos básicos do jogo. Às vezes, as melhores decisões não são de todo truques técnicos.

Você pode usar outra função útil do Glitch, que consiste na capacidade de baixar ou exportar seu próprio projeto através das Opções avançadas no canto superior esquerdo:

Source: https://habr.com/ru/post/pt418411/


All Articles