Godot do Zero: Organização de projeto e boas práticas iniciais em GDScript

Capítulo 16

Tempo estimado de leitura: 11 minutos

+ Exercício

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): PascalCase combinando com a cena (ex.: Player.gd para Player.tscn).
  • Recursos (.tres): PascalCase ou snake_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.6

Estrutura 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.tscn

Regras 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/core ou scripts/utils.
  • Recursos (Stats, configs, curvas): em resources/, não misture com cenas.
  • Levels: separe levels/ para fases e scenes/gameplay para 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.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

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 = false

Agora, 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 export sem 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
	pass

Boas práticas com sinais

  • Prefira conectar sinais no _ready() do “ouvinte” (quem reage), não do “emissor”.
  • Nomeie handlers com padrão _on_Emissor_sinal ou _on_sinal, mas seja consistente.
  • Evite sinais “genéricos demais” (ex.: changed) quando o projeto cresce; prefira health_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.player apontando 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 var com nós próximos ou NodePath exportado 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

  1. Crie as pastas base: scenes/actors, scenes/ui, scripts/core, resources, levels.
  2. Mova cenas e scripts para as pastas correspondentes.
  3. Abra a aba FileSystem e verifique se alguma cena ficou com dependências quebradas (ícones de erro). Reatribua recursos se necessário.
  4. Rode a cena principal e valide: movimento, inimigos, UI, áudio e salvamento continuam funcionando.

Passo 2: padronizar nomes de nós e arquivos

  1. Renomeie cenas para PascalCase e scripts para o mesmo padrão.
  2. Dentro das cenas, renomeie nós para nomes descritivos e consistentes (ex.: Sprite, Hurtbox, Hitbox, Health).
  3. Evite nomes genéricos repetidos como Node2D2, Area2D3.

Passo 3: extrair o componente Health e conectar por sinais

  1. Crie scenes/components/Health.tscn e Health.gd (como no exemplo).
  2. No Player e em inimigos que recebem dano, instancie Health.tscn como filho chamado Health.
  3. Substitua lógica duplicada de vida/invencibilidade nos scripts originais por chamadas ao componente Health.
  4. Conecte sinais: died para lógica de morte; damaged para feedback (piscar, som); health_changed para HUD.

Passo 4: criar grupos e aplicar convenções

  1. Adicione o Player ao grupo player.
  2. Adicione inimigos ao grupo enemies.
  3. Adicione hurtboxes ao grupo hurtboxes e hitboxes ao grupo hitboxes (se você usa essa separação).
  4. 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

  1. Crie scripts/core/GameEvents.gd (exemplo acima).
  2. Adicione como Autoload em Project Settings > Autoload com nome GameEvents.
  3. Use GameEvents apenas 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

  1. Em scripts principais (Player, Enemy, HUD), agrupe exports com @export_group.
  2. Remova variáveis não usadas, prints de debug e sinais não conectados.
  3. 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)

ProblemaEvitePrefira
Compartilhar lógica de vida/danoDuplicar código em Player e EnemyComponente Health + sinais
UI precisa refletir estado do playerUI chamando métodos de gameplayUI ouvindo sinais (health_changed)
Identificar categorias (inimigos, coletáveis)Checar nome do nó / caminhosGrupos (enemies, pickups)
Comunicação entre cenas diferentesReferências globais a nós instanciadosAutoload de eventos/serviços + sinais
Configurar parâmetros no InspectorExports soltos e sem unidade@export_group + nomes/unidades consistentes

Agora responda o exercício sobre o conteúdo:

Ao refatorar um projeto Godot para ficar mais fácil de manter, qual abordagem melhor reduz duplicação e acoplamento ao lidar com vida/dano e atualização do HUD?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Um componente Health reutilizável evita duplicar lógica em Player/Enemy e, ao emitir sinais, permite que o HUD apenas ouça eventos (ex.: health_changed), reduzindo dependências diretas e acoplamento.

Próximo capitúlo

Godot do Zero: Polimento do primeiro jogo 2D e empacotamento do build

Arrow Right Icon
Capa do Ebook gratuito Godot do Zero: Criando seu Primeiro Jogo 2D com GDScript
94%

Godot do Zero: Criando seu Primeiro Jogo 2D com GDScript

Novo curso

17 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.