IA no Desenvolvimento de Jogos: Geração Procedural e NPCs Inteligentes

Explore o uso de inteligência artificial no desenvolvimento de jogos modernos. Aprenda sobre geração procedural, NPCs com machine learning, e ferramentas de IA para acelerar produção.
IA no Desenvolvimento de Jogos: Geração Procedural e NPCs Inteligentes
Antes de mais nada: a maior parte do que você vê de "IA em jogos" não é machine learning. É matemática esperta e algoritmos determinísticos rodando desde os anos 80. Pathfinding, máquinas de estado, geração por ruído. Isso resolve 90% dos problemas e roda em qualquer máquina.
A parte realmente nova (redes neurais para comportamento, diálogo com LLM, geração de arte por difusão) é poderosa, mas vem com custo: latência, imprevisibilidade, dependência de servidor e contas de API que sobem rápido. Vale a pena saber onde cada coisa entra antes de jogar uma rede neural em cima de um problema que um if resolveria.
Este guia separa as três famílias de técnica, mostra código que de fato roda, e aponta onde cada uma faz sentido.
As três famílias de IA em games
Não existe "uma IA". Existem três abordagens com tradeoffs bem diferentes:
IA clássica (determinística). Pathfinding (A*, Dijkstra), máquinas de estado, árvores de comportamento, sistemas baseados em regras. Previsível, barata, fácil de debugar. É o que move quase todo NPC de jogo comercial. Quando você vê um inimigo de Souls ou um guarda de stealth se comportando bem, quase sempre é isso, não rede neural.
Geração procedural (algorítmica). Terreno por ruído, dungeons por regras, loot por tabelas de afixos, quests por templates. Cria variedade a partir de uma semente (seed). É determinística no sentido de que a mesma seed gera o mesmo mundo, o que é ótimo para reproduzir bugs e compartilhar mapas.
Machine learning (adaptativa). Redes neurais, reinforcement learning, modelos de linguagem. Aprende padrões em vez de seguir regras escritas à mão. Resolve problemas onde escrever as regras manualmente seria inviável, mas troca controle por imprevisibilidade. Use quando regras explícitas não dão conta, não como atalho.
Na prática, jogos bons misturam as três. Uma árvore de comportamento decide a ação de alto nível (atacar, fugir, patrulhar) e o pathfinding determinístico cuida da locomoção. ML, quando entra, costuma ser uma peça pequena dentro disso.
Geração procedural: variedade a partir de uma semente
Ruído como base do terreno
Quase todo terreno procedural começa com uma função de ruído. Perlin e Simplex são as mais comuns: dado um par de coordenadas, retornam um valor suave entre 0 e 1 que varia de forma orgânica. O truque para terreno crível é somar várias "oitavas" de ruído, cada uma com mais frequência e menos amplitude. As primeiras oitavas dão as montanhas grandes, as últimas dão os detalhes finos.
Em Godot (GDScript), usando o FastNoiseLite nativo:
extends Node
func gerar_heightmap(largura: int, altura: int, seed: int) -> Image:
var ruido := FastNoiseLite.new()
ruido.seed = seed
ruido.noise_type = FastNoiseLite.TYPE_PERLIN
ruido.fractal_octaves = 6 # somatorio de 6 oitavas
ruido.fractal_lacunarity = 2.0 # cada oitava dobra a frequencia
ruido.fractal_gain = 0.5 # e tem metade da amplitude
ruido.frequency = 0.01 # escala geral do terreno
var img := Image.create(largura, altura, false, Image.FORMAT_RF)
for x in largura:
for y in altura:
# get_noise_2d retorna [-1, 1]; normaliza pra [0, 1]
var h := (ruido.get_noise_2d(x, y) + 1.0) * 0.5
img.set_pixel(x, y, Color(h, 0, 0))
return img
O fractal_octaves faz internamente aquele loop de somar oitavas. Se você quiser o controle manual (por exemplo, para terreno com ridges ou domain warping), dá para chamar get_noise_2d várias vezes com frequências diferentes e somar você mesmo. Mas para começar, deixe a engine fazer.
A mesma ideia em C# no Unity usa Mathf.PerlinNoise, que não tem fractal embutido, então você soma as oitavas na mão:
float AlturaEm(int x, int y, int largura, int altura, float escala)
{
float total = 0f;
float amplitude = 1f;
float frequencia = 1f;
float somaAmplitudes = 0f; // pra normalizar no final
for (int oitava = 0; oitava < 6; oitava++)
{
float xc = (float)x / largura * escala * frequencia;
float yc = (float)y / altura * escala * frequencia;
total += Mathf.PerlinNoise(xc, yc) * amplitude;
somaAmplitudes += amplitude;
amplitude *= 0.5f;
frequencia *= 2f;
}
return total / somaAmplitudes; // garante resultado em [0, 1]
}
Repare que normalizo dividindo pela soma das amplitudes. Sem isso, somar seis oitavas estoura o intervalo e seu terreno fica sempre saturado no máximo. É o erro número um de quem implementa isso pela primeira vez.
Biomas a partir de mais de uma camada de ruído
Altura sozinha gera relevo, mas não gera bioma. Para isso você roda duas ou três camadas de ruído independentes (use seeds ou offsets diferentes para temperatura e umidade) e classifica cada ponto cruzando os valores. Whittaker, o ecólogo, mapeou biomas reais exatamente nesses dois eixos, e o esquema funciona bem em jogo:
enum Bioma { OCEANO, MONTANHA, DESERTO, TUNDRA, FLORESTA, CAMPO }
func classificar(altura: float, temperatura: float, umidade: float) -> Bioma:
if altura < 0.3:
return Bioma.OCEANO
if altura > 0.8:
return Bioma.MONTANHA
if temperatura < 0.3:
return Bioma.TUNDRA if umidade < 0.5 else Bioma.FLORESTA
if temperatura > 0.7:
return Bioma.DESERTO if umidade < 0.3 else Bioma.FLORESTA
return Bioma.CAMPO if umidade < 0.4 else Bioma.FLORESTA
O ponto importante: use offsets diferentes nas funções de ruído de temperatura e umidade. Se reaproveitar o mesmo ruído da altura, montanha vira sempre tundra e o mundo fica previsível.
Dungeons: Wave Function Collapse, com expectativa correta
Wave Function Collapse (WFC) virou moda para gerar mapas baseados em tiles. A ideia, emprestada de mecânica quântica como metáfora, é simples: cada célula da grade começa em "superposição" (pode ser qualquer tile). Você repete dois passos até a grade ficar inteira resolvida:
- Observação: escolha a célula com menor entropia (menos tiles possíveis ainda válidos) e "colapse" ela para um tile, sorteado com peso.
- Propagação: remova das células vizinhas todos os tiles que se tornaram incompatíveis com a escolha. Isso pode reduzir as possibilidades de vizinhos dos vizinhos, então a propagação se espalha em onda.
O algoritmo é mais sutil do que parece em pseudocódigo, e os detalhes que matam uma implementação ingênua são dois: o que fazer quando uma célula chega a zero possibilidades (contradição) e como definir as regras de adjacência entre tiles. WFC pode travar em contradições, e a solução comum é backtracking ou simplesmente reiniciar a geração daquela região com outra seed.
Para um projeto real, não escreva WFC do zero antes de estudar a implementação de referência do Maxim Gumin (mxgmn/WaveFunctionCollapse no GitHub, MIT). Ela é a base de praticamente todas as portas para Godot e Unity, e entender o Model.cs dela economiza dias. Para dungeons mais tradicionais (salas conectadas por corredores), aliás, WFC costuma ser exagero. Um BSP (Binary Space Partitioning) ou um random walk com regras de conexão entrega salas bem distribuídas com muito menos dor de cabeça, e é o que a maioria dos roguelikes usa.
Loot procedural por afixos
O sistema de loot de Diablo e similares não gera itens "do nada". Ele parte de um item base e empilha afixos (modificadores de atributo), cada um com um nível mínimo e um peso de raridade. A raridade do item decide quantos afixos ele recebe:
public Weapon GerarArma(int nivelItem, Rarity raridade)
{
var arma = new Weapon
{
damage = Random.Range(nivelItem * 5, nivelItem * 8),
speed = Random.Range(0.8f, 1.5f)
};
int qtdAfixos = raridade switch
{
Rarity.Common => 0,
Rarity.Rare => Random.Range(1, 3),
Rarity.Epic => Random.Range(3, 5),
Rarity.Legendary => Random.Range(5, 7),
_ => 0
};
var aplicados = new List<Affix>();
for (int i = 0; i < qtdAfixos; i++)
{
// GetRandomAffix filtra por nivel minimo e evita duplicar o que ja saiu
Affix afixo = GetRandomAffix(nivelItem, aplicados);
if (afixo == null) break; // sem afixo valido sobrando
arma.AddAffix(afixo);
aplicados.Add(afixo);
}
arma.name = MontarNome(arma.baseType, aplicados);
return arma;
}
O nome do item normalmente segue um padrão prefixo + base + sufixo ("Cruel Espada da Chama"), onde prefixo e sufixo saem dos próprios afixos aplicados. O detalhe que faz a diferença é o GetRandomAffix respeitar nível mínimo e não repetir afixo já aplicado, senão você gera "Espada da Chama da Chama" e itens fora de faixa de nível.
NPCs: comece pela árvore de comportamento, não pela rede neural
A esmagadora maioria dos NPCs de jogo comercial não usa machine learning. Usa máquina de estados ou árvore de comportamento: estruturas explícitas onde você, designer, escreve exatamente o que o personagem faz em cada situação. É previsível, debugável e ajustável. Quando o playtester reclama que o inimigo é burro, você abre a árvore e conserta o nó.
Uma árvore de comportamento avalia nós da raiz para baixo. Sequências rodam os filhos em ordem e param no primeiro que falha. Seletores rodam os filhos em ordem e param no primeiro que tem sucesso (fallback). Folhas são condições ("inimigo à vista?") ou ações ("atacar"). Em Godot, dá para montar isso com nós e o padrão é bem suportado por plugins como o LimboAI; em Unity, com Behavior Designer ou a própria Behavior do pacote oficial. O conceito vale mais que a ferramenta:
Seletor (faz a primeira coisa que der certo)
├── Sequência: "fugir se ferido"
│ ├── Condição: vida < 30%?
│ └── Ação: recuar para cobertura
├── Sequência: "atacar se vê inimigo"
│ ├── Condição: inimigo à vista?
│ └── Ação: atacar
└── Ação: patrulhar (fallback padrão)
Esse comportamento de "fugir quando ferido, atacar quando vê alguém, patrulhar quando não tem nada acontecendo" cobre boa parte dos inimigos de ação. Não tem rede neural nenhuma, e está certo que não tenha. ML para combate só compensa quando você quer um adversário que se adapta ao jogador ao longo da partida, e isso é caso de jogo competitivo, não de inimigo de fase.
Quando ML faz sentido: reinforcement learning com ML-Agents
Reinforcement learning treina um agente por tentativa e erro: ele toma ações, recebe recompensa (ou punição) e ajusta a política para maximizar recompensa ao longo do tempo. No Unity, o caminho oficial é o pacote ML-Agents, que conecta a engine a um treinador em Python (PPO ou SAC). Você não escreve a rede neural na mão; descreve o que o agente observa, o que ele pode fazer e como é recompensado, e o ML-Agents treina.
O código que você escreve no Unity é a classe Agent:
using Unity.MLAgents;
using Unity.MLAgents.Sensors;
using Unity.MLAgents.Actuators;
public class AgenteCombate : Agent
{
public Transform alvo;
private float vida;
public override void OnEpisodeBegin()
{
// reinicia o cenario a cada episodio de treino
transform.localPosition = PosicaoInicial();
vida = 100f;
}
public override void CollectObservations(VectorSensor sensor)
{
// tudo que o agente "ve" entra aqui (entradas da rede)
sensor.AddObservation(transform.localPosition);
sensor.AddObservation(alvo.localPosition);
sensor.AddObservation(vida / 100f);
}
public override void OnActionReceived(ActionBuffers acoes)
{
// saidas da rede viram movimento/ataque
float moveX = acoes.ContinuousActions[0];
float moveZ = acoes.ContinuousActions[1];
transform.localPosition += new Vector3(moveX, 0, moveZ) * Time.deltaTime * 5f;
if (ChegouNoAlvo())
{
SetReward(1.0f); // recompensa por acertar o objetivo
EndEpisode();
}
}
public override void Heuristic(in ActionBuffers acoesOut)
{
// controle manual pra testar o cenario antes de treinar
var continuas = acoesOut.ContinuousActions;
continuas[0] = Input.GetAxis("Horizontal");
continuas[1] = Input.GetAxis("Vertical");
}
}
O treino em si roda fora do jogo, pela linha de comando (mlagents-learn config.yaml --run-id=meu_treino), e o resultado é um arquivo de modelo .onnx que você pluga no agente para inferência em runtime. Vale o aviso honesto: treinar um agente que se comporta bem dá trabalho. A função de recompensa é onde mora o sofrimento. Recompensa mal desenhada produz agentes que "trapaceiam" o objetivo (ficam girando em círculo pegando uma micro-recompensa em vez de cumprir a tarefa). Para a maioria dos jogos, uma árvore de comportamento bem feita chega no mesmo resultado com uma fração do esforço.
Diálogo com LLM: poderoso e cheio de pegadinhas
Conectar um NPC a um modelo de linguagem (GPT, Claude, Llama) permite conversa aberta em vez de árvore de diálogo fixa. A mecânica é direta: você monta um prompt com a personalidade do NPC, o contexto do mundo e o histórico recente da conversa, manda para a API e mostra a resposta.
async Task<string> GerarResposta(string falaDoJogador)
{
string prompt = $@"Voce e {nomeNpc}, um {papelNpc} num RPG de fantasia.
Personalidade: {personalidade}
Quest atual: {questAtual}
Regras: responda em ate 2 frases, no personagem, sem quebrar o clima medieval.
Conversa recente:
{string.Join(""\n"", historico.TakeLast(5))}
Jogador: {falaDoJogador}
{nomeNpc}:";
var resposta = await ChamarApiLLM(prompt, maxTokens: 100, temperature: 0.8f);
string texto = resposta.Trim();
historico.Add($""Jogador: {falaDoJogador}"");
historico.Add($""{nomeNpc}: {texto}"");
return texto;
}
Funciona, mas antes de colocar isso num jogo de verdade, encare os problemas reais:
- Latência. Uma chamada de API leva de centenas de milissegundos a alguns segundos. Não dá para travar o frame esperando. Você precisa de UI assíncrona (NPC "pensando", animação de espera) e tratar timeout.
- Custo. Cada conversa consome tokens, e tokens custam dinheiro. Mil jogadores conversando à vontade viram uma conta relevante. Modelos locais (Llama via Ollama, por exemplo) tiram o custo por chamada mas pedem hardware do jogador.
- Coerência e segurança. O modelo pode contradizer a lore, sair do personagem ou dizer algo que você não quer no seu jogo. Prompt bem feito ajuda, mas não garante. Diálogo gerado é difícil de testar e de dublar.
- Sem internet, sem NPC. API na nuvem significa dependência de conexão. Para single-player offline, isso é um problema sério.
LLM em NPC é uma ferramenta genuinamente nova e interessante, mas hoje ela brilha mais em jogos de nicho (narrativos, experimentais) do que como substituto geral da árvore de diálogo escrita à mão, que continua sendo barata, testável e sob seu controle.
IA generativa na produção: arte, textura e áudio
Fora do runtime, modelos generativos entraram forte no pipeline de produção. Aqui o ganho é mais claro e os riscos são mais de processo do que de performance.
Concept art e referência com difusão. Stable Diffusion (e similares) gera concepts em segundos. Para quem trabalha sozinho, isso destrava a fase de exploração visual: dá para gerar dezenas de variações de um personagem antes de comprometer tempo desenhando. A API do diffusers em Python é o caminho mais direto:
from diffusers import StableDiffusionPipeline
import torch
pipe = StableDiffusionPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
torch_dtype=torch.float16
).to("cuda") # precisa de GPU com VRAM suficiente
imagem = pipe(
prompt="concept art de guerreiro anao, pintura digital, alto detalhe",
negative_prompt="maos malfeitas, deformado, borrado",
num_inference_steps=30,
guidance_scale=7.5
).images[0]
imagem.save("concept.png")
Dois avisos honestos. Primeiro: arte gerada serve muito bem como referência e ponto de partida, e mal como asset final, especialmente em estilos consistentes (o modelo varia demais entre gerações). Segundo: a questão de direitos autorais de imagens geradas ainda é cinzenta em vários países. Para um projeto comercial, trate isso com cuidado e não como detalhe.
Texturas e materiais. Existem ferramentas que geram mapas PBR (albedo, normal, roughness) a partir de descrição ou de uma foto. Útil para preencher biblioteca de materiais. Mas normal map e roughness gerados quase sempre precisam de retoque manual para tilear sem costura e responder bem à luz. Não é botão mágico.
Áudio e música. Geração procedural de música existe há anos (sistemas que remixam stems conforme a intensidade do gameplay), e modelos de IA começam a entrar nesse espaço. Para a maioria dos projetos, porém, música adaptativa por camadas (layers que entram e saem conforme o estado do jogo) feita por um compositor ainda entrega resultado mais controlado do que geração por modelo.
Geração de código. Copilot e assistentes parecidos aceleram a escrita de código repetitivo (boilerplate de spawner, getters, estruturas de dados). São úteis como autocomplete turbinado. Mas geram bugs com confiança, e em lógica de gameplay sutil (timing, física, networking) confiar cegamente custa caro. Trate como par de programação com um júnior rápido: revise tudo.
Onde isso entra por gênero
Roguelikes. É o gênero que mais ganha com procedural. Dungeon, distribuição de inimigos e tabela de loot gerados por seed dão rejogabilidade quase infinita. A lição de quem já lançou: gere a estrutura, mas valide que ela é jogável (todas as salas conectadas, existe caminho da entrada ao boss, o loot necessário é alcançável). Geração sem validação produz runs impossíveis.
Estratégia. É onde IA adaptativa de adversário faz mais sentido, porque o jogador humano busca explorar padrões e um oponente que se ajusta mantém o desafio vivo. Ainda assim, a maioria dos jogos de estratégia comerciais usa heurísticas afinadas à mão e dificuldade escalonada, não redes neurais, porque é mais previsível de balancear.
Sandbox e simulação. Comportamento emergente de muitos agentes simples segue regras locais (steering, boids, autômatos celulares) mais do que ML pesado. A graça está na interação entre regras simples, não na complexidade de cada agente.
Otimização: IA também tem orçamento de frame
Por mais esperta que seja a lógica, ela divide os 16 milissegundos do frame com renderização, física e tudo mais. Duas técnicas resolvem a maior parte dos problemas de performance de IA:
Orçamento de tempo e escalonamento. Em vez de rodar a lógica de todos os NPCs todo frame, fatie o trabalho. Processe os agentes próximos sempre e os distantes em intervalos maiores. Uma fila por prioridade com um teto de tempo por frame evita que um pico de IA derrube o framerate.
LOD de comportamento. Assim como modelos 3D têm níveis de detalhe por distância, a IA pode ter. O inimigo do outro lado do mapa não precisa de pathfinding A* completo nem de decisão por rede neural; basta uma máquina de estados simples, ou até nada além da animação. Conforme ele se aproxima do jogador, você sobe o nível de detalhe da lógica. Em mundo aberto com centenas de NPCs, isso é a diferença entre rodar e travar.
func nivel_de_ia(distancia: float) -> int:
if distancia < 10.0: return 3 # completo: pathfinding + decisao a cada frame
if distancia < 30.0: return 2 # reduzido: atualiza a cada poucos frames
if distancia < 100.0: return 1 # minimo: steering simples, sem pathfinding
return 0 # so anima, sem logica
O número exato de frames e as distâncias você calibra no seu jogo. O princípio é universal: gaste processamento perto do jogador, economize longe.
O que levar daqui
IA em jogos não é uma coisa só, e quase nunca é a coisa mais cara que você imagina. Antes de treinar uma rede neural, pergunte se uma árvore de comportamento ou uma tabela de afixos não resolve. Quase sempre resolve, com mais controle e menos dor.
A ordem prática de aprendizado, se você está começando: pathfinding decente primeiro, depois máquina de estados ou árvore de comportamento para os NPCs, depois geração procedural com ruído e seed. Só então, quando bater num problema que regras explícitas não dão conta, vale puxar ML-Agents ou um LLM, sabendo dos custos de latência, dinheiro e imprevisibilidade que vêm junto.
A IA generativa muda mesmo o jogo do dev solo, mas no pipeline de produção (concept, referência, boilerplate) mais do que dentro do runtime. Use onde ela acelera de verdade e desconfie de quem promete botão mágico.
O que você vai construir é decisão sua. A ferramenta é só ferramenta.
