Work in progress
The content of this page was not yet updated for Godot
4.4
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? В этой статье рассматриваются различные темы, связанные с этими дилеммами.
Примечание
В этой статье есть ссылки на операции "[что-то] -время". Эта терминология взята из анализа алгоритмов "О" большое и "о" малое.
Короче говоря, он описывает наихудший сценарий продолжительности времени выполнения. Проще говоря:
«По мере увеличения размера проблемной области время выполнения алгоритма ...»
Постоянное время,
O(1): "...не увеличивается."Логарифмическое время,
O(log n): "... увеличивается с медленной скоростью."Линейное время,
O(n): "... увеличивается с той же скоростью."И т.п.
Представьте, что нужно обработать 3 миллиона точек данных в одном кадре. Было бы невозможно создать эту функцию с помощью алгоритма линейного времени, поскольку сам размер данных увеличит время выполнения намного больше отведенного времени. Для сравнения, использование алгоритма с постоянным временем может справиться с операцией без проблем.
По большому счету, разработчики хотят по возможности избегать операций с линейным временем. Но если сохранить небольшой масштаб операции с линейным временем, и если нет необходимости выполнять операцию часто, то это может быть приемлемым. Уравновешивание этих требований и выбор правильного алгоритма / структуры данных для работы - это часть того, что делает навыки программистов ценными.
Array против Dictionary против Object
Godot хранит все переменные в API скрипта в классе Variant. Варианты могут хранить Variant-совместимые структуры данных, такие как Array и Dictionary, а также Object.
Godot реализует Array как Vector<Variant>. Движок хранит содержимое Array в непрерывном разделе памяти, то есть они находятся в строке рядом друг с другом.
Примечание
Для тех, кто не знаком с C++, вектор - это имя массива в традиционных библиотеках C++. Это "шаблонный" тип, что означает, что его записи могут содержать только определенный тип (обозначенный угловыми скобками). Так, например, a PackedStringArray будет чем-то вроде `` Vector <String>``.
Непрерывные хранилища памяти подразумевают следующую производительность операций:
Итерация: Самый быстрый. Отлично подходит для циклов.
Операции: всё, что он делает, это увеличивает счётчик, чтобы перейти к следующей записи.
Вставка, стирание, перемещение: В зависимости от позиции. Обычно медленно.
Операции: добавление/удаление/перемещение содержимого включает перемещение соседних записей (чтобы освободить место/заполнить пространство).
Быстрое добавление/удаление с конца.
Медленное добавление/удаление из произвольной позиции.
Самое медленное добавление/удаление спереди.
Если делать много вставок/удалений спереди, то ...
инвертировать массив.
выполнить цикл, который выполняет изменения Array в конце.
повторно инвертировать массив.
Это делает только 2 копии массива (по-прежнему с постоянным временем, но медленно) по сравнению с копированием примерно 1/2 массива, в среднем, N раз (линейное время).
Получить, установить: Самый быстрый по позиции. Может запрашивать 0-ю, 2-ю, 10-ю запись и т. д., но не может указать, какую запись вы хотите.
Операции: 1 операция сложения от начальной позиции массива до желаемого индекса.
Поиск: Самый медленный. Определяет индекс/позицию значения.
Операции: необходимо перебирать массив и сравнивать значения, пока не будет найдено совпадение.
Производительность также зависит от того, нужен ли вам исчерпывающий поиск.
Если сохранять порядок, пользовательские операции поиска могут привести его к логарифмическому времени (относительно быстро). Однако пользователям-непрофессионалам это не понравится. Выполняется путем повторной сортировки массива после каждого редактирования и написания алгоритма сортированного поиска.
Godot implements Dictionary as an HashMap<Variant, Variant, VariantHasher, StringLikeVariantComparator>. The engine
stores a small array (initialized to 2^3 or 8 records) of key-value pairs. When
one attempts to access a value, they provide it a key. It then hashes the
key, i.e. converts it into a number. The "hash" is used to calculate the index
into the array. As an array, the HM then has a quick lookup within the "table"
of keys mapped to values. When the HashMap becomes too full, it increases to
the next power of 2 (so, 16 records, then 32, etc.) and rebuilds the structure.
Хеши предназначены для уменьшения вероятности конфликта ключей. Если это произойдет, таблица должна пересчитать другой индекс для значения, учитывающего предыдущую позицию. В целом это приводит к константному времени доступу ко всем записям за счет памяти и некоторой незначительной операционной эффективности.
Хеширование каждого ключа произвольное количество раз.
Операции хеширования выполняются с постоянным временем, поэтому, даже если алгоритм должен выполнять несколько раз, пока количество вычислений хеша не станет слишком зависимым от плотности таблицы, все будет оставаться быстрым. Что приведет к...
Поддержанию постоянно растущего размера таблицы.
HashMap сохраняют промежутки неиспользуемой памяти, разбросанные по таблице, с целью уменьшения хеш-коллизий и поддержания скорости доступа. Вот почему он постоянно увеличивается в размере квадратично в степени 2.
Как можно догадаться, Dictionary специализируются на задачах, которых Array не выполняют. Ниже приводится обзор их рабочих характеристик:
Итерация: Быстро.
Операции: обойти внутренний вектор хешей. Вернуть каждый ключ. После этого пользователи используют ключ, чтобы перейти к желаемому значению и получить его.
Вставка, стирание, перемещение: Самый быстрый.
Операции: Хешировать данный ключ. Выполнить 1 операцию сложения, чтобы найти соответствующее значение (начало массива + смещение). Ход - два таких (одна вставка, одна стирание). Карта должна быть обновлена, чтобы сохранить свои возможности:
обновлению упорядоченного List записей.
определению того, требует ли плотность таблицы увеличения ее размера.
Dictionary запоминает, в каком порядке пользователи вставляли его ключи. Это позволяет ему выполнять надежные итерации.
Получить, установить: Самый быстрый. То же, что и поиск по ключу.
Операции: То же, что и вставка/стирание/перемещение.
Поиск: Самый медленный. Определяет индекс/позицию значения.
Операции: необходимо перебирать записи и сравнивать значение, пока не будет найдено совпадение.
Обратите внимание, что Godot не предоставляет эту функцию "из коробки" (потому что она не предназначены для этой задачи).
Godot реализует Object как глупые, но динамические контейнеры содержимого данных. Object запрашивают источники данных, когда задают вопросы. Например, чтобы ответить на вопрос "есть ли у вас свойство под названием 'position'?", Он может спросить его у script или ClassDB. Более подробную информацию о том, что такое объекты и как они работают, можно найти в статье Применение принципов объектно-ориентированного программирования в Godot.
Важной деталью здесь является сложность задачи Object. Каждый раз, когда он выполняет один из этих запросов с несколькими источниками, он проходит через несколько итерационных циклов и поисков HashMap. Более того, запросы представляют собой линейные операции, зависящие от размера иерархии наследования объекта. Если класс, который запрашивает Object (его текущий класс), ничего не находит, запрос передается следующему базовому классу, вплоть до исходного класса Object. Хотя каждая из этих операций является быстрой по отдельности, тот факт, что она должна выполнять так много проверок, делает их медленнее, чем обе альтернативы для поиска данных.
Примечание
Когда разработчики упоминают, насколько медленным является API скриптов, они ссылаются именно на эту цепочку запросов. По сравнению со скомпилированным кодом C++, в котором приложение точно знает, куда идти, чтобы что-то найти, операции API скриптов неизбежно займут гораздо больше времени. Они должны найти источник любых соответствующих данных, прежде чем они смогут попытаться получить к нему доступ.
Причина по которой GDScript работает медленно, является то, что каждая операция должна проходит через эту систему.
C# может обрабатывать некоторый контент на более высоких скоростях за счет более оптимизированного байт-кода. Но если скрипт на C# вызывает содержимое класса движка или если скрипт пытается получить доступ к чему-то внешнему по отношению к нему, он будет проходить через этот конвейер.
NativeScript C++ идет еще дальше и по умолчанию сохраняет все внутри. Вызовы во внешние структуры будут проходить через скриптовый API. В NativeScript C++ регистрация методов для предоставления их скриптовому API выполняется вручную. Именно в этот момент внешние классы, не относящиеся к C++, будут использовать API для их поиска.
Итак, если предположить, что один из них расширяется от Reference для создания структуры данных, таких как Array или Dictionary, то зачем тогда выбирать Object вместо двух других вариантов?
Контроль: С объектами появляется возможность создавать более сложные структуры. Можно накладывать абстракции на данные, чтобы гарантировать, что внешний API не изменится в ответ на изменения внутренней структуры данных. Более того, Object могут иметь сигналы, позволяющие реагировать на них.
Ясность: Объекты являются надежным источником данных, когда речь идет о данных, которые для них определяют сценарии и классы движка. Свойства могут не содержать ожидаемых значений, но не нужно беспокоиться о том, существует ли это свойство вообще.
Удобство: Если кто-то уже имеет в виду аналогичную структуру данных, то расширение существующего класса значительно упростит задачу построения структуры данных. Для сравнения: Array и Dictionary не подходят для всех возможных вариантов использования.
Object также дают пользователям возможность создавать еще более специализированные структуры данных. С его помощью можно создать собственный список, дерево двоичного поиска, кучу, дерево двоичного поиска, граф, непересекающийся множество и любые другие варианты.
"Почему бы не использовать Node для древовидных структур?" можно спросить. Ну, класс Node содержит вещи, которые не имеют отношения к пользовательской структуре данных. Таким образом, при построении древовидных структур может быть полезно создать собственный тип дерева.
extends Object
class_name TreeNode
var _parent: TreeNode = null
var _children := []
func _notification(p_what):
match p_what:
NOTIFICATION_PREDELETE:
# Destructor.
for a_child in _children:
a_child.free()
using Godot;
using System.Collections.Generic;
// Can decide whether to expose getters/setters for properties later
public partial class TreeNode : GodotObject
{
private TreeNode _parent = null;
private List<TreeNode> _children = [];
public override void _Notification(int what)
{
switch (what)
{
case NotificationPredelete:
foreach (TreeNode child in _children)
{
node.Free();
}
break;
}
}
}
Здесь можно создавать свои собственные структуры с определенными функциями, ограниченными только собственным воображением.
Перечисления: int vs. string
Большинство языков предлагают выбор типа перечисления. GDScript ничем не отличается, но, в отличие от большинства других языков, позволяет использовать для значений перечисления либо целые числа, либо строки (последнее только при использовании ключевого слова @export_enum в GDScript). Тогда возникает вопрос: "какой использовать?"
Короткий ответ: «то, что вам удобнее». Это особенность, специфическая для GDScript, а не для скриптов Godot в целом; Языки ставят удобство использования выше производительности.
На техническом уровне, сравнения целых чисел (время константы) будут происходить быстрее, чем сравнения строк (линейное время). Однако, если кто-то хочет сохранить конвенции других языков, то следует использовать целые числа.
Основная проблема с использованием целых чисел возникает, когда кто-то хочет распечатать значение перечисления. В качестве целых чисел при попытке вывести MY_ENUM будет выведено 5 или что-то ещё, а не что-то вроде «MyEnum». Чтобы напечатать целочисленное перечисление, нужно написать Dictionary, который отображает соответствующее строковое значение для каждого перечисления.
Если основной целью использования перечисления является печать значений и кто-то желает сгруппировать их вместе как связанные концепции, то имеет смысл использовать их как строки. Таким образом, отдельная структура данных для выполнения при печати не нужна.
AnimatedTexture против AnimatedSprite2D против AnimationPlayer против AnimationTree
При каких обстоятельствах следует использовать каждый из классов анимации Godot? Ответ может быть не сразу понятен новым пользователям Godot.
AnimatedTexture - это текстура, которую движок рисует как анимированный цикл, а не как статическое изображение. Пользователи могут манипулировать ...
скоростью, с которой он перемещается по каждому участку текстуры (кадров в секунду).
количеством областей, содержащихся в текстуре (кадрах).
Затем RenderingServer Godot последовательно рисует регионы с заданной скоростью. Хорошая новость заключается в том, что это не требует дополнительной логики со стороны движка. Плохая новость в том, что у пользователей очень мало контроля.
Также обратите внимание, что AnimatedTexture — это Resource в отличие от других объектов Node, обсуждаемых здесь. Можно создать узел Sprite2D, который использует AnimatedTexture в качестве текстуры. Или (что другие сделать не могут) можно добавить AnimatedTextures как тайлов в TileSet и интегрировать его с TileMapLayer для множества автоматически анимированных фонов, которые все визуализируются в одном пакетном вызове отрисовки.
Узел AnimatedSprite2D в сочетании с ресурсом SpriteFrames позволяет создавать различные последовательности анимации с помощью спрайт-листов, переключаться между анимациями и управлять их скоростью, региональным смещением и ориентацией. Это делает их хорошо подходящими для управления 2D-анимациями на основе кадров.
Если необходимо вызвать другие эффекты, связанные с изменениями анимации (например, создать эффекты частиц, вызвать функции или манипулировать другими периферийными элементами, помимо покадровой анимации), то необходимо использовать узел AnimationPlayer совместно с AnimatedSprite2D.
AnimationPlayer - это также инструмент, который необходимо использовать, если вы хотите создавать более сложные системы 2D-анимации, такие как ...
Перекладная мультипликация: редактирование преобразований спрайтов во время выполнения.
Анимации 2D-мешей: определяют области для текстуры спрайта и привязки к ней скелета. Затем анимируются кости, которые растягивают и изгибают текстуру пропорционально соотношению костей друг с другом.
Смесь вышеперечисленного.
Несмотря на то, что для разработки каждой отдельной последовательности анимации для игры нужен AnimationPlayer, также может быть полезно комбинировать анимации для смешивания, то есть для обеспечения плавных переходов между этими анимациями. Также может существовать иерархическая структура между анимациями, которую человек планирует для своего объекта. Это случаи, когда нужен AnimationTree. Можно найти подробное руководство по использованию AnimationTree здесь.