Tirador de red del navegador en Node.js

El desarrollo de juegos multijugador es complicado por muchas razones: su alojamiento puede ser costoso, la estructura no es obvia y la implementación es difícil. En este tutorial intentaré ayudarte a superar la última barrera.

Este artículo está destinado a desarrolladores que pueden crear juegos y están familiarizados con JavaScript, pero nunca antes han escrito juegos en línea para varios jugadores. Después de completar este tutorial, dominarás la implementación de componentes básicos de red en tu juego y podrás desarrollarlo en algo más. Esto es lo que crearemos:


¡Puedes jugar el juego terminado aquí ! Cuando presionas las teclas W o "arriba", la nave se acerca al cursor, cuando haces clic con el mouse, dispara. (Si no hay nadie en línea, para comprobar cómo funciona el modo multijugador, abra dos ventanas del navegador en una computadora o una en el teléfono). Si quieres ejecutar el juego localmente, entonces el código fuente completo está disponible en GitHub .

Al crear el juego, utilicé los recursos gráficos de Kenney's Pirate Pack y el marco del juego Phaser . En este tutorial se le asigna el rol de programador de red. El punto de partida será una versión del juego para un solo usuario totalmente funcional, y nuestra tarea será escribir un servidor en Node.js usando Socket.io para la parte de la red. Para no sobrecargar el tutorial, me enfocaré en partes relacionadas con el multijugador y omitiré conceptos relacionados con Phaser y Node.js.

¡No necesitas configurar nada localmente, porque crearemos este juego completamente en el navegador en Glitch.com ! Glitch es una herramienta increíble para crear aplicaciones web, incluidos backends, bases de datos y más. Es excelente para la creación de prototipos, la capacitación y la colaboración, y me complacerá presentarle sus capacidades en este tutorial.

Empecemos

1. Preparación


Publiqué el borrador del proyecto en Glitch.com .

Consejos de interfaz: puede iniciar la vista previa de la aplicación haciendo clic en el botón Mostrar (arriba a la izquierda).


La barra lateral vertical de la izquierda contiene todos los archivos de la aplicación. Para editar esta aplicación, debe crear su "remix". Por lo tanto, crearemos una copia en nuestra cuenta (o "bifurcación" en la jerga de git). Haga clic en Remix este botón.


En este punto, está editando la aplicación con una cuenta anónima. Para guardar su trabajo, puede iniciar sesión (arriba a la derecha).

Ahora, antes de continuar, es importante que te familiarices con el juego en el que agregaremos un modo multijugador. Echa un vistazo a index.html . Tiene tres funciones importantes que debe conocer: preload (línea 99), create (línea 115) y GameLoop (línea 142), así como el objeto jugador (línea 35).

Si prefiere aprender practicando, asegúrese de comprender el trabajo del juego completando las siguientes tareas:

  • Aumente el tamaño del mundo (línea 29) : observe que hay un tamaño mundial separado para el mundo del juego y un tamaño de ventana para el lienzo de la página .
  • Haga posible avanzar con la ayuda del "espacio" (línea 53).
  • Cambia el tipo de barco del jugador (línea 129).
  • Disminuya la velocidad del movimiento de las conchas (línea 155).

Instalar Socket.io


Socket.io es una biblioteca para administrar las comunicaciones en tiempo real dentro de un navegador usando WebSockets (en lugar de usar protocolos como UDP, que se usan para crear juegos multijugador clásicos). Además, la biblioteca tiene formas redundantes para garantizar el funcionamiento, incluso cuando no se admiten WebSockets. Es decir, se ocupa de los protocolos de mensajería y permite el uso de un conveniente sistema de mensajería basado en eventos.

Lo primero que debemos hacer es instalar el módulo Socket.io. En Glitch, esto se puede hacer yendo al archivo package.json y luego ingresando el módulo requerido en las dependencias, o haciendo clic en Agregar paquete e ingresando "socket.io".


Ahora es el momento adecuado para lidiar con los registros del servidor. Haga clic en el botón Registros a la izquierda para abrir el registro del servidor. Debería ver que instala Socket.io con todas sus dependencias. Aquí es donde debe buscar todos los errores y la salida del código del servidor.


Ahora vamos a server.js . Aquí es donde se encuentra nuestro código de servidor. Hasta ahora, solo hay un código básico para servir nuestro HTML. Agregue una línea en la parte superior del archivo para habilitar Socket.io:

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

Ahora también necesitamos habilitar Socket.io en el cliente, así que volvamos a index.html y agreguemos las siguientes líneas dentro de la etiqueta <head> :

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

Nota: Socket.io procesa automáticamente la carga de la biblioteca del cliente a lo largo de esta ruta, por lo que esta línea funciona incluso si no hay un directorio /socket.io/ en sus carpetas.

¡Ahora Socket.io está incluido en el proyecto y listo para funcionar!

2. Reconocimiento y desove de jugadores.


Nuestro primer paso real será aceptar conexiones en el servidor y crear nuevos jugadores en el cliente.

Aceptar conexiones de servidor


Agregue este código al final de server.js :

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

Por lo tanto, le pedimos a Socket.io que escuche todos los eventos de connection que ocurren automáticamente cuando un cliente se conecta. La biblioteca crea un nuevo objeto de socket para cada cliente, donde socket.id es el identificador único para este cliente.

Para verificar que esto funciona, regrese al cliente ( index.html ) y agregue esta línea en algún lugar de la función de creación :

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

Si inicia el juego y mira el registro del servidor (haga clic en el botón Registros ), verá que el servidor ha registrado este evento de conexión.

Ahora, al conectar un nuevo jugador, esperamos que nos brinde información sobre su estado. En nuestro caso, necesitamos saber al menos x , y y ángulo para crearlo correctamente en el punto correcto.

El evento de connection fue un evento en línea desencadenado por Socket.io. Podemos escuchar cualquier evento establecido independientemente. Voy a nombrar mi evento como new-player y esperaré que el cliente lo envíe tan pronto como se conecte con información sobre su posición. Se verá así:

 //  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); }) }) 

Si ejecuta este código, entonces hasta que vea algo en el registro del servidor, porque aún no le hemos dicho al cliente que genere este evento para new-player . Pero imaginemos por un momento que ya lo hemos hecho y seguimos trabajando en el servidor. ¿Qué debería suceder después de obtener la ubicación de un nuevo jugador que se une?

Podemos enviar un mensaje a todos los demás jugadores conectados para que sepan que ha aparecido un nuevo jugador. Socket.io tiene una función conveniente para esto:

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

Cuando socket.emit llama a socket.emit mensaje simplemente se pasa a este único cliente. Cuando se llama a socket.broadcast.emit se envía a cada cliente conectado al servidor, excepto en cuyo socket se llamó a esta función.

La función io.emit envía un mensaje a cada cliente conectado al servidor sin ninguna excepción. En nuestro esquema, no necesitamos esto, porque si recibimos un mensaje del servidor que nos pide que creemos nuestro propio barco, obtendremos un duplicado del sprite, porque ya creamos nuestro propio barco cuando comenzó el juego. Aquí hay un consejo útil sobre los diversos tipos de funciones de mensajería que utilizaremos en este tutorial.

El código del servidor ahora debería verse así:

 //  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); }) }) 

Es decir, cada vez que un jugador se conecta, esperamos que nos envíe un mensaje con información sobre su ubicación, y enviamos esta información a todos los demás jugadores para que puedan crear su sprite.

Desove del cliente


Ahora, para completar este ciclo, debemos realizar dos acciones en el cliente:

  1. Genere un mensaje con los datos de nuestra ubicación después de la conexión.
  2. Escuche eventos de create-player y cree un jugador en este punto.

Para realizar la primera acción después de crear un reproductor en la función de creación (aproximadamente en la línea 135), podemos generar un mensaje que contenga los datos de ubicación que necesitamos enviar:

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

No necesitamos preocuparnos por serializar los datos que se envían. Puede transferirlos en cualquier tipo de objeto, y Socket.io lo procesará por nosotros.

Antes de continuar, pruebe el código . Deberíamos ver un mensaje similar en los registros del servidor:

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

¡Ahora sabemos que nuestro servidor recibe una notificación sobre la conexión de un nuevo reproductor y lee correctamente los datos sobre su ubicación!

A continuación, queremos escuchar las solicitudes para crear un nuevo reproductor. Podemos colocar este código inmediatamente después de generar el mensaje, debería verse así:

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

Ahora prueba el código . Abre dos ventanas con el juego y asegúrate de que funcione.

Debería ver que después de abrir dos clientes, el primer cliente tiene dos naves creadas, y el segundo tiene solo uno.

Tarea: ¿puedes averiguar por qué sucedió? ¿O cómo puedes arreglar esto? Paso a paso, siga la lógica cliente / servidor que escribimos e intente depurarla.

¡Espero que hayas intentado resolverlo por tu cuenta! Lo siguiente sucede: cuando el primer jugador se conecta, el servidor envía un evento de create-player a todos los demás jugadores, pero aún no hay jugadores que puedan recibirlo. Después de conectar al segundo jugador, el servidor envía sus mensajes nuevamente, y el primer jugador lo recibe y crea correctamente el sprite, mientras que el segundo jugador perdió el mensaje del primer jugador.

Es decir, el problema es que el segundo jugador se conecta al juego más tarde y necesita saber el estado del juego. Debemos informar a todos los nuevos jugadores de conexión que los jugadores ya existen (así como otros eventos que han tenido lugar en el mundo) para que puedan orientarse. Antes de comenzar a resolver este problema, tengo una breve advertencia.

Advertencia de sincronización del estado del juego


Hay dos enfoques para implementar la sincronización de todos los jugadores. El primero es enviar una cantidad mínima de información sobre los cambios que se han producido en la red. Es decir, cada vez que se conecte un nuevo jugador, enviaremos a todos los demás jugadores solo información sobre este nuevo jugador (y enviaremos una lista de todos los demás jugadores del mundo a este nuevo jugador), y después de desconectar, informaremos a todos los jugadores que este jugador en particular se ha desconectado.

El segundo enfoque es transmitir todo el estado del juego. En este caso, cada vez que se conecta o desconecta, enviamos a todos una lista completa de todos los jugadores.

El primer enfoque es mejor porque minimiza la cantidad de información transmitida a través de la red, pero puede ser muy difícil de implementar y tiene la probabilidad de que los jugadores no se sincronicen. El segundo asegura que los jugadores estén siempre sincronizados, pero cada mensaje tendrá que enviar más datos.

En nuestro caso, en lugar de intentar enviar mensajes cuando un jugador está conectado para crearlo y cuando está desconectado para eliminarlo, así como cuando se mueve para actualizar su posición, podemos combinar todo esto en un evento de update común. Este evento de actualización siempre enviará las posiciones de cada jugador a todos los clientes. Esto es lo que debe hacer el servidor. La tarea del cliente es mantener el cumplimiento del mundo con el estado recibido.

Para implementar dicho esquema, haré lo siguiente:

  1. Mantendré un diccionario de jugadores, cuya clave será su ID, y el valor serán los datos de su ubicación.
  2. Agregue un reproductor a este diccionario cuando esté conectado y envíe un evento de actualización.
  3. Elimine el reproductor de este diccionario cuando esté apagado y envíe un evento de actualización.

Puede intentar implementar este sistema usted mismo, porque estos pasos son bastante simples ( mi consejo sobre características puede ser útil aquí). Así es como podría verse la implementación 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]; //    }) }) 

El lado del cliente es un poco más complicado. Por un lado, ahora solo deberíamos preocuparnos por el evento de update-players , pero por otro lado, deberíamos considerar crear nuevas naves si el servidor envía más naves de las que conocemos, o eliminarlas si hay demasiadas.

Así es como manejo este evento en el 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]; } } }) 

En el lado del cliente, other_players envíos en el diccionario other_players , que acabo de definir en la parte superior del script (no se muestra aquí). Dado que el servidor envía datos del jugador a todos los jugadores, debo agregar un cheque para que el cliente no cree un sprite adicional para sí mismo. (Si tiene problemas con la estructuración, aquí está el código completo que debería estar en index.html en este momento).

Ahora prueba el código . ¡Debería poder crear varios clientes y ver la cantidad correcta de barcos creados en las posiciones correctas!

3. Sincronización de las posiciones del barco.


Aquí comienza una parte muy interesante. Queremos sincronizar las posiciones de envío en todos los clientes. Esto revelará la simplicidad de la estructura que hemos creado en este momento. Ya tenemos un evento de actualización que puede sincronizar las ubicaciones de todos los barcos. Es suficiente para nosotros hacer lo siguiente:

  1. Obligue al cliente a generar un mensaje cada vez que se mueva a una nueva posición.
  2. Enseñe al servidor a escuchar este mensaje de movimiento y actualice el elemento de datos del jugador en el diccionario de players .
  3. Genere un evento de actualización para todos los clientes.

¡Y eso debería ser suficiente! Ahora es tu turno de tratar de implementar esto tú mismo.

Si está completamente confundido y necesita una pista, mire el proyecto terminado .

Nota sobre cómo minimizar los datos transmitidos a través de la red


La forma más sencilla de implementarlo es actualizar las posiciones de todos los jugadores cada vez que un jugador recibe un evento de movimiento. Es genial si los jugadores siempre obtienen la información más reciente inmediatamente después de que aparece, pero la cantidad de mensajes transmitidos a través de la red puede crecer fácilmente a cientos por fotograma. Imagina que tienes 10 jugadores, cada uno de los cuales envía un mensaje de movimiento en cada cuadro. El servidor debe reenviarlos a los 10 jugadores. ¡Ya son 100 mensajes por fotograma!

Sería mejor hacer esto: espere hasta que el servidor reciba todos los mensajes de todos los jugadores, y luego envíe a todos los jugadores una gran actualización que contenga toda la información. Por lo tanto, reduciremos la cantidad de mensajes transmitidos a la cantidad de usuarios presentes en el juego (en lugar del cuadrado de este número). El problema aquí es que todos los usuarios experimentarán el mismo retraso que el jugador con la conexión más lenta.

Otra solución es enviar las actualizaciones del servidor a una frecuencia constante, independientemente de la cantidad de mensajes recibidos del reproductor. Un estándar común es actualizar el servidor aproximadamente 30 veces por segundo.

Sin embargo, al elegir la estructura de su servidor, debe evaluar la cantidad de mensajes transmitidos en cada marco en las primeras etapas del desarrollo del juego.

4. Sincronización de shell


Ya casi hemos terminado! La última parte seria es la sincronización a través de una red de shells. Podemos implementarlo de la misma manera que los reproductores sincronizados:

  • Cada cliente envía las posiciones de todos sus shells en cada cuadro.
  • El servidor los redirige a cada jugador.

Pero hay un problema.

Protección contra trampas


Si redirige todo lo que el cliente envía como las verdaderas posiciones de los proyectiles, el jugador puede engañar fácilmente modificando su cliente y transmitiéndole datos falsos, por ejemplo, teletransportadores a las posiciones de los barcos. Puede verificarlo usted mismo fácilmente descargando la página web, cambiando el código a JavaScript y volviéndola a abrir. Y este es un problema no solo para los juegos de navegador. En el caso general, nunca podemos confiar en los datos que provienen del usuario.

Para hacer frente parcialmente a este problema, intentaremos usar otro esquema:

  • El cliente genera un mensaje sobre el proyectil con su posición y dirección.
  • El servidor simula el movimiento de los depósitos.
  • El servidor actualiza los datos de cada cliente, pasando la posición de todos los shells.
  • Los clientes procesan shells en las posiciones recibidas del servidor.

Por lo tanto, el cliente es responsable de la posición del proyectil, pero no de su velocidad ni de su movimiento adicional. El cliente puede cambiar la posición de los depósitos por sí mismo, pero esto no cambiará lo que otros clientes ven.

Para implementar dicho esquema, agregaremos la generación de mensajes cuando se active. Ya no crearé el sprite en sí, porque su existencia y ubicación estarán completamente determinadas por el servidor. Ahora nuestro nuevo disparo de proyectil en index.html se verá así:

 //   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}) } 

También ahora podemos comentar todo el fragmento de código actualizando los shells en el 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, necesitamos que el cliente escuche las actualizaciones de shell. Decidí implementar esto de la misma manera que con los jugadores, es decir, el servidor simplemente envía una matriz de todas las posiciones de shell en un evento llamado bullets-update , y el cliente crea o destruye shells para mantener la sincronización. Así es como se ve:

 //     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--; } }) 

Y eso es todo lo que debería estar en el cliente. Asumiré que ya sabe dónde incrustar estos fragmentos de código y cómo juntarlos, pero si tiene algún problema, siempre puede ver el resultado final .

Ahora en server.js necesitamos rastrear y simular shells. Primero, crearemos una matriz para rastrear shells, similar a una matriz para jugadores:

 var bullet_array = []; //         

A continuación, escuchamos el evento de disparo de proyectil:

 //   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); }); 

Ahora simulamos proyectiles 60 veces 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); 

Y el último paso es enviar el evento de actualización en algún lugar dentro de esta función (pero definitivamente fuera del ciclo for):

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

¡Ahora por fin podemos probar el juego! Si todo salió bien, debería ver que los shells están sincronizados correctamente en todos los clientes. El hecho de que implementamos esto en el servidor nos obligó a hacer más trabajo, pero nos dio mucho más control. Por ejemplo, cuando recibimos un evento de un disparo de proyectil, podemos verificar si la velocidad del proyectil está dentro de un cierto intervalo, y si no es así, sabremos que este jugador está haciendo trampa.

5. Colisión con conchas


Esta es la última mecánica básica que implementamos. Espero que ya esté acostumbrado al procedimiento para planificar su implementación, primero complete la implementación del cliente por completo y luego vaya al servidor (o viceversa). Este método es mucho menos propenso a errores que saltar cuando se implementa de un lado a otro.

La comprobación de colisiones es una mecánica de juego crucial, por lo que queremos que esté protegida contra las trampas. Lo implementamos en el servidor de la misma manera que lo hicimos con los shells. Necesitamos lo siguiente:

  • Compruebe si el proyectil está lo suficientemente cerca de cualquier jugador en el servidor.
  • Genera un evento para todos los clientes cuando un proyectil golpea a un jugador.
  • Enseñe al cliente a escuchar el evento exitoso y haga que el barco parpadee cuando sea golpeado.

Puede intentar implementar esta parte usted mismo. Para hacer que la nave del jugador parpadee cuando sea golpeada, simplemente configure su canal alfa en 0:

 player.sprite.alpha = 0; 

Y volverá sin problemas a la opacidad total (esto se hace en la actualización del reproductor). Para otros jugadores, la acción será similar, pero tendrás que devolver su canal alfa a uno en la función de actualización con algo similar:

 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; } } 

La única parte difícil puede ser verificar que el jugador no golpee sus propios proyectiles (de lo contrario, recibirá daños cada vez que dispare).

Tenga en cuenta que en este esquema, incluso si el cliente intenta hacer trampa y se niega a aceptar el mensaje de éxito que le envía el servidor, esto solo cambiará lo que ve en su propia pantalla. Todos los demás jugadores aún verán que golpean al jugador.

6. Movimiento de suavizado


Si ha completado todos los pasos hasta este punto, puedo felicitarlo. ¡Acabas de crear un juego multijugador que funciona! ¡Envíe el enlace a un amigo y vea cómo la magia del multijugador en línea puede unir a los jugadores!

El juego es completamente funcional, pero nuestro trabajo no termina ahí. Hay un par de problemas que pueden afectar negativamente la jugabilidad, y debemos resolverlos:

  • Si no todos tienen una conexión rápida, el movimiento de los otros jugadores se ve muy nervioso.
  • Los proyectiles parecen ser lentos porque no se disparan de inmediato. Antes de aparecer en la pantalla del cliente, esperan un mensaje de respuesta del servidor.

Podemos resolver el primer problema interpolando nuestros datos de posición del barco en el cliente. Por lo tanto, si no recibimos actualizaciones lo suficientemente rápido, podemos mover el barco sin problemas al lugar donde debería estar, y no solo teletransportarlo allí.

Los depósitos requieren una solución más compleja. Queremos que el servidor procese proyectiles para proteger contra las trampas, pero también necesitamos una reacción instantánea: un disparo y un proyectil volador. La mejor solución es un enfoque híbrido. Tanto el servidor como el cliente pueden simular shells, y el servidor aún enviará actualizaciones a las posiciones de los shells. Si no están sincronizados, asumimos que el servidor es correcto y redefinimos la posición del proyectil en el cliente.

No implementaremos este sistema de shell en este tutorial, pero es bueno saber que este método existe.

Realizar una interpolación simple de las posiciones del barco es muy simple. En lugar de establecer una posición directamente en el evento de actualización, donde primero recibimos nuevos datos de posición, simplemente guardamos la posición 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; } 

Luego, en la función de actualización (también en el lado del cliente), recorremos a todos los otros jugadores y los empujamos hacia su 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; } } 

Por lo tanto, el servidor nos envía actualizaciones 30 veces por segundo, ¡pero aún podemos jugar a 60 fps y el juego aún se ve suave!

Conclusión


Examinamos muchos problemas. Vamos a enumerarlos: aprendimos cómo transferir mensajes entre el cliente y el servidor, cómo sincronizar el estado del juego, transmitiéndolo desde el servidor a todos los jugadores. Esta es la forma más fácil de implementar un juego multijugador en línea.

También aprendimos cómo proteger el juego de las trampas, simulando sus partes importantes en el servidor e informando a los clientes sobre los resultados. Cuanto menos confíes en el cliente, más seguro será el juego.

Finalmente, aprendimos cómo superar los retrasos mediante la interpolación de clientes. La compensación por retrasos es un tema serio y es muy importante (algunos juegos con un retraso suficientemente grande simplemente no se pueden reproducir). Interpolar mientras espera la próxima actualización del servidor es solo una forma de reducir el problema. Otro es predecir los próximos fotogramas por adelantado y corregirlos al recibir datos reales del servidor, pero, por supuesto, este enfoque puede ser muy difícil.

Una forma completamente diferente de reducir el impacto de los retrasos es hacer que el diseño del sistema evite este problema. La ventaja de los giros lentos de la nave es que es una mecánica de movimiento única y que es una forma de evitar cambios repentinos en el movimiento. Por lo tanto, incluso con una conexión lenta, aún no destruirán el juego. Es muy importante tener en cuenta la demora al desarrollar los elementos básicos del juego. A veces las mejores decisiones no son en absoluto trucos técnicos.

Puede usar otra función útil de Glitch, que consiste en la capacidad de descargar o exportar su propio proyecto a través de Opciones avanzadas en la esquina superior izquierda:

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


All Articles