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.

Вибір логіки

Ви ніколи не замислювались, чи слід підходити до проблеми 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 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")

Попереднє завантаження дозволяє скрипту обробляти все завантаження в момент його завантаження. Попереднє завантаження корисне, але бувають випадки, коли його не хочеться використовувати. Ось кілька міркувань, які слід враховувати при виборі того, який варіант використовувати:

  1. Якщо неможливо визначити, коли може завантажитися скрипт, то попереднє завантаження ресурсу (особливо сцени чи скрипта) може призвести до додаткових завантажень, яких не очікуєш. Це може призвести до ненавмисного, змінного часу завантаження понад операції завантаження оригінального скрипта.

  2. Якщо щось інше може замінити значення (наприклад, ініціалізація експортованої сцени), то попереднє завантаження значення не має сенсу. Цей момент не є суттєвим фактором, якщо кожен має намір завжди створювати скрипт самостійно.

  3. Якщо хтось бажає лише 'імпортувати' інший ресурс класу (скрипт, або сцену), то використання попередньо завантаженої константи часто є найкращим рішенням. Однак у виняткових випадках можна не бажати цього робити:

    1. Якщо «імпортований» клас підлягає зміні, тоді це має бути властивість, ініціалізована за допомогою @export або load() (і, можливо, навіть не ініціалізована пізніше).

    2. Якщо скрипт вимагає багато залежностей, і не хочеться споживати стільки пам'яті, то можна завантажувати та вивантажувати різні залежності під час виконання, коли змінюються обставини. Якщо попередньо завантажувати ресурси в константи, то єдиним способом вивантажити ці ресурси буде вивантаження всього скрипта. Якщо ж вони завантажуються як властивості, то можна встановити ці властивості в значення null та видалити всі посилання на ресурс (що, як тип, що розширює RefCounted, призведе до того, що ресурси видалятимуться з пам'яті).

Великі рівні: статичний та динамічний

Якщо створюється великий рівень, які обставини є найбільш доцільними? Чи краще створювати рівень як один статичний простір? Чи краще завантажувати рівень частинами та зміщувати вміст світу за потреби?

Що ж, проста відповідь: «коли того вимагає продуктивність». Дилема, пов’язана з двома варіантами, є одним із давніх варіантів програмування: оптимізувати пам’ять замість швидкості чи навпаки?

Наївна відповідь полягає у використанні статичного рівня, який завантажує все відразу. Залежно від проєкту, це може зайняти великий обсяг пам'яті. Витрата оперативної пам'яті користувачів призводить до того, що програми працюють повільно або взагалі вилітають.

Незважаючи ні на що, слід розбивати великі сцени на менші (для сприяння повторному використанню активів). Потім розробники можуть спроєктувати вузол, який керує створенням/завантаженням та видаленням/вивантаженням ресурсів та вузлів у режимі реального часу. Ігри з великим та різноманітним середовищем, або процедурно сформованими елементами, часто реалізують ці стратегії, щоб уникнути марнотратства пам'яті.

З іншого боку, кодування динамічної системи є складнішим; воно використовує більше запрограмованої логіки, що створює можливості для помилок та багів. Якщо не бути обережним, можна розробити систему, яка перебільшить технічний борг програми.

Таким чином, найкращими варіантами будуть ...

  1. Використовуйте статичні рівні для невеликих ігор.

  2. Якщо у вас є час/ресурси для гри середнього/великого розміру, створіть бібліотеку або плагін, який може керувати вузлами та ресурсами за допомогою коду. Якщо з часом його вдосконалювати для покращення зручності використання та стабільності, то він може перетворитися на надійний інструмент для різних проектів.

  3. Використовуйте динамічну логіку для гри середнього/великого розміру, оскільки у гравця є навички програмування, але немає часу чи ресурсів для вдосконалення коду (гру треба закінчити). Потенційно можна було б рефакторингувати пізніше, щоб передати код на аутсорсинг у плагін.

Для прикладу різних способів обміну сценами під час виконання дивіться документацію "Change scenes manually".