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 , una poderosa herramienta para crear gráficos 3D basados en utilizando la tecnología . React Three Fiber Three.js React Acerca de la pila React Three Fiber es un contenedor de que utiliza la estructura y los principios de para crear gráficos 3D en la web. Esta pila permite a los desarrolladores combinar el poder de con la conveniencia y flexibilidad de , haciendo que el proceso de creación de una aplicación sea más intuitivo y organizado. React Three Fiber Three.js React Three.js React En el corazón de está la idea de que todo lo que creas en una escena es un componente . Esto permite a los desarrolladores aplicar patrones y metodologías familiares. React Three Fiber de React Una de las principales ventajas de es su facilidad de integración con el ecosistema . Cualquier otra herramienta aún se puede integrar fácilmente al usar esta biblioteca. React Three Fiber React de React Relevancia de Web-GameDev ha experimentado cambios importantes en los últimos años, evolucionando desde simples juegos 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. Web-GameDev Flash 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 , , y se optimizan y desarrollan constantemente para garantizar un alto rendimiento, lo que los convierte en una plataforma ideal para desarrollar aplicaciones complejas. Chrome Firefox Edge otros Una de las herramientas clave que ha impulsado el desarrollo de los juegos basados en navegador es . 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, abre nuevas posibilidades para crear impresionantes aplicaciones web directamente en el navegador. WebGL WebGL 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 https://codesandbox.io/p/github/JI0PATA/fps-game?embedable=true Repositorio en GitHub ¡Ahora comencemos! Configurando el proyecto e instalando paquetes. En primer lugar, necesitaremos una plantilla de proyecto . Así que comencemos instalándolo. de React 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 todo lo innecesario de nuestro proyecto. elimine Código de sección Personalizando la visualización del lienzo En el archivo , agregue un elemento div que se mostrará en la página como alcance. Inserte un componente y configure el campo de visión de la cámara. Dentro del componente , coloque el componente . main.jsx Canvas Canvas App Agreguemos estilos a 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. index.css En el componente agregamos un componente , que se mostrará como fondo en nuestra escena de juego en forma de cielo. Aplicación Cielo Código de sección Superficie del suelo Creemos un componente y colóquelo en el componente . Ground App En , 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 Aunque especificamos el gris como color del material, el avión parece completamente negro. 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 que ilumina el objeto desde todos los lados y no tiene un haz dirigido. Como parámetro establece la intensidad del brillo. ambientLight 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 agregue una imagen PNG con una textura. de activos Para cargar una textura en la escena, usemos el gancho del paquete . 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. useTexture @react-tres/drei Código de sección Movimiento de cámara Usando el componente del paquete , 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. PointerLockControls @react-tres/drei Hagamos una pequeña edición para el componente . Tierra 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> Ahora mismo simplemente está colgado en el espacio. Utilice el componente del paquete 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. Física @react-tres/rapier <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 del paquete . RigidBody @react-tres/rapier Después de eso, veremos inmediatamente que cada vez que la página se recarga, el cubo cae bajo la influencia de la gravedad. 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 y agreguemos un componente como envoltura sobre la superficie del piso. Tierra RigidBody Ahora, al caer, el cubo permanece en el suelo como un objeto físico real. Código de sección Someter a un personaje a las leyes de la física. Creemos un componente que controlará al personaje en la escena. de jugador 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 . Y hagamos el personaje en forma de cápsula. RigidBody Coloque el componente dentro del componente Física. Reproductor Ahora nuestro personaje ha aparecido en escena. Código de sección Mover un personaje - crear un gancho El personaje será controlado usando las teclas y saltará usando la . WASD barra espaciadora Con nuestro propio gancho de reacción, implementamos la lógica de mover al personaje. Creemos un archivo y agreguemos una nueva función allí. hooks.js usePersonControls 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"}. Código de sección Mover un personaje: implementar un gancho Después de implementar el gancho , debe usarse al controlar el personaje. En el componente agregaremos seguimiento del estado de movimiento y actualizaremos el vector de la dirección del movimiento del personaje. usePersonControls Reproductor También definiremos variables que almacenarán los estados de las direcciones de movimiento. Para actualizar la posición del personaje, usemos proporcionado por el paquete . Este gancho funciona de manera similar a y ejecuta el cuerpo de la función aproximadamente 60 veces por segundo. el marco @react-tres/fiber requestAnimationFrame Explicación del código: Cree un enlace para el objeto del jugador. Este enlace permitirá la interacción directa con el objeto jugador en la escena. 1. const playerRef = useRef(); 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. 2. const {adelante, atrás, izquierda, derecha, saltar} = usePersonControls(); 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. 3. useFrame((estado) => {... }); 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. 4. si (!playerRef.current) regresa; Obtenga la velocidad lineal actual del jugador. 5. velocidad constante = playerRef.current.linvel(); Establezca el vector de movimiento hacia adelante/atrás según los botones presionados. 6. frontVector.set(0, 0, atrás - adelante); Establezca el vector de movimiento izquierda/derecha. 7. sideVector.set(izquierda - derecha, 0, 0); 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. 8. dirección.subVectors(frontVector, sideVector).normalize().multiplyScalar(MOVE_SPEED); "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. 9. playerRef.current.wakeUp(); 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). 10. playerRef.current.setLinvel({ x: dirección.x, y: velocidad.y, z: dirección.z }); Como resultado, al presionar las teclas , el personaje comenzó a moverse por la escena. También puede interactuar con el cubo, porque ambos son objetos físicos. WASD Código de sección Mover un personaje - saltar Para implementar el salto, usemos la funcionalidad de los paquetes y . 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. @dimforge/rapier3d-compat @react-tres/rapier Para 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. Player Explicación del código: Obteniendo acceso a la escena del motor de física . Contiene todos los objetos físicos y gestiona su interacción. mundo constante = estoque.mundo; Rapier 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. const ray = world.castRay(new RAPIER.Ray(playerRef.current.translation(), { x: 0, y: -1, z: 0 })); La condición se verifica si el jugador está en el suelo: const conectado a tierra = rayo && ray.collider && Math.abs(ray.toi) <= 1,5; : si se creó el ; rayo rayo : si el rayo chocó con algún objeto en la escena; ray.collider : 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". Math.abs(ray.toi) También es necesario modificar el componente 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 Levantemos la cámara un poco más para ver mejor la escena. Código de sección Primer compromiso Segundo compromiso 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 Explicación del código: El método 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 . 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. applyEuler de dirección Ajustemos ligeramente el tamaño de y hagámoslo más alto en relación con el cubo, aumentando el tamaño de y arreglando la lógica de "salto". Player CapsuleCollider Código de sección Primer compromiso Segundo compromiso 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 , en el que enumeraremos una matriz de coordenadas. cubes.json [ [0, 0, -7], [2, 0, -7], [4, 0, -7], [6, 0, -7], [8, 0, -7], [10, 0, -7] ] En el archivo , cree un componente , que generará cubos en un bucle. Y el componente será un objeto generado directamente. Cube.jsx Cubes Cube 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 creado al componente eliminando el cubo único anterior. Cubos Aplicación 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 , reconvierta el modelo del al , 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 . gltf-pipeline formato GLTF formato GLB 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 de los desarrolladores de . recurso oficial @react-tres/fiber Para ir al convertidor será necesario cargar el archivo convertido. arma.glb Usando arrastrar y soltar o la búsqueda del Explorador, busque este archivo y descárguelo. En el convertidor veremos el componente de reacción generado, cuyo código transferiremos a nuestro proyecto en un nuevo archivo , cambiando el nombre del componente por el mismo nombre que el archivo. WeaponModel.jsx Código de sección Mostrando el modelo de arma en la escena. Ahora importemos el modelo creado a la escena. En el archivo agregue el componente . App.jsx WeaponModel Código de sección Agregando sombras En este punto de nuestra escena, ninguno de los objetos proyecta sombras. Para habilitar en la escena, debe agregar el atributo al componente . sombras de sombras Canvas A continuación, debemos agregar una nueva fuente de luz. A pesar de que ya tenemos 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 y configurémosla. El atributo para habilitar el modo de sombra " " es . Es la adición de este parámetro lo que indica que este objeto puede proyectar una sombra sobre otros objetos. luz ambiental direccionalLight proyectar castShadow Después de eso, agreguemos otro atributo al componente , lo que significa que el componente en la escena puede recibir y mostrar sombras sobre sí mismo. recibir Sombra Tierra Se deberían agregar atributos similares a otros objetos en la escena: cubos y jugador. Para los cubos agregaremos , porque pueden proyectar y recibir sombras, y para el jugador agregaremos solo . castShadow yceivedShadow castShadow Agreguemos para . castShadow Player Agregue y para . castShadow recibaShadow Cube 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. La razón de esto es que, de forma predeterminada, la cámara captura solo una pequeña área de las sombras mostradas desde . Podemos usar el componente agregando atributos adicionales 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 . direccionalLight direccionalLight de cámara de sombra (arriba, abajo, izquierda, derecha) Shadow-MapSize Código de sección Vincular armas a un personaje Ahora agreguemos la visualización de armas en primera persona. Cree un nuevo componente , que contendrá la lógica de comportamiento del arma y el modelo 3D en sí. de arma import {WeaponModel} from "./WeaponModel.jsx"; export const Weapon = (props) => { return ( <group {...props}> <WeaponModel /> </group> ); } Coloquemos este componente en el mismo nivel que el del personaje y en el gancho estableceremos la posición y el ángulo de rotación en función de la posición de los valores de la cámara. RigidBody useFrame 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 instalada. tween.js El componente se incluirá en una etiqueta de grupo para que pueda agregarle una referencia a través del gancho . Arma useRef Agreguemos algo para guardar la animación. de useState Creemos una función para inicializar la animación. Explicación del código: Creando una animación de un objeto "balanceándose" desde su posición actual a una nueva posición. const twSwayingAnimation = 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. const twSwayingBackAnimation = new TWEEN.Tween(currentPosition) ... Conectar dos animaciones para que cuando se complete la primera animación, la segunda animación comience automáticamente. twSwayingAnimation.chain(twSwayingBackAnimation); En llamamos a la función de inicialización de la animación. useEffect 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. Explicación del código: 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. const isMoving = dirección.longitud() > 0; Este estado se ejecuta si el objeto se está moviendo y la animación de "oscilación" ha finalizado. if (isMoving && isSwayingAnimationFinished) { ... } En el componente , agreguemos un donde actualizaremos la animación de interpolación. de la aplicación useFrame actualiza todas las animaciones activas en la biblioteca . Este método se llama en cada cuadro de animación para garantizar que todas las animaciones se ejecuten sin problemas. TWEEN.update() TWEEN.js Código de sección: Primer compromiso Segundo compromiso 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 para almacenar este estado, para almacenar una referencia al objeto arma y dos controladores de eventos para presionar y soltar el botón del mouse. useState useRef Implementemos una animación de retroceso al hacer clic con el botón del mouse. Usaremos la biblioteca para este propósito. tween.js Definamos constantes para la fuerza de retroceso y la duración de la animación. 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. Creemos funciones para obtener un vector aleatorio de animación de retroceso: y . generateRecoilOffset generateNewPositionOfRecoil Crea una función para inicializar la animación de retroceso. También agregaremos , 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. useEffect Y en , 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. useFrame 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 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. 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í