Настройки логики
Вы когда-нибудь задумывались, следует ли подходить к проблеме X со стратегией Y или Z? В этой статье рассматриваются различные темы, связанные с этими дилеммами.
Добавление узлов и изменение свойств: что сначала?
При инициализации узлов из скрипта во время выполнения может потребоваться изменить такие свойства, как имя или положение узла. Часто возникающая дилемма: когда следует изменять эти значения?
Лучше всего изменять значения узла перед добавлением его в дерево сцены. Некоторые сеттеры свойств содержат код для обновления других соответствующих значений, и этот код может быть медленным! В большинстве случаев этот код не влияет на производительность игры, но в тяжелых случаях, таких как процедурная генерация, он может замедлить работу игры.
По этим причинам обычно лучше всего устанавливать начальные значения узла перед его добавлением в дерево сцены. Существуют некоторые исключения, когда значения нельзя устанавливать перед добавлением в дерево сцены, например, установка глобальной позиции.
Загрузка против предварительной загрузки
В GDScript существует глобальный метод preload. Он загружает ресурсы как можно раньше, чтобы предварительно загрузить «загрузочные» операции и избежать загрузки ресурсов в середине кода, чувствительного к производительности.
Его аналог, метод 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 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 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");
}
};
Предварительная загрузка позволяет скрипту обрабатывать всю загрузку в момент загрузки скрипта. Предварительная загрузка полезна, но бывают случаи, когда этого не хочется. Чтобы различать эти ситуации, следует учесть несколько вещей:
Если невозможно определить, когда может загрузиться сценарий, то предварительная загрузка ресурса, особенно сцены или сценария, может привести к дальнейшим загрузкам, которых нельзя было ожидать. Это может привести к непреднамеренной загрузке переменной длины поверх операций загрузки исходного сценария.
Если что-то еще может заменить значение (например, экспортированная инициализация сцены), то предварительная загрузка значения не имеет смысла. Этот момент не является существенным фактором, если кто-то намеревается всегда создавать скрипт самостоятельно.
Если кто-то хочет только 'импортировать' ресурс другого класса (скрипт или сцену), то использование предварительно загруженной константы часто является лучшим способом действий. Однако в исключительных случаях стоит не делать этого:
Если 'импортированный' класс подлежит изменению, то вместо этого он должен быть свойством, инициализированным либо с помощью
export, либоload(и, возможно, даже не инициализированного до более позднего времени).Если для сценария требуется очень много зависимостей, и кто-то не желает расходовать так много памяти, то может потребоваться загружать и выгружать различные зависимости во время выполнения по мере изменения обстоятельств. Если предварительно загрузить ресурсы в константы, то единственный способ выгрузить эти ресурсы — это выгрузить весь сценарий. Если вместо этого, ресурсы это загруженные свойства, то можно установить для них значение как `` null `` и полностью удалить все ссылки на ресурс (что, как расширяющийся тип Reference, приведет к самоудалению ресурсов из памяти).
Большие уровни: статические против динамических
Если кто-нибудь создает большой уровень, какие обстоятельства являются самыми подходящими? Должны ли они создавать уровень как одно статичное пространство? Или они должны загружать уровень по частям и перемещать содержимое мира по мере необходимости?
Что ж, простой ответ: «когда этого требует производительность». Дилемма, связанная с этими двумя вариантами, является одним из старых вопросов программирования: оптимизировать ли память вместо скорости или наоборот?
Наивный ответ - использовать статический уровень, который загружает все сразу. Но, в зависимости от проекта, это может потреблять большой объем памяти. Исчерпание оперативной памяти пользователей приводит к тому, что программы работают медленно или полностью вылетают из-за всего остального, что компьютер пытается делать одновременно.
Несмотря ни на что, нужно разбивать большие сцены на более мелкие (чтобы способствовать повторному использованию ресурсов). Затем разработчики могут проектировать узел, который управляет созданием/загрузкой и удалением/выгрузкой ресурсов и узлов в режиме реального времени. Игры с большими и разнообразными средами или процедурно генерируемыми элементами часто реализуют эти стратегии, чтобы избежать траты памяти.
С другой стороны, кодирование динамической системы является более сложным, т.к. использует больше программируемой логики, которая приводит к возможностям для ошибок и багов. Если кто-то не будет осторожен, то может быть разработана система, которая раздувает технический долг приложения.
Таким образом, лучшими вариантами были бы ...
Использование статических уровней для небольших игр.
Если у вас есть время/ресурсы для средней/большой игры, создайте библиотеку или плагин, который может кодировать управление узлами и ресурсами. Если со временем доработать его, чтобы улучшить удобство использования и стабильность, то он может превратиться в надежный инструмент для всех проектов.
Программируйте динамическую логику для средней/большой игры, если у вас есть навыки программирования, но нет времени или ресурсов для улучшения кода (игра должна быть завершена). Позже потенциально можно провести рефакторинг, чтобы создать из кода плагин.
Примеры различных способов, которыми можно менять местами сцены во время выполнения, можно найти в документации «Изменить сцены вручную».