paint-brush
Criando seu próprio jogo de tiro 3D usando React e Three.js Stack - Parte 1por@varlab
918 leituras
918 leituras

Criando seu próprio jogo de tiro 3D usando React e Three.js Stack - Parte 1

por Ivan Zhukov17m2023/10/21
Read on Terminal Reader

Muito longo; Para ler

Na era do desenvolvimento ativo de tecnologias web e aplicativos interativos, os gráficos 3D estão se tornando cada vez mais relevantes e procurados. Mas como criar uma aplicação 3D sem perder as vantagens do desenvolvimento web? Neste artigo, veremos como combinar o poder do Three.js com a flexibilidade do React para criar seu próprio jogo diretamente no navegador. Este artigo apresentará a biblioteca React Three Fiber e ensinará como criar jogos 3D interativos.
featured image - Criando seu próprio jogo de tiro 3D usando React e Three.js Stack - Parte 1
Ivan Zhukov HackerNoon profile picture
0-item
1-item

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 React Three Fiber - uma ferramenta poderosa para criar gráficos 3D baseados em Three.js usando a tecnologia React .

Sobre a pilha React Three Fiber

React Three Fiber é um wrapper sobre Three.js que usa a estrutura e os princípios do React para criar gráficos 3D na web. Essa pilha permite que os desenvolvedores combinem o poder do Three.js com a conveniência e flexibilidade do React , tornando o processo de criação de um aplicativo mais intuitivo e organizado.


No cerne do React Three Fiber está a ideia de que tudo o que você cria em uma cena é um componente do React . Isso permite que os desenvolvedores apliquem padrões e metodologias familiares.

Uma das principais vantagens do React Three Fiber é a facilidade de integração com o ecossistema React . Quaisquer outras ferramentas React ainda podem ser facilmente integradas ao usar esta biblioteca.

Relevância do Web-GameDev

O Web-GameDev passou por grandes mudanças nos últimos anos, evoluindo de simples jogos Flash 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.


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 Chrome , Firefox , Edge e outros são constantemente otimizados e desenvolvidos para garantir alto desempenho, tornando-os uma plataforma ideal para o desenvolvimento de aplicações complexas.


Uma das principais ferramentas que impulsionou o desenvolvimento de jogos baseados em navegador é o WebGL . 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, o WebGL abre novas possibilidades para a criação de aplicações web impressionantes diretamente no navegador.


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


Repositório no GitHub


Agora vamos começar!

Configurando o projeto e instalando pacotes

Primeiro de tudo, precisaremos de um modelo de projeto React . Então, vamos começar instalando-o.


 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, exclua tudo o que for desnecessário do nosso projeto.


Código da seção

Personalizando a exibição do Canvas

No arquivo main.jsx , adicione um elemento div que será exibido na página como escopo. Insira um componente Canvas e defina o campo de visão da câmera. Dentro do componente Canvas coloque o componente App .


principal.jsx


Vamos adicionar estilos a index.css para esticar os elementos da UI até a altura total da tela e exibir o escopo como um círculo no centro da tela.


índice.css


No componente App adicionamos um componente Sky , que será exibido como plano de fundo em nossa cena de jogo na forma de um céu.


Aplicativo.jsx


Exibindo o céu na cena


Código da seção

Superfície do piso

Vamos criar um componente Ground e colocá-lo no componente App .


Aplicativo.jsx


Em Ground , 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.


Chão.jsx


Embora tenhamos especificado cinza como cor do material, o plano parece completamente preto.


Plano em cena


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 ambientLight , que ilumina o objeto por todos os lados e não possui feixe direcionado. Como parâmetro defina a intensidade do brilho.


Aplicativo.jsx


Avião iluminado


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 de ativos adicione uma imagem PNG com textura.


Textura adicionada


Para carregar uma textura na cena, vamos usar o gancho useTexture do pacote @react-two/drei . 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.


Chão.jsx


Textura em um avião


Código da seção

Movimento da câmera

Usando o componente PointerLockControls do pacote @react-two/drei , 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.


Aplicativo.jsx


Demonstração de movimento da câmera


Vamos fazer uma pequena edição no componente Ground .


Chão.jsx


Código da seção

Adicionando física

Para maior clareza, vamos adicionar um cubo simples à cena.


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


O cubo em cena


Neste momento ele está apenas pendurado no espaço.


Use o componente Physics do pacote @react-two/rapier para adicionar "física" à cena. Como parâmetro, configure o campo gravitacional, onde definimos as forças gravitacionais ao longo dos eixos.


 <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 RigidBody do pacote @react-two/rapier .


Aplicativo.jsx


Depois disso, veremos imediatamente que cada vez que a página é recarregada, o cubo cai sob a influência da gravidade.


Queda do cubo


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 Ground e adicionar um componente RigidBody como um wrapper sobre a superfície do piso.


Chão.jsx


Agora, ao cair, o cubo permanece no chão como um objeto físico real.


Cubo caindo em um avião


Código da seção

Submetendo um personagem às leis da física

Vamos criar um componente Player que controlará o personagem em cena.


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 RigidBody . E vamos fazer o personagem em forma de cápsula.


Jogador.jsx


Coloque o componente Player dentro do componente Física.


Aplicativo.jsx


Agora nosso personagem apareceu em cena.


Um personagem em forma de cápsula


Código da seção

Movendo um personagem – criando um gancho

O personagem será controlado através das teclas WASD , e saltará através da 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 hooks.js e adicionar uma nova função usePersonControls nele.


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"}.


ganchos.js


Código da seção

Movendo um personagem – implementando um gancho

Após implementar o gancho usePersonControls , ele deve ser usado ao controlar o personagem. No componente Player adicionaremos rastreamento do estado de movimento e atualizaremos o vetor da direção do movimento do personagem.


Também definiremos variáveis que armazenarão os estados das direções de movimento.


Jogador.jsx


Para atualizar a posição do personagem, vamos usarFrame fornecido pelo pacote @react-two/fiber . Este gancho funciona de forma semelhante a requestAnimationFrame e executa o corpo da função cerca de 60 vezes por segundo.


Jogador.jsx


Explicação do código:

1. const playerRef = useRef(); Crie um link para o objeto do jogador. Este link permitirá a interação direta com o objeto do jogador na cena.

2. const {avançar, retroceder, esquerda, direita, pular} = usePersonControls(); Quando um gancho é usado, é retornado um objeto com valores booleanos indicando quais botões de controle estão pressionados no momento pelo jogador.

3. useFrame((estado) => {... }); O gancho é chamado em cada quadro da animação. Dentro deste gancho, a posição e a velocidade linear do jogador são atualizadas.

4. if (!playerRef.current) retornar; 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.

5. velocidade const = playerRef.current.linvel(); Obtenha a velocidade linear atual do jogador.

6. frontVector.set(0, 0, para trás - para frente); Defina o vetor de movimento para frente/trás com base nos botões pressionados.

7. sideVector.set(esquerda - direita, 0, 0); Defina o vetor de movimento esquerda/direita.

8. direção.subVectors(frontVector, sideVector).normalize().multiplyScalar(MOVE_SPEED); 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.

9.playerRef.current.wakeUp(); "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.

10. playerRef.current.setLinvel({ x: direção.x, y: velocidade.y, z: direção.z }); 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).


Como resultado, ao pressionar as teclas WASD , 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.


Movimento do personagem


Código da seção

Movendo um personagem - pule

Para implementar o salto, vamos usar a funcionalidade dos pacotes @dimforge/rapier3d-compat e @react-two/rapier . 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.


Para o Player 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.


Jogador.jsx


Explicação do código:

  1. const mundo = rapier.mundo; Obtendo acesso ao cenário do motor de física Rapier . Ele contém todos os objetos físicos e gerencia sua interação.
  1. const ray = world.castRay(new RAPIER.Ray(playerRef.current.translation(), { x: 0, y: -1, z: 0 })); É 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.
  1. const aterrado = ray && ray.collider && Math.abs(ray.toi) <= 1,5; A condição é verificada se o jogador estiver no chão:
  • raio - se o raio foi criado;
  • ray.collider - se o raio colidiu com algum objeto na cena;
  • Math.abs(ray.toi) - 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”.


Você também precisa modificar o componente Ground 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.


Chão.jsx


Vamos levantar um pouco mais a câmera para uma melhor visualização da cena.


principal.jsx


Saltos de personagem


Código da seção

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 .


Jogador.jsx


Explicação do código:

O método applyEuler aplica rotação a um vetor com base em ângulos de Euler especificados. Neste caso, a rotação da câmera é aplicada ao vetor de direção . 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.


Vamos ajustar levemente o tamanho do Player e torná-lo mais alto em relação ao cubo, aumentando o tamanho do CapsuleCollider e corrigindo a lógica de "salto".


Jogador.jsx


Movendo a câmera


Código da seção

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 cubes.json , no qual listaremos um array de coordenadas.


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


No arquivo Cube.jsx , crie um componente Cubes , que gerará cubos em um loop. E o componente Cube será um objeto gerado diretamente.


 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 Cubes criado ao componente App excluindo o cubo único anterior.


Aplicativo.jsx


Geração de cubos


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 gltf-pipeline , reconverta o modelo do formato GLTF para o formato GLB , 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 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 recurso oficial dos desenvolvedores @react-two/fiber .


Ir para o conversor exigirá que você carregue o arquivo arma.glb convertido.


Usando arrastar e soltar ou pesquisa no Explorer, encontre este arquivo e baixe-o.


Modelo convertido


No conversor veremos o componente react gerado, cujo código transferiremos para o nosso projeto em um novo arquivo WeaponModel.jsx , alterando o nome do componente para o mesmo nome do arquivo.


Código da seção

Exibindo o modelo da arma na cena

Agora vamos importar o modelo criado para a cena. No arquivo App.jsx , adicione o componente WeaponModel .


Aplicativo.jsx


Demonstração do modelo importado


Código da seção

Adicionando sombras

Neste ponto da nossa cena, nenhum dos objetos está projetando sombras.

Para habilitar sombras na cena você precisa adicionar o atributo shadows ao componente Canvas .


principal.jsx


Em seguida, precisamos adicionar uma nova fonte de luz. Apesar de já termos ambientLight 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 direcionalLight e configurá-la. O atributo para ativar o modo de sombra " cast " é castShadow . É a adição deste parâmetro que indica que este objeto pode projetar sombra sobre outros objetos.


Aplicativo.jsx


Depois disso, vamos adicionar outro atributo recebeShadow ao componente Ground , o que significa que o componente na cena pode receber e exibir sombras sobre si mesmo.


Chão.jsx


O modelo lança uma sombra


Atributos semelhantes devem ser adicionados a outros objetos na cena: cubos e jogador. Para os cubos adicionaremos castShadow e ReceiveShadow , pois ambos podem lançar e receber sombras, e para o jogador adicionaremos apenas castShadow .


Vamos adicionar castShadow para Player .


Jogador.jsx


Adicione castShadow e recebaShadow para Cube .


Cubo.jsx


Todos os objetos na cena projetam uma sombra


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.


Corte de sombra


A razão para isso é que, por padrão, a câmera captura apenas uma pequena área das sombras exibidas em direcionalLight . Podemos fazer isso para o componente direcionalLight adicionando atributos adicionais shadow-camera-(top, bottom, left, right) para expandir esta área de visibilidade. Depois de adicionar esses atributos, a sombra ficará ligeiramente desfocada. Para melhorar a qualidade, adicionaremos o atributo shadow-mapSize .


Aplicativo.jsx


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 Arma , que conterá a lógica de comportamento da arma e o próprio modelo 3D.


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


Vamos colocar este componente no mesmo nível do RigidBody do personagem e no gancho useFrame definiremos a posição e o ângulo de rotação com base na posição dos valores da câmera.


Jogador.jsx


Exibição do modelo de arma em primeira pessoa


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 tween.js instalada.


O componente Arma será agrupado em uma tag de grupo para que você possa adicionar uma referência a ele por meio do gancho useRef .


Jogador.jsx


Vamos adicionar useState para salvar a animação.


Jogador.jsx


Vamos criar uma função para inicializar a animação.


Jogador.jsx


Explicação do código:

  1. const twSwayingAnimation = new TWEEN.Tween(currentPosition) ... Criando uma animação de um objeto "balançando" de sua posição atual para uma nova posição.
  1. const twSwayingBackAnimation = new TWEEN.Tween(currentPosition) ... Criando uma animação do objeto retornando à sua posição inicial após a conclusão da primeira animação.
  1. twSwayingAnimation.chain(twSwayingBackAnimation); Conectar duas animações para que, quando a primeira animação for concluída, a segunda animação seja iniciada automaticamente.


Em useEffect chamamos a função de inicialização da animação.


Jogador.jsx


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.


Jogador.jsx


Explicação do código:

  1. const isMoving = direção.comprimento() > 0; 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.
  1. if (isMoving && isSwayingAnimationFinished) { ... } Este estado é executado se o objeto estiver se movendo e a animação de "balanço" tiver terminado.


No componente App , vamos adicionar um useFrame onde atualizaremos a animação de interpolação.


Aplicativo.jsx


TWEEN.update() atualiza todas as animações ativas na biblioteca TWEEN.js . Este método é chamado em cada quadro de animação para garantir que todas as animações sejam executadas sem problemas.


Código da seção:

Animação de recuo

Precisamos definir o momento em que um tiro é disparado – ou seja, quando o botão do mouse é pressionado. Vamos adicionar useState para armazenar esse estado, useRef para armazenar uma referência ao objeto arma e dois manipuladores de eventos para pressionar e soltar o botão do mouse.


Arma.jsx


Arma.jsx


Arma.jsx


Vamos implementar uma animação de recuo ao clicar com o botão do mouse. Usaremos a biblioteca tween.js para essa finalidade.


Vamos definir constantes para força de recuo e duração da animação.


Arma.jsx


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.


Arma.jsx


Vamos criar funções para obter um vetor aleatório de animação de recuo - generateRecoilOffset e generateNewPositionOfRecoil .


Arma.jsx


Crie uma função para inicializar a animação de recuo. Também adicionaremos useEffect , 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.


Arma.jsx


Arma.jsx


E em useFrame , 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.


Arma.jsx


Animação de recuo


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 .


Jogador.jsx


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.


Jogador.jsx


Animação ociosa


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 .