Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

Organização da cena

Este artigo cobre tópicos relacionados à organização eficaz do conteúdo da cena. Quais nós devemos usar? Onde devemos 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:

Eles criam sua primeira cena e a preenchem com conteúdo apenas para, eventualmente, acabar salvando ramos de sua cena em cenas separadas, à medida que a sensação incômoda de que devem dividir as coisas começa a se acumular. No entanto, eles percebem que as referências concretas nas quais podiam confiar antes não são mais possíveis. Reutilizar a cena em vários lugares cria problemas porque os caminhos dos nós não encontram seus alvos e as conexões de sinal estabelecidas na quebra do editor.

Para resolver estes problemas, é preciso instanciar as subcenas sem que elas exijam detalhes sobre seu ambiente. É preciso ser capaz de confiar que a subcena criará a si mesma sem ser exigente quanto à forma como se a utiliza.

Uma das maiores coisas a considerar no POO é manter as classes focadas e de propósito único, com acoplamento fraco para outras partes da base do código. Isto mantém o tamanho dos objetos pequenos (para manutenção) e melhora a sua reusabilidade.

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.

Se possível, deve-se projetar cenas para não ter dependências. Ou seja, deve-se criar cenas que mantenham tudo o que precisam dentro de si.

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 isto, é necessário expor dados e, em seguida, confiar em um contexto pai para inicializá-los:

  1. Connect to a signal. Extremely safe, but should be used only to "respond" to behavior, not start it. By convention, signal names are usually past-tense verbs like "entered", "skill_activated", or "item_collected".

    # Parent
    $Child.signal_name.connect(method_on_the_object)
    
    # Child
    signal_name.emit() # Triggers parent-defined 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-defined 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-defined method (can come from anywhere).
    
  4. Inicialize um Nó ou outra referência de Objeto.

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

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

These options hide the points of access from the child node. This in turn keeps the child loosely coupled to its environment. One can reuse it in another context without any extra changes to its API.

Nota

Embora os exemplos acima ilustrem as relações entre pai-filho, os mesmos princípios se aplicam a todas as relações de objetos. Nós que são irmãos só devem estar cientes de suas hierarquias enquanto um ancestral media 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 não-Nó que mantêm dependências em outros objetos. Qualquer objeto que realmente possui os objetos deve gerenciar as relações entre eles.

Aviso

Deve-se favorecer a manutenção dos dados internamente (internos a uma cena), embora colocar uma dependência em um contexto externo, mesmo um vagamente acoplado, ainda signifique que o nó esperará que algo em seu ambiente seja verdadeiro. As filosofias de design do projeto devem evitar que isso aconteça. Do contrário, as responsabilidades inerentes ao código forçarão os desenvolvedores a usar a documentação para controlar as relações de objeto em uma escala microscópica; isso também é conhecido como inferno do desenvolvimento. Escrever código que depende de documentação externa para usá-lo com segurança está sujeito a erros por padrão.

To avoid creating and maintaining such documentation, one converts the dependent node ("child" above) into a tool script that implements _get_configuration_warnings(). Returning a non-empty PackedStringArray from it will make the Scene dock generate a warning icon with the string(s) as a tooltip by the node. This is the same icon that appears for nodes such as the Area2D node when it has no child CollisionShape2D nodes defined. The editor then self-documents the scene through the script code. No content duplication via documentation is necessary.

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

So, why does all this complex switcharoo work? Well, because scenes operate best when they operate alone. If unable to work alone, then working with others anonymously (with minimal hard dependencies, i.e. loose coupling) is the next best thing. Inevitably, changes may need to be made to a class and if these changes cause it to interact with other scenes in unforeseen ways, then things will start to break down. The whole point of all this indirection is to avoid ending up in a situation where changing one class results in adversely effecting other classes dependent on it.

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

Então, um desenvolvedor começa a trabalhar em um jogo apenas para parar nas vastas possibilidades à sua frente. Eles podem saber o que querem fazer, que sistemas desejam ter, mas onde colocar todos? Bem, como alguém vai fazer seu jogo depende sempre dele. Pode-se construir árvores de nós de uma infinidade de maneiras. Mas, para aqueles que não têm certeza, este guia útil pode dar uma amostra de uma estrutura decente para começar.

Um jogo deve sempre ter uma espécie de "ponto de entrada"; em algum lugar, o desenvolvedor pode rastrear definitivamente onde as coisas começam para que possam seguir a lógica enquanto ela continua em outro lugar. Este local também serve como uma visão geral de todos os outros dados e lógicas do programa. Para aplicações tradicionais, esta seria a função "main". Nesse caso, seria um nó Main.

  • Nó "Main" (main.gd)

O script main.gd então serviria como o controlador principal do jogo.

Então, se tem seu "Mundo" real no jogo (um 2D ou 3D). Este pode ser um filho de Main. Além disso, será necessária uma GUI principal para o jogo que gerencie os vários menus e widgets de que o projeto precisa.

  • Nó "Main" (main.gd)
    • Node2D/Node3D "World" (game_world.gd)

    • Control "GUI" (gui.gd)

Ao mudar de fase, é possível trocar os filhos do nó "Mundo". Mudar as cenas manualmente dá aos usuários controle total sobre como suas transições de mundo de jogo.

O próximo passo é considerar quais sistemas de jogabilidade o projeto requer. Se houver um sistema que...

  1. rastreia todos os seus dados internamente

  2. deve ser globalmente acessível

  3. deve existir isoladamente

... então deve-se criar um nó autoload 'singleton'.

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 GUI também precisaria ser um singleton; ser uma parte transitória do "Mundo"; ou ser adicionado manualmente como filho direto da raiz. Caso contrário, os nós da GUI também se excluiriam durante as transições de cena.

Se alguém tem sistemas que modificam os dados de outros sistemas, deve-se defini-los como seus próprios scripts ou cenas, em vez de cargas automáticas. Para obter mais informações sobre os motivos, consulte a documentação Autoloads versus regular nodes.

Cada subsistema dentro do jogo deve ter sua própria seção dentro da SceneTree. Deve-se usar relacionamentos pai-filho apenas nos casos em que os nós são efetivamente elementos de seus pais. Remover o pai significa razoavelmente que também se deve remover os filhos? Do contrário, deve ter seu próprio lugar na hierarquia como irmão ou algum outro parente.

Nota

Em alguns casos, é necessário que esses nós separados também se posicionem uns em relação aos outros. É possível usar os nós RemoteTransform / RemoteTransform2D para este propósito. Eles permitirão que um nó de destino herde condicionalmente os elementos de transformação selecionados do nó Remoto*. Para atribuir o :ref: NodePath <class_NodePath> target, use um dos seguintes:

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

  2. Um grupo, para puxar facilmente uma referência para o nó desejado (assumindo que haverá apenas um dos alvos).

Quando fazer isto? Bem, isto é subjetivo. O dilema surge quando é necessário microgerenciar quando um nó deve se mover ao redor da SceneTree para se preservar. Por exemplo...

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

  • É necessário mudar de sala, portanto, deve-se excluir a sala atual.

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

    A memória é uma preocupação?

    • Caso não, pode-se simplesmente criar as duas salas, mover o jogador e deletar a antiga. Sem problemas.

    Se sim, será necessário...

    • Mover o jogador para outro lugar na árvore.

    • Deletar a sala.

    • Instanciar e adicionar a nova sala.

    • Adicionar novamente o jogador.

A questão é que o jogador aqui é um "caso especial"; aquele em que os desenvolvedores devem saber que precisam lidar com o jogador desta maneira para o projeto. Como tal, a única maneira confiável de compartilhar essas informações em equipe é documentá-las. Manter os detalhes da implementação na documentação, entretanto, é perigoso. É um fardo de manutenção, restringe a legibilidade do código e incha o conteúdo intelectual de um projeto desnecessariamente.

Em um jogo mais complexo com assets maiores, pode ser uma ideia melhor simplesmente manter o jogador inteiramente em outro lugar no SceneTree. Isto 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.

Em contraste, se alguém precisa ter um nó filho que não herda a transformação de seu pai, tem-se as seguintes opções:

  1. A solução declarativa: coloque um Node entre eles. Como nós sem transformação, Nodes não passarão essas informações para seus filhos.

  2. The imperative solution: Use the top_level property for the CanvasItem or Node3D node. This will make the node ignore its inherited transform.

Nota

Se estiver criando um jogo em rede, tenha em mente quais nós e sistemas de jogo são relevantes para todos os jogadores e aqueles apenas pertinentes ao servidor autorizado. Por exemplo, nem todos os usuários precisam ter uma cópia da lógica "PlayerController" de todos os jogadores. Em vez disso, eles precisam apenas dos seus próprios. Como tal, mantê-los em um ramo separado do "mundo" pode ajudar a simplificar o gerenciamento de conexões de jogos e similares.

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 modo algum. As árvores de nós do Godot formam uma relação de agregação, não de composição. Mas, embora ainda se tenha a flexibilidade de mover nós, ainda é melhor quando esses movimentos são desnecessários por padrão.