Sea, pirates - Juego en línea en 3D en el navegador

Saludos a los usuarios de Habr y lectores casuales. Esta es la historia del desarrollo de un juego multijugador en línea basado en navegador con gráficos 3D de baja poli y física 2D simple.



Hay muchos minijuegos 2D basados ​​en el navegador, pero ese proyecto es nuevo para mí. En gamedev, resolver problemas que aún no ha encontrado puede ser bastante emocionante e interesante. Lo principal es no quedarse atascado con piezas de molienda y comenzar un juego de trabajo mientras haya un deseo y motivación, ¡así que no perdamos el tiempo y comencemos a desarrollar!





El juego en pocas palabras



Survival Fight es el único modo de juego en este momento. Batallas de 2 a 6 barcos sin renacimiento, donde el último jugador superviviente se considera el ganador y recibe x3 puntos y oro.



Controles Arcade : botones W, A, D o flechas para mover, barra espaciadora para disparar a las naves enemigas. No necesita apuntar, no puede fallar, el daño depende de la aleatoriedad y el ángulo del disparo. El daño mayor va acompañado de una medalla "justo en el blanco".



Ganamos oro al tomar los primeros lugares en las clasificaciones de los jugadores en 24 horas y en 7 días (reinicio a las 00:00 hora de Moscú) y al completar las tareas diarias (una de las tres se emite por un día, a su vez). También hay oro para las batallas, pero menos.



Gastando oroPoner velas negras en su barco durante 24 horas. Los planes para agregar la capacidad de despertar al Kraken, que llevará al fondo de cualquier barco enemigo sus tentáculos gigantes :)



PVP o cobarde zassal ? Una característica que quería implementar incluso antes de elegir un tema pirata es la capacidad de pelear con amigos en un par de clics. Sin registro y gestos innecesarios, puede enviar un enlace de invitación a sus amigos y esperar hasta que ingresen al juego usando el enlace: una sala privada que se puede abrir para todos se crea automáticamente cuando alguien sigue el enlace, siempre que el "autor" del enlace no haya iniciado otro batalla.



Pila de tecnología



Three.js es una de las bibliotecas más populares para trabajar con 3D en el navegador con buena documentación y muchos ejemplos diferentes. Además, he usado Three.js antes, la elección es obvia.



La falta de un motor de juego se debe a la falta de experiencia relevante y al deseo de aprender algo sin lo cual todo funciona bien de todos modos :)



Node.js porque es simple, rápido y conveniente, aunque no tenía experiencia en Node.js directamente. Consideré Java como una alternativa, realicé un par de experimentos locales, incluso con sockets web, pero no me atreví a averiguar si era difícil ejecutar Java en un VPS. Otra opción, ir, su sintaxis me desanima, no ha avanzado ni un ápice en su estudio.



Para sockets web, use el módulo ws en Node.js.



PHP y MySQLelección menos obvia, pero el criterio sigue siendo el mismo, rápida y fácilmente, ya que existe experiencia en estas tecnologías.



Resulta así: en







primer lugar, se necesita PHP para devolver páginas web al cliente y para solicitudes raras de AJAX, pero en su mayor parte el cliente aún se comunica con el servidor del juego en Node.js a través de sockets web.



No quería vincular el servidor del juego a la base de datos, por lo que todo pasa por PHP. En mi opinión, hay ventajas aquí, aunque no estoy seguro de si son significativas. Por ejemplo, dado que los datos ya preparados llegan a Node.js en la forma requerida, Node.js no pierde el procesamiento de tiempo y las consultas adicionales en la base de datos, sino que se ocupa de cosas más importantes: "digiere" las acciones de los jugadores y cambia el estado del mundo del juego en las habitaciones.



Modelo primero



El desarrollo comenzó con una cosa simple y más importante: un cierto modelo del mundo del juego, que describe las batallas navales desde el punto de vista del servidor. El lienzo 2D simple es ideal para la visualización esquemática del modelo en la pantalla.







Inicialmente, establecí la física normal "verlet", y tomé en cuenta la diferente resistencia al movimiento del barco en diferentes direcciones en relación con la dirección del casco. Pero preocupándome por el rendimiento del servidor, reemplacé la física normal por la más simple, donde los contornos de la nave permanecían solo en lo visual, mientras que físicamente las naves son objetos redondos que ni siquiera tienen inercia. En lugar de inercia, hay una aceleración hacia adelante limitada.



Los disparos y los golpes se reducen a operaciones simples con los vectores de la dirección del barco y la dirección del disparo. No hay conchas aquí. Si el producto escalar de los vectores normalizados se ajusta a los valores aceptables teniendo en cuenta la distancia al objetivo, habrá un disparo y un golpe si el jugador presiona el botón.



El JavaScript del lado del cliente para representar el modelo del mundo del juego, manejar el movimiento de naves y disparos, lo porté al servidor Node.js casi sin cambios.



Servidor de juegos



El servidor Node.js WebSocket consta de solo 3 scripts:



  • main.js : el script principal que recibe mensajes WS de los jugadores, crea salas y hace girar los engranajes de esta máquina
  • room.js : un guión responsable de la jugabilidad dentro de la sala: actualizar el mundo del juego, enviar actualizaciones a los jugadores en la sala
  • funcs.js : incluye una clase para trabajar con vectores, un par de funciones auxiliares y una clase que implementa una lista doblemente vinculada


A medida que avanzaba el desarrollo, se agregaron nuevas clases: casi todas están directamente relacionadas con el juego y terminaron en el archivo room.js. A veces es conveniente trabajar con clases por separado (en archivos separados), pero la opción todo en uno tampoco es mala, siempre que no haya demasiadas clases (es conveniente desplazarse hacia arriba y recordar qué parámetros acepta un método de otra clase).



La lista actual de clases de servidores de juegos:



  • WaitRoom : la sala donde los jugadores esperan el comienzo de la batalla, tiene su propio método de tick que envía sus actualizaciones y comienza la creación de la sala de juegos cuando más de la mitad de los jugadores están listos para la batalla.
  • Room — , : /, ,
  • Player — «» :
  • Ship — : , , ,
  • PhysicsEngine — ,
  • PhysicsBody


Room
let upd = {p: [], t: this.gamet};
let t = Date.now();
let dt = t - this.lt;
let nalive = 0;

for (let i in this.players) {
	this.players[i].tick(t, dt);
}

this.physics.run(dt);

for (let i in this.players) {
	upd.p.push(this.players[i].getUpd());
}

this.chronology.addLast(clone(upd));
if (this.chronology.n > 30) this.chronology.remFirst();

let updjson = JSON.stringify(upd);

for (let i in this.players) {
	let pl = this.players[i];
	if (pl.ship.health > 0) nalive++;
	if (pl.deadLeave) continue;
	pl.cl.ws.send(updjson);
}

this.lt = t;
this.gamet += dt;

if (nalive <= 1) return false;
return true;




Además de las clases, hay funciones como obtener datos del usuario, actualizar una tarea diaria, obtener una recompensa, comprar una máscara. Estas funciones básicamente envían solicitudes https a PHP, que ejecuta una o más consultas MySQL y devuelve el resultado.



Retrasos en la red



La compensación de latencia de red es una parte importante del desarrollo de juegos en línea. Sobre este tema, he leído repetidamente una serie de artículos aquí sobre Habré . En el caso de una batalla de veleros, la compensación del retraso puede ser simple, pero aún debe hacer compromisos.



La interpolación se realiza constantemente en el cliente: el cálculo del estado del mundo del juego entre dos momentos en el tiempo, cuyos datos ya se han obtenido. Hay un pequeño margen de tiempo, que reduce la probabilidad de saltos repentinos, y con retrasos significativos en la red y la ausencia de nuevos datos, la interpolación se reemplaza por extrapolación. La extrapolación da resultados no muy correctos, pero es barato para el procesador y no depende de cómo se implemente el movimiento de los barcos en el servidor y, por supuesto, a veces puede salvar la situación.



Al resolver el problema de los retrasos, mucho depende del juego y su ritmo. Sacrifico una respuesta rápida a las acciones del jugador a favor de una animación suave y una correspondencia exacta de la imagen con el estado del mundo del juego en un determinado momento. La única excepción es que una salva de cañón se juega inmediatamente con solo presionar un botón. El resto puede atribuirse a las leyes del universo y al excedente de ron de la tripulación del barco :)



Interfaz



Desafortunadamente, no existe una estructura clara o jerarquía de clases y métodos. Todo JS se divide en objetos con sus propias funciones, que en cierto sentido son iguales. Casi todos mis proyectos anteriores fueron más lógicos que este. Esto se debe en parte a que el primer objetivo era depurar el modelo del mundo del juego en el servidor y la interacción de la red sin prestar atención a la interfaz y al componente visual del juego. Cuando llegó el momento de agregar 3D, literalmente lo agregué a la versión de prueba existente, más o menos, reemplacé la función 2D drawShip por exactamente lo mismo, pero 3D, aunque de una manera amigable valió la pena revisar toda la estructura y preparar la base para futuros cambios.



Barco 3D



Three.js admite el uso de modelos 3D listos para usar en varios formatos. Elegí el formato GLTF / GLB para mí, donde se pueden incrustar texturas y animaciones, es decir, el desarrollador no debería preguntarse "¿han cargado todas las texturas?"



Nunca he tratado con editores 3D antes. El paso lógico era contactar a un especialista en un intercambio independiente con la tarea de crear un modelo 3D de un velero con una animación incrustada de una salva de cañón. Pero no pude resistir los pequeños cambios en el modelo especializado terminado por mi cuenta, y terminé con el hecho de que creé mi modelo desde cero en Blender. Crear un modelo de baja poli sin casi texturas es simple, difícil sin un modelo ya preparado por un especialista para estudiar en un editor 3D lo que se necesita para una tarea específica (al menos moralmente :).







Shaders al dios de los shaders



La razón principal por la que necesito mis sombreadores es la capacidad de manipular la geometría de un objeto en la tarjeta de video durante el renderizado, que tiene un buen rendimiento. Three.js no solo te permite crear tus propios sombreadores, sino que también puede asumir parte del trabajo.



El mecanismo o método que utilicé al crear un sistema de partículas para animar el daño a un barco, una superficie de agua dinámica o un fondo marino estático es el mismo: el ShaderMaterial especial proporciona una interfaz simplificada para usar su sombreador (su código GLSL), BufferGeometry le permite crear geometría a partir de datos arbitrarios ...



Un espacio en blanco vacío, una estructura de código que fue conveniente para mí copiar, complementar y modificar para crear mi objeto 3D de una manera similar:



Mostrar código
let vs = `
	attribute vec4 color;
	varying vec4 vColor;

	void main(){
		vColor = color;
		gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
		// gl_PointSize = 5.0; // for particles
	}
`;
let fs = `
	uniform float opacity;
	varying vec4 vColor;

	void main() {
		gl_FragColor = vec4(vColor.xyz, vColor.w * opacity);
	}
`;

let material = new THREE.ShaderMaterial( {
	uniforms: {
		opacity: {value: 0.5}
	},
	vertexShader: vs,
	fragmentShader: fs,
	transparent: true
});

let geometry = new THREE.BufferGeometry();

//let indices = [];
let vertices = [];
let colors = [];

/* ... */

//geometry.setIndex( indices );
geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( vertices, 3 ) );
geometry.setAttribute( 'color', new THREE.Float32BufferAttribute( colors, 4 ) );

let mesh = new THREE.Mesh(geometry, material);




Daño de la nave



Las animaciones de daño de la nave son partículas en movimiento que cambian su tamaño y color, cuyo comportamiento está determinado por sus atributos y el código de sombreador GLSL. La generación de partículas (geometría y material) ocurre de antemano, luego para cada barco se crea su propia instancia (malla) de partículas dañadas (la geometría es común para todos, el material se clona). Hay bastantes atributos de partículas, pero el sombreador creado implementa simultáneamente grandes nubes de polvo que se mueven lentamente y escombros que vuelan rápidamente, y partículas de fuego, cuya actividad depende del grado de daño a la nave.







Mar



El mar también se implementa utilizando ShaderMaterial. Cada vértice se mueve en las 3 direcciones a lo largo de una sinusoide, formando ondas aleatorias. Los atributos definen las amplitudes para cada dirección de movimiento y la fase de la sinusoide.



Para diversificar los colores en el agua y hacer que el juego sea más interesante y agradable a la vista, se decidió agregar el fondo y las islas. El color del fondo depende de la altura / profundidad y brilla a través de la superficie del agua creando áreas oscuras y claras.



El fondo marino se creó a partir de un mapa de altura, que se creó en 2 etapas: primero, el fondo sin islas se creó en un editor gráfico (en mi caso, las herramientas se renderizaron -> nubes y desenfoque gaussiano), luego las islas se agregaron en orden aleatorio usando Canvas JS en línea en jsFiddle dibujando un círculo y difuminando. Algunas islas son bajas, a través de ellas puedes disparar a los oponentes, otras tienen una cierta altura, los disparos no pasan a través de ellas. Además del mapa de altura en sí mismo, en la salida recibo datos en formato json sobre las islas (su posición y tamaño) para la física en el servidor.







¿Que sigue?



Hay muchos planes para el desarrollo del juego. Los principales son los nuevos modos de juego. Pequeños: crea sombras / reflejos en el agua, teniendo en cuenta las limitaciones de rendimiento de WebGL y JS. Ya he mencionado la oportunidad de despertar al Kraken :) La unificación de jugadores en salas en función de su experiencia acumulada aún no se ha implementado. Una mejora obvia, pero no demasiado importante, es crear varios mapas del fondo marino y las islas y elegir uno de ellos al azar para una nueva batalla.



Puede crear muchos efectos visuales dibujando repetidamente la escena "en la memoria" y luego combinando todos los datos en una imagen (de hecho, se puede llamar postprocesamiento), pero mi mano no se eleva para aumentar la carga en el cliente de esta manera, porque el cliente todavía es un navegador en lugar de una aplicación nativa. Quizás algún día decida este paso.



También hay preguntas que ahora me resulta difícil responder: cuántos jugadores en línea puede soportar un servidor virtual barato, si será posible recolectar al menos un cierto número de jugadores interesados ​​y cómo hacerlo.



huevo de Pascua



¿A quién no le gusta recordar viejos juegos de computadora que dieron tantas emociones? Me encanta reproducir el juego Corsairs 2 (también conocido como Sea Dogs 2) una y otra vez hasta ahora. No pude evitar agregar un secreto a mi juego y recordar explícita e indirectamente a "Corsairs 2". No revelaré todas las cartas, pero daré una pista: mi huevo de Pascua es un cierto objeto que puedes encontrar mientras exploras el mar (no necesitas navegar lejos a través del mar infinito, el objeto está dentro de lo razonable, pero la probabilidad de encontrarlo no es alta). El huevo de Pascua repara completamente el barco dañado.



Que pasó



Video minuto (prueba de 2 dispositivos):





Enlace al juego: https://sailfire.pw



También hay un formulario de contacto, los mensajes se me envían por telegrama: https://sailfire.pw/feedback/

Enlaces para aquellos que desean mantenerse al tanto de las noticias y actualizaciones: VK Public , Telegram channel



All Articles