No desenvolvimento web moderno, as fronteiras entre aplicativos clássicos e aplicativos web estão se confundindo a cada dia. Hoje podemos criar não apenas sites interativos, mas também jogos completos direto no navegador. Uma das ferramentas que torna isso possível é a biblioteca - uma ferramenta poderosa para criar gráficos 3D baseados em usando a tecnologia . React Three Fiber Three.js React Sobre a pilha React Three Fiber é um wrapper sobre que usa a estrutura e os princípios do para criar gráficos 3D na web. Essa pilha permite que os desenvolvedores combinem o poder do com a conveniência e flexibilidade do , tornando o processo de criação de um aplicativo mais intuitivo e organizado. React Three Fiber Three.js React Three.js React No cerne do está a ideia de que tudo o que você cria em uma cena é um componente . Isso permite que os desenvolvedores apliquem padrões e metodologias familiares. React Three Fiber do React Uma das principais vantagens do é a facilidade de integração com o ecossistema . Quaisquer outras ferramentas ainda podem ser facilmente integradas ao usar esta biblioteca. React Three Fiber React React Relevância do Web-GameDev passou por grandes mudanças nos últimos anos, evoluindo de simples jogos 2D para projetos 3D complexos comparáveis a aplicativos de desktop. Este crescimento em popularidade e capacidades torna o Web-GameDev uma área que não pode ser ignorada. O Web-GameDev Flash Uma das principais vantagens dos jogos na web é a sua acessibilidade. Os jogadores não precisam baixar e instalar nenhum software adicional – basta clicar no link em seu navegador. Isto simplifica a distribuição e promoção de jogos, disponibilizando-os para um amplo público em todo o mundo. Por fim, o desenvolvimento de jogos para web pode ser uma ótima maneira para os desenvolvedores experimentarem o desenvolvimento de jogos usando tecnologias familiares. Graças às ferramentas e bibliotecas disponíveis, mesmo sem experiência em gráficos 3D, é possível criar projetos interessantes e de alta qualidade! Desempenho do jogo em navegadores modernos Os navegadores modernos percorreram um longo caminho, evoluindo de ferramentas de navegação bastante simples para plataformas poderosas para executar aplicativos e jogos complexos. Os principais navegadores como , , e são constantemente otimizados e desenvolvidos para garantir alto desempenho, tornando-os uma plataforma ideal para o desenvolvimento de aplicações complexas. Chrome Firefox Edge outros Uma das principais ferramentas que impulsionou o desenvolvimento de jogos baseados em navegador é . Esse padrão permitiu que os desenvolvedores usassem aceleração gráfica de hardware, o que melhorou significativamente o desempenho dos jogos 3D. Juntamente com outras webAPIs, abre novas possibilidades para a criação de aplicações web impressionantes diretamente no navegador. o WebGL o WebGL No entanto, ao desenvolver jogos para o navegador, é crucial considerar vários aspectos de desempenho: otimização de recursos, gestão de memória e adaptação para diferentes dispositivos são pontos-chave que podem afetar o sucesso de um projeto. Em sua marca! No entanto, palavras e teoria são uma coisa, mas a experiência prática é outra bem diferente. Para realmente compreender e apreciar todo o potencial do desenvolvimento de jogos web, a melhor maneira é mergulhar no processo de desenvolvimento. Portanto, como exemplo de desenvolvimento bem-sucedido de jogos para web, criaremos nosso próprio jogo. Este processo nos permitirá aprender os principais aspectos do desenvolvimento, enfrentar problemas reais e encontrar soluções para eles, e ver o quão poderosa e flexível uma plataforma de desenvolvimento de jogos web pode ser. Em uma série de artigos, veremos como criar um jogo de tiro em primeira pessoa usando os recursos desta biblioteca e mergulhar no emocionante mundo do web-gamedev! Demonstração final https://codesandbox.io/p/github/JI0PATA/fps-game?embedable=true Repositório no GitHub Agora vamos começar! Configurando o projeto e instalando pacotes Primeiro de tudo, precisaremos de um modelo de projeto . Então, vamos começar instalando-o. React npm create vite@latest selecione a biblioteca ; React selecione . JavaScript Instale pacotes npm adicionais. npm install three @react-three/fiber @react-three/drei @react three/rapier zustand @tweenjs/tween.js Em seguida, tudo o que for desnecessário do nosso projeto. exclua Código da seção Personalizando a exibição do Canvas No arquivo , adicione um elemento div que será exibido na página como escopo. Insira um componente e defina o campo de visão da câmera. Dentro do componente coloque o componente . main.jsx Canvas Canvas App Vamos adicionar estilos a para esticar os elementos da UI até a altura total da tela e exibir o escopo como um círculo no centro da tela. index.css No componente adicionamos um componente , que será exibido como plano de fundo em nossa cena de jogo na forma de um céu. App Sky Código da seção Superfície do piso Vamos criar um componente e colocá-lo no componente . Ground App Em , crie um elemento de superfície plana. No eixo Y mova-o para baixo para que este plano fique no campo de visão da câmera. E também vire o plano no eixo X para torná-lo horizontal. Ground Embora tenhamos especificado cinza como cor do material, o plano parece completamente preto. Código da seção Iluminação básica Por padrão, não há iluminação na cena, então vamos adicionar uma fonte de luz , que ilumina o objeto por todos os lados e não possui feixe direcionado. Como parâmetro defina a intensidade do brilho. ambientLight Código da seção Textura para a superfície do piso Para que a superfície do piso não pareça homogênea, adicionaremos textura. Faça um padrão na superfície do piso na forma de células que se repetem ao longo de toda a superfície. Na pasta adicione uma imagem PNG com textura. de ativos Para carregar uma textura na cena, vamos usar o gancho do pacote . E como parâmetro para o gancho passaremos a imagem da textura importada para o arquivo. Defina a repetição da imagem nos eixos horizontais. useTexture @react-two/drei Código da seção Movimento da câmera Usando o componente do pacote , fixe o cursor na tela para que ele não se mova quando você move o mouse, mas mude a posição da câmera na cena. PointerLockControls @react-two/drei Vamos fazer uma pequena edição no componente . Ground Código da seção Adicionando física Para maior clareza, vamos adicionar um cubo simples à cena. <mesh position={[0, 3, -5]}> <boxGeometry /> </mesh> Neste momento ele está apenas pendurado no espaço. Use o componente do pacote para adicionar "física" à cena. Como parâmetro, configure o campo gravitacional, onde definimos as forças gravitacionais ao longo dos eixos. Physics @react-two/rapier <Physics gravity={[0, -20, 0]}> <Ground /> <mesh position={[0, 3, -5]}> <boxGeometry /> </mesh> </Physics> Porém, nosso cubo está dentro do componente físico, mas nada acontece com ele. Para fazer o cubo se comportar como um objeto físico real, precisamos envolvê-lo no componente do pacote . RigidBody @react-two/rapier Depois disso, veremos imediatamente que cada vez que a página é recarregada, o cubo cai sob a influência da gravidade. Mas agora há outra tarefa - é preciso fazer do chão um objeto com o qual o cubo possa interagir e além do qual não caia. Código da seção O chão como objeto físico Vamos voltar ao componente e adicionar um componente como um wrapper sobre a superfície do piso. Ground RigidBody Agora, ao cair, o cubo permanece no chão como um objeto físico real. Código da seção Submetendo um personagem às leis da física Vamos criar um componente que controlará o personagem em cena. Player O personagem é o mesmo objeto físico que o cubo adicionado, portanto ele deve interagir com a superfície do chão e também com o cubo na cena. É por isso que adicionamos o componente . E vamos fazer o personagem em forma de cápsula. RigidBody Coloque o componente dentro do componente Física. Player Agora nosso personagem apareceu em cena. Código da seção Movendo um personagem – criando um gancho O personagem será controlado através das teclas , e saltará através da . WASD barra de espaço Com nosso próprio gancho de reação, implementamos a lógica de movimentação do personagem. Vamos criar um arquivo e adicionar uma nova função nele. hooks.js usePersonControls Vamos definir um objeto no formato {"keycode": "ação a ser executada"}. Em seguida, adicione manipuladores de eventos para pressionar e soltar teclas do teclado. Quando os manipuladores forem acionados, determinaremos as ações atuais que estão sendo executadas e atualizaremos seu estado ativo. Como resultado final, o gancho retornará um objeto no formato {"action in progress": "status"}. Código da seção Movendo um personagem – implementando um gancho Após implementar o gancho , ele deve ser usado ao controlar o personagem. No componente adicionaremos rastreamento do estado de movimento e atualizaremos o vetor da direção do movimento do personagem. usePersonControls Player Também definiremos variáveis que armazenarão os estados das direções de movimento. Para atualizar a posição do personagem, vamos fornecido pelo pacote . Este gancho funciona de forma semelhante a e executa o corpo da função cerca de 60 vezes por segundo. usarFrame @react-two/fiber requestAnimationFrame Explicação do código: Crie um link para o objeto do jogador. Este link permitirá a interação direta com o objeto do jogador na cena. 1. const playerRef = useRef(); Quando um gancho é usado, é retornado um objeto com valores booleanos indicando quais botões de controle estão pressionados no momento pelo jogador. 2. const {avançar, retroceder, esquerda, direita, pular} = usePersonControls(); O gancho é chamado em cada quadro da animação. Dentro deste gancho, a posição e a velocidade linear do jogador são atualizadas. 3. useFrame((estado) => {... }); Verifica a presença de um objeto de jogador. Se não houver nenhum objeto player, a função interromperá a execução para evitar erros. 4. if (!playerRef.current) retornar; Obtenha a velocidade linear atual do jogador. 5. velocidade const = playerRef.current.linvel(); Defina o vetor de movimento para frente/trás com base nos botões pressionados. 6. frontVector.set(0, 0, para trás - para frente); Defina o vetor de movimento esquerda/direita. 7. sideVector.set(esquerda - direita, 0, 0); Calcule o vetor final de movimento do jogador subtraindo os vetores de movimento, normalizando o resultado (de forma que o comprimento do vetor seja 1) e multiplicando pela constante de velocidade de movimento. 8. direção.subVectors(frontVector, sideVector).normalize().multiplyScalar(MOVE_SPEED); "Acorda" o objeto do jogador para garantir que ele reaja às mudanças. Se você não usar este método, depois de algum tempo o objeto “adormecerá” e não reagirá às mudanças de posição. 9.playerRef.current.wakeUp(); Defina a nova velocidade linear do jogador com base na direção calculada do movimento e mantenha a velocidade vertical atual (para não afetar saltos ou quedas). 10. playerRef.current.setLinvel({ x: direção.x, y: velocidade.y, z: direção.z }); Como resultado, ao pressionar as teclas , o personagem começou a se movimentar pelo cenário. Ele também pode interagir com o cubo, pois ambos são objetos físicos. WASD Código da seção Movendo um personagem - pule Para implementar o salto, vamos usar a funcionalidade dos pacotes e . Neste exemplo, vamos verificar se o personagem está no chão e se a tecla de pular foi pressionada. Neste caso, definimos a direção do personagem e a força de aceleração no eixo Y. @dimforge/rapier3d-compat @react-two/rapier Para adicionaremos massa e bloquearemos a rotação em todos os eixos, para que ele não caia em direções diferentes ao colidir com outros objetos no cenário. o Player Explicação do código: Obtendo acesso ao cenário do motor de física . Ele contém todos os objetos físicos e gerencia sua interação. const mundo = rapier.mundo; Rapier É aqui que ocorre o "raycasting" (raycasting). É criado um raio que começa na posição atual do jogador e aponta para baixo no eixo y. Este raio é “projetado” na cena para determinar se ele intercepta algum objeto na cena. const ray = world.castRay(new RAPIER.Ray(playerRef.current.translation(), { x: 0, y: -1, z: 0 })); A condição é verificada se o jogador estiver no chão: const aterrado = ray && ray.collider && Math.abs(ray.toi) <= 1,5; - se o foi criado; raio raio - se o raio colidiu com algum objeto na cena; ray.collider - o "tempo de exposição" do raio. Se este valor for inferior ou igual ao valor indicado, pode indicar que o jogador está suficientemente próximo da superfície para ser considerado “no chão”. Math.abs(ray.toi) Você também precisa modificar o componente para que o algoritmo raytraced para determinar o status de "aterrissagem" funcione corretamente, adicionando um objeto físico que irá interagir com outros objetos na cena. Ground Vamos levantar um pouco mais a câmera para uma melhor visualização da cena. Código da seção Primeiro commit Segundo commit Movendo a câmera atrás do personagem Para mover a câmera, obteremos a posição atual do player e alteraremos a posição da câmera toda vez que o quadro for atualizado. E para que o personagem se mova exatamente ao longo da trajetória para onde a câmera está direcionada, precisamos adicionar . applyEuler Explicação do código: O método aplica rotação a um vetor com base em ângulos de Euler especificados. Neste caso, a rotação da câmera é aplicada ao vetor . Isto é usado para combinar o movimento relativo à orientação da câmera, de modo que o jogador se mova na direção em que a câmera é girada. applyEuler de direção Vamos ajustar levemente o tamanho do e torná-lo mais alto em relação ao cubo, aumentando o tamanho do e corrigindo a lógica de "salto". Player CapsuleCollider Código da seção Primeiro commit Segundo commit Geração de cubos Para que a cena não pareça completamente vazia, vamos adicionar a geração de cubos. No arquivo json, liste as coordenadas de cada um dos cubos e exiba-as na cena. Para fazer isso, crie um arquivo , no qual listaremos um array de coordenadas. cubes.json [ [0, 0, -7], [2, 0, -7], [4, 0, -7], [6, 0, -7], [8, 0, -7], [10, 0, -7] ] No arquivo , crie um componente , que gerará cubos em um loop. E o componente será um objeto gerado diretamente. 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> ); } Vamos adicionar o componente criado ao componente excluindo o cubo único anterior. Cubes App Código da seção Importando o modelo para o projeto Agora vamos adicionar um modelo 3D à cena. Vamos adicionar um modelo de arma para o personagem. Vamos começar procurando um modelo 3D. Por exemplo, vamos pegar . este Baixe o modelo no formato GLTF e descompacte o arquivo na raiz do projeto. Para obter o formato que precisamos para importar o modelo para a cena, precisaremos instalar o pacote complementar . gltf-pipeline npm i -D gltf-pipeline Usando o pacote , reconverta o modelo do para o , pois neste formato todos os dados do modelo são colocados em um arquivo. Como diretório de saída para o arquivo gerado, especificamos a pasta . gltf-pipeline formato GLTF formato GLB pública gltf-pipeline -i weapon/scene.gltf -o public/weapon.glb Então precisamos gerar um componente react que conterá a marcação deste modelo para adicioná-lo à cena. Vamos usar o dos desenvolvedores . recurso oficial @react-two/fiber Ir para o conversor exigirá que você carregue o arquivo convertido. arma.glb Usando arrastar e soltar ou pesquisa no Explorer, encontre este arquivo e baixe-o. No conversor veremos o componente react gerado, cujo código transferiremos para o nosso projeto em um novo arquivo , alterando o nome do componente para o mesmo nome do arquivo. WeaponModel.jsx Código da seção Exibindo o modelo da arma na cena Agora vamos importar o modelo criado para a cena. No arquivo , adicione o componente . App.jsx WeaponModel Código da seção Adicionando sombras Neste ponto da nossa cena, nenhum dos objetos está projetando sombras. Para habilitar na cena você precisa adicionar o atributo ao componente . sombras shadows Canvas Em seguida, precisamos adicionar uma nova fonte de luz. Apesar de já termos na cena, ele não consegue criar sombras para objetos, pois não possui feixe de luz direcional. Então, vamos adicionar uma nova fonte de luz chamada e configurá-la. O atributo para ativar o modo de sombra " " é . É a adição deste parâmetro que indica que este objeto pode projetar sombra sobre outros objetos. ambientLight direcionalLight cast castShadow Depois disso, vamos adicionar outro atributo ao componente , o que significa que o componente na cena pode receber e exibir sombras sobre si mesmo. recebeShadow Ground Atributos semelhantes devem ser adicionados a outros objetos na cena: cubos e jogador. Para os cubos adicionaremos e , pois ambos podem lançar e receber sombras, e para o jogador adicionaremos apenas . castShadow ReceiveShadow castShadow Vamos adicionar para . castShadow Player Adicione e para . castShadow recebaShadow Cube Código da seção Adicionando sombras - corrigindo o recorte de sombras Se você olhar atentamente agora, descobrirá que a área da superfície sobre a qual a sombra é projetada é bem pequena. E ao ultrapassar esta área, a sombra é simplesmente cortada. A razão para isso é que, por padrão, a câmera captura apenas uma pequena área das sombras exibidas em . Podemos fazer isso para o componente adicionando atributos adicionais para expandir esta área de visibilidade. Depois de adicionar esses atributos, a sombra ficará ligeiramente desfocada. Para melhorar a qualidade, adicionaremos o atributo . direcionalLight direcionalLight shadow-camera-(top, bottom, left, right) shadow-mapSize Código da seção Vinculando armas a um personagem Agora vamos adicionar a exibição de armas em primeira pessoa. Crie um novo componente , que conterá a lógica de comportamento da arma e o próprio modelo 3D. Arma import {WeaponModel} from "./WeaponModel.jsx"; export const Weapon = (props) => { return ( <group {...props}> <WeaponModel /> </group> ); } Vamos colocar este componente no mesmo nível do do personagem e no gancho definiremos a posição e o ângulo de rotação com base na posição dos valores da câmera. RigidBody useFrame Código da seção Animação de arma balançando enquanto caminha Para tornar a marcha do personagem mais natural, adicionaremos um leve movimento da arma durante o movimento. Para criar a animação usaremos a biblioteca instalada. tween.js O componente será agrupado em uma tag de grupo para que você possa adicionar uma referência a ele por meio do gancho . Arma useRef Vamos adicionar para salvar a animação. useState Vamos criar uma função para inicializar a animação. Explicação do código: Criando uma animação de um objeto "balançando" de sua posição atual para uma nova posição. const twSwayingAnimation = new TWEEN.Tween(currentPosition) ... Criando uma animação do objeto retornando à sua posição inicial após a conclusão da primeira animação. const twSwayingBackAnimation = new TWEEN.Tween(currentPosition) ... Conectar duas animações para que, quando a primeira animação for concluída, a segunda animação seja iniciada automaticamente. twSwayingAnimation.chain(twSwayingBackAnimation); Em chamamos a função de inicialização da animação. useEffect Agora é necessário determinar o momento em que ocorre o movimento. Isso pode ser feito determinando o vetor atual da direção do personagem. Se ocorrer movimento do personagem, atualizaremos a animação e a executaremos novamente quando terminar. Explicação do código: Aqui o estado de movimento do objeto é verificado. Se o vetor de direção tiver comprimento maior que 0, significa que o objeto tem uma direção de movimento. const isMoving = direção.comprimento() > 0; Este estado é executado se o objeto estiver se movendo e a animação de "balanço" tiver terminado. if (isMoving && isSwayingAnimationFinished) { ... } No componente , vamos adicionar um onde atualizaremos a animação de interpolação. App useFrame atualiza todas as animações ativas na biblioteca . Este método é chamado em cada quadro de animação para garantir que todas as animações sejam executadas sem problemas. TWEEN.update() TWEEN.js Código da seção: Primeiro commit Segundo commit Animação de recuo Precisamos definir o momento em que um tiro é disparado – ou seja, quando o botão do mouse é pressionado. Vamos adicionar para armazenar esse estado, para armazenar uma referência ao objeto arma e dois manipuladores de eventos para pressionar e soltar o botão do mouse. useState useRef Vamos implementar uma animação de recuo ao clicar com o botão do mouse. Usaremos a biblioteca para essa finalidade. tween.js Vamos definir constantes para força de recuo e duração da animação. Tal como acontece com a animação de movimento da arma, adicionamos dois estados useState para a animação de recuo e retorno à posição inicial e um estado com o status final da animação. Vamos criar funções para obter um vetor aleatório de animação de recuo - e . generateRecoilOffset generateNewPositionOfRecoil Crie uma função para inicializar a animação de recuo. Também adicionaremos , no qual especificaremos o estado "shot" como uma dependência, para que a cada disparo a animação seja inicializada novamente e novas coordenadas finais sejam geradas. useEffect E em , vamos adicionar uma verificação para "segurar" a tecla do mouse para disparar, para que a animação de disparo não pare até que a tecla seja liberada. useFrame Código da seção Animação durante inatividade Realize a animação de “inatividade” para o personagem, para que não haja sensação de jogo “travando”. Para fazer isso, vamos adicionar alguns novos estados via . useState Vamos corrigir a inicialização da animação "wiggle" para usar valores do estado. A ideia é que diferentes estados: caminhando ou parando, utilizem valores diferentes para a animação e cada vez que a animação seja inicializada primeiro. Conclusão Nesta parte implementamos a geração de cena e movimentação de personagens. Também adicionamos um modelo de arma, animação de recuo ao disparar e em modo inativo. Na próxima parte continuaremos a refinar nosso jogo, adicionando novas funcionalidades. Também publicado . aqui