Voltar para o Blog
Quest Log

Como Fazer uma State Machine no Godot: Máquina de Estados para Personagens

Diagrama de máquina de estados conectando os estados de um personagem de jogo no Godot

Aprenda a criar uma state machine no Godot 4 com GDScript: máquina de estados com nodes, estados idle, run e jump, e código organizado que escala.

Como Fazer uma State Machine no Godot: Máquina de Estados para Personagens

Todo personagem de jogo vive em estados: parado, correndo, no ar, atacando, morto. Enquanto seu player só anda e pula, um script único dá conta. O problema aparece quando você adiciona dash, ataque, dano, escada... e de repente o _physics_process virou uma torre de if que ninguém mais entende. Uma state machine no Godot resolve exatamente isso: cada estado vira um pedaço isolado de código, com regra clara de quando entra e quando sai.

Nesse tutorial eu monto uma máquina de estados completa pra um personagem de plataforma com idle, run e jump, usando o padrão de nodes que é o jeito mais natural de fazer isso no Godot 4. Todo código é GDScript e roda como está.

O problema que a state machine resolve

Antes da solução, vale olhar o buraco. O caminho natural de quem está começando é controlar o personagem com flags booleanas:

var is_jumping = false
var is_running = false
var is_attacking = false
var is_dashing = false

func _physics_process(delta):
    if is_jumping and not is_attacking:
        # lógica de pulo
        pass
    elif is_running and is_on_floor() and not is_dashing and not is_attacking:
        # lógica de corrida
        pass
    # ... e mais 40 linhas disso

Com 4 flags você já tem 16 combinações possíveis, e a maioria não faz sentido (dá pra estar is_jumping e is_dashing ao mesmo tempo? quem ganha?). Cada feature nova multiplica as combinações e cada bug vira uma caça por qual flag ficou em estado inválido.

A máquina de estados (FSM, finite state machine) corta isso pela raiz com três regras:

  1. O personagem está em exatamente um estado por vez.
  2. Cada estado tem sua própria lógica, isolada das outras.
  3. A troca de estado é explícita: você sai de um e entra em outro, nunca fica no meio.

Combinação inválida deixa de existir porque não existe combinação, existe um estado só.

A versão simples: enum e match

Pra personagens pequenos, um enum com match já organiza bem e não pede estrutura nenhuma:

extends CharacterBody2D

enum State { IDLE, RUN, AIR }

const SPEED = 300.0
const JUMP_VELOCITY = -400.0

var state: State = State.IDLE
var gravity = ProjectSettings.get_setting("physics/2d/default_gravity")

func _physics_process(delta):
    match state:
        State.IDLE:
            if Input.get_axis("move_left", "move_right"):
                state = State.RUN
            if Input.is_action_just_pressed("jump") and is_on_floor():
                velocity.y = JUMP_VELOCITY
                state = State.AIR
        State.RUN:
            var direction = Input.get_axis("move_left", "move_right")
            velocity.x = direction * SPEED
            if direction == 0:
                state = State.IDLE
            if Input.is_action_just_pressed("jump") and is_on_floor():
                velocity.y = JUMP_VELOCITY
                state = State.AIR
        State.AIR:
            velocity.y += gravity * delta
            velocity.x = Input.get_axis("move_left", "move_right") * SPEED
            if is_on_floor():
                state = State.IDLE

    move_and_slide()

Isso funciona e é honesto: pra um jogo de game jam com 3 estados, talvez seja tudo que você precisa. O limite aparece quando os estados crescem. Cada match vira um bloco gigante, lógica de "entrar no estado" (tocar animação, resetar timer) fica espalhada, e o arquivo volta a ser um monolito, só que com cerca melhor.

Quando o personagem passa de uns 4 estados, ou quando os estados têm setup e teardown (tocar som ao entrar, limpar hitbox ao sair), vale subir pro padrão de verdade: um node por estado.

Implementando a state machine no Godot com nodes

A ideia: cada estado é um node com script próprio, todos filhos de um node StateMachine que delega os callbacks do Godot pro estado ativo. A árvore do player fica assim:

Player (CharacterBody2D)
├── AnimatedSprite2D
├── CollisionShape2D
└── StateMachine (Node)
    ├── Idle (Node)
    ├── Run (Node)
    └── Air (Node)

Isso casa com o jeito que o Godot pensa: composição por nodes. Você enxerga os estados na dock de cena, adiciona um novo arrastando um node, e cada script fica pequeno o suficiente pra caber numa tela.

A classe base State

Todo estado herda dessa classe. Ela define o contrato: o que acontece ao entrar, ao sair, e a cada frame.

class_name State
extends Node

var state_machine = null

func enter() -> void:
    pass

func exit() -> void:
    pass

func physics_update(_delta: float) -> void:
    pass

enter() roda uma vez quando o estado assume, exit() uma vez quando ele sai, e physics_update() substitui o _physics_process enquanto o estado está ativo. Se o seu jogo precisar, dá pra adicionar update(delta) pro _process e handle_input(event) pro _unhandled_input no mesmo molde.

O node StateMachine

O gerente. Ele registra os filhos, mantém o estado atual e faz as trocas:

class_name StateMachine
extends Node

@export var initial_state: State

var current_state: State
var states: Dictionary = {}

func _ready() -> void:
    for child in get_children():
        if child is State:
            states[child.name.to_lower()] = child
            child.state_machine = self

    if initial_state:
        initial_state.enter()
        current_state = initial_state

func _physics_process(delta: float) -> void:
    if current_state:
        current_state.physics_update(delta)

func transition_to(state_name: String) -> void:
    var new_state = states.get(state_name.to_lower())
    if not new_state or new_state == current_state:
        return

    if current_state:
        current_state.exit()
    new_state.enter()
    current_state = new_state

Dois detalhes de design que valem a pena:

  • O initial_state é um @export, então você escolhe o estado inicial no Inspector arrastando o node. Nada chumbado no código.
  • O transition_to ignora transição pro mesmo estado. Isso evita enter() rodando de novo sem querer e reiniciando animação que já estava tocando.

Os estados: Idle, Run e Air

Agora cada estado vira um script curto. Todos acessam o player via owner, que aponta pra raiz da cena (funciona porque os nodes de estado foram salvos como parte da cena do Player).

O script do player em si fica mínimo, só constantes e dados compartilhados:

extends CharacterBody2D

const SPEED = 300.0
const JUMP_VELOCITY = -400.0

var gravity = ProjectSettings.get_setting("physics/2d/default_gravity")

Repare: sem _physics_process no player. Quem dirige é a máquina.

Idle. Parado no chão, esperando input:

extends State

@onready var player: CharacterBody2D = owner

func enter() -> void:
    player.velocity.x = 0
    player.get_node("AnimatedSprite2D").play("idle")

func physics_update(delta: float) -> void:
    player.velocity.y += player.gravity * delta
    player.move_and_slide()

    if not player.is_on_floor():
        state_machine.transition_to("Air")
        return
    if Input.is_action_just_pressed("jump"):
        player.velocity.y = player.JUMP_VELOCITY
        state_machine.transition_to("Air")
        return
    if Input.get_axis("move_left", "move_right"):
        state_machine.transition_to("Run")

Run. Movimento horizontal no chão:

extends State

@onready var player: CharacterBody2D = owner

func enter() -> void:
    player.get_node("AnimatedSprite2D").play("run")

func physics_update(delta: float) -> void:
    var direction := Input.get_axis("move_left", "move_right")
    player.velocity.x = direction * player.SPEED
    player.velocity.y += player.gravity * delta
    player.move_and_slide()

    if not player.is_on_floor():
        state_machine.transition_to("Air")
        return
    if Input.is_action_just_pressed("jump"):
        player.velocity.y = player.JUMP_VELOCITY
        state_machine.transition_to("Air")
        return
    if direction == 0:
        state_machine.transition_to("Idle")

Air. Cobre subida e queda (pulo e cair da plataforma são o mesmo estado, o que muda é o sinal da velocidade):

extends State

@onready var player: CharacterBody2D = owner

func enter() -> void:
    player.get_node("AnimatedSprite2D").play("jump")

func physics_update(delta: float) -> void:
    player.velocity.y += player.gravity * delta
    player.velocity.x = Input.get_axis("move_left", "move_right") * player.SPEED
    player.move_and_slide()

    if player.is_on_floor():
        if Input.get_axis("move_left", "move_right"):
            state_machine.transition_to("Run")
        else:
            state_machine.transition_to("Idle")

Pronto, a máquina está completa. Cada arquivo tem menos de 30 linhas, e a pergunta "o que o personagem faz quando está no ar?" tem uma resposta de um arquivo só.

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

Por que isso escala bem

A diferença real aparece quando você adiciona o próximo estado. Quer um dash? Cria um node Dash, escreve o script dele (velocidade alta por X segundos, depois transiciona pra Run ou Air), e adiciona as transições de entrada nos estados que podem dashar. Você não toca no código de Idle, Run ou Air além de uma linha de transição. Num script monolítico, qualquer feature nova mexe no mesmo arquivo que todas as outras, e é aí que as regressões nascem.

Outros ganhos práticos:

Animação fica no lugar óbvio. O enter() de cada estado toca a animação dele. Acabou aquele bloco de if decidindo qual animação tocar baseado em velocidade e flags.

Debug fica trivial. Quer saber em que estado o personagem está? print(state_machine.current_state.name). Quer logar toda transição? Um print no transition_to e você tem o histórico completo do comportamento.

Inimigos usam a mesma base. As classes State e StateMachine são genéricas. Um inimigo com Patrol, Chase e Attack usa exatamente os mesmos dois scripts base, só com estados diferentes pendurados. Escreveu uma vez, usa no jogo inteiro.

Erros comuns (e como evitar)

Alguns tropeços que eu vejo repetidamente em código de quem está adotando o padrão:

Lógica de transição duplicada. Se três estados no chão checam o mesmo "se apertou pulo, pula", e amanhã você adiciona coyote time, vai ter que mexer nos três. Quando perceber repetição assim, extraia pra uma função no script do player (algo como func try_jump() -> bool) e chame dos estados. O estado decide quando checar, o player sabe como pular.

Estado que conhece demais os outros. O estado Air não deve perguntar "eu vim do Idle ou do Run?". Se você precisa desse tipo de informação, passe ela na transição ou repense a divisão dos estados. Estado bom é autocontido: olha o mundo (input, chão, velocidade) e decide, sem saber a história.

Esquecer o return depois de transicionar. Olhe os estados acima: toda chamada de transition_to é seguida de return. Sem isso, o resto do physics_update continua rodando no frame da troca e pode disparar uma segunda transição em cima da primeira. É bug silencioso e chato de achar.

Criar estado pra tudo. Nem toda variação merece estado próprio. Andar e correr com o shift apertado é o mesmo estado com velocidade diferente, não dois estados. A pergunta que separa: o comportamento muda de regra, ou só de número? Regra nova, estado novo. Número novo, variável.

Quando a FSM simples não basta

Vale ser honesto sobre os limites. A máquina de estados clássica assume um estado por vez, e às vezes o personagem faz duas coisas ao mesmo tempo: correr e atirar, por exemplo. As saídas comuns:

  • Duas máquinas paralelas: uma pra movimento (Idle/Run/Air), outra pra ação (Aim/Shoot/Reload). Cada uma cuida da sua dimensão. Como a estrutura é por nodes, é só pendurar dois StateMachine no player.
  • Máquina hierárquica: estados que contêm subestados (Grounded contém Idle e Run, e a checagem de pulo vive no pai). Resolve duplicação de transição, ao custo de mais estrutura.

Pra IA de inimigo mais elaborada existem behavior trees, mas isso é outro artigo. Pra personagem jogável, FSM com nodes cobre a esmagadora maioria dos casos, de jogo de jam a projeto comercial.

Fechando

State machine não é arquitetura sofisticada, é faxina: cada comportamento na sua gaveta, com etiqueta de quando entra e quando sai. A versão com enum serve pra protótipo, a versão com nodes serve pra projeto que vai crescer, e a base que montamos aqui (State, StateMachine, três estados) é reaproveitável em qualquer personagem do seu jogo.

Meu conselho prático: pegue um personagem que você já tem com movimento funcionando e refatore pra essa estrutura. Migrar código que existe ensina mais que começar do zero, porque você vê na prática o que estava emaranhado. Depois adicione um estado novo, um dash ou um ataque, e sinta a diferença de encaixar uma peça em vez de operar um monolito.