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.

Data preferences

Вы когда-нибудь задумывались, следует ли подходить к проблеме 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>.

Непрерывные хранилища памяти подразумевают следующую производительность операций:

  • Итерация: Самый быстрый. Отлично подходит для циклов.

    • Операции: всё, что он делает, это увеличивает счётчик, чтобы перейти к следующей записи.

  • Вставка, стирание, перемещение: В зависимости от позиции. Обычно медленно.

    • Операции: добавление/удаление/перемещение содержимого включает перемещение соседних записей (чтобы освободить место/заполнить пространство).

    • Быстрое добавление/удаление с конца.

    • Медленное добавление/удаление из произвольной позиции.

    • Самое медленное добавление/удаление спереди.

    • Если делать много вставок/удалений спереди, то ...

      1. инвертировать массив.

      2. выполнить цикл, который выполняет изменения массива в конце.

      3. повторно инвертировать массив.

      Это делает только 2 копии массива (по-прежнему с постоянным временем, но медленно) по сравнению с копированием примерно 1/2 массива, в среднем, N раз (линейное время).

  • Получить, установить: Самый быстрый по позиции. Может запрашивать 0-ю, 2-ю, 10-ю запись и т. д., но не может указать, какую запись вы хотите.

    • Операции: 1 операция сложения от начальной позиции массива до желаемого индекса.

  • Поиск: Самый медленный. Определяет индекс/позицию значения.

    • Операции: необходимо перебирать массив и сравнивать значения, пока не будет найдено совпадение.

      • Производительность также зависит от того, нужен ли вам исчерпывающий поиск.

    • Если сохранять порядок, пользовательские операции поиска могут привести его к логарифмическому времени (относительно быстро). Однако пользователям-непрофессионалам это не понравится. Выполняется путем повторной сортировки массива после каждого редактирования и написания алгоритма поиска с упорядочением.

Godot реализует словарь как OrderedHashMap<Variant, Variant>. Движок хранит небольшой массив (инициализированный 2^3 = 8 записями) пар ключ-значение. Когда кто-то пытается получить доступ к значению, они предоставляют ему ключ. Затем он хеширует ключ, т.е. преобразует его в число. «Хеш» используется для вычисления индекса в массиве. В качестве массива OHM затем выполняет быстрый поиск в «таблице» ключей, сопоставленных со значениями. Когда HashMap становится слишком полным, он увеличивается до следующей степени 2 (так, 16 записей, затем 32 и т.д.) и перестраивает структуру.

Хеши предназначены для уменьшения вероятности конфликта ключей. Если это произойдет, таблица должна пересчитать другой индекс для значения, учитывающего предыдущую позицию. В целом это приводит к постоянному доступу ко всем записям за счет памяти и некоторой незначительной операционной эффективности.

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

    • Операции хеширования выполняются с постоянным временем, поэтому, даже если алгоритм должен выполнять несколько операций, пока количество вычислений хеша не станет слишком зависимым от плотности таблицы, все будет оставаться быстрым. Что приводит к...

  2. Поддержание постоянно растущего размера стола.

    • HashMaps сохраняют промежутки неиспользуемой памяти, разбросанные по таблице, с целью уменьшения хеш-коллизий и поддержания скорости доступа. Вот почему он постоянно увеличивается в размере квадратично в степени 2.

Как можно догадаться, словари специализируются на задачах, которых массивы не выполняют. Ниже приводится обзор их рабочих характеристик:

  • Итерация: Быстро.

    • Операции: обойти внутренний вектор хешей карты. Вернуть каждый ключ. После этого пользователи используют клавишу, чтобы перейти к желаемому значению и вернуть его.

  • Вставка, стирание, перемещение: Самый быстрый.

    • Операции: Хешировать данный ключ. Выполнить 1 операцию сложения, чтобы найти соответствующее значение (начало массива + смещение). Ход - два таких (одна вставка, одна стирание). Карта должна быть обновлена, чтобы сохранить свои возможности:

      • обновить упорядоченный список записей.

      • определить, требует ли плотность стола необходимость увеличения вместимости стола.

    • Словарь запоминает, в каком порядке пользователи вставляли его ключи. Это позволяет ему выполнять надежные итерации.

  • Получить, установить: Самый быстрый. То же, что и поиск по ключу.

    • Операции: То же, что и вставка/стирание/перемещение.

  • Поиск: Самый медленный. Определяет индекс/позицию значения.

    • Операции: необходимо перебирать записи и сравнивать значение, пока не будет найдено совпадение.

    • Обратите внимание, что Godot не предоставляет эту функцию "из коробки" (потому что она не предназначены для этой задачи).

Godot реализует Объекты как глупые, но динамические контейнеры содержимого данных. Объекты запрашивают источники данных, когда задают вопросы. Например, чтобы ответить на вопрос "есть ли у вас свойство под названием 'position'?", Он может спросить его script или ClassDB. Более подробную информацию о том, что такое объекты и как они работают, можно найти в статье Применение объектно-ориентированного подхода в Godot.

Важной деталью здесь является сложность задачи Объекта. Каждый раз, когда он выполняет один из этих запросов с несколькими источниками, он проходит через несколько итерационных циклов и поисков HashMap. Более того, запросы представляют собой линейные операции, зависящие от размера иерархии наследования объекта. Если класс, который запрашивает Object (его текущий класс), ничего не находит, запрос передается следующему базовому классу, вплоть до исходного класса Object. Хотя каждая из этих операций является быстрой по отдельности, тот факт, что она должна выполнять так много проверок, делает их медленнее, чем обе альтернативы для поиска данных.

Примечание

Когда разработчики упоминают, насколько медленным является API сценариев, они ссылаются именно на эту цепочку запросов. По сравнению с скомпилированным кодом C++, в котором приложение точно знает, куда идти, чтобы что-то найти, операции API сценариев неизбежно займут гораздо больше времени. Они должны найти источник любых соответствующих данных, прежде чем они смогут попытаться получить к нему доступ.

Причина по кторой GDScript работает медленно это потому что каждоыа операцыа дожна проходит через эту систему.

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

NativeScript C++ идет еще дальше и по умолчанию сохраняет все внутри. Вызовы во внешние структуры будут проходить через скриптовый API. В NativeScript C++ регистрация методов для предоставления их скриптовому API выполняется вручную. Именно в этот момент внешние классы, не относящиеся к C++, будут использовать API для их поиска.

Итак, если предположить, что один из них расширяется от ссылки для создания структуры данных, такой как массив или словарь, зачем выбирать объект вместо двух других вариантов?

  1. Control: С объектами появляется возможность создавать более сложные структуры. Можно накладывать абстракции на данные, чтобы гарантировать, что внешний API не изменится в ответ на изменения внутренней структуры данных. Более того, объекты могут иметь сигналы, позволяющие реагировать на них.

  2. Clarity: Объекты являются надежным источником данных, когда речь идет о данных, которые для них определяют сценарии и классы движка. Свойства могут не содержать ожидаемых значений, но не нужно беспокоиться о том, существует ли это свойство вообще.

  3. Convenience: Если кто-то уже имеет в виду аналогичную структуру данных, то расширение существующего класса значительно упрощает задачу построения структуры данных. Для сравнения: массивы и словари не подходят для всех возможных вариантов использования.

Объекты также дают пользователям возможность создавать еще более специализированные структуры данных. С его помощью можно создать собственный список, дерево двоичного поиска, множество, дерево отображения, график, непересекающийся набор и любые другие варианты.

"Почему бы не использовать Node для древовидных структур?" можно спросить. Ну, класс Node содержит вещи, которые не имеют отношения к пользовательской структуре данных. Таким образом, при построении древовидных структур может быть полезно создать собственный тип узла.

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 vs. 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 Mesh animations: определение области для текстуры спрайта и привязка к ней скелета. Затем анимируются кости, которые растягивают и изгибают текстуру пропорционально соотношению костей друг с другом.

  3. Смесь вышеперечисленного.

Несмотря на то, что для разработки каждой отдельной последовательности анимации для игры нужен AnimationPlayer, также может быть полезно комбинировать анимации для смешивания, то есть для обеспечения плавных переходов между этими анимациями. Также может существовать иерархическая структура между анимациями, которую человек планирует для своего объекта. Это случаи, когда светится AnimationTree. Можно найти подробное руководство по использованию AnimationTree здесь.