Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

Organização da cena

Este artigo aborda tópicos relacionados à organização eficiente do conteúdo das cenas. Quais nós você deve usar? Onde deve colocá-los? Como eles devem interagir?

Como construir relações de forma eficaz

Quando os usuários do Godot começam a criar suas próprias cenas, eles geralmente se deparam com o seguinte problema:

They create their first scene and fill it with content only to eventually end up saving branches of their scene into separate scenes as the nagging feeling that they should split things up starts to accumulate. However, they then notice that the hard references they were able to rely on before are no longer possible. Reusing the scene in multiple places creates issues because the node paths do not find their targets and signal connections established in the editor break.

Para corrigir esses problemas, você deve instanciar as subcenas sem que elas exijam detalhes sobre o ambiente em que estão. Você precisa poder confiar que a subcena será criada sozinha sem depender de como será utilizada.

Uma das maiores coisas a se considerar na Programação Orientada a Objetos (POO) é manter classes focadas e de propósito único, com baixo acoplamento com outras partes da base de código. Isso mantém o tamanho dos objetos pequeno (para facilidade de manutenção) e melhora sua reutilização.

Essas melhores práticas de POO têm várias implicações para as melhores práticas na estrutura da cena e uso de script.

Sempre que possível, você deve projetar cenas sem dependências. Ou seja, deve criar cenas que mantenham dentro de si tudo o que precisam.

Se uma cena deve interagir com um contexto externo, desenvolvedores experientes recomendam o uso de Injeção de Dependência. Esta técnica envolve ter uma API de alto nível que fornece as dependências da API de baixo nível. Por que fazer isto? Porque classes que dependem de seu ambiente externo podem inadvertidamente acionar bugs e comportamentos inesperados.

Para fazer isso, você deve expor dados e então depender de um contexto pai para inicializá-los:

  1. Conecte-se a um sinal. É extremamente seguro, mas deve ser usado apenas para responder a comportamentos, não para iniciá-los. Por convenção, nomes de sinais geralmente são verbos no particípio ou passado, como "entered", "skill_activated" ou "item_collected".

    # Parent
    $Child.signal_name.connect(method_on_the_object)
    
    # Child
    signal_name.emit() # Triggers parent-specified behavior.
    
  2. Chame um método. Usado para iniciar o comportamento.

    # Parent
    $Child.method_name = "do"
    
    # Child, assuming it has String property 'method_name' and method 'do'.
    call(method_name) # Call parent-specified method (which child must own).
    
  3. Inicialize uma propriedade Callable. Mais seguro do que um método, pois a propriedade do método é desnecessária. Utilizado para iniciar o comportamento.

    # Parent
    $Child.func_property = object_with_method.method_on_the_object
    
    # Child
    func_property.call() # Call parent-specified method (can come from anywhere).
    
  4. Inicialize um Nó ou outra referência de Objeto.

    # Parent
    $Child.target = self
    
    # Child
    print(target) # Use parent-specified node.
    
  5. Inicialize um NodePath.

    # Parent
    $Child.target_path = ".."
    
    # Child
    get_node(target_path) # Use parent-specified NodePath.
    

Essas opções ocultam os pontos de acesso do nó filho. Isso mantém o nó filho fracamente acoplado ao seu ambiente. Você pode reutilizá-lo em outro contexto sem alterações adicionais em sua API.

Nota

Embora os exemplos acima ilustrem relações entre pai e filho, os mesmos princípios se aplicam a todas as relações entre objetos. Nós que são irmãos devem conhecer apenas suas próprias hierarquias, enquanto um ancestral deve mediar suas comunicações e referências.

# Parent
$Left.target = $Right.get_node("Receiver")

# Left
var target: Node
func execute():
    # Do something with 'target'.

# Right
func _init():
    var receiver = Receiver.new()
    add_child(receiver)

Os mesmos princípios também se aplicam a objetos que não são nós e que mantêm dependências de outros objetos. O objeto que possui os demais deve gerenciar os relacionamentos entre eles.

Aviso

Você deve favorecer manter os dados internamente (dentro da cena). No entanto, criar uma dependência de um contexto externo, mesmo que fracamente acoplada, ainda significa que o nó esperará que algo em seu ambiente seja verdadeiro. As filosofias de design do projeto devem impedir que isso aconteça. Caso contrário, as limitações inerentes ao código forçarão os desenvolvedores a usar documentação para rastrear relações entre objetos em escala microscópica; isso é conhecido como inferno de desenvolvimento. Escrever código que depende de documentação externa para ser utilizado com segurança é, por padrão, propenso a erros.

Para evitar criar e manter esse tipo de documentação, você pode transformar o nó dependente (o "filho" acima) em um script de ferramenta que implemente _get_configuration_warnings(). Retornar um PackedStringArray não vazio fará com que o painel Scene exiba um ícone de aviso com as strings como dica de ferramenta ao lado do nó. Esse é o mesmo ícone exibido por nós como Area2D quando não possuem nós filhos CollisionShape2D definidos. Assim, o editor passa a documentar a cena automaticamente por meio do código do script. Nenhuma duplicação de conteúdo via documentação é necessária.

Uma Interface Gráfica de Usuário (GUI) como esta pode informar melhor os usuários do projeto sobre informações críticas de um Nó. Ele possui dependências externas? Essas dependências foram atendidas? Outros programadores, e especialmente designers e escritores, precisarão de instruções claras nas mensagens dizendo-lhes o que fazer para configurá-lo.

Então, por que toda essa troca complexa funciona? Porque cenas funcionam melhor quando operam sozinhas. Se não puderem funcionar sozinhas, então trabalhar com outras de forma anônima (com o mínimo possível de dependências rígidas, ou seja, baixo acoplamento) é a melhor alternativa. Inevitavelmente, mudanças precisarão ser feitas em uma classe e, se essas mudanças fizerem com que ela interaja com outras cenas de maneiras inesperadas, as coisas começarão a se deteriorar. Todo o objetivo dessa indireção é evitar acabar em uma situação em que alterar uma classe afete negativamente outras classes dependentes dela.

Scripts e cenas, como extensões de classes do motor, devem obedecer a todos os princípios de POO. Exemplos incluem...

Escolhendo uma estrutura de árvore de nós

Você pode começar a trabalhar em um jogo e acabar sobrecarregado pelas vastas possibilidades diante de você. Talvez saiba o que quer fazer, quais sistemas deseja ter, mas onde colocá-los? A forma como você desenvolve seu jogo sempre depende de você. É possível construir árvores de nós de inúmeras maneiras. Se estiver em dúvida, este guia pode fornecer um exemplo de uma estrutura adequada para começar.

Um jogo deve sempre ter um "ponto de entrada"; algum lugar onde você possa identificar definitivamente onde as coisas começam para então acompanhar a lógica conforme ela continua em outros lugares. Ele também serve como uma visão geral de todos os outros dados e lógicas do programa. Em aplicações tradicionais, isso normalmente é uma função "main". No Godot, é um nó Main.

  • Nó "Main" (main.gd)

O script main.gd servirá como o controlador principal do seu jogo.

Em seguida, você tem um "World" dentro do jogo (2D ou 3D). Ele pode ser um filho de Main. Além disso, você precisará de uma interface gráfica principal para o seu jogo que gerencie os diversos menus e widgets necessários ao projeto.

  • Nó "Main" (main.gd)

    • Node2D/Node3D "World" (game_world.gd)

    • Control "GUI" (gui.gd)

Ao trocar de fase, você poderá substituir os filhos do nó "World". Changing scenes manually fornece controle total sobre como o mundo do seu jogo realiza transições.

O próximo passo é considerar quais sistemas de jogabilidade seu projeto requer. Se você tiver um sistema que...

  1. rastreia todos os seus dados internamente

  2. deve ser globalmente acessível

  3. deve existir isoladamente

... então você deve criar um autoload 'singleton' node.

Nota

Para jogos menores, uma alternativa mais simples com menos controle seria ter um singleton "Game" que simplesmente chama o método SceneTree.change_scene_to_file() para trocar o conteúdo da cena principal. Esta estrutura mantém mais ou menos o "Mundo" como o nó principal do jogo.

Qualquer interface gráfica também precisará ser um singleton, uma parte transitória do "World" ou ser adicionada manualmente como filha direta da raiz. Caso contrário, os nós da interface também serão removidos durante as transições de cena.

Se você tem sistemas que modificam dados de outros sistemas, você deve defini-los como seus próprios scripts ou cenas, em vez de autoloads. Para mais informações, veja Autoloads versus nós regulares.

Cada subsistema do seu jogo deve possuir sua própria seção dentro da SceneTree. Você deve usar relações pai-filho apenas nos casos em que os nós sejam efetivamente elementos de seus pais. Remover o pai significa razoavelmente que os filhos também devem ser removidos? Se não, então ele deve ter seu próprio lugar na hierarquia como irmão ou outro tipo de relação.

Nota

In some cases, you need these separated nodes to also position themselves relative to each other. You can use the RemoteTransform2D / RemoteTransform3D nodes for this purpose. They will allow a target node to conditionally inherit selected transform elements from the Remote* node. To assign the target NodePath, use one of the following:

  1. Um terceiro confiável, provavelmente um nó pai, para mediar a atribuição.

  2. Um grupo, para obter uma referência ao nó desejado (assumindo que exista apenas um alvo desse tipo).

Quando você deve fazer isso é algo subjetivo. O dilema surge quando é necessário gerenciar manualmente a movimentação de um nó pela SceneTree para preservá-lo. Por exemplo...

  • Adicionar um nó "jogador" a uma "sala".

  • É necessário trocar de sala, então você deve excluir a sala atual.

  • Antes que a sala possa ser excluída, você deve preservar e/ou mover o jogador.

    Se a memória não for uma preocupação, você pode...

    • Crie a nova sala.

    • Mova o jogador para a nova sala.

    • Exclua a sala antiga.

    Se memória for uma preocupação, então você precisará...

    • Mover o jogador para outro lugar na árvore.

    • Deletar a sala.

    • Instanciar e adicionar a nova sala.

    • Adicionar novamente o jogador à nova sala.

O problema é que o jogador, nesse caso, torna-se um "caso especial", no qual os desenvolvedores devem saber que precisam tratá-lo dessa forma para o projeto. A única maneira de compartilhar essa informação de forma confiável em equipe é documentá-la. Manter detalhes de implementação na documentação é perigoso. Isso gera custo de manutenção, prejudica a legibilidade do código e aumenta desnecessariamente a carga intelectual do projeto.

Em um jogo mais complexo com recursos maiores, pode ser melhor manter o jogador em outro local da SceneTree. Isso resulta em:

  1. Mais consistência.

  2. Nenhum "caso especial" que deva ser documentado e mantido em algum lugar.

  3. Nenhuma oportunidade para erros ocorrerem porque estes detalhes não são levados em consideração.

Por outro lado, se você precisar de um nó filho que não herde a transformação de seu pai, você possui as seguintes opções:

  1. A solução declarativa: colocar um Node entre eles. Como ele não possui transformação, essa informação não será repassada aos seus filhos.

  2. A solução imperativa: Use la propriedade top_level para o nó CanvasItem ou Node3D. Isso fará com que o nó ignore sua transformação herdada.

Nota

Ao desenvolver um jogo em rede, tenha em mente quais nós e sistemas de jogabilidade são relevantes para todos os jogadores e quais são pertinentes apenas ao servidor autoritativo. Por exemplo, os usuários não precisam possuir uma cópia da lógica "PlayerController" de todos os jogadores — apenas da sua própria. Mantê-los em um ramo separado do "world" pode ajudar a simplificar o gerenciamento das conexões de jogo e aspectos relacionados.

A chave para a organização da cena é considerar a SceneTree em termos relacionais, em vez de termos espaciais. Os nós são dependentes da existência de seus pais? Do contrário, eles podem prosperar sozinhos em outro lugar. Se forem dependentes, é lógico que devam ser filhos desse pai (e provavelmente parte da cena desse pai, se ainda não o forem).

Isso significa que os próprios nós são componentes? De forma alguma. As árvores de nós do Godot formam uma relação de agregação, não de composição. Ainda assim, embora você tenha flexibilidade para mover nós, é melhor quando esses movimentos não são necessários por padrão.