image

Acesse bootcamps ilimitados e +650 cursos pra sempre

75
%OFF
Article image
Nathan Ferreira
Nathan Ferreira19/11/2025 10:28
Compartilhe

⚔️ Anatomia do Jogo: Desenvolvendo Player, Monstros e IA em LÖVE2D/Lua

    Olá, comunidade DIO! Meu nome é Nathan Delgado Ferreira, e este é um mergulho técnico no meu projeto, o shooter Arena Zeta. Esta análise foca nos componentes centrais de qualquer jogo: O Jogador e Seus Inimigos. Vou detalhar como a arquitetura do jogo gerencia o Player, a IA dos Zumbis e como a constante luta contra bugs de movimento e crashes moldou o código.

    Ato I: A Entidade Player e a Saga dos Assets

    A entidade principal, o player.lua, foi o primeiro a receber mecânicas e a sofrer com a integração de assets.

    👤 1. Estrutura e Cheats do Jogador

    A estrutura do Player é um módulo central que gerencia estado e atributos: player.x, player.y, player.health, player.coins, e o ângulo de rotação (player.rotation) (o vetor de mira para o mouse).

    • Ajustes de Rotação: O player.rotation é essencial para a mira, mas nos causou bugs no desenho do sprite. O debugging final confirmou que o corpo do jogador deveria ter rotação fixa (0), e somente a arma deve usar o player.rotation.
    • O Menu Dev e o Cheat State: Para facilitar o teste, o player.lua recebeu variáveis de estado globais: player.infiniteCoins, player.oneHitKill, e player.infiniteAmmo. Essas flags são verificadas antes de qualquer alteração nos atributos (ex: if not player.oneHitKill then apply_damage() end), ativadas através de uma senha secreta no dev_menu.lua.

    Exemplo 1.1: O Módulo do Jogador e os Estados de Cheat (player.lua)

    O módulo do jogador armazena as flags de cheat e a lógica central do jogo verifica essas flags antes de qualquer operação que altere o estado (dano, munição, moedas).

    -- modules/player.lua
    
    Player = {
      health = 100,
      equippedWeapon = {},
      -- === ESTADOS DE CHEAT ===
      oneHitKill = false,
      infiniteAmmo = false,
      infiniteCoins = false,
    }
    
    -- Funções chamadas após a senha ser confirmada no dev_menu.lua
    function Player.activateOneHitKill()
      Player.oneHitKill = true
      print("CHEATS: One-Hit Kill Ativado!")
    end
    
    function Player.activateInfiniteAmmo()
      Player.infiniteAmmo = true
      print("CHEATS: Munição Infinita Ativada!")
    end
    
    -- Lógica de Disparo (Verificação do Cheat)
    function Player.shoot()
      if Player.equippedWeapon.magazine > 0 or Player.infiniteAmmo then
          -- Lógica de criar a bala...
    
          -- Apenas reduz a munição se o cheat não estiver ativo
          if not Player.infiniteAmmo then
              Player.equippedWeapon.magazine = Player.equippedWeapon.magazine - 1
          end
      end
    end
    

    🔫 2. O Sistema de Armas e Quads

    O player.lua interage diretamente com o weapons.lua. Em vez de armazenar o caminho da imagem da arma, o módulo usa Quads (instâncias de recorte da spritesheet guns.png) para a renderização da arma equipada (player.equippedWeapon.spriteQuad).

    • Bug de Nulo: Tentativas de calcular o offset (centro do sprite para rotação) falharam com o erro attempt to perform arithmetic on global 'playerSpriteWidth' (a nil value). A correção foi garantir que as dimensões do sprite fossem salvas em variáveis acessíveis globalmente após o carregamento, evitando o erro de escopo.
    • Transparência vs. Fundo Branco: O bug do fundo branco persistente nos obrigou a forçar a transparência via código (love.graphics.setBlendMode('alpha')), garantindo que o PNG fosse renderizado corretamente com seu canal alfa.

    Exemplo 1.2: Rotação e Offset de Renderização (main.lua)

    O bug de o sprite do jogador girar descontroladamente foi corrigido forçando a rotação para 0 no corpo e ajustando o Offset (o ponto de giro) para o centro do sprite.

    -- Renderização Corrigida do Player (em main.lua ou player.lua:draw)
    
    local playerSprite = Assets.playerSprite -- Sprite já carregado
    local spriteWidth = playerSprite:getWidth()
    local spriteHeight = playerSprite:getHeight()
    
    -- Calcula o OFFSET (Centro do Sprite)
    local offsetX = spriteWidth / 2
    local offsetY = spriteHeight / 2
    
    -- Define a escala (para reduzir o tamanho do sprite)
    local scaleValue = 0.25 
    
    function Player.draw()
      -- Corrigido: Rotação deve ser ZERO para o corpo do jogador
      -- Ele deve ser desenhado em sua posição (Player.x, Player.y)
      -- usando o centro (offsetX, offsetY)
      love.graphics.draw(
          playerSprite,
          Player.x, Player.y,
          0,                        -- ROTAÇÃO FIXA: 0 (Corpo não gira)
          scaleValue, scaleValue,   -- ESCALA
          offsetX, offsetY          -- OFFSET (Centro do Sprite)
      )
    
      -- A ARMA, por outro lado, usa Player.rotation para mirar
      Weapon.draw(Player.rotation)
    end
    


    Ato II: A Entidade Zumbi e a Batalha da IA

    A complexidade da IA reside no zombie.lua e em sua dependência do pathfinder.lua. A implementação do movimento inteligente foi a maior fonte de bugs e crashes.

    🧠 3. A Navegação A* e o Path Consumption

    Os zumbis (entidades que gerenciam zombie.health, zombie.speed) precisavam navegar em um mapa gerado aleatoriamente.

    • A Tática de Cerco: A IA não mira diretamente no jogador, mas em um ponto deslocado (flanqueamento) para cercá-lo. O zombie.lua armazena o caminho calculado (zombie.currentPath).
    • O Bug do Congelamento (Avanço): Zumbis que ficavam "burros e congelados" ou "cercando" demais indicavam uma falha na tolerância de avanço. A solução foi garantir que o zumbi só consumisse o próximo nó do currentPath se estivesse a uma distância mínima segura (ex: 10 pixels), e que o Pathfinder estivesse retornando coordenadas em Pixels (e não índices de Grid).

    Exemplo 2.1: A Estrutura do Zumbi e o Fallback de Movimento (zombie.lua)

    O módulo do zumbi gerencia seu caminho (currentPath) e implementa a lógica de fallback para evitar que ele congele se o A* falhar.

    -- modules/zombie.lua
    
    -- Lógica de Movimento no Update
    function Zombie.update(dt)
      -- 1. Recalcular caminho A* a cada 0.8s
      if Zombie.timer >= 0.8 then
          Zombie.currentPath = Pathfinder.findPath(Zombie.x, Zombie.y, Player.x, Player.y)
          Zombie.timer = 0
      end
      
      local dx, dy = 0, 0
      local targetReached = false
    
      -- 2. Lógica de Pathfinding (SE O CAMINHO EXISTIR E NÃO ESTIVER VAZIO)
      if Zombie.currentPath and #Zombie.currentPath > 0 then
          local nextNode = Zombie.currentPath[1]
          local dist = calculateDistance(Zombie.x, Zombie.y, nextNode.x, nextNode.y)
    
          -- Consumo do Nó (Correção de Tolerância)
          if dist < 10 then -- Se a distância for menor que 10px (tolerância)
              table.remove(Zombie.currentPath, 1) -- Remove o nó e avança
          end
    
          -- ... Lógica de mover em direção a nextNode para calcular dx, dy
      end
    
      -- 3. Lógica de FALLBACK (Se o Pathfinding falhou ou o caminho acabou)
      if not Zombie.currentPath or #Zombie.currentPath == 0 then
          -- Mover em linha reta (Fallback) para garantir que o zumbi não congele
          local angle = math.atan2(Player.y - Zombie.y, Player.x - Zombie.x)
          dx = math.cos(angle) * Zombie.speed * dt
          dy = math.sin(angle) * Zombie.speed * dt
      end
    
      -- 4. Aplica a Colisão (Ver Exemplo 3.1)
      applyCollisionResponse(dx, dy)
    end
    

    💥 4. A Crise do Crash e o Fallback de Segurança

    O crash na inicialização da Rodada 1 foi o nosso maior desafio, causado por loops infinitos no A* ou na Geração Procedural de Mapas.

    • Diagnóstico: O crash ocorria quando o A* falhava em encontrar um caminho em um grid bloqueado ou ao atingir o limite de busca.
    • Solução Crítica (Pathfinder):
    1. Limite de Iterações: Adicionamos um fail-safe (ex: 5000) no algoritmo A*, garantindo que ele retorne caminho vazio em vez de travar o jogo.
    2. Movimento de Fallback: O zombie.lua foi corrigido para sempre executar um movimento (mirar em linha reta para o jogador) se o currentPath estivesse vazio. Isso garante que o zumbi nunca fique "congelado".

    Exemplo 2.2: O Fail-Safe no A* Pathfinding (pathfinder.lua)

    O crash por loop infinito foi resolvido adicionando um contador e um limite máximo dentro do loop de busca A*.

    -- modules/pathfinder.lua (Dentro da função findPath)
    
    function Pathfinder.findPath(startX, startY, targetX, targetY)
      local openList = {}
      local closedSet = {}
      
      local iterationCount = 0
      local MAX_ITERATIONS = 5000  -- O nosso Fail-Safe!
    
      while #openList > 0 do
          iterationCount = iterationCount + 1
          
          -- CHECAGEM DE SEGURANÇA CRÍTICA
          if iterationCount > MAX_ITERATIONS then
              print("AVISO: Limite de iterações A* atingido. Retornando caminho vazio.")
              return {} -- Retorna caminho vazio para evitar o CRASH
          end
    
          -- ... O restante da lógica de busca A* (custos F, G, H)
      end
      
      -- ... Lógica de reconstrução do caminho se o alvo for encontrado
      return finalPath
    end
    


    Ato III: Colisão e Estabilidade

    A estabilidade final do jogo dependeu de uma implementação robusta de física que evitasse bugs de travamento e travessia.

    🧱 5. Colisão com Resposta (X/Y)

    O erro de o jogador ficar preso e os zumbis atravessarem obstáculos foi resolvido com a Colisão com Resposta que testa e impede o movimento em eixos separados:

    • Lógica: No player.lua e zombie.lua, o movimento é dividido:
    1. Tentar mover em X. Se houver colisão, reverter o movimento X.
    2. Tentar mover em Y. Se houver colisão, reverter o movimento Y.
    • Benefício: Essa lógica permite que as entidades deslizem ao longo das paredes, em vez de ficarem presas ou pararem completamente.

    Exemplo 3.1: Colisão com Resposta em Eixos Separados (zombie.lua / player.lua)

    Esta lógica é crucial para o slipping (deslizar) e deve ser executada após o cálculo do movimento (dx, dy) e antes da renderização.

    -- Função de aplicação de movimento e colisão (Usada por Player e Zombie)
    
    function applyCollisionResponse(entity, dx, dy, dt)
      -- Mover e testar em X
      local new_x = entity.x + dx
      if not Map.checkCollision(new_x, entity.y, entity.radius) then
          entity.x = new_x
      end
    
      -- Mover e testar em Y
      local new_y = entity.y + dy
      if not Map.checkCollision(entity.x, new_y, entity.radius) then
          entity.y = new_y
      end
    end
    
    -- OBS: Map.checkCollision(x, y, radius) deve retornar true se houver sobreposição com walls
    

    Conclusão: Lições de uma Arena em Construção

    O desenvolvimento de Arena Zeta me ensinou que a construção de um jogo é a arte de gerenciar a complexidade e aceitar o debugging como a regra, e não a exceção.

    As entidades Player e Zumbi, embora pareçam simples, dependem de uma orquestração perfeita entre Gerenciamento de Assets (Quads), Física (Colisão X/Y) e IA Tática (A* com Fail-Safe). Cada crash e cada bug de rotação foi uma lição valiosa sobre a arquitetura de código em LÖVE2D.

    Convido você a aplicar esses princípios de debugging em seus próprios projetos!

    Compartilhe
    Recomendados para você
    CI&T - Backend com Java & AWS
    CAIXA - Inteligência Artificial na Prática
    Binance - Blockchain Developer with Solidity 2025
    Comentários (0)