Voltar para o Blog
Quest Log

Steering Behaviors no Godot 4: Movimento de IA que Não Parece Robô

Vários agentes se movendo em grupo de forma orgânica num jogo 2D no Godot

Steering behaviors no Godot 4 em GDScript: seek, flee, arrive, pursuit, wander e flocking com código que roda. Faça a IA do seu jogo se mexer de forma orgânica.

Steering Behaviors no Godot 4: Movimento de IA que Não Parece Robô

Um inimigo que vira de direção na hora, sem inércia, denuncia o código na cara do jogador. Steering behaviors no Godot resolvem exatamente isso: dão à sua IA um movimento que acelera, curva e freia de forma orgânica, como se houvesse peso por trás dela. Neste guia eu mostro os comportamentos clássicos (seek, flee, arrive, pursuit, wander e flocking) em GDScript de Godot 4 que você copia, cola e roda.

Esse conjunto de técnicas vem de um trabalho do Craig Reynolds dos anos 80, e até hoje é a base de movimento de IA em jogo que parece vivo. A ideia é mais simples do que o nome sugere, e quando você entende a conta central, todos os comportamentos viram variação da mesma fórmula. Este post é um aprofundamento da seção de steering do nosso guia de IA para jogos no Godot; aqui a gente cobre os comportamentos que ficaram de fora lá.

A conta central: velocidade desejada menos velocidade atual

Todo steering behavior responde a uma única pergunta: para onde eu quero ir, e o quão diferente isso é de para onde eu já estou indo? A resposta é um vetor de força:

# desired = a velocidade que eu gostaria de ter agora
# velocity = a velocidade que eu realmente tenho
var steering := desired - velocity

Esse vetor steering é a correção. Se você o aplica de uma vez, volta ao movimento robótico. O truque é aplicar aos poucos, limitando a intensidade da correção por frame. É isso que cria a curva suave em vez do giro seco.

A estrutura base num CharacterBody2D fica assim:

extends CharacterBody2D

@export var max_speed := 250.0
@export var max_force := 600.0  # quão rápido ele consegue corrigir a rota

func apply_steering(steering: Vector2, delta: float) -> void:
    # limita a força de correção por frame
    steering = steering.limit_length(max_force)
    velocity += steering * delta
    # nunca passa da velocidade máxima
    velocity = velocity.limit_length(max_speed)
    move_and_slide()

max_speed define o teto de velocidade; max_force define a manobrabilidade. Força alta dá um agente ágil que vira quase na hora; força baixa dá um caminhão que demora a corrigir. Brincar com esses dois números é metade do trabalho de dar personalidade ao movimento.

Seek e Flee: perseguir e fugir

Seek é ir até um ponto. A velocidade desejada aponta para o alvo, na velocidade máxima:

func seek(target_pos: Vector2) -> Vector2:
    var desired := (target_pos - global_position).normalized() * max_speed
    return desired - velocity

Flee é o oposto exato: a velocidade desejada aponta para longe da ameaça. Mesma fórmula, sinal trocado:

func flee(threat_pos: Vector2) -> Vector2:
    var desired := (global_position - threat_pos).normalized() * max_speed
    return desired - velocity

Repare que os dois retornam só o vetor de steering, sem mover nada. Quem move é o apply_steering. Manter cada comportamento como uma função pura que devolve um Vector2 é o que vai te deixar somar vários deles mais pra frente.

Arrive: chegar sem ultrapassar o alvo

Seek tem um problema: o agente chega no alvo em velocidade máxima, ultrapassa, volta, ultrapassa de novo, e fica oscilando em cima do ponto. Arrive resolve fazendo o agente desacelerar conforme se aproxima.

A ideia é um raio de frenagem: fora dele, vai na velocidade máxima; dentro dele, a velocidade cai proporcional à distância.

@export var slowing_radius := 120.0

func arrive(target_pos: Vector2) -> Vector2:
    var to_target := target_pos - global_position
    var distance := to_target.length()
    if distance < 1.0:
        return -velocity  # praticamente em cima do alvo, freia tudo

    var ramped_speed := max_speed
    if distance < slowing_radius:
        ramped_speed = max_speed * (distance / slowing_radius)

    var desired := to_target.normalized() * ramped_speed
    return desired - velocity

Use arrive em vez de seek sempre que o alvo for um ponto fixo: um waypoint de patrulha, a posição de uma cobertura, o destino de um caminho calculado. O movimento fica muito mais natural, sem aquele tranco no fim.

Pursuit e Evade: mirar onde o alvo vai estar

Seek mira onde o alvo está agora. Contra um player que se mexe, isso faz o inimigo correr sempre atrás, como um cachorro perseguindo o próprio rabo. Pursuit corrige isso prevendo a posição futura do alvo e indo pra lá.

A previsão é simples: pega a velocidade do alvo, multiplica por um tempo de antecipação e soma na posição atual. Quanto mais longe o alvo está, mais à frente vale prever.

func pursuit(target: CharacterBody2D) -> Vector2:
    var to_target := target.global_position - global_position
    var distance := to_target.length()
    # tempo de previsão cresce com a distância, mas com teto
    var prediction := min(distance / max_speed, 1.0)
    var future_pos := target.global_position + target.velocity * prediction
    return seek(future_pos)

Evade é o pursuit ao contrário: prevê para onde a ameaça vai e foge desse ponto, trocando o seek por flee. Um inimigo que usa evade é muito mais difícil de encurralar, porque ele antecipa o seu movimento em vez de só reagir a ele.

Próximo nível
Quer aprender isso na prática?

No CursoGame.Dev você sai dos tutoriais soltos e constrói jogos publicáveis, com trilha progressiva, quests práticas e feedback real.

Conhecer a plataforma
+500 alunos4.9/5Garantia 7 dias

Wander: vagar sem parecer bêbado

Para um inimigo em patrulha ou um NPC ambiente, você quer movimento aleatório, mas suave. Sortear uma direção nova todo frame dá um zigue-zague nervoso e feio. O wander clássico resolve projetando um círculo um pouco à frente do agente e mirando num ponto que desliza devagar pela borda desse círculo.

var wander_angle := 0.0

@export var wander_distance := 80.0   # quão à frente fica o círculo
@export var wander_radius := 40.0     # tamanho do círculo
@export var wander_jitter := 3.0      # quão rápido o alvo desliza

func wander(delta: float) -> Vector2:
    # o ângulo muda pouco a cada frame, daí a suavidade
    wander_angle += randf_range(-1.0, 1.0) * wander_jitter * delta

    var heading := velocity.normalized()
    if heading == Vector2.ZERO:
        heading = Vector2.RIGHT  # parado: escolhe uma direção qualquer

    var circle_center := global_position + heading * wander_distance
    var offset := Vector2(cos(wander_angle), sin(wander_angle)) * wander_radius
    return seek(circle_center + offset)

O segredo está no wander_angle mudar pouquinho por frame. Como o alvo se desloca devagar pela borda do círculo, o agente faz curvas longas e orgânicas em vez de espasmos.

Flocking: vários agentes que se comportam como grupo

A parte mais bonita de steering aparece quando você tem muitos agentes. Três comportamentos somados, descritos por Reynolds como "boids", dão um bando que se move junto sem nenhuma coordenação central:

  • Separation: afaste-se dos vizinhos muito próximos. É o que impede o empilhamento.
  • Alignment: aponte na média da direção dos vizinhos. É o que faz o grupo andar junto.
  • Cohesion: mova-se em direção ao centro do grupo. É o que mantém o bando coeso.
@export var neighbor_radius := 100.0
@export var separation_radius := 50.0

func flock(neighbors: Array) -> Vector2:
    var separation := Vector2.ZERO
    var alignment := Vector2.ZERO
    var cohesion := Vector2.ZERO
    var count := 0

    for other in neighbors:
        if other == self:
            continue
        var offset := global_position - other.global_position
        var distance := offset.length()
        if distance > neighbor_radius or distance == 0.0:
            continue

        # separação: empurra para longe, mais forte quanto mais perto
        if distance < separation_radius:
            separation += offset / distance

        alignment += other.velocity
        cohesion += other.global_position
        count += 1

    if count == 0:
        return Vector2.ZERO

    alignment = (alignment / count).normalized() * max_speed - velocity
    cohesion = ((cohesion / count) - global_position).normalized() * max_speed - velocity

    return separation * 1.5 + alignment * 1.0 + cohesion * 1.0

O neighbors aqui é a lista de agentes próximos. Em projeto pequeno dá pra passar o grupo inteiro; quando crescer, vale usar um Area2D em cada agente pra coletar só quem está perto, ou marcar todos com um grupo e filtrar por distância. Se você ainda não usa grupos pra isso, o nosso guia de IA de inimigo que persegue e patrulha mostra o padrão na prática.

Combinando forças com pesos

O poder real de steering é que tudo é vetor, e vetor soma. Um inimigo pode perseguir o player, vagar quando o perde de vista e ainda se separar dos colegas, tudo ao mesmo tempo, com pesos diferentes pra cada força:

func _physics_process(delta: float) -> void:
    var steering := Vector2.ZERO

    if player_spotted:
        steering += pursuit(player) * 1.0
        steering += flock(get_nearby_enemies()) * 0.6
    else:
        steering += wander(delta) * 0.4

    apply_steering(steering, delta)

Os pesos (1.0, 0.6, 0.4) são chute inicial. Não espere acertar de primeira: você vai rodar, ver os agentes fazendo algo esquisito e ajustar no olho. É assim mesmo, faz parte do processo. Um peso de separação alto demais espalha o bando até dispersar; baixo demais e eles empilham. O ponto certo só aparece testando com o jogo rodando.

Steering não substitui pathfinding

Um aviso pra não te frustrar: steering sozinho não desvia de paredes. Seek vai mandar o agente em linha reta contra o muro. Steering resolve o movimento orgânico em espaço aberto e o desvio entre agentes; pra contornar a geometria do nível você precisa de pathfinding de verdade.

A combinação que funciona é usar os dois em camadas: o pathfinding no Godot com Navigation2D te dá o próximo ponto do caminho, e o steering (arrive nesse ponto, mais separation dos vizinhos) move o corpo até lá com suavidade. O caminho vem do navmesh; a vida no movimento vem do steering.

Por onde começar

Não tente jogar flocking completo no primeiro inimigo. Vá por partes, do mais simples pro mais complexo:

  1. Seek primeiro. Faça um agente ir até o mouse. Sinta a inércia mudando max_force.
  2. Troque por arrive. Veja a diferença na chegada: sem o tranco, ele para suave.
  3. Adicione separation quando tiver mais de um inimigo, pra eles pararem de se empilhar.
  4. Pursuit e wander entram quando você quiser inimigos que antecipam e NPCs que vagam.

Steering é uma daquelas ferramentas que parecem mágica quando você vê funcionando e óbvias quando entende a conta por trás. Abre o Godot, faz um CharacterBody2D seguir o mouse com seek, e vai empilhando comportamento por cima. Em pouco tempo você tem inimigos que se mexem de um jeito que o jogador sente, mesmo sem saber explicar por quê.

Perguntas frequentes

O que são steering behaviors em jogos?

São um conjunto de técnicas de movimento criadas por Craig Reynolds para fazer um agente se mover de forma orgânica. Em vez de teletransportar a velocidade para o valor desejado, você calcula uma força de direção a cada frame (a diferença entre a velocidade que você quer e a que tem agora) e aplica essa força aos poucos. O resultado é um movimento com inércia, que acelera e curva de forma natural em vez de parecer um robô trocando de direção no susto.

Qual a diferença entre steering behaviors e pathfinding no Godot?

Pathfinding decide o caminho (quais pontos seguir para ir de A até B sem bater em parede). Steering decide como percorrer esse caminho de um jeito que pareça vivo: acelerando, freando ao chegar, desviando dos vizinhos. Os dois trabalham juntos: o pathfinding te dá o próximo ponto, e o steering move o corpo até ele com suavidade. No Godot você usa NavigationServer para o caminho e steering para o movimento.

Steering behaviors funcionam com CharacterBody2D?

Funcionam, e é a combinação mais comum em 2D. Você acumula a força de steering num Vector2, soma na propriedade velocity do CharacterBody2D, limita o resultado à velocidade máxima e chama move_and_slide(). O CharacterBody2D cuida da colisão; o steering cuida só de para onde e com que intensidade o corpo quer ir.

Como fazer vários inimigos não se empilharem no mesmo ponto?

Use o comportamento de separação (separation) do flocking. Para cada vizinho dentro de um raio, some um vetor que aponta para longe dele, mais forte quanto mais perto estiver. Somando isso ao seek do alvo, os inimigos perseguem o player mas se espalham em vez de virar uma bola única em cima dele.