Recomendaciones de lógica

Alguna vez te has preguntado si uno debe enfocarse en un problema X con una estrategia Y o Z? Este articulo cubre una variedad de temas relacionados a estos dilemas.

Cargar (load) vs. pre-cargar (preload)

En GDScript, existe el método global preload. El carga los recursos lo más rapido posible para cargar frontalmente las operaciones de "carga" y evitar el cargar recursos mientras se encuentra en medio del código que se considera sensitivo para el rendimiento.

Su contraparte, el metodo load, carga un recurso solo cuando este llega a la declaración de carga. Esto es, el va a cargar un recurso en su lugar, y puede causar ralentizamiento en el medio de procesos importantes. La función de load también es un alias de ResourceLoader.load(path) que es accesible a todos los lenguajes de scripting.

Entonces, cuando el precargar exactamente ocurre versus el cargar, y cuando uno debería de usar cualquiera de los dos? veamos un ejemplo:

# 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");
    }
}

Precargar permite al script manejar toda la carga en el momento en que uno lee el script. Precargar es util, pero hay tambien tiempos donde uno no lo desea. Para distinguir entre estas situaciones, hay algunas cosas que uno puede considerar:

  1. Si no se puede determinar cuándo el script podría cargarse, entonces precargar un recurso, especialmente una escena o script, puede resultar en más cargas que no son esperadas. Esto puede llevar a unos tiempo de carga variables no intencionales por sobre de las operaciones de carga originales.
  2. Si algo puede reemplazar el valor (como la inicialización de una escena exportada), entonces precargar el valor no tiene sentido. Este punto no es un factor significante si uno quiere crear siempre los scripts.
  3. Si se desea sólo 'importar' un recurso de otra clase (script o escena), entonces una constante precargada es normalmente la mejor opción. Sin embargo, en casos excepcionales, puede que no queramos esto:
    1. Si la clase 'importada' es capaz de cambiar, entonces debería ser una propiedad, inicializada ya sea con un export o un load (y tal vez no inicializada sino más adelante).
    2. Si el script posee demasiadas dependencias y no se quiere consumir mucha memoria, entonces lo deseable sería cargar y liberar dependencias en tiempo de ejecución según la circunstancia. Si se precargan recursos en constantes, entonces el único modo de liberar esos recursos será liberar el script por completo. Pero si son propiedades cargadas, entonces se las puede colocar en null y remover todas las referencias al recurso por completo (el que, como un tipo que extiende Reference, causará que los recorsos se remuevan solos de memoria).

Niveles grandes: estático vs dinámico

¿Si se está creando un nivel muy grande, cuáles son las circunstancias más apropiadas? ¿Se debería crear el nivel como un espacio estático? ¿O debería cargarse el nivel en partes y cambiar el contenido del mundo a medida se requiera?

Bien, la respuesta simple es "cuando la performance lo requiera". El dilema asociado con las dos opciones es una de las viejas opciones de programación: se optimiza memoria por sobre velocidad o viceversa?

La respuesta inexperta es de usar un nivel estático que cargue todo de una vez pero, dependiendo del proyecto, esto puede consumir una gran cantidad de memoria. El desperdicio de la RAM de los usuario hace que los programas comiencen a funcionar más lento o directamente cuelgues de cualquier otra cosa que la computadora esté tratando de ejecutar al mismo tiempo.

Sin importar qué, se debe romper escenas largas en otras más pequeñas (para ayudar en la reusabilidad de contenido). Los desarrolladores pueden entonces designar un nodo que manipule la creación/carga y borrado/descarga de recursos y nodos en tiempo real. Juegos con gra variedad y tamaño de entornos o elementos generados proceduralmente normalmente emplean esas estrategias para evitar desperdiciar memoria.

Por el otro lado, crear un sistema dinámico es más complejo, por ejemplo, utiliza mucha más lógica programada lo que resulta en oportunidades de errores y otros problemas. Si no se tiene cuidado, pueden desarrollar un sistema que agranda la deuda técnica de la aplicación.

Como tales, las mejores opciones serían...

  1. Usar niveles estáticos para juegos pequeños.
  2. Si se tiene tiempo/recursos en un juego mediano/largo, crea una librería o plugin que pueda administrar nodos y recursos. Si se mejora con el tiempo, tanto para mejorar usabilidad como estabilidad, entonces puede evolucionar en una buena herramienta para usar en otros proyectos.
  3. Programar la lógica dinámica para un juego mediano/grande porque se poseen las habilidades de programación, pero no el tiempo o recursos para refinar el código (el juego debe completarse). Puede realizarse un refactor más adelante para colocar código en un plugin externo.

Para ejemplos de los varios modos en que se pueden cambiar escenas en tiempo de ejecución, lee la documentación de "Cambiar escenas manualmente".