⚔️ 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):
- 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.
- 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:
- Tentar mover em X. Se houver colisão, reverter o movimento X.
- 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!



