Вибір логіки

Ви ніколи не замислювались, чи слід підходити до проблеми X із стратегією Y чи Z? Ця стаття охоплює різноманітні теми, пов’язані з цими дилемами.

Завантаження та попереднє завантаження

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

Його аналог, метод завантаження, завантажує ресурс лише тоді, коли він досягає оператора завантаження. Тобто він буде завантажувати ресурс на місці, що може спричинити уповільнення, коли це відбувається в середині чутливих процесів. Функція load також є псевдонімом для ResourceLoader.load(шлях), який доступний для всіх скриптових мов.

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

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

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

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

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

  3. If one wishes only to 'import' another class resource (script or scene), then using a preloaded constant is often the best course of action. However, in exceptional cases, one may wish not to do this:

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

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

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

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

Ну, проста відповідь: "коли виконання цього вимагає". Дилема, пов’язана з цими двома варіантами, є одним із найстаріших питань програмування: чи оптимізувати пам’ять над швидкістю, чи навпаки?

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

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

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

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

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

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

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

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