Voltar para o Blog
Quest Log

@export no Godot: variaveis no Inspector explicadas

Painel do Inspector do Godot com sliders e campos de variaveis exportadas ao lado de um personagem de jogo

Domine o @export no Godot 4: range, enum, grupos, NodePath, PackedScene e Resource. Ajuste gameplay pelo Inspector sem abrir codigo. Guia de export godot.

Ferramenta:Godot

Se tem uma anotação que muda a forma de trabalhar no Godot, é o @export. O export godot pega uma variável do seu script e transforma num campo editável no Inspector: velocidade vira um número que você muda com o jogo aberto do lado, vida máxima vira um slider, o tipo de inimigo vira um dropdown. E o melhor: cada instância da cena pode ter um valor diferente, sem duplicar uma linha de código.

Eu demorei pra dar o devido valor a isso quando comecei. Achava que era só conveniência, um jeito chique de evitar abrir o script. Não é. O @export é a fronteira entre programar o jogo e ajustar o jogo, e separar essas duas coisas é o que permite balancear gameplay rápido, reaproveitar a mesma cena em dez contextos e até deixar alguém que não programa mexer nos números. Nesse artigo eu passo pelo básico, pelos hints mais úteis (@export_range, @export_enum, @export_group) e pelos exports de NodePath, PackedScene e Resource. Tudo em GDScript do Godot 4.x.

O export godot básico: tipo define o campo

A forma mínima é uma anotação antes da declaração da variável:

extends CharacterBody2D

@export var speed: float = 200.0
@export var max_health: int = 100
@export var player_name: String = "Heroi"
@export var can_double_jump: bool = false

Salvou o script, o Inspector do node já mostra os quatro campos. O tipo da variável define o widget: float e int viram campos numéricos, String vira caixa de texto, bool vira checkbox. Por isso vale sempre tipar a variável exportada, ou com a anotação de tipo (: float) ou com um valor inicial que deixe o tipo óbvio. @export var speed sem tipo nem valor não funciona, o Godot precisa saber o que desenhar no Inspector.

Dois comportamentos que confundem todo iniciante:

O valor do Inspector vence o valor do script. O = 200.0 é só o padrão. Se você mudou pra 350 no Inspector, é 350 que vale, mesmo que depois edite o script pra 500. Quando um número "não muda de jeito nenhum", olhe o Inspector: provavelmente tem um valor sobrescrito ali (ele aparece com um ícone de reverter ao lado).

Cada instância guarda o próprio valor. Esse é o superpoder. Uma cena enemy.tscn com @export var speed vira inimigo lento, médio e rápido só arrastando três instâncias pra fase e mudando o número de cada uma. Mesma cena, mesmo script, três comportamentos.

E o uso é o de qualquer variável:

func _physics_process(delta: float) -> void:
    var direction: float = Input.get_axis("move_left", "move_right")
    velocity.x = direction * speed
    move_and_slide()

Se você ainda está se acostumando com variáveis e tipos no GDScript, o guia de GDScript do zero cobre essa base antes de chegar aqui.

@export_range: slider com limites

Número solto no Inspector é convite pra valor absurdo. O @export_range transforma o campo num slider com mínimo e máximo:

@export_range(0, 500) var speed: float = 200.0
@export_range(0.0, 1.0, 0.05) var drop_chance: float = 0.25
@export_range(1, 10, 1) var enemy_level: int = 1

O terceiro argumento é o passo do slider. No drop_chance, cada arrastada anda de 0.05 em 0.05, o que evita aquele 0.2499999 que ninguém pediu.

Tem dois sufixos que eu uso direto. O "or_greater" deixa digitar acima do máximo (o slider para no limite, mas o campo aceita mais):

@export_range(0, 100, 1, "or_greater") var max_health: int = 100

E o "suffix:..." mostra a unidade no campo, o que parece detalhe mas evita confusão de quem balanceia:

@export_range(0, 1000, 10, "suffix:px/s") var speed: float = 300.0

A regra que eu sigo: todo número exportado que tem limite lógico ganha range. Chance de drop nunca passa de 1.0, velocidade nunca é negativa. O range documenta a intenção e impede o erro antes dele acontecer.

@export_enum: dropdown em vez de string mágica

Quando a variável só aceita um conjunto fixo de opções, exportar uma String solta é pedir typo. O @export_enum vira um dropdown:

@export_enum("Melee", "Ranged", "Magic") var attack_type: String = "Melee"

No Inspector aparece uma lista com as três opções, impossível digitar "Rangd" sem querer. Com int, cada opção vira um índice (Melee é 0, Ranged é 1, Magic é 2), e dá pra fixar os valores com dois pontos:

@export_enum("Lento:10", "Normal:25", "Rapido:50") var speed_tier: int = 25

Se você já tem um enum declarado no script, melhor ainda, exporta direto:

enum EnemyState { PATROL, CHASE, ATTACK }

@export var initial_state: EnemyState = EnemyState.PATROL

O Godot monta o dropdown com os nomes do enum sozinho. É o meu formato favorito: o código compara contra EnemyState.CHASE em vez de string, e o Inspector continua amigável.

Curso de Godot
Quer fazer seu primeiro jogo na Godot?

No curso de Godot da CursoGame.Dev você vai do zero ao primeiro jogo, com um desafio prático em cada level e suporte individual no WhatsApp quando travar.

Conhecer o curso de Godot
Suporte no WhatsAppAcesso vitalícioGarantia de 7 dias

@export_group: Inspector organizado escala melhor

Um script de player de verdade exporta facilmente quinze variáveis. Sem organização, o Inspector vira uma lista plana onde ninguém acha nada. O @export_group cria seções recolhíveis:

extends CharacterBody2D

@export_group("Movimento")
@export var speed: float = 300.0
@export var acceleration: float = 1500.0
@export var friction: float = 1200.0

@export_group("Pulo")
@export var jump_velocity: float = -400.0
@export var coyote_time: float = 0.1
@export_range(1, 3) var max_jumps: int = 1

@export_group("Combate")
@export var max_health: int = 100
@export var invincibility_time: float = 1.0

Tudo que vem depois de um @export_group entra naquele grupo, até o próximo grupo aparecer. Pra subdivisões dentro de um grupo existe o @export_subgroup, e pra um título sem recolhimento, o @export_category. Na prática, @export_group resolve 95% dos casos.

Parece cosmético, e em script de cinco variáveis é mesmo. Mas o ponto do export é justamente outra pessoa (ou você daqui a três meses) abrir o Inspector e ajustar o jogo sem ler o código. Um Inspector agrupado é a interface desse trabalho.

Exportando NodePath, PackedScene e Resource

Aqui o @export deixa de ser "número editável" e vira ferramenta de arquitetura.

Node e NodePath. No Godot 4 dá pra exportar uma referência de node direto, e o Inspector deixa você arrastar o node da cena pro campo:

@export var health_bar: ProgressBar
@export var muzzle: Marker2D

func shoot() -> void:
    var bullet: Node2D = BULLET_SCENE.instantiate()
    bullet.global_position = muzzle.global_position
    get_tree().current_scene.add_child(bullet)

Isso substitui o @onready var health_bar = $UI/HUD/HealthBar com caminho longo e frágil. Se a UI for reorganizada, o caminho hardcoded quebra; a referência exportada o editor atualiza sozinho. Quando você precisa do caminho em si (pra passar adiante, por exemplo), exporta NodePath e resolve com get_node():

@export var target_path: NodePath

@onready var target: Node = get_node(target_path)

PackedScene. Exportar uma cena é o padrão de todo spawner genérico:

extends Node2D

@export var enemy_scene: PackedScene
@export_range(0.5, 10.0, 0.5) var spawn_interval: float = 2.0

func _on_spawn_timer_timeout() -> void:
    var enemy: Node2D = enemy_scene.instantiate()
    enemy.global_position = global_position
    get_tree().current_scene.add_child(enemy)

Esse spawner não sabe o que spawna. Você arrasta slime.tscn numa instância, bat.tscn em outra, e o mesmo script vira dois spawners diferentes. Sem @export, isso seria um preload() hardcoded e um script novo por tipo de inimigo.

Resource. O nível seguinte: exportar um Resource customizado e transformar dados de gameplay em arquivos:

@export var weapon_data: WeaponData

func attack() -> void:
    deal_damage(weapon_data.damage)

Onde WeaponData é uma classe que estende Resource com damage, fire_rate e o que mais a arma precisar. Cada arma vira um arquivo .tres editável no Inspector, e balancear o jogo inteiro vira editar arquivos de dados, não código. Esse padrão merece artigo próprio, e tem: Resources customizados no Godot.

Outros hints que valem conhecer de passagem: @export_file("*.json") pra caminho de arquivo, @export_multiline pra texto longo (diálogo, descrição), @export_color_no_alpha pra cor sem canal alfa, e exportar Array[PackedScene] quando o spawner precisa sortear entre várias cenas.

As variaveis exportadas ficam ainda mais poderosas quando o no reage a elas dentro do editor. Isso e feito com scripts @tool, que rodam codigo no editor da Godot.

Setter no export: quando mudar o valor precisa fazer algo

Às vezes mudar o número não basta: o node precisa reagir na hora. Você arrasta o slider do raio de visão de um inimigo e quer ver o alcance mudar na cena, não só o número no Inspector. Pra isso, o @export aceita um bloco set junto da declaração, que roda toda vez que o valor muda, inclusive quando você edita pelo Inspector:

@export var raio_visao: float = 100.0:
    set(valor):
        raio_visao = valor
        if is_inside_tree():
            queue_redraw()  # redesenha o alcance na tela

O is_inside_tree() é o detalhe que evita o erro mais comum aqui: durante o carregamento da cena, o setter pode rodar antes de o node estar pronto, e chamar coisas como queue_redraw() ou acessar filhos cedo demais estoura um erro. A checagem segura isso. Esse padrão de reagir ao valor fica ainda mais visível quando combinado com os scripts @tool, que fazem o setter rodar dentro do próprio editor.

Um hint que vale fechar a lista é o @export_flags, pra quando a variável aceita várias opções ao mesmo tempo (não uma só, como o enum). O caso clássico é camada de colisão ou um conjunto de estados:

@export_flags("Fogo", "Gelo", "Veneno", "Eletrico") var resistencias: int = 0

func resiste_a(bit: int) -> bool:
    return resistencias & bit != 0

No Inspector, isso vira uma lista de checkboxes independentes, e o valor guardado é um inteiro com os bits ligados. Um inimigo pode resistir a Fogo e Gelo ao mesmo tempo marcando as duas caixas, sem você criar quinze combinações de enum. É a forma certa de exportar "zero ou mais destas opções".

Por que isso muda o jeito de fazer jogo

O argumento que eu dei na introdução merece ser fechado. Balancear um jogo é um trabalho de iteração: muda o número, joga trinta segundos, muda de novo. Se cada ajuste exige abrir script, achar a constante e salvar, a iteração desacelera e você ajusta menos do que devia. Com tudo exportado, você roda o jogo, ajusta no Inspector da cena e testa de novo em segundos.

Tem um efeito colateral importante em equipe: designer ajusta gameplay sem encostar em código. O programador define o que é ajustável (e os limites, via range), e quem balanceia trabalha só no Inspector e nos arquivos .tres. Cada um no seu terreno, ninguém quebra o do outro.

Meu critério pra decidir o que exportar: se é um valor que eu mudaria durante o balanceamento, exporta. Se é detalhe interno de implementação (um acumulador, uma flag de estado), fica privado. Exportar tudo indiscriminadamente polui o Inspector e convida a mexer onde não devia; exportar nada engessa a iteração. O meio termo é exportar o que descreve o comportamento, não o que implementa ele.

Fechando

O @export básico cobre número, texto e booleana; @export_range impõe limites e vira slider; @export_enum troca string mágica por dropdown; @export_group mantém o Inspector legível quando o script cresce. E os exports de Node, PackedScene e Resource são onde a anotação vira arquitetura: cenas genéricas configuradas por instância e gameplay descrito em dados.

Pra fixar, pegue um script seu que tenha constantes de gameplay hardcoded e converta uma a uma em variáveis exportadas, com range e grupo. Depois rode o jogo e balanceie só pelo Inspector. A diferença na velocidade de iteração aparece na primeira sessão, e a partir daí é difícil voltar atrás.