paint-brush
Creando tu propio shooter 3D usando React y Three.js Stack - Parte 1by@varlab
773
773

Creando tu propio shooter 3D usando React y Three.js Stack - Parte 1

Ivan Zhukov17m2023/10/21
Read on Terminal Reader

En la era del desarrollo activo de tecnologías web y aplicaciones interactivas, los gráficos 3D son cada vez más relevantes y demandados. Pero ¿cómo crear una aplicación 3D sin perder las ventajas del desarrollo web? En este artículo, veremos cómo combinar el poder de Three.js con la flexibilidad de React para crear tu propio juego directamente en el navegador. Este artículo le presentará la biblioteca React Three Fiber y le enseñará cómo crear juegos 3D interactivos.
featured image - Creando tu propio shooter 3D usando React y Three.js Stack - Parte 1
Ivan Zhukov HackerNoon profile picture
0-item
1-item

En el desarrollo web moderno, los límites entre las aplicaciones web clásicas y las aplicaciones web se difuminan cada día. Hoy en día podemos crear no sólo sitios web interactivos, sino también juegos completos directamente en el navegador. Una de las herramientas que hace esto posible es la biblioteca React Three Fiber , una poderosa herramienta para crear gráficos 3D basados en Three.js utilizando la tecnología React .

Acerca de la pila React Three Fiber

React Three Fiber es un contenedor de Three.js que utiliza la estructura y los principios de React para crear gráficos 3D en la web. Esta pila permite a los desarrolladores combinar el poder de Three.js con la conveniencia y flexibilidad de React , haciendo que el proceso de creación de una aplicación sea más intuitivo y organizado.


En el corazón de React Three Fiber está la idea de que todo lo que creas en una escena es un componente de React . Esto permite a los desarrolladores aplicar patrones y metodologías familiares.

Una de las principales ventajas de React Three Fiber es su facilidad de integración con el ecosistema React . Cualquier otra herramienta de React aún se puede integrar fácilmente al usar esta biblioteca.

Relevancia de Web-GameDev

Web-GameDev ha experimentado cambios importantes en los últimos años, evolucionando desde simples juegos Flash 2D hasta complejos proyectos 3D comparables a aplicaciones de escritorio. Este crecimiento en popularidad y capacidades hace de Web-GameDev un área que no se puede ignorar.


Una de las principales ventajas de los juegos web es su accesibilidad. Los jugadores no necesitan descargar e instalar ningún software adicional; simplemente hagan clic en el enlace en su navegador. Esto simplifica la distribución y promoción de juegos, poniéndolos a disposición de una amplia audiencia en todo el mundo.


Finalmente, el desarrollo de juegos web puede ser una excelente manera para que los desarrolladores prueben el desarrollo de juegos utilizando tecnologías familiares. Gracias a las herramientas y bibliotecas disponibles, incluso sin experiencia en gráficos 3D, es posible crear proyectos interesantes y de alta calidad.

Rendimiento del juego en navegadores modernos

Los navegadores modernos han recorrido un largo camino, evolucionando desde herramientas de navegación web bastante simples hasta plataformas potentes para ejecutar aplicaciones y juegos complejos. Los principales navegadores como Chrome , Firefox , Edge y otros se optimizan y desarrollan constantemente para garantizar un alto rendimiento, lo que los convierte en una plataforma ideal para desarrollar aplicaciones complejas.


Una de las herramientas clave que ha impulsado el desarrollo de los juegos basados en navegador es WebGL . Este estándar permitió a los desarrolladores utilizar la aceleración de gráficos por hardware, lo que mejoró significativamente el rendimiento de los juegos 3D. Junto con otras webAPI, WebGL abre nuevas posibilidades para crear impresionantes aplicaciones web directamente en el navegador.


Sin embargo, a la hora de desarrollar juegos para navegador, es fundamental tener en cuenta varios aspectos del rendimiento: la optimización de recursos, la gestión de la memoria y la adaptación a diferentes dispositivos son puntos clave que pueden afectar al éxito de un proyecto.

¡En sus marcas!

Sin embargo, las palabras y la teoría son una cosa, pero la experiencia práctica es otra muy distinta. Para comprender y apreciar realmente todo el potencial del desarrollo de juegos web, la mejor manera es sumergirse en el proceso de desarrollo. Por lo tanto, como ejemplo de desarrollo exitoso de un juego web, crearemos nuestro propio juego. Este proceso nos permitirá aprender aspectos clave del desarrollo, afrontar problemas reales y encontrarles soluciones, y ver lo potente y flexible que puede ser una plataforma de desarrollo de juegos web.


En una serie de artículos, veremos cómo crear un juego de disparos en primera persona utilizando las funciones de esta biblioteca y nos sumergiremos en el apasionante mundo de los desarrolladores de juegos web.


Demostración final


Repositorio en GitHub


¡Ahora comencemos!

Configurando el proyecto e instalando paquetes.

En primer lugar, necesitaremos una plantilla de proyecto de React . Así que comencemos instalándolo.


 npm create vite@latest


  • seleccione la biblioteca React ;
  • seleccione JavaScript .


Instale paquetes npm adicionales.


 npm install three @react-three/fiber @react-three/drei @react three/rapier zustand @tweenjs/tween.js


Luego elimine todo lo innecesario de nuestro proyecto.


Código de sección

Personalizando la visualización del lienzo

En el archivo main.jsx , agregue un elemento div que se mostrará en la página como alcance. Inserte un componente Canvas y configure el campo de visión de la cámara. Dentro del componente Canvas , coloque el componente App .


principal.jsx


Agreguemos estilos a index.css para estirar los elementos de la interfaz de usuario a la altura completa de la pantalla y mostrar el alcance como un círculo en el centro de la pantalla.


índice.css


En el componente Aplicación agregamos un componente Cielo , que se mostrará como fondo en nuestra escena de juego en forma de cielo.


aplicación.jsx


Mostrando el cielo en la escena.


Código de sección

Superficie del suelo

Creemos un componente Ground y colóquelo en el componente App .


aplicación.jsx


En Tierra , cree un elemento de superficie plana. En el eje Y muévalo hacia abajo para que este plano esté en el campo de visión de la cámara. Y también voltea el plano en el eje X para hacerlo horizontal.


Tierra.jsx


Aunque especificamos el gris como color del material, el avión parece completamente negro.


Piso en la escena


Código de sección

Iluminación básica

De forma predeterminada, no hay iluminación en la escena, así que agreguemos una fuente de luz ambientLight que ilumina el objeto desde todos los lados y no tiene un haz dirigido. Como parámetro establece la intensidad del brillo.


aplicación.jsx


Avion iluminado


Código de sección

Textura para la superficie del suelo.

Para que la superficie del suelo no luzca homogénea, añadiremos textura. Haga un patrón de la superficie del piso en forma de celdas que se repiten a lo largo de toda la superficie.

En la carpeta de activos agregue una imagen PNG con una textura.


Textura agregada


Para cargar una textura en la escena, usemos el gancho useTexture del paquete @react-tres/drei . Y como parámetro para el gancho pasaremos la imagen de textura importada al archivo. Establece la repetición de la imagen en los ejes horizontales.


Tierra.jsx


Textura en un avión


Código de sección

Movimiento de cámara

Usando el componente PointerLockControls del paquete @react-tres/drei , fije el cursor en la pantalla para que no se mueva cuando mueva el mouse, sino que cambie la posición de la cámara en la escena.


aplicación.jsx


Demostración del movimiento de la cámara.


Hagamos una pequeña edición para el componente Tierra .


Tierra.jsx


Código de sección

Añadiendo física

Para mayor claridad, agreguemos un cubo simple a la escena.


 <mesh position={[0, 3, -5]}> <boxGeometry /> </mesh> 


El cubo en escena


Ahora mismo simplemente está colgado en el espacio.


Utilice el componente Física del paquete @react-tres/rapier para agregar "física" a la escena. Como parámetro configuramos el campo de gravedad, donde configuramos las fuerzas gravitacionales a lo largo de los ejes.


 <Physics gravity={[0, -20, 0]}> <Ground /> <mesh position={[0, 3, -5]}> <boxGeometry /> </mesh> </Physics>


Sin embargo, nuestro cubo está dentro del componente de física, pero no le pasa nada. Para que el cubo se comporte como un objeto físico real, debemos envolverlo en el componente RigidBody del paquete @react-tres/rapier .


aplicación.jsx


Después de eso, veremos inmediatamente que cada vez que la página se recarga, el cubo cae bajo la influencia de la gravedad.


Caída del cubo


Pero ahora hay otra tarea: es necesario hacer del suelo un objeto con el que el cubo pueda interactuar y más allá del cual no caiga.


Código de sección

El suelo como objeto físico

Volvamos al componente Tierra y agreguemos un componente RigidBody como envoltura sobre la superficie del piso.


Tierra.jsx


Ahora, al caer, el cubo permanece en el suelo como un objeto físico real.


Cubo que cae en un avión.


Código de sección

Someter a un personaje a las leyes de la física.

Creemos un componente de jugador que controlará al personaje en la escena.


El personaje es el mismo objeto físico que el cubo agregado, por lo que debe interactuar con la superficie del piso así como con el cubo en la escena. Por eso agregamos el componente RigidBody . Y hagamos el personaje en forma de cápsula.


Reproductor.jsx


Coloque el componente Reproductor dentro del componente Física.


aplicación.jsx


Ahora nuestro personaje ha aparecido en escena.


Un personaje en forma de cápsula.


Código de sección

Mover un personaje - crear un gancho

El personaje será controlado usando las teclas WASD y saltará usando la barra espaciadora .

Con nuestro propio gancho de reacción, implementamos la lógica de mover al personaje.


Creemos un archivo hooks.js y agreguemos una nueva función usePersonControls allí.


Definamos un objeto en el formato {"código clave": "acción a realizar"}. A continuación, agregue controladores de eventos para presionar y soltar teclas del teclado. Cuando se activen los controladores, determinaremos las acciones actuales que se están realizando y actualizaremos su estado activo. Como resultado final, el gancho devolverá un objeto con el formato {"acción en curso": "estado"}.


ganchos.js


Código de sección

Mover un personaje: implementar un gancho

Después de implementar el gancho usePersonControls , debe usarse al controlar el personaje. En el componente Reproductor agregaremos seguimiento del estado de movimiento y actualizaremos el vector de la dirección del movimiento del personaje.


También definiremos variables que almacenarán los estados de las direcciones de movimiento.


Reproductor.jsx


Para actualizar la posición del personaje, usemos el marco proporcionado por el paquete @react-tres/fiber . Este gancho funciona de manera similar a requestAnimationFrame y ejecuta el cuerpo de la función aproximadamente 60 veces por segundo.


Reproductor.jsx


Explicación del código:

1. const playerRef = useRef(); Cree un enlace para el objeto del jugador. Este enlace permitirá la interacción directa con el objeto jugador en la escena.

2. const {adelante, atrás, izquierda, derecha, saltar} = usePersonControls(); Cuando se utiliza un gancho, se devuelve un objeto con valores booleanos que indican qué botones de control están presionados actualmente por el jugador.

3. useFrame((estado) => {... }); El gancho se llama en cada cuadro de la animación. Dentro de este gancho, se actualizan la posición y la velocidad lineal del jugador.

4. si (!playerRef.current) regresa; Comprueba la presencia de un objeto de jugador. Si no hay ningún objeto de jugador, la función detendrá la ejecución para evitar errores.

5. velocidad constante = playerRef.current.linvel(); Obtenga la velocidad lineal actual del jugador.

6. frontVector.set(0, 0, atrás - adelante); Establezca el vector de movimiento hacia adelante/atrás según los botones presionados.

7. sideVector.set(izquierda - derecha, 0, 0); Establezca el vector de movimiento izquierda/derecha.

8. dirección.subVectors(frontVector, sideVector).normalize().multiplyScalar(MOVE_SPEED); Calcule el vector final de movimiento del jugador restando los vectores de movimiento, normalizando el resultado (de modo que la longitud del vector sea 1) y multiplicando por la constante de velocidad de movimiento.

9. playerRef.current.wakeUp(); "Despierta" el objeto del jugador para asegurarse de que reacciona a los cambios. Si no utiliza este método, después de un tiempo el objeto "dormirá" y no reaccionará a los cambios de posición.

10. playerRef.current.setLinvel({ x: dirección.x, y: velocidad.y, z: dirección.z }); Establezca la nueva velocidad lineal del jugador en función de la dirección de movimiento calculada y mantenga la velocidad vertical actual (para no afectar los saltos o caídas).


Como resultado, al presionar las teclas WASD , el personaje comenzó a moverse por la escena. También puede interactuar con el cubo, porque ambos son objetos físicos.


Movimiento de personajes


Código de sección

Mover un personaje - saltar

Para implementar el salto, usemos la funcionalidad de los paquetes @dimforge/rapier3d-compat y @react-tres/rapier . En este ejemplo, comprobemos que el personaje está en el suelo y que se ha pulsado la tecla de salto. En este caso, establecemos la dirección del personaje y la fuerza de aceleración en el eje Y.


Para Player agregaremos masa y bloquearemos la rotación en todos los ejes, para que no se caiga en diferentes direcciones al chocar con otros objetos en la escena.


Reproductor.jsx


Explicación del código:

  1. mundo constante = estoque.mundo; Obteniendo acceso a la escena del motor de física Rapier . Contiene todos los objetos físicos y gestiona su interacción.
  1. const ray = world.castRay(new RAPIER.Ray(playerRef.current.translation(), { x: 0, y: -1, z: 0 })); Aquí es donde tiene lugar el "raycasting" (raycasting). Se crea un rayo que comienza en la posición actual del jugador y apunta hacia el eje y. Este rayo se "proyecta" en la escena para determinar si se cruza con algún objeto en la escena.
  1. const conectado a tierra = rayo && ray.collider && Math.abs(ray.toi) <= 1,5; La condición se verifica si el jugador está en el suelo:
  • rayo : si se creó el rayo ;
  • ray.collider : si el rayo chocó con algún objeto en la escena;
  • Math.abs(ray.toi) : el "tiempo de exposición" del rayo. Si este valor es menor o igual al valor dado, puede indicar que el jugador está lo suficientemente cerca de la superficie como para ser considerado "en el suelo".


También es necesario modificar el componente Tierra para que el algoritmo de trazado de rayos para determinar el estado de "aterrizaje" funcione correctamente, agregando un objeto físico que interactuará con otros objetos en la escena.


Tierra.jsx


Levantemos la cámara un poco más para ver mejor la escena.


principal.jsx


Saltos de personajes


Código de sección

Mover la cámara detrás del personaje.

Para mover la cámara, obtendremos la posición actual del reproductor y cambiaremos la posición de la cámara cada vez que se actualice el fotograma. Y para que el personaje se mueva exactamente a lo largo de la trayectoria hacia donde apunta la cámara, necesitamos agregar applyEuler .


Reproductor.jsx


Explicación del código:

El método applyEuler aplica rotación a un vector basándose en ángulos de Euler específicos. En este caso, la rotación de la cámara se aplica al vector de dirección . Esto se utiliza para hacer coincidir el movimiento relativo a la orientación de la cámara, de modo que el jugador se mueva en la dirección en la que se gira la cámara.


Ajustemos ligeramente el tamaño de Player y hagámoslo más alto en relación con el cubo, aumentando el tamaño de CapsuleCollider y arreglando la lógica de "salto".


Reproductor.jsx


moviendo la camara


Código de sección

Generación de cubos

Para que la escena no parezca completamente vacía, agreguemos generación de cubos. En el archivo json, enumere las coordenadas de cada uno de los cubos y luego muéstrelas en la escena. Para hacer esto, cree un archivo cubes.json , en el que enumeraremos una matriz de coordenadas.


 [ [0, 0, -7], [2, 0, -7], [4, 0, -7], [6, 0, -7], [8, 0, -7], [10, 0, -7] ]


En el archivo Cube.jsx , cree un componente Cubes , que generará cubos en un bucle. Y el componente Cube será un objeto generado directamente.


 import {RigidBody} from "@react-three/rapier"; import cubes from "./cubes.json"; export const Cubes = () => { return cubes.map((coords, index) => <Cube key={index} position={coords} />); } const Cube = (props) => { return ( <RigidBody {...props}> <mesh castShadow receiveShadow> <meshStandardMaterial color="white" /> <boxGeometry /> </mesh> </RigidBody> ); }


Agreguemos el componente Cubos creado al componente Aplicación eliminando el cubo único anterior.


aplicación.jsx


Generación de cubos


Código de sección

Importando el modelo al proyecto.

Ahora agreguemos un modelo 3D a la escena. Agreguemos un modelo de arma para el personaje. Empecemos buscando un modelo 3D. Por ejemplo, tomemos este .


Descargue el modelo en formato GLTF y descomprima el archivo en la raíz del proyecto.

Para obtener el formato que necesitamos para importar el modelo a la escena, necesitaremos instalar el paquete complementario gltf-pipeline .


npm i -D gltf-pipeline


Usando el paquete gltf-pipeline , reconvierta el modelo del formato GLTF al formato GLB , ya que en este formato todos los datos del modelo se colocan en un solo archivo. Como directorio de salida para el archivo generado especificamos la carpeta pública .


gltf-pipeline -i weapon/scene.gltf -o public/weapon.glb


Luego necesitamos generar un componente de reacción que contendrá el marcado de este modelo para agregarlo a la escena. Utilicemos el recurso oficial de los desarrolladores de @react-tres/fiber .


Para ir al convertidor será necesario cargar el archivo arma.glb convertido.


Usando arrastrar y soltar o la búsqueda del Explorador, busque este archivo y descárguelo.


Modelo convertido


En el convertidor veremos el componente de reacción generado, cuyo código transferiremos a nuestro proyecto en un nuevo archivo WeaponModel.jsx , cambiando el nombre del componente por el mismo nombre que el archivo.


Código de sección

Mostrando el modelo de arma en la escena.

Ahora importemos el modelo creado a la escena. En el archivo App.jsx agregue el componente WeaponModel .


aplicación.jsx


Demostración del modelo importado.


Código de sección

Agregando sombras

En este punto de nuestra escena, ninguno de los objetos proyecta sombras.

Para habilitar sombras en la escena, debe agregar el atributo de sombras al componente Canvas .


principal.jsx


A continuación, debemos agregar una nueva fuente de luz. A pesar de que ya tenemos luz ambiental en escena, no puede crear sombras para los objetos porque no tiene un haz de luz direccional. Entonces, agreguemos una nueva fuente de luz llamada direccionalLight y configurémosla. El atributo para habilitar el modo de sombra " proyectar " es castShadow . Es la adición de este parámetro lo que indica que este objeto puede proyectar una sombra sobre otros objetos.


aplicación.jsx


Después de eso, agreguemos otro atributo recibir Sombra al componente Tierra , lo que significa que el componente en la escena puede recibir y mostrar sombras sobre sí mismo.


Tierra.jsx


El modelo proyecta una sombra.


Se deberían agregar atributos similares a otros objetos en la escena: cubos y jugador. Para los cubos agregaremos castShadow yceivedShadow , porque pueden proyectar y recibir sombras, y para el jugador agregaremos solo castShadow .


Agreguemos castShadow para Player .


Reproductor.jsx


Agregue castShadow y recibaShadow para Cube .


cubo.jsx


Todos los objetos en escena proyectan una sombra.


Código de sección

Agregar sombras: corregir el recorte de sombras

Si miras de cerca ahora, encontrarás que el área de superficie sobre la que se proyecta la sombra es bastante pequeña. Y al ir más allá de esta zona, la sombra simplemente se corta.


Recorte de sombras


La razón de esto es que, de forma predeterminada, la cámara captura solo una pequeña área de las sombras mostradas desde direccionalLight . Podemos usar el componente direccionalLight agregando atributos adicionales de cámara de sombra (arriba, abajo, izquierda, derecha) para expandir esta área de visibilidad. Después de agregar estos atributos, la sombra se volverá ligeramente borrosa. Para mejorar la calidad, agregaremos el atributo Shadow-MapSize .


aplicación.jsx


Código de sección

Vincular armas a un personaje

Ahora agreguemos la visualización de armas en primera persona. Cree un nuevo componente de arma , que contendrá la lógica de comportamiento del arma y el modelo 3D en sí.


 import {WeaponModel} from "./WeaponModel.jsx"; export const Weapon = (props) => { return ( <group {...props}> <WeaponModel /> </group> ); }


Coloquemos este componente en el mismo nivel que el RigidBody del personaje y en el gancho useFrame estableceremos la posición y el ángulo de rotación en función de la posición de los valores de la cámara.


Reproductor.jsx


Visualización del modelo de arma en primera persona.


Código de sección

Animación del arma blandiendo al caminar.

Para que el andar del personaje sea más natural, agregaremos un ligero movimiento del arma mientras se mueve. Para crear la animación usaremos la biblioteca tween.js instalada.


El componente Arma se incluirá en una etiqueta de grupo para que pueda agregarle una referencia a través del gancho useRef .


Reproductor.jsx


Agreguemos algo de useState para guardar la animación.


Reproductor.jsx


Creemos una función para inicializar la animación.


Reproductor.jsx


Explicación del código:

  1. const twSwayingAnimation = new TWEEN.Tween(currentPosition) ... Creando una animación de un objeto "balanceándose" desde su posición actual a una nueva posición.
  1. const twSwayingBackAnimation = new TWEEN.Tween(currentPosition) ... Creando una animación del objeto que regresa a su posición inicial después de que se haya completado la primera animación.
  1. twSwayingAnimation.chain(twSwayingBackAnimation); Conectar dos animaciones para que cuando se complete la primera animación, la segunda animación comience automáticamente.


En useEffect llamamos a la función de inicialización de la animación.


Reproductor.jsx


Ahora es necesario determinar el momento durante el cual se produce el movimiento. Esto se puede hacer determinando el vector actual de dirección del personaje.


Si se produce movimiento del personaje, actualizaremos la animación y la ejecutaremos nuevamente cuando termine.


Reproductor.jsx


Explicación del código:

  1. const isMoving = dirección.longitud() > 0; Aquí se comprueba el estado de movimiento del objeto. Si el vector de dirección tiene una longitud mayor que 0, significa que el objeto tiene una dirección de movimiento.
  1. if (isMoving && isSwayingAnimationFinished) { ... } Este estado se ejecuta si el objeto se está moviendo y la animación de "oscilación" ha finalizado.


En el componente de la aplicación , agreguemos un useFrame donde actualizaremos la animación de interpolación.


aplicación.jsx


TWEEN.update() actualiza todas las animaciones activas en la biblioteca TWEEN.js . Este método se llama en cada cuadro de animación para garantizar que todas las animaciones se ejecuten sin problemas.


Código de sección:

Animación de retroceso

Necesitamos definir el momento en que se dispara un tiro, es decir, cuando se presiona el botón del mouse. Agreguemos useState para almacenar este estado, useRef para almacenar una referencia al objeto arma y dos controladores de eventos para presionar y soltar el botón del mouse.


Arma.jsx


Arma.jsx


Arma.jsx


Implementemos una animación de retroceso al hacer clic con el botón del mouse. Usaremos la biblioteca tween.js para este propósito.


Definamos constantes para la fuerza de retroceso y la duración de la animación.


Arma.jsx


Al igual que con la animación de movimiento del arma, agregamos dos estados useState para la animación de retroceso y regreso a la posición inicial y un estado con el estado final de la animación.


Arma.jsx


Creemos funciones para obtener un vector aleatorio de animación de retroceso: generateRecoilOffset y generateNewPositionOfRecoil .


Arma.jsx


Crea una función para inicializar la animación de retroceso. También agregaremos useEffect , en el que especificaremos el estado de "disparo" como una dependencia, de modo que en cada disparo la animación se inicialice nuevamente y se generen nuevas coordenadas finales.


Arma.jsx


Arma.jsx


Y en useFrame , agreguemos una marca para "mantener presionada" la tecla del mouse para disparar, de modo que la animación de disparo no se detenga hasta que se suelte la tecla.


Arma.jsx


Animación de retroceso


Código de sección

Animación durante la inactividad.

Realiza la animación de "inactividad" del personaje, para que no haya sensación de que el juego está "colgado".


Para hacer esto, agreguemos algunos estados nuevos mediante useState .


Reproductor.jsx


Arreglemos la inicialización de la animación de "meneo" para usar valores del estado. La idea es que diferentes estados: caminar o detenerse, usarán diferentes valores para la animación y cada vez la animación se inicializará primero.


Reproductor.jsx


Animación inactiva


Conclusión

En esta parte hemos implementado la generación de escenas y el movimiento de personajes. También agregamos un modelo de arma, animación de retroceso al disparar y al ralentí. En la siguiente parte continuaremos perfeccionando nuestro juego, agregando nuevas funciones.


También publicado aquí .