A maioria dos tutoriais de Godot mostra como construir um jogo pequeno. Muito poucos mostram o que acontece quando o projeto cresce: quando você tem mais do que um punhado de cenas, quando seus scripts começam a se referenciar de formas que você não planejou, quando uma mudança em um sistema quebra algo em um sistema que você esqueceu que estava conectado. Tenho construído um RPG tático em Godot 4 com C# há algum tempo, e o projeto cresceu. Os números que uso aqui são um retrato tirado em fevereiro de 2026: 123+ scripts, 334+ testes entre as suítes de unidade, integração e performance, e 28 shaders. Este post é sobre as decisões arquiteturais que mantiveram o projeto gerenciável nessa escala.
Quero ser direto sobre o contexto: sou um desenvolvedor solo. Os padrões que uso são otimizados para uma pessoa que precisa entender o codebase inteiro e mantê-lo ao longo de meses de desenvolvimento intermitente. Alguns desses padrões seriam diferentes em um contexto de equipe.
Estrutura de Pastas: Reflita o Domínio, Não o Motor
A estrutura padrão de projeto Godot coloca tudo em um diretório plano ou levemente organizado. Para projetos pequenos, funciona. Para um projeto em escala, você precisa de uma estrutura de pastas que torne a navegação intuitiva, onde você possa encontrar qualquer script sem buscar porque sua localização decorre do que ele faz.
Minha organização de nível superior reflete os domínios do jogo, não as categorias técnicas do Godot. Não tenho uma pasta chamada Scripts no nível superior. Tenho pastas chamadas Combat, Units, Grid, Skills, AI, UI, Campaign, Audio e Shaders. Dentro de cada pasta de domínio, tenho sub-organização por tipo técnico (models, services, events, components), mas o eixo primário de organização é o domínio do jogo.
- src/Combat/. Cálculo de dano, resolução de ataque, aplicação de efeitos de status, log de combate
- src/Units/. Dados de unidade, sistemas de stats, máquinas de estado de unidade, fábrica de unidades
- src/Grid/. Representação do mapa em grid, pathfinding, cálculo de alcance de movimento, consultas de tiles
- src/Skills/. Definições de habilidades como Resources, pipeline de execução de habilidades, lógica de targeting
- src/AI/. Árvores de decisão inimigas, avaliação de ameaças, seleção de ações
- src/UI/. Todos os componentes de UI, organizados por tela ou painel
- src/Campaign/. Sequenciamento de batalhas, estado da campanha, save/load
- assets/resources/. Todos os arquivos Resource .tres, organizados espelhando src/
- tests/. Espelha a estrutura de src/ exatamente para fácil descoberta de testes
O diretório tests/ espelhando src/ é intencional. Quando abro CombatCalculator.cs, posso navegar imediatamente para tests/Combat/CombatCalculatorTests.cs sem pensar sobre onde o arquivo de teste pode estar. A previsibilidade da estrutura reduz overhead cognitivo ao trocar contexto entre implementação e testes.
Autoload vs Dependência: Saber Quando Usar Cada
O sistema Autoload do Godot é efetivamente singletons globais. Você registra uma cena ou script como Autoload e ele fica acessível de qualquer lugar do jogo sem nenhum gerenciamento explícito de referência. Isso é poderoso e perigoso em igual medida.
Minha regra é que Autoloads são para preocupações de nível de aplicação que genuinamente abrangem todo o ciclo de vida do jogo: o gerenciador de áudio, o sistema de save, o gerenciador de input, o event bus. São coisas que todo sistema pode legitimamente precisar referenciar, e torná-las globalmente acessíveis é apropriado porque representam infraestrutura transversal, não lógica de domínio.
Serviços específicos de domínio não são Autoloads. O CombatResolver não precisa ser globalmente acessível: apenas sistemas envolvidos em combate precisam de uma referência a ele. Injeto referências a esses serviços através de composição de cena: um node BattleScene segura referências ao CombatResolver, MovementController, TurnManager e SkillExecutor, e passa essas referências para nodes filhos que precisam delas. Isso mantém dependências explícitas e testáveis.
// BattleScene.cs - Composition root for battle context
public partial class BattleScene : Node
{
[Export] private BattleConfiguration _config;
// Services constructed and owned here
private CombatResolver _combatResolver;
private MovementController _movementController;
private TurnManager _turnManager;
private SkillExecutor _skillExecutor;
public override void _Ready()
{
// Build the service graph
var gridQuery = new GridQueryService(GetNode<GridMap>("GridLayer"));
var pathfinder = new AStarPathfinder(gridQuery);
var movementRange = new MovementRangeCalculator(gridQuery);
_movementController = new MovementController(pathfinder, movementRange, gridQuery);
_combatResolver = new CombatResolver(_config.CombatRules);
_skillExecutor = new SkillExecutor(_combatResolver, gridQuery);
_turnManager = new TurnManager();
// Inject into child nodes that need them
GetNode<UnitCommandController>("UnitCommandController")
.Initialize(_movementController, _skillExecutor, _turnManager);
GetNode<EnemyAIController>("EnemyAIController")
.Initialize(_movementController, _skillExecutor, _turnManager, gridQuery);
}
}O Padrão Signal Bus
O sistema de sinais integrado do Godot conecta sinais de um emissor específico a um receptor específico. Funciona bem para relações diretas pai-filho, mas cria acoplamento forte quando dois sistemas que não têm motivo para conhecer um ao outro precisam se comunicar, o que acontece frequentemente em um jogo com muitos sistemas interagindo.
O padrão Signal Bus resolve isso com um único Autoload que atua como message broker. Sistemas emitem eventos para o bus; outros sistemas se inscrevem em eventos no bus. Nenhum sistema tem referência direta ao outro. Isso é efetivamente um event bus ou sistema pub/sub, território familiar para quem trabalhou com serviços do Angular ou Redux.
// GameEvents.cs - Autoload signal bus
public partial class GameEvents : Node
{
public static GameEvents Instance { get; private set; }
// Combat events
[Signal] public delegate void UnitDamagedEventHandler(Unit target, DamageResult result);
[Signal] public delegate void UnitDefeatedEventHandler(Unit unit);
[Signal] public delegate void StatusEffectAppliedEventHandler(Unit target, StatusEffect effect);
[Signal] public delegate void StatusEffectRemovedEventHandler(Unit target, StatusEffect effect);
// Movement events
[Signal] public delegate void UnitMovedEventHandler(Unit unit, Vector2I fromCell, Vector2I toCell);
[Signal] public delegate void UnitTeleportedEventHandler(Unit unit, Vector2I toCell);
// Turn events
[Signal] public delegate void TurnStartedEventHandler(Unit activeUnit);
[Signal] public delegate void TurnEndedEventHandler(Unit unit);
[Signal] public delegate void BattleEndedEventHandler(BattleOutcome outcome);
public override void _Ready()
{
Instance = this;
}
}
// Usage - emitting
GameEvents.Instance.EmitSignal(GameEvents.SignalName.UnitDamaged, unit, damageResult);
// Usage - subscribing
GameEvents.Instance.UnitDamaged += OnUnitDamaged;Uma restrição da qual o exemplo acima depende: parâmetros de sinal em C# no Godot precisam ser compatíveis com Variant. Tipos nativos, structs do Godot como Vector2I e qualquer coisa que derive de GodotObject ou Resource funcionam. Uma classe C# comum não funciona, e o source generator rejeita a declaração [Signal] de imediato. Tipos de payload como DamageResult ou StatusEffect ou derivam de GodotObject ou Resource, ou você os achata em primitivos antes de emitir.
O Signal Bus tem um benefício significativo para testes: nos testes, posso me conectar ao bus para verificar que um sistema emitiu o evento esperado, sem precisar ter uma scene tree completa montada. Também posso injetar um test double do bus para verificar que sinais específicos foram emitidos com parâmetros específicos.
Configuração Baseada em Resource: O Padrão Data-Driven
A decisão arquitetural de maior impacto que tomei no início do projeto foi me comprometer com configuração baseada em Resource para todo conteúdo do jogo. Cada habilidade, cada template de unidade, cada tipo de terreno, cada item e cada configuração de encontro é um Godot Resource: um objeto de dados serializável armazenado como arquivo .tres e editável no inspector do Godot.
Isso significa que quase nunca hardcodo dados do jogo em scripts. Os stats base de um guerreiro não são definidos em uma classe C#: são definidos em um arquivo UnitDefinition.tres. A fórmula de dano de uma bola de fogo, alcance, área de efeito e efeitos visuais são definidos em um arquivo AbilityDefinition.tres. Quando quero balancear o jogo, abro o arquivo de resource no inspector e mudo os valores. Quando quero adicionar uma nova habilidade, duplico um resource AbilityDefinition existente e modifico as propriedades. Sem mudanças de código.
// AbilityDefinition.cs - A Resource subclass for ability configuration
[GlobalClass]
public partial class AbilityDefinition : Resource
{
[Export] public string AbilityId { get; set; } = string.Empty;
[Export] public string DisplayName { get; set; } = string.Empty;
[Export(PropertyHint.MultilineText)] public string Description { get; set; } = string.Empty;
[ExportGroup("Cost")]
[Export] public int ActionPointCost { get; set; } = 1;
[Export] public int ManaCost { get; set; } = 0;
[ExportGroup("Targeting")]
[Export] public TargetingType TargetingType { get; set; } = TargetingType.SingleEnemy;
[Export] public int Range { get; set; } = 1;
[Export] public int AreaOfEffect { get; set; } = 0;
[ExportGroup("Effects")]
[Export] public AbilityEffect[] Effects { get; set; } = Array.Empty<AbilityEffect>();
[ExportGroup("Visual")]
[Export] public PackedScene AnimationScene { get; set; }
[Export] public AudioStream SoundEffect { get; set; }
}Composição de Cena vs Herança: Uma Lição Conquistada a Duras Penas
No início do projeto, me apoiei fortemente em herança de cena. O Godot permite criar uma cena base e herdar dela para criar variações. Tinha uma cena BaseUnit, e então WarriorUnit, MageUnit e ArcherUnit herdavam dela. Parecia limpo no início. Virou um problema de manutenção rapidamente.
O problema com hierarquias de cena pesadas em herança é que mudanças na cena base propagam para todos os filhos de formas que nem sempre são previsíveis. Adicionar um novo node à cena base reestrutura a hierarquia de cada cena herdeira. Quando cenas herdeiras sobrescreveram propriedades, essas sobrescritas podem conflitar com mudanças da cena base de formas sutis. Debugar um comportamento que existe em cinco cenas herdeiras mas não na cena base é doloroso.
Refatorei para composição: todos os tipos de unidade usam a mesma cena base Unit, e seu comportamento específico é definido inteiramente pelo seu resource UnitDefinition e por nodes de componente adicionados à scene tree para capacidades específicas. Uma unidade que pode voar tem um FlyingMovementComponent acoplado. Uma unidade que tem um trigger de habilidade passiva tem um PassiveAbilityComponent acoplado. A cena base não é subclasseada. É configurada.
Esse é o mesmo princípio de design baseado em componentes em engines como Unity, e é o princípio certo. Composição sobre herança não é apenas um bom conselho para software orientado a objetos. É especialmente importante em desenvolvimento de jogos baseado em scene graph.
Namespaces C# em Godot: Não Pule Isso
Projetos Godot em C# não impõe namespaces por padrão. No início do projeto, escrevi classes sem namespaces, e quando tinha 30-40 scripts, já tinha problemas de colisão de nomes. Uma classe Unit, uma classe UnitManager, uma classe Unit resource: sem namespaces, precisavam de nomes longos esquisitos ou colidiam.
Refatorei tudo para usar namespaces que espelham a estrutura de pastas. Todas as classes em src/Combat/ estão no namespace TacticalRPG.Combat. Todas as classes em src/Grid/ estão em TacticalRPG.Grid. Essa é prática padrão em C# mas requer setup intencional em Godot porque o motor não impõe.
Uma ressalva: namespaces param no compilador C#. O Godot registra um tipo [GlobalClass] pelo nome curto da classe, não pelo nome com namespace, então classes Resource globais ainda precisam de nomes únicos em todo o projeto.
Mantendo 334+ Testes Sob Controle
334+ testes entre as suítes de unidade, integração e performance parece muito para um projeto de jogo, e é incomum no espaço indie. Mas no nível de complexidade de um RPG tático, é genuinamente necessário. Aqui está como organizo e mantenho sem que a suíte de testes se torne um fardo.
- Testes são organizados pelo domínio que testam, em uma árvore tests/ que espelha src/, não em uma única pasta plana. Encontrar testes para um sistema específico é imediato.
- Nomeação de classe de teste: [NomeDoSistema]Tests.cs. Nomeação de método: [NomeDoMetodo]_[Cenario]_[ResultadoEsperado]. Isso torna mensagens de falha de teste auto-documentadas.
- Testes são marcados por categoria: Unit, Integration, Performance. Rodo a categoria Unit de novo pelo test runner do Rider enquanto trabalho, Integration antes de commitar e Performance periodicamente.
- Fixtures e fábricas de teste compartilhados ficam em um diretório TestUtilities/ que tanto testes unitários quanto de integração podem usar. Um GridMapFactory que monta grades pequenas de teste é um exemplo típico.
- Nunca testo comportamento de node Godot em testes unitários. Eles cobrem apenas lógica C# pura, sob xUnit, sem processo do engine. Comportamento de node é testado por testes de integração GdUnit4 que instanciam uma cena mínima. Isso mantém a suíte unitária rápida.
A Consideração de Performance Que Ninguém Menciona
C# no Godot 4 usa o runtime .NET, o que significa que garbage collection é uma realidade que você precisa gerenciar para lógica de jogo sensível a performance. Em um RPG tático, as operações mais sensíveis ao GC são pathfinding (que aloca coleções intermediárias) e o pipeline de resolução de combate (que cria muitos objetos de vida curta durante uma única resolução).
Minha abordagem: profile antes de otimizar, e use object pooling apenas onde o profiling mostra que é necessário. O profiler integrado do Godot não faz profiling de scripts C#, então encontrar hotspots de pressão de GC exige ferramental .NET; a documentação do Godot aponta para JetBrains Rider e dotTrace. Para pathfinding especificamente, pré-aloco os conjuntos open e closed e limpo-os entre chamadas em vez de alocar novas coleções a cada requisição de pathfinding. Para objetos de resultado de dano que são criados e descartados rapidamente, uso um pool simples.
A estrutura de turnos do RPG tático na verdade ajuda aqui: a maioria dos cálculos complexos acontece em momentos de transição (quando um turno começa, quando uma ação resolve), não a cada frame. Não estou fazendo pathfinding para 50 unidades 60 vezes por segundo. Faço uma vez quando o jogador seleciona uma unidade, e uma vez por unidade inimiga durante o turno da IA. O budget de performance é generoso, e a pressão de GC é gerenciável com atenção modesta.
Arquitetura é o conjunto de decisões que se tornam caras de mudar. Em um projeto de jogo, as decisões caras são as que tocam seu modelo de dados e seu sistema de eventos. Acerte essas cedo, e o resto é iteração.
Lições Para Outros Devs Solo Escalando um Projeto Godot
- Configure namespaces no primeiro dia. Retrofitar depois é tedioso e propenso a erros.
- Use Resources tipados para todo conteúdo de jogo desde o início. A integração com o inspector vale o custo de setup inicial.
- Estabeleça o Signal Bus antes de ter mais de cinco ou seis sistemas. Adicioná-lo depois requer tocar em muitos scripts existentes.
- Escreva testes para lógica determinística desde o início. Adicionar testes a código existente sem testes é significativamente mais difícil do que escrever testes junto com código novo.
- Prefira composição sobre herança no design de cenas. Você vai refatorar isso de qualquer forma. Melhor começar com o padrão certo.
- Pastas organizadas por domínio superam pastas por categoria técnica em qualquer escala não-trivial.
- Mantenha a lista de Autoloads o mais curta possível. Cada Autoload é uma dependência global que torna testes mais difíceis.
Gerenciar um projeto Godot grande sozinho é um problema de design de sistemas tanto quanto de design de jogo. Os padrões que descrevi não são exóticos: são práticas padrão de engenharia de software aplicadas a um contexto de game dev. Vindo de um background profissional de software, tive uma vantagem significativa em conhecer esses padrões. Se você vem de um background de game dev e os conceitos de engenharia de software são novos, investir tempo em entender gerenciamento de dependências, design orientado a eventos e configuração data-driven vai gerar dividendos que se acumulam ao longo de toda a vida do seu projeto.
Referências
- Godot C# scripting. https://docs.godotengine.org/en/stable/tutorials/scripting/c_sharp/index.html
- GdUnit4 testing framework. https://github.com/MikeSchulze/gdUnit4
- Godot GDScript basics. https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_basics.html