Настройки логики

Вы когда-нибудь задумывались, следует ли подходить к проблеме X со стратегией Y или Z? В этой статье рассматриваются различные темы, связанные с этими дилеммами.

Добавление узлов и изменение свойств: что сначала?

При инициализации узлов из скрипта во время выполнения может потребоваться изменить такие свойства, как имя или положение узла. Часто возникающая дилемма: когда следует изменять эти значения?

It is the best practice to change values on a node before adding it to the scene tree. Some properties' setters have code to update other corresponding values, and that code can be slow! For most cases, this code has no impact on your game's performance, but in heavy use cases such as procedural generation, it can bring your game to a crawl.

По этим причинам обычно лучше всего устанавливать начальные значения узла перед его добавлением в дерево сцены. Существуют некоторые исключения, когда значения нельзя устанавливать перед добавлением в дерево сцены, например, установка глобальной позиции.

Загрузка против предварительной загрузки

В GDScript существует глобальный метод preload. Он загружает ресурсы как можно раньше, чтобы предварительно загрузить "loading" операции и избежать загрузки ресурсов в середине кода, чувствительного к производительности.

Его аналог, метод load, загружает ресурс только тогда, когда он достигает оператора load. То есть он загрузит ресурс на месте, что может вызвать замедление, когда это произойдет в середине важных процессов. Функция load() также является псевдонимом для ResourceLoader.load(path), который доступен для всех скриптовых языков.

Итак, когда именно происходит предварительная загрузка во время загрузки и когда следует ее использовать? Посмотрим на пример:

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

Preloading allows the script to handle all the loading the moment one loads the script. Preloading is useful, but there are also times when one doesn't wish to use it. Here are a few considerations when determining which to use:

  1. If one cannot determine when the script might load, then preloading a resource (especially a scene or script) could result in additional loads one does not expect. This could lead to unintentional, variable-length load times on top of the original script's load operations.

  2. Если что-то еще может заменить значение (например, экспортированная инициализация сцены), то предварительная загрузка значения не имеет смысла. Этот момент не является существенным фактором, если кто-то намеревается всегда создавать скрипт самостоятельно.

  3. Если кто-то хочет только 'импортировать' ресурс другого класса (скрипт или сцену), то использование предварительно загруженной константы часто является лучшим способом действий. Однако в исключительных случаях стоит не делать этого:

    1. Если 'импортированный' класс подлежит изменению, то вместо этого он должен быть свойством, инициализированным либо с помощью export, либо load (и, возможно, даже не инициализированного до более позднего времени).

    2. If the script requires a great many dependencies, and one does not wish to consume so much memory, then one may wish to load and unload various dependencies at runtime as circumstances change. If one preloads resources into constants, then the only way to unload these resources would be to unload the entire script. If they are instead loaded as properties, then one can set these properties to null and remove all references to the resource (which, as a RefCounted-extending type, will cause the resources to delete themselves from memory).

Большие уровни: статические против динамических

If one is creating a large level, which circumstances are most appropriate? Is it better to create the level as one static space? Or is it better to load the level in pieces and shift the world's content as needed?

Что ж, простой ответ: «когда этого требует производительность». Дилемма, связанная с этими двумя вариантами, является одним из старых вопросов программирования: оптимизировать ли память вместо скорости или наоборот?

Наивный ответ - использовать статический уровень, который загружает все сразу. Но, в зависимости от проекта, это может потреблять большой объем памяти. Исчерпание оперативной памяти пользователей приводит к тому, что программы работают медленно или полностью вылетают из-за всего остального, что компьютер пытается делать одновременно.

Несмотря ни на что, нужно разбивать большие сцены на более мелкие (чтобы способствовать повторному использованию ресурсов). Затем разработчики могут проектировать узел, который управляет созданием/загрузкой и удалением/выгрузкой ресурсов и узлов в режиме реального времени. Игры с большими и разнообразными средами или процедурно генерируемыми элементами часто реализуют эти стратегии, чтобы избежать траты памяти.

On the flip side, coding a dynamic system is more complex; it uses more programmed logic which results in opportunities for errors and bugs. If one isn't careful, they can develop a system that bloats the technical debt of the application.

Таким образом, лучшими вариантами были бы ...

  1. Use static levels for smaller games.

  2. If one has the time/resources on a medium/large game, create a library or plugin that can manage nodes and resources with code. If refined over time so as to improve usability and stability, then it could evolve into a reliable tool across projects.

  3. Use dynamic logic for a medium/large game because one has the coding skills, but not the time or resources to refine the code (game's gotta get done). Could potentially refactor later to outsource the code into a plugin.

Примеры различных способов, которыми можно менять местами сцены во время выполнения, можно найти в документации «Изменить сцены вручную».