Вибір логіки

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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