Montando um 3d jogo de tiro do zero: o que realmente funciona
Você provavelmente já tentou criar um jogo de tiro em 3D e desistiu depois de duas semanas porque o sistema de recoil fez seu personagem voar para o mapa, ou então porque os disparos travavam completamente no momento errado. Eu passei por isso. O problema não é falta de tutorial na internet; é que quase todo mundo explica o caminho fácil e ninguém fala do caminho que dá trabalho. Vou começar pela parte que todo mundo ignora: a estrutura dos dados. Antes de escrever uma linha de código de tiro, você precisa definir o que é uma bala e como ela existe no mundo. Na minha experiência, a maioria dos devs novatos faz bala como um GameObject com Rigidbody e Physics.Raycast por frame. Isso funciona para cinco tiros. Quando você sobe para cinquenta, o hardware começa a implodir.
A solução que eu uso hoje: bala como dado, não como GameObject. Cada disparo é um registro com posição inicial, direção, velocidade e tempo de vida. A detecção de colisão é feita exclusivamente com Raycast ou SphereCast manual, e o resultado vira um evento que o sistema de danoprocessa. NENHUMGameObject é criado durante o gameplay. Isso reduziu meu custo de CPU em cerca de 80% em comparação com a abordagem tradicional.
3d jogo de tiro: mecânicas que precisam existir antes de qualquer coisa
O primeiro passo técnico é o sistema de disparo em si. Parece simples, mas é onde a maioria dos jogos indie trava. Você vai precisar de três coisas fundamentais: input handling, raycasting e feedback visual. Aqui está o que funciona na prática. O input precisa ser separado do dano. Um erro comum é colocar o Raycast dentro da lógica de input. Quando o jogador pressiona o gatilho, você dispara um evento, não um cálculo. Isso permite que o mesmo sistema de input seja reutilizado para tiro automático, tiro mirado, recarregamento e qualquer outra ação futura sem reescrever nada.
O Raycast em si deve usar Camera.main.ViewportPointToRay no PC ou um transform.forward a partir da arma no mobile. A diferença entre essas duas abordagens é o que separa um jogo que funciona bem de um que tem disparos que acertam o chão quando o jogador mira no céu. Eu levei três dias pra descobrir isso no meu primeiro projeto. O feedback visual é o que faz o tiro parecer real. Muzzle flash, som, recuo de câmera e o hit marker. O recuo de câmera é especialmente importante. Sem ele, o jogo parece flutuante. Eu costumo aplicar um lerp suave de 0.1 segundos para retornar à posição original. Não use MovePosition direto no Rigidbody da câmera, porque isso quebra a física e gera jitter.
O problema do recoil que ninguém avisa
Recoil é um dos sistemas mais mal implementados em jogos indie. A tentação é adicionar uma força ao Rigidbody do jogador quando ele atira. Isso gera caos. Personagens deslizam, colisões ficam instáveis, e o jogador sente que perdeu o controle. O workaround que eu desenvolvi foi aplicar o recoil APENAS na rotação da câmera, nunca no corpo do personagem. A câmera gira para cima com uma força proporcional ao calibre da arma, e o corpo permanece imóvel. Para simular impacto, eu adiciono uma pequena trepidação no animador do modelo 3D da arma, não no character controller. Isso dá a sensação de peso sem comprometer a precisão do movimento.
Se você realmente quer que o personagem seja empurrado (como em jogos mais realistas), use um impulse direction negativo oposto ao disparo, mas limite a velocidade resultante a 0.5 m/s. Qualquer coisa além disso já escorrega para o território de bugs de colisão.
Sistema de dano ebalas traiçoeiras
Aqui está algo que vai surpreender você: a maioria dos jogos de tiro não usa dano fixo por bala. Usar dano fixo é rápido de programar, mas deixa os combates sem profundidade. O que eu recomendo é um sistema de dano baseado em distância e penetração. Cada bala carrega um valor de dano base, um fator de queda por metro (damage falloff) e um índice de penetração. Se a bala acertar um escudo ou armadura, ela perde uma porcentagem do dano e continua voando. Se não tiver penetração suficiente, ela para. Isso significa que um atirador de elite pode eliminar um inimigo a 200 metros com um tiro na cabeça, enquanto alguém de perto com uma arma fraca pode não conseguir nada contra armadura pesada.
O cálculo é simples: dano_final = dano_base * max(0, 1 - (distancia * fator_queda)) * penetração_alvo. Coloque esse cálculo no momento em que o Raycast retorna um hit, não em coroutine, não em Update. Um cálculo por hit é extremamente barato.
Otimização que realmente importa
Depois que o sistema básico está funcionando, o próximo desafio é performance. Aqui estão os três pontos que mais impactam FPS em jogos de tiro 3D. Primeiro, object pooling para partículas de impacto. Cada vez que uma bala acerta uma superfície, você cria spark, fumaça, poeira. Se estiver instanciando e destruindo esses efeitos a cada tiro, seu garbage collector vai entrar em panic a cada 30 segundos. Pool de pelo menos 50 instâncias de cada efeito cobre a maioria dos cenários.
👉 Clique no botão abaixo para saber mais sobre o assunto!
Segundo, LOD para armas e modelos de inimigos. Armas são renderizadas em alta resolução por padrão porque estão sempre próximas da câmera. Inimigos podem usar LOD0 para perto, LOD1 para médio, LOD2 para longe. A diferença de polígonos entre LOD0 e LOD2 em um personagem pode ser de 50 mil triângulos. Multiplicado por dez inimigos, isso é meio milhão de triângulos a mais no draw call. Terceiro, culling de Raycast. Se você tem muitos disparos simultâneos (metralhadora, granada, tiro espalhado), o Raycast pode se tornar caríssimo. A solução é limitar o número de Rays por frame por arma. Uma espingarda que dispara nove projéteis não precisa de nove Raycasts independentes. Use SphereCast em vez de Raycast único, ou agrupe múltiplas balas em um único teste volumétrico. Isso reduziu meu custo de raycasting em tiros de escopeta de 9 chamadas para 1.
Networking: o pesadelo que todo mundo subestima
Se o seu 3d jogo de tiro for multiplayer, prepare-se para uma semana inteira só de sincronização. O problema principal é que o Raycast é determinístico do lado do servidor, mas cada cliente vê o mundo de ângulos ligeiramente diferentes. O resultado são disparos que parecem acertar no seu monitor mas não acertam no servidor. A abordagem que eu adoto é server-authoritative com client-side prediction. O cliente prevê o impacto visualmente, mas o servidor é quem decide se o dano foi aplicado. Para minimizar a sensação de delay, eu uso um sistema de lag compensation que replaya o Raycast na posição do jogador no momento do disparo, não na posição atual. Isso faz uma diferença absurda na sensação de responsividade.
Outro problema frequente: o servidor não consegue sincronizar animações de recarga perfeitamente. A solução prática é ter um short windows de tolerância de 0.2 segundos. Se o cliente diz que começou a recargar e o servidor recebe dentro dessa janela, aceita. Fora disso, ignora. Isso evita o problema clássico de o jogador recarregar e a munição não ser consumida porque o servidor "não viu" o comando.
Construção do mapa para tiro
Mapas para jogo de tiro precisam de três coisas: coberturas claras, rotas de flanqueio e pontos altos. A regra prática é que toda cobertura deve permitir que um jogador se esconda parcialmente (meio corpo) mas ainda consiga atirar. Se a cobertura esconde o jogador inteiro, ninguém vai usá-la. Se não esconde nada, todo mundo morre instantaneamente. O erro mais comum em mapas indie é o excesso de espaços abertos. Um mapa grande com áreas abertas demais vira um jogo de quem tem a melhor internet, não de quem é melhor jogador. Insira ângulos mortais, corredores estreitos e rooms que forçam confrontos corpo a corpo. O ritmo do jogo depende disso.
Para otimização de mapa, use NavMesh baking com áreas de cover. Isso permite que inimigos IA usem coberturas automaticamente sem lógica personalizada. Cada zona de cover no NavMesh deve ter um tag identificável para que o sistema de dano saiba se o alvo estava parcialmente protegido ou exposto.
Armazenamento de dados e progresso
Se o jogo vai ter progressão, skinas, desbloqueios, armamentos — não salve isso no PlayerPrefs. Eu vi projetos inteiros serem arruinados porque o jogador mudou de PC e perdeu tudo. Use um sistema de salvar baseado em JSON ou binário compactado, preferencialmente com backup em nuvem se o jogo tiver conta de usuário. Um erro frequente é salvar o estado do inventário a cada tiro. Isso é desnecessário e gera escrita excessiva de disco. Salve a cada mudança significativa: compra de arma, desbloqueio de skin, finalização de partida. Eventos pontuais, não contínuos.
Debugging de tiro: o guia rápido
Quando o tiro não está funcionando, aqui está minha checklist prática. Verifique estas coisas na ordem: 1. O Raycast está sendo chamado? Desenhe linhas de debug no Scene View. Se não houver linhas, o problema é no input. Se houver linhas mas não houver hits, o problema é nos layers ou na escala do mapa.
2. A arma está apontando para onde você acha que está? Diferença entre forward da arma e forward da câmera é uma causa frequente de disparos desalinhados. Teste com a câmera em TPS e FPS separadamente. 3. O dano está sendo aplicado? Adicione um log temporário no hit. Se o log aparece mas o inimigo não leva dano, o problema está no sistema de dano, não no sistema de tiro.
4. O recoil está quebrando o controle? Se sim, verifique se você está modificando o Rigidbody do jogador ou apenas a câmera. A segunda opção funciona; a primeira gera problemas de física que levam horas pra diagnosticar. 5. A munição está contando certo? Um bug silencioso mas frequente é o contador de munição decrementar tanto no cliente quanto no servidor em multiplayer, resultando em arma descarregando duas vezes mais rápido do que deveria.
Construir um jogo de tiro 3D funciona bem quando você trata cada subsysteme como uma peça separada. O tiro, o dano, o recoil, a otimização, o networking — cada um tem seus próprios problemas e suas próprias soluções. Tentar resolver tudo junto desde o início é o motivo pelo qual tantos projetos nunca saem do protótipo.