Custom Resources no Godot: Dados de Jogo em .tres e Design Data-Driven

Aprenda a criar custom Resources no Godot e organizar itens, fichas e dados de jogo em arquivos .tres. Tutorial prático de resource Godot com GDScript.
Custom Resources no Godot: Dados de Jogo em .tres e Design Data-Driven
Se cada item do seu jogo é uma cena com script, ou pior, um dicionário gigante num autoload, você está carregando peso à toa. O custom Resource no Godot existe exatamente pra isso: separar dado de comportamento. A espada deixa de ser uma cena de 40 nodes e vira um arquivo .tres com nome, dano, ícone e preço, que qualquer sistema do jogo lê sem instanciar nada.
Esse é provavelmente o recurso mais subestimado da engine. Todo mundo aprende cena, node e sinal na primeira semana, e passa meses ignorando que Texture2D, AudioStream e até o próprio script GDScript já são Resources. Quando você cria os seus, ganha de graça: edição pelo Inspector, serialização em arquivo, type safety no código e um fluxo onde game designer balanceia o jogo sem encostar em código.
Nesse tutorial eu monto um sistema de itens completo com custom Resources, mostro as pegadinhas de compartilhamento por referência (a fonte de bug mais traiçoeira do sistema) e explico quando vale usar .tres em vez de JSON. Tudo em GDScript, Godot 4.x.
O que é um Resource no Godot
No Godot existem duas grandes famílias de objetos: Node e Resource. Node é coisa que vive na árvore da cena, processa frame a frame e tem posição no mundo. Resource é um contêiner de dados: não entra na árvore, não tem _process, não ocupa lugar na cena. Ele só guarda informação e oferece dois superpoderes:
- Serialização nativa. Um Resource salva e carrega de arquivo (
.tresem texto,.resem binário) sem você escrever uma linha de parser. - Cache automático. Quando dois pontos do código carregam o mesmo arquivo de Resource, a engine entrega a mesma instância na memória. Cem inimigos usando a mesma textura compartilham um único objeto.
Você já usa isso o tempo todo: toda textura, som, fonte, material e animação do projeto é Resource. A diferença é que a engine permite criar os seus, com os campos que o seu jogo precisa.
A regra prática pra decidir o que vira Resource: se a informação descreve "o que algo é", e não "o que algo faz na cena", é dado, e dado bom mora em Resource. Ficha de item, atributos de inimigo, definição de habilidade, tabela de drop, configuração de fase, diálogo. Tudo isso é candidato.
Criando um custom Resource: a ficha de item
Um custom Resource é um script que estende Resource. O class_name registra o tipo na engine, o que faz ele aparecer nos menus do editor:
class_name ItemData
extends Resource
@export var nome: String = ""
@export_multiline var descricao: String = ""
@export var icone: Texture2D
@export var preco: int = 0
@export var empilhavel: bool = true
@export var quantidade_maxima: int = 99
Só isso. Repare que os campos usam @export, igual num node. É o que permite editar os valores pelo Inspector.
Criando os arquivos .tres no editor
Com o script salvo, vá no painel FileSystem, clique com o botão direito na pasta onde quer guardar os itens e escolha Create New > Resource. Na busca, digite ItemData: o seu tipo aparece na lista junto com os nativos. Crie, dê um nome tipo pocao_vida.tres, e o Inspector abre com os campos do seu script prontos pra preencher.
Esse é o momento em que a ficha cai: cada .tres é uma instância salva em disco. pocao_vida.tres, espada_ferro.tres, escudo_madeira.tres, cada um com seus valores, todos editáveis sem abrir código. Você acabou de criar um mini banco de dados versionável no Git, porque .tres é texto puro e gera diff legível em code review.
Resource não é só dado: pode ter lógica
Resource aceita métodos normalmente. O bom uso é lógica que opera sobre os próprios dados, tipo formatação e cálculo derivado:
class_name ItemData
extends Resource
@export var nome: String = ""
@export var preco: int = 0
@export var raridade: Raridade = Raridade.COMUM
enum Raridade { COMUM, RARO, EPICO, LENDARIO }
func get_preco_venda() -> int:
# Lojas compram pela metade do preço.
return preco / 2
func get_nome_formatado() -> String:
return "%s [%s]" % [nome, Raridade.keys()[raridade]]
O que não deve ir aqui é lógica de cena: Resource não sabe o que é posição, colisão nem input. Se o método precisa de um node pra funcionar, ele pertence ao node.
Usando o Resource no jogo
Pra consumir os dados existem dois caminhos, e os dois se complementam.
Pelo Inspector, exportando o tipo no script de um node. Um item dropado no chão, por exemplo, é uma cena genérica que recebe a ficha:
extends Area2D
@export var dados: ItemData
@onready var sprite: Sprite2D = $Sprite2D
func _ready() -> void:
sprite.texture = dados.icone
func _on_body_entered(body: Node2D) -> void:
if body.is_in_group("player"):
body.inventario.adicionar(dados)
queue_free()
Uma única cena item_chao.tscn serve pra todo item do jogo. Arrastou espada_ferro.tres pro campo dados no Inspector, virou uma espada no chão. Arrastou a poção, virou poção. Zero duplicação de cena.
Por código, carregando o arquivo direto:
var pocao: ItemData = preload("res://itens/pocao_vida.tres")
# Ou em runtime, quando o caminho é dinâmico:
var item: ItemData = load("res://itens/%s.tres" % id_do_item)
E o inventário que recebe esses itens não precisa saber nada sobre cenas:
class_name Inventario
extends Node
var itens: Array[ItemData] = []
func adicionar(item: ItemData) -> void:
itens.append(item)
func valor_total() -> int:
var total := 0
for item in itens:
total += item.preco
return total
Percebe o desacoplamento? O inventário opera sobre dados puros. A UI que desenha os slots lê item.icone e item.nome. O sistema de loja lê item.get_preco_venda(). Nenhum deles conhece a cena do item no chão, e a cena não conhece nenhum deles.
Data-driven design: Resources aninhados e fichas compostas
Aqui o sistema mostra a força de verdade: um campo @export pode ser outro custom Resource, e arrays de Resources funcionam direto no Inspector.
Uma ficha de inimigo com tabela de drop fica assim:
class_name DropEntry
extends Resource
@export var item: ItemData
@export_range(0.0, 1.0) var chance: float = 0.5
class_name InimigoData
extends Resource
@export var nome: String = ""
@export var vida_maxima: int = 10
@export var dano: int = 1
@export var velocidade: float = 80.0
@export var sprite_frames: SpriteFrames
@export var drops: Array[DropEntry] = []
No Inspector, o campo drops vira uma lista expansível: você adiciona entradas, e em cada uma arrasta um .tres de item e ajusta a chance no slider. O goblin dropa poção com 30% de chance e moeda com 80%, e isso está descrito num arquivo de dados, não enterrado num match dentro do script de morte.
A cena do inimigo vira casca genérica que lê a ficha:
extends CharacterBody2D
@export var dados: InimigoData
var vida: int
func _ready() -> void:
vida = dados.vida_maxima
$AnimatedSprite2D.sprite_frames = dados.sprite_frames
func morrer() -> void:
for entrada in dados.drops:
if randf() < entrada.chance:
spawnar_item(entrada.item)
queue_free()
Esse é o coração do design data-driven: conteúdo novo sem código novo. Quer dez inimigos? São dez arquivos .tres e uma cena. Balancear o jogo inteiro vira uma tarde editando valores no Inspector, e cada mudança aparece no Git como diff de texto. Em equipe, isso significa que o designer trabalha nos .tres enquanto o programador trabalha nos sistemas, sem conflito de merge em cena.
As pegadinhas que todo mundo cai
Custom Resource tem duas armadilhas clássicas. Conhecer as duas economiza horas de debug.
Resources são compartilhados por referência
Lembra do cache automático? Ele tem um efeito colateral: se três slots do inventário guardam a mesma poção carregada de pocao_vida.tres, os três apontam pro mesmo objeto. Se o seu código fizer item.quantidade -= 1 num slot, os três mudam juntos. Pior: a mudança vive enquanto o jogo roda, então a "ficha" que era pra ser imutável foi corrompida pra todo mundo que carregar ela depois.
A solução é separar definição de instância. A ficha (ItemData) guarda o que nunca muda. O estado de runtime vive fora dela:
class_name ItemStack
extends RefCounted
var dados: ItemData # referência compartilhada, só leitura
var quantidade: int # estado próprio deste stack
func _init(p_dados: ItemData, p_quantidade: int = 1) -> void:
dados = p_dados
quantidade = p_quantidade
Quando você realmente precisa de uma cópia independente do Resource (um item que pode ser encantado e modificado, por exemplo), use duplicate():
var copia: ItemData = item_original.duplicate()
# duplicate(true) copia também os sub-Resources aninhados (cópia profunda).
Existe ainda a propriedade resource_local_to_scene: marcada, cada instância da cena recebe sua própria cópia do Resource automaticamente. Útil em casos pontuais, mas eu prefiro o padrão definição mais estado, porque ele deixa explícito no código o que é compartilhado e o que não é.
Carregar .tres de fora do projeto é risco de segurança
Arquivo .tres pode referenciar e embutir scripts. Dentro do seu projeto, ótimo. Mas se o seu jogo aceita mods ou baixa conteúdo da internet, carregar um .tres de terceiros com load() pode executar código que veio junto no arquivo. Pra conteúdo externo, prefira formatos inertes como JSON e valide tudo na entrada. Resource é pra dado que você controla.
Resource vs JSON: quando usar cada um
A pergunta aparece em todo projeto, então vale a comparação honesta.
Use custom Resource quando o dado é criado por quem tem o editor do Godot aberto: itens, inimigos, habilidades, configuração de fase. Você ganha Inspector com type safety, arrastar e soltar de texturas e sons direto no campo, referências entre arquivos resolvidas pela engine e autocomplete no código. Com JSON, cada um desses você implementa na mão, incluindo o parser e a validação de campo faltando.
Use JSON quando o dado atravessa a fronteira do jogo: save game (em conjunto com validação), comunicação com servidor, conteúdo de mod, ou quando uma ferramenta externa (planilha, CMS, script Python) gera os dados. JSON é universal e inerte; .tres só o Godot lê.
O erro comum é escolher JSON "porque é o que todo mundo usa" e reconstruir, em código manual, tudo que o Inspector daria de graça. Pra dados internos do jogo, Resource ganha quase sempre.
Fechando
Custom Resource é daquelas ferramentas que mudam como você estrutura projeto: dado separado de comportamento, conteúdo em arquivos .tres versionáveis, cenas genéricas que leem fichas. O custo de entrada é baixo (um script com class_name e @export) e o retorno cresce com o tamanho do jogo.
Pra fixar, pega um projeto seu que tenha qualquer coisa duplicada (três cenas de inimigo quase iguais, um dicionário de itens num autoload) e refatora pra Resources. Começa pela ficha mais simples, cria dois ou três .tres, faz a cena ler os dados. No dia em que você precisar adicionar o vigésimo item e levar dois minutos nisso, o padrão se paga.


