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.

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 somente quando ele atinge a instrução de carregamento. Ou seja, ele carregará um recurso no local que pode causar lentidão quando ele ocorre no meio de processos sensíveis. A função load também é um apelido para ResourceLoader.load(path) que é acessível a todas as linguagens de scripting.

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 benefit anyone.
#
# 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. It is when one instantiates this script on its own with .new() that
#    one will load "office.tscn" rather than the exported value.
export(PackedScene) var a_building = 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 System;
using Godot;

// C# and other languages have no concept of "preloading".
public 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");
    }
}

O pré-carregamento permite que o script manuseie todo o carregamento no momento em que se carrega o script. Pré-carregar é útil, mas também há momentos em que não se deseja. Para distinguir estas situações há algumas coisas que podemos considerar:

  1. Se não for possível determinar quando o script pode carregar, então pré-carregar um recurso, especialmente uma cena ou script, pode resultar em carregamentos adicionais inesperados. Isso pode levar a tempos de carregamento não intencionais e de comprimento variável em cima das operações de carregamento do script.

  2. 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.

  3. If one wishes only to 'import' another class resource (script or scene), then using a preloaded constant is often the best course of action. However, in exceptional cases, one may wish not to do this:

    1. Se a classe 'importada' estiver sujeita a mudanças, então ela deve ser uma propriedade, inicializada utilizando um export ou um load (e talvez nem mesmo inicializada até mais tarde).

    2. Se o script requer muitas dependências, e não se deseja consumir tanta memória, então pode-se desejar carregar e descarregar várias dependências durante a execução, conforme as circunstâncias mudam. Se alguém pré-carregar recursos em constantes, então a única maneira de descarregar estes recursos seria descarregar o script inteiro. Se ao invés disso eles são carregados, então pode-se defini-los como null e remover todas as referências ao recurso por completo (o que, como um tipo de extensão Referência, fará com que os recursos se apaguem da memória).

Fases grandes: estática vs. dinâmica

Se alguém está criando uma fase grande, quais circunstâncias são mais apropriadas? Eles deveriam criar a fase como um espaço estático? Ou eles deveriam carregar a fase em pedaços e mudar o conteúdo do mundo conforme necessário?

Bem, a resposta simples é "quando o desempenho assim o exigir." O dilema associado às duas opções é uma das antigas opções de programação: a memória é otimizada em relação à velocidade ou vice-versa?

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, codificar um sistema dinâmico é mais complexo, ou seja, usa lógica mais programada, o que resulta em oportunidades para erros e bugs. Se não tomar cuidado, é possível desenvolver um sistema que incha a dívida técnica da aplicação.

Sendo assim, as melhores opções seriam...

  1. Para usar uma fase estática para jogos menores.

  2. Se alguém tiver tempo/recursos em um jogo médio/grande, crie uma biblioteca ou plugin que possa codificar a gestão de nós e recursos. Se aperfeiçoada ao longo do tempo, de modo a melhorar a usabilidade e estabilidade, então ela pode evoluir para uma ferramenta confiável em todos os projetos.

  3. Codificar a lógica dinâmica para um jogo médio/grande porque se tem as habilidades de codificação, mas não o tempo ou recursos para refinar o código (o jogo tem que ser feito). Poderia potencialmente refatorar mais tarde para terceirizar o 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".