Work in progress

The content of this page was not yet updated for Godot 4.2 and may be outdated. If you know how to improve this page or you can confirm that it's up to date, feel free to open a pull request.

Налаштування даних

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

Примітка

Ця стаття містить посилання на операції "[щось] -час". Ця термінологія виходить з алгоритму аналізу Big O Notation.

Коротко кажучи, він описує найгірший сценарій тривалості роботи. Якщо говорити неспеціалістами:

"Зі збільшенням розміру проблемного домену тривалість виконання алгоритму ..."

  • Постійний час, O(1): "... не збільшується."

  • Логарифмічний час, O(log n): "... збільшується повільно."

  • Лінійний час, O(n): "... збільшується з тією ж швидкістю."

  • І так далі.

Уявіть, якби потрібно було обробити 3 мільйони точок даних за один кадр. Було б неможливо створити функцію за допомогою алгоритму лінійного часу, оскільки обсяг даних збільшив би час виконання значно більше, ніж відведено часу. Для порівняння, використання алгоритму з постійним часом може впоратися з операцією без проблем.

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

Масив, Словник та Об'єкт

Godot stores all variables in the scripting API in the Variant class. Variants can store Variant-compatible data structures such as Array and Dictionary as well as Objects.

Godot реалізує Масив як Vector<Variant>. Механізм зберігає вміст масиву у суміжній частині пам'яті, тобто вони знаходяться в ряду, що прилягають один до одного.

Примітка

For those unfamiliar with C++, a Vector is the name of the array object in traditional C++ libraries. It is a "templated" type, meaning that its records can only contain a particular type (denoted by angled brackets). So, for example, a PackedStringArray would be something like a Vector<String>.

Суміжні сховища пам’яті передбачають такі характерні операції:

  • Ітерація: Найшвидша. Відмінно підходить для циклів.

    • Oп: Все, що вона робить, це збільшує лічильник, щоб перейти до наступного запису.

  • Вставка, Стирання, Переміщення: Залежить від положення. Загалом повільна.

    • Oп: Додавання/видалення/переміщення вмісту включно з переміщенням сусідніх записів (щоб звільнити/заповнити місце).

    • Швидке додавання/видалення з кінця.

    • Повільне додавання/видалення з довільної позиції.

    • Найповільніше додавання/видалення спереду.

    • Якщо ви робите багато вставок/вилучень спереду, тоді ...

      1. інвертуйте масив.

      2. виконайте цикл, який виконує зміни масиву в кінці.

      3. повторно інвертуйте масив.

      Це дає лише 2 копії масиву (все ще постійний час, хоча й повільний) проти копіювання приблизно 1/2 масиву, в середньому N разів (лінійний час).

  • Отримання, Встановлення: Найшвидше за позицією. Наприклад, може запитувати 0-й, 2-й, 10-й запис, тощо, але не може вказати, який запис ви хочете.

    • Oп: 1 операція додавання від початкової позиції масиву до бажаного індексу.

  • Пошук: Найповільніша. Визначає індекс/позицію значення.

    • Oп: Мусить перебирати масив і порівнювати значення, поки не знайдеться збіг.

      • Ефективність також залежить від того, чи потрібен вичерпний пошук.

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

Godot реалізує Словник як OrderedHashMap<Variant, Variant>. Механізм зберігає невеликий масив (ініціалізований до 2^3 чи 8 записів) пар ключ-значення. Коли хтось намагається отримати доступ до значення, вони надають йому ключ. Потім він хешує ключ, тобто перетворює його в число. "Хеш" використовується для обчислення індексу в масиві. Як масив, OHM має швидкий пошук у "таблиці" ключів, зіставлених із значеннями. Коли HashMap стає занадто заповненим, він збільшується до наступної степені 2 (тобто 16 записів, потім 32 тощо) і відновлює структуру.

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

  1. Хешування кожного ключа довільну кількість разів.

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

  2. Зберігання постійно зростаючого розміру таблиці.

    • HashMaps підтримують прогалини невикористаної пам’яті, вбудовані в таблицю з метою зменшення колізійних зіткнень і підтримки швидкості доступу. Ось чому вона постійно збільшується в розмірі квадратично на ступінь 2.

Як можна було б зрозуміти, Словники спеціалізуються на завданнях, які Масиви не виконують. Для них характерні такі операції:

  • Ітерація: Швидка.

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

  • Вставка, Видалення, Переміщення: Найшвидша.

    • Oп: Хешує заданий ключ. Виконує 1 операцію додавання, щоб знайти відповідне значення (початок масиву + зміщення). Переміщення включає дві операції (одна вставка, одна видалення). Карта мусить виконати певні дії, щоб зберегти свої можливості:

      • оновити впорядкований Список записів.

      • визначити, чи вимагає заповненість таблиці розширення ємності таблиці.

    • Словник пам’ятає, в якому порядку користувачі вставляли його ключі. Це дозволяє йому виконувати надійні ітерації.

  • Отримання, встановлення: Найшвидша. Як і, що пошук за ключем.

    • Oп: Як і вставлення/видалення/переміщення.

  • Пошук: Найповільніша. Ідентифікує ключ значення.

    • Oп: Потрібно перебрати записи та порівняти значення, поки не буде знайдено збіг.

    • Зауважте, що Godot не надає цю функцію out-of-the-box (з коробки) (оскільки вона не призначена для цього завдання).

Godot реалізує об’єкти як дурні, але динамічні контейнери вмісту даних. Об’єкти запитують джерела даних, коли ставлять запитання. Наприклад, щоб відповісти на запитання "чи є у вас властивість, що називається, 'position'?", він може запитати свій скрипт або ClassDB. Більше інформації про те, що таке об’єкти та як вони працюють, можна знайти в статті Застосування об’єктно-орієнтованого підходу у Godot.

Важливою деталлю тут є складність завдання Об’єкта. Кожного разу, коли він виконує один із цих запитів із декількома джерелами, він проходить через кілька циклів ітерацій та пошук HashMap. Більше того, запити - це операції лінійного часу, що залежать від розміру ієрархії успадкування Об’єкта. Якщо клас, який запитує Об'єкт (його поточний клас), нічого не знаходить, запит переходить до наступного базового класу і так аж до початкового класу Об'єкта. Хоча окремо кожна така операція швидка, той факт, що вона повинна зробити стільки перевірок, робить їх повільнішою, за обидві альтернативи пошуку даних.

Примітка

Коли розробники згадують, наскільки повільним є скриптове API, вони мають на увазі саме цей ланцюжок запитів. Порівняно зі скомпільованим кодом С++, де додаток точно знає, куди звернутися, щоб знайти що-небудь, неминуче операції скриптового API займуть набагато більше часу. Вони повинні знайти джерело будь-яких відповідних даних, перш ніж вони зможуть спробувати отримати до них доступ.

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

C# може обробляти деякий вміст на вищих швидкостях за допомогою більш оптимізованого байт-коду. Але якщо скрипт C# викликає вміст класу движка, або якщо скрипт намагається отримати доступ до чогось зовнішнього для нього, він все-одно пройде цей конвеєр.

NativeScript C++ іде ще далі і зберігає все внутрішнє за замовчуванням. Виклики у зовнішні структури будуть проходити через скриптове API. У NativeScript C++ реєстрація методів, щоб піддати їх скриптовому API, є ручним завданням. Саме на цьому етапі зовнішні класи, що не належать C++, використовуватимуть API для їх пошуку.

Отже, якщо припустити, що Посилання можна розширити, щоб створити структуру даних, наприклад, Масив чи Словник, навіщо обирати Об’єкт коли є інші два варіанти?

  1. Контроль: З об’єктами з’являється можливість створювати досконаліші структури. Можна накласти абстракції на дані, щоб переконатися, що зовнішній API не змінюється у відповідь на внутрішні зміни структури даних. Більше того, об’єкти можуть мати сигнали, що дозволяють реагувати на поведінку.

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

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

Об'єкти також дають користувачам можливість створювати ще більш спеціалізовані структури даних. За допомогою них можна створити власний Список, Двійкове Дерево Пошуку, Heap, Дерево Відтворення, Графік, Неперервний Набір та будь-який набір інших варіантів.

Може виникнути запитання: "Чому б не використовувати Вузли для структур дерева?". Ну, клас Вузол містить речі, які не будуть стосуватися власної структури даних. Таким чином, може бути корисно побудувати власний тип вузла при побудові структур дерева.

extends Object
class_name TreeNode

var _parent: TreeNode = null
var _children: = [] setget

func _notification(p_what):
    match p_what:
        NOTIFICATION_PREDELETE:
            # Destructor.
            for a_child in _children:
                a_child.free()

Звідси можна створити власні структури зі специфічними особливостями, обмеженими лише уявою.

Перерахунки: int та string

Більшість мов пропонує перерахунки. GDScript не відрізняється, але на відміну від більшості інших мов, він дозволяє використовувати цілі числа або текст для значень перерахування (останнє лише при використанні ключового слова export в GDScript). Тоді виникає запитання: "що використовувати?"

Коротка відповідь: "що тобі зручніше". Це особливість, специфічна для GDScript, а не скриптів Godot загалом; Мови надають перевагу зручності використання порівняно з продуктивністю.

На технічному рівні цілочисельні порівняння (постійний час) відбуватимуться швидше текстових порівнянь (лінійний час). Якщо хтось хоче дотримуватися конвенцій інших мов, тоді слід використовувати цілі числа.

The primary issue with using integers comes up when one wants to print an enum value. As integers, attempting to print MY_ENUM will print 5 or what-have-you, rather than something like "MyEnum". To print an integer enum, one would have to write a Dictionary that maps the corresponding string value for each enum.

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

AnimatedTexture vs. AnimatedSprite2D vs. AnimationPlayer vs. AnimationTree

За яких обставин слід користуватися кожним із класів анімації Godot? Відповідь може бути не одразу зрозумілою для нових користувачів Godot.

AnimatedTexture це текстура, яку движок малює як анімований цикл, а не як статичне зображення. Користувачі можуть маніпулювати ...

  1. the rate at which it moves across each section of the texture (FPS).

  2. кількість областей, що містяться в текстурі (кадри).

Godot's RenderingServer then draws the regions in sequence at the prescribed rate. The good news is that this involves no extra logic on the part of the engine. The bad news is that users have very little control.

Also note that AnimatedTexture is a Resource unlike the other Node objects discussed here. One might create a Sprite2D node that uses AnimatedTexture as its texture. Or (something the others can't do) one could add AnimatedTextures as tiles in a TileSet and integrate it with a TileMap for many auto-animating backgrounds that all render in a single batched draw call.

The AnimatedSprite2D node, in combination with the SpriteFrames resource, allows one to create a variety of animation sequences through spritesheets, flip between animations, and control their speed, regional offset, and orientation. This makes them well-suited to controlling 2D frame-based animations.

If one needs trigger other effects in relation to animation changes (for example, create particle effects, call functions, or manipulate other peripheral elements besides the frame-based animation), then will need to use an AnimationPlayer node in conjunction with the AnimatedSprite2D.

AnimationPlayers - це також інструмент, який потрібно буде використовувати, якщо ви хочете розробляти складніші 2D-анімаційні системи, такі як ...

  1. Cut-out animations: editing sprites' transforms at runtime.

  2. Анімація 2D Меша: визначення області текстури спрайту та встановлення скелета до нього. Потім анімуюються кістки, які розтягують і згинають текстуру пропорційно відношенню кісток між собою.

  3. Суміш вищезазначеного.

Хоча для розробки кожної з окремих послідовностей анімації для гри потрібен AnimationPlayer, також може бути корисним поєднання анімації для змішування, тобто забезпечення плавних переходів між цими анімаціями. Також може існувати ієрархічна структура між анімацією, яку планується для свого об’єкта. Це випадки для AnimationTree. Можна знайти поглиблене керівництво по використанню AnimationTree тут.