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.
Checking the stable version of the documentation...
Preferências de lógica
Já se perguntou se devemos abordar o problema X com a estratégia Y ou Z? Este artigo cobre uma variedade de tópicos relacionados a esses dilemas.
Adicionar nós e alterar propriedades: qual fazer primeiro?
Ao inicializar nós a partir de um script em tempo de execução, pode ser necessário alterar propriedades como o nome ou a posição do nó. Uma dúvida comum é: quando você deve alterar esses valores?
É uma boa prática alterar valores de um nó antes de adicioná-lo à árvore de cena. Alguns setters de propriedades possuem código para atualizar outros valores correspondentes, e esse código pode ser lento. Na maioria dos casos isso não afeta o desempenho do jogo, mas em situações intensivas como geração procedural, pode reduzir drasticamente a performance.
Por essas razões, geralmente é uma melhor prática definir os valores iniciais de um nó antes de adicioná-lo à árvore de cenas. Existem algumas exceções onde os valores não podem ser definidos antes de serem adicionados à árvore de cenas, como definir a posição global.
Carregamento vs. pré-carregamento
No GDScript, existe o método global preload. Ele carrega recursos o mais cedo possível para antecipar as operações de "carregamento" e evitar o carregamento de recursos no meio de um código sensível ao desempenho.
Sua contraparte, o método load, carrega um recurso apenas quando ele alcança a instrução de carregamento. Ou seja, ele carregará um recurso localmente, o que pode causar lentidões quando ocorre no meio de processos sensíveis. A função load() também é um alias para ResourceLoader.load(path), que é acessível a todas as linguagens de script.
Então, quando exatamente ocorre o pré-carregamento versus carregamento, e quando se deve usar qualquer algum deles? Vejamos um exemplo:
# my_buildings.gd
extends Node
# Note how constant scripts/scenes have a different naming scheme than
# their property variants.
# This value is a constant, so it spawns when the Script object loads.
# The script is preloading the value. The advantage here is that the editor
# can offer autocompletion since it must be a static path.
const BuildingScn = preload("res://building.tscn")
# 1. The script preloads the value, so it will load as a dependency
# of the 'my_buildings.gd' script file. But, because this is a
# property rather than a constant, the object won't copy the preloaded
# PackedScene resource into the property until the script instantiates
# with .new().
#
# 2. The preloaded value is inaccessible from the Script object alone. As
# such, preloading the value here actually does not provide any benefit.
#
# 3. Because the user exports the value, if this script stored on
# a node in a scene file, the scene instantiation code will overwrite the
# preloaded initial value anyway (wasting it). It's usually better to
# provide `null`, empty, or otherwise invalid default values for exports.
#
# 4. Instantiating the script on its own with .new() triggers
# `load("office.tscn")`, ignoring any value set through the export.
@export var a_building : PackedScene = preload("office.tscn")
# Uh oh! This results in an error!
# One must assign constant values to constants. Because `load` performs a
# runtime lookup by its very nature, one cannot use it to initialize a
# constant.
const OfficeScn = load("res://office.tscn")
# Successfully loads and only when one instantiates the script! Yay!
var office_scn = load("res://office.tscn")
using Godot;
// C# and other languages have no concept of "preloading".
public partial class MyBuildings : Node
{
//This is a read-only field, it can only be assigned when it's declared or during a constructor.
public readonly PackedScene Building = ResourceLoader.Load<PackedScene>("res://building.tscn");
public PackedScene ABuilding;
public override void _Ready()
{
// Can assign the value during initialization.
ABuilding = GD.Load<PackedScene>("res://Office.tscn");
}
}
using namespace godot;
class MyBuildings : public Node {
GDCLASS(MyBuildings, Node)
public:
const Ref<PackedScene> building = ResourceLoader::get_singleton()->load("res://building.tscn");
Ref<PackedScene> a_building;
virtual void _ready() override {
// Can assign the value during initialization.
a_building = ResourceLoader::get_singleton()->load("res://office.tscn");
}
};
O pré-carregamento permite que o script realize todo o carregamento assim que o próprio script for carregado. O preload é útil, mas há situações em que você pode não querer utilizá-lo. Aqui estão algumas considerações para decidir entre as abordagens:
Se não for possível determinar quando o script será carregado, então pré-carregar um recurso (especialmente uma cena ou script) pode resultar em carregamentos adicionais inesperados. Isso pode causar tempos de carregamento variáveis e não intencionais além das operações normais do script.
Se algo mais pudesse substituir o valor (como a inicialização exportada de uma cena), então pre-carregar o valor não teria sentido. Este ponto não é um fator significativo se a intenção é sempre criar o script por conta própria.
Se alguém deseja apenas 'importar' outro recurso de classe (script ou cena), usar uma constante pré-carregada é geralmente o melhor curso de ação. No entanto, em casos excepcionais, não se deseja fazer isso:
Se a classe 'importada' estiver sujeita a mudanças, então ela deveria ser uma propriedade, inicializada por meio de
@exportouload()(e talvez nem mesmo inicializada até um momento posterior).Se o script exigir uma grande quantidade de dependências e você não quiser consumir tanta memória, então você pode carregar e descarregar várias dependências em tempo de execução conforme as circunstâncias mudam. Se você pré-carregar recursos em constantes, a única maneira de descarregar esses recursos seria descarregar o script inteiro. Se eles forem carregados como propriedades, você poderá definir essas propriedades como
nulle remover todas as referências ao recurso (o que, sendo um tipo que estende de RefCounted, fará com que os recursos se excluam da memória).
Fases grandes: estática vs. dinâmica
Se você estiver criando um nível grande, qual abordagem é mais apropriada? É melhor criar o nível como um espaço estático único? Ou é melhor carregá-lo em partes e deslocar o conteúdo do mundo conforme necessário?
Bem, a resposta simples é: "quando o desempenho exigir". O dilema associado às duas opções é uma das escolhas clássicas da programação: otimizar memória em detrimento de velocidade, ou o contrário?
A resposta ingênua é usar uma fase estática que carrega tudo ao mesmo tempo. Mas, dependendo do projeto, isto pode consumir uma grande quantidade de memória. Desperdiçar a RAM dos usuários leva a programas executarem devagar ou travamento total de tudo o que o computador tenta fazer ao mesmo tempo.
Não importa o que aconteça. deve-se quebrar cenas maiores em cenas menores (para ajudar na reutilização de assets). Desenvolvedores podem então projetar um nó que gerencia criação/carregamento e exclusão/descarregamento de recursos e nós em tempo real. Jogos com ambientes variáveis e grandes ou elementos gerados proceduralmente geralmente implementam estas estratégias para evitar o desperdício de memória.
Por outro lado, programar um sistema dinâmico é mais complexo; ele utiliza mais lógica programada, o que cria oportunidades para erros e bugs. Se não houver cuidado, pode-se desenvolver um sistema que aumente significativamente a dívida técnica da aplicação.
Sendo assim, as melhores opções seriam...
Use níveis estáticos para jogos menores.
Se você tiver tempo e recursos em um jogo de médio ou grande porte, crie uma biblioteca ou plugin capaz de gerenciar nós e recursos via código. Se ele for refinado ao longo do tempo para melhorar usabilidade e estabilidade, poderá evoluir para uma ferramenta confiável reutilizável entre projetos.
Use lógica dinâmica em um jogo de médio ou grande porte quando possuir as habilidades de programação necessárias, mas não o tempo ou os recursos para transformar o código em um plugin refinado (afinal, o jogo precisa ser concluído). Posteriormente, talvez seja possível refatorar e externalizar esse código em um plugin.
Para um exemplo das várias formas de trocar cenas durante a execução, por favor veja a documentação "Mudar cenas manualmente".