Por que organização e boas práticas importam no seu projeto Godot
Quando o projeto cresce (mais fases, inimigos, UI, áudio, salvamento), o maior risco deixa de ser “não saber fazer” e passa a ser “não conseguir manter”. Organização reduz bugs, acelera refatorações e facilita reutilizar cenas e scripts. Neste capítulo, você vai consolidar padrões práticos: convenções de nomes, estrutura de pastas, cenas reutilizáveis, scripts curtos e coesos, sinais e grupos para comunicação e categorização, e uso responsável de autoloads (Singletons). No final, você fará uma refatoração guiada do projeto construído até aqui.
Convenções de nomes (padrões simples que evitam confusão)
Arquivos e pastas
- Pastas:
snake_case(ex.:scenes,ui,audio,levels). - Cenas (
.tscn):PascalCase(ex.:Player.tscn,EnemyPatrol.tscn,Hud.tscn). - Scripts (
.gd):PascalCasecombinando com a cena (ex.:Player.gdparaPlayer.tscn). - Recursos (
.tres):PascalCaseousnake_case, mas seja consistente (ex.:PlayerStats.tres,audio_bus_layout.tres).
Nós e variáveis
- Nós na cena:
PascalCase(ex.:Hitbox,Hurtbox,Sprite,AnimationPlayer). - Variáveis:
snake_case(ex.:move_speed,invincibility_time). - Constantes:
UPPER_SNAKE_CASE(ex.:MAX_HEALTH). - Sinais: verbos no passado ou ação (ex.:
damaged,died,requested_pause). - Grupos:
snake_case(ex.:player,enemies,damageables,pickups).
Organizando exports e categorias
Padronize nomes e categorias para o Inspector ficar legível. Em Godot 4, você pode agrupar exports com @export_group e @export_subgroup.
extends CharacterBody2D
@export_group("Movement")
@export var move_speed: float = 180.0
@export var acceleration: float = 1200.0
@export_group("Combat")
@export var max_health: int = 5
@export var invincibility_time: float = 0.6Estrutura de pastas recomendada (pensada para crescer)
Uma estrutura simples e escalável evita “assets soltos” e reduz caminhos quebrados. Exemplo:
res://
scenes/
actors/
player/
Player.tscn
Player.gd
enemies/
EnemyPatrol.tscn
EnemyPatrol.gd
gameplay/
Level.tscn
Spawner.tscn
ui/
Hud.tscn
PauseMenu.tscn
scripts/
core/
GameEvents.gd
SceneLoader.gd
utils/
Math.gd
autoload/
GameState.gd
resources/
stats/
PlayerStats.tres
audio/
sfx/
music/
art/
sprites/
tiles/
levels/
Level01.tscn
Level02.tscnRegras práticas para manter a estrutura saudável
- Scripts “colados” na cena: se o script é específico daquela cena, mantenha dentro da pasta da cena (ex.:
scenes/actors/player/). - Scripts reutilizáveis: coloque em
scripts/coreouscripts/utils. - Recursos (Stats, configs, curvas): em
resources/, não misture com cenas. - Levels: separe
levels/para fases escenes/gameplaypara cenas genéricas (spawners, triggers, etc.).
Cenas reutilizáveis: componha, não copie
Copiar e colar nós entre cenas cria divergência: você corrige um bug em um lugar e esquece do outro. Prefira cenas pequenas e reutilizáveis, instanciadas onde necessário.
Padrões úteis de reutilização
- Componente de vida (ex.:
Health.tscn): um nó que gerencia vida, dano e invencibilidade e emite sinais. - Hitbox/Hurtbox como cenas: padroniza colisões e filtros.
- Detector (Area2D) reutilizável para visão/alcance.
- UI como cenas independentes: HUD, barra de vida, contador, etc.
Exemplo: transformar “vida” em componente
Crie uma cena Health.tscn com nó raiz Node (ou Node2D se precisar) e script Health.gd. Ela não deve conhecer Player nem Enemy: apenas gerenciar números e eventos.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
extends Node
class_name Health
signal health_changed(current: int, max: int)
signal damaged(amount: int)
signal died
@export var max_health: int = 5
@export var invincibility_time: float = 0.6
var current_health: int
var _invincible := false
func _ready() -> void:
current_health = max_health
health_changed.emit(current_health, max_health)
func apply_damage(amount: int) -> void:
if amount <= 0:
return
if _invincible:
return
current_health = max(current_health - amount, 0)
damaged.emit(amount)
health_changed.emit(current_health, max_health)
if current_health == 0:
died.emit()
return
_start_invincibility()
func heal(amount: int) -> void:
if amount <= 0:
return
current_health = min(current_health + amount, max_health)
health_changed.emit(current_health, max_health)
func _start_invincibility() -> void:
_invincible = true
await get_tree().create_timer(invincibility_time).timeout
_invincible = falseAgora, no Player/Enemy, você instancia Health como filho e conecta sinais para reagir (animação, knockback, UI), sem duplicar lógica.
Scripts curtos e coesos (uma responsabilidade por script)
Um script “Deus” (movimento + combate + UI + áudio + salvamento) vira um ponto único de falha. Use coesão: cada script deve ter um motivo claro para mudar.
Checklist de coesão
- O script tem mais de ~200–300 linhas? Considere separar.
- Ele mexe em UI e gameplay ao mesmo tempo? Separe.
- Ele acessa muitos nós por caminho (
$"../../...")? Provável acoplamento excessivo. - Ele tem muitos
exportsem organização? Agrupe e padronize.
Padrão prático: “Orquestrador + Componentes”
- Player.gd: orquestra (lê input, chama componentes, decide estados).
- Health.gd: vida/dano.
- Weapon.gd ou Attack.gd: ataque e cooldown.
- PlayerAnimator.gd: traduz estado em animação.
Você não precisa criar todos esses componentes agora; comece pelos que já estão “misturados” e doendo para manter.
Sinais para comunicação (reduzindo dependências diretas)
Sinais permitem que um nó anuncie eventos sem conhecer quem vai reagir. Isso reduz acoplamento e ajuda a evitar dependências circulares (A depende de B e B depende de A).
Exemplo: inimigo não atualiza HUD diretamente
Em vez de o inimigo chamar algo como hud.update_health(), ele apenas aplica dano no componente Health do player, e o HUD escuta o sinal health_changed.
# Hud.gd (exemplo)
extends Control
@export var player_path: NodePath
@onready var _player: Node = get_node(player_path)
func _ready() -> void:
var health: Health = _player.get_node("Health")
health.health_changed.connect(_on_health_changed)
func _on_health_changed(current: int, max: int) -> void:
# Atualize barra/label aqui
passBoas práticas com sinais
- Prefira conectar sinais no
_ready()do “ouvinte” (quem reage), não do “emissor”. - Nomeie handlers com padrão
_on_Emissor_sinalou_on_sinal, mas seja consistente. - Evite sinais “genéricos demais” (ex.:
changed) quando o projeto cresce; prefirahealth_changed,ammo_changed.
Grupos para categorizar entidades (e reduzir ifs)
Grupos permitem identificar categorias sem depender de classe específica. Isso é útil para colisões, detecção, seleção de alvos e lógica de “quem pode receber dano”.
Definindo grupos
- No editor: selecione o nó > aba Node > Groups.
- Via código (em
_ready()):add_to_group("enemies").
Exemplo: aplicar dano em qualquer “damageable”
# Em um hitbox/ataque
func _on_area_entered(area: Area2D) -> void:
if area.is_in_group("hurtboxes"):
var health := area.get_node_or_null("../Health")
if health:
health.apply_damage(1)O ponto aqui é: o ataque não precisa saber se é Player ou Enemy, apenas que existe uma convenção (grupo + componente).
Autoloads (Singletons): use com responsabilidade
Autoload é ótimo para dados globais e serviços (estado do jogo, eventos globais, carregamento de cenas). O risco é transformar tudo em global e criar dependências invisíveis.
Quando faz sentido usar Autoload
- GameState: dados persistentes da sessão (vidas, fase atual, flags).
- GameEvents: “barramento” de sinais globais (ex.:
pause_requested,player_died). - SceneLoader: transições e carregamento centralizado.
Quando evitar Autoload
- Para acessar nós de cena diretamente (ex.:
Game.playerapontando para um nó instanciado). - Para lógica específica de fase ou inimigo.
- Para “atalhar” comunicação que deveria ser local (pai-filho) ou via sinais.
Padrão recomendado: Autoload como serviço + sinais
Crie um autoload GameEvents.gd que só emite sinais. Assim, você reduz referências diretas entre sistemas.
extends Node
signal pause_toggled(paused: bool)
signal player_died
signal checkpoint_reached(id: String)
func toggle_pause(current: bool) -> void:
pause_toggled.emit(!current)Qualquer cena pode ouvir GameEvents.pause_toggled sem precisar conhecer quem pediu a pausa.
Checklist de revisão do projeto (antes de adicionar mais features)
1) Evitar caminhos quebrados e referências frágeis
- Procure por
$"../../"e caminhos longos; prefira@onready varcom nós próximos ouNodePathexportado quando necessário. - Evite depender da ordem de filhos (ex.:
get_child(0)). - Ao mover arquivos, verifique cenas com ícones de erro e reatribua recursos.
2) Remover dependências circulares
- Se Player chama Enemy e Enemy chama Player diretamente, substitua por sinais ou por um componente neutro (ex.:
Health). - Evite que UI conheça gameplay profundamente (UI deve reagir a sinais/dados, não controlar física/combate).
3) Separar lógica de UI e gameplay
- Gameplay em nós do mundo (CharacterBody2D, Node2D).
- UI em
Control/CanvasLayer. - Conexão entre eles via sinais (ex.:
health_changed) ou leitura de estado (ex.:GameState).
4) Padronizar parâmetros exportados
- Agrupe exports por categoria (
@export_group). - Use unidades claras: segundos para tempo, pixels/segundo para velocidade.
- Evite duplicar o mesmo parâmetro em vários scripts (ex.: dano base). Centralize em recurso (
.tres) quando fizer sentido.
5) Cenas reutilizáveis e scripts coesos
- Se você copiou um conjunto de nós 3+ vezes, transforme em cena reutilizável.
- Se um script tem “muitos motivos para mudar”, extraia componentes.
Refatoração guiada do projeto construído até aqui
O objetivo é reorganizar sem quebrar o jogo. Faça em pequenos passos e teste a cada etapa.
Passo 1: criar a estrutura de pastas e mover arquivos com segurança
- Crie as pastas base:
scenes/actors,scenes/ui,scripts/core,resources,levels. - Mova cenas e scripts para as pastas correspondentes.
- Abra a aba FileSystem e verifique se alguma cena ficou com dependências quebradas (ícones de erro). Reatribua recursos se necessário.
- Rode a cena principal e valide: movimento, inimigos, UI, áudio e salvamento continuam funcionando.
Passo 2: padronizar nomes de nós e arquivos
- Renomeie cenas para
PascalCasee scripts para o mesmo padrão. - Dentro das cenas, renomeie nós para nomes descritivos e consistentes (ex.:
Sprite,Hurtbox,Hitbox,Health). - Evite nomes genéricos repetidos como
Node2D2,Area2D3.
Passo 3: extrair o componente Health e conectar por sinais
- Crie
scenes/components/Health.tscneHealth.gd(como no exemplo). - No Player e em inimigos que recebem dano, instancie
Health.tscncomo filho chamadoHealth. - Substitua lógica duplicada de vida/invencibilidade nos scripts originais por chamadas ao componente
Health. - Conecte sinais:
diedpara lógica de morte;damagedpara feedback (piscar, som);health_changedpara HUD.
Passo 4: criar grupos e aplicar convenções
- Adicione o Player ao grupo
player. - Adicione inimigos ao grupo
enemies. - Adicione hurtboxes ao grupo
hurtboxese hitboxes ao grupohitboxes(se você usa essa separação). - Atualize scripts que usavam checagens por nome/classe para checagens por grupo quando fizer sentido.
Passo 5: reduzir caminhos frágeis com NodePath exportado (onde necessário)
Quando um nó precisa referenciar outro que não é filho direto, prefira expor um NodePath no Inspector. Isso facilita reorganizar a árvore sem quebrar código.
extends Node
@export var target_path: NodePath
@onready var target: Node = get_node(target_path)- Use isso com moderação: se muitos scripts precisam de muitos caminhos, talvez falte uma camada de composição (um nó “orquestrador” que injeta referências) ou sinais.
Passo 6: introduzir um Autoload mínimo (se ainda não existir) para eventos globais
- Crie
scripts/core/GameEvents.gd(exemplo acima). - Adicione como Autoload em Project Settings > Autoload com nome
GameEvents. - Use
GameEventsapenas para eventos que atravessam cenas (pausa, troca de fase, morte do player), evitando guardar referências a nós de gameplay.
Passo 7: padronizar exports e limpar scripts
- Em scripts principais (Player, Enemy, HUD), agrupe exports com
@export_group. - Remova variáveis não usadas, prints de debug e sinais não conectados.
- Se um script ficou grande, extraia uma parte para um componente (ex.: animação/feedback).
Tabela rápida: decisões de arquitetura (para consultar durante o desenvolvimento)
| Problema | Evite | Prefira |
|---|---|---|
| Compartilhar lógica de vida/dano | Duplicar código em Player e Enemy | Componente Health + sinais |
| UI precisa refletir estado do player | UI chamando métodos de gameplay | UI ouvindo sinais (health_changed) |
| Identificar categorias (inimigos, coletáveis) | Checar nome do nó / caminhos | Grupos (enemies, pickups) |
| Comunicação entre cenas diferentes | Referências globais a nós instanciados | Autoload de eventos/serviços + sinais |
| Configurar parâmetros no Inspector | Exports soltos e sem unidade | @export_group + nomes/unidades consistentes |