GDScript

Теги: gamedev 

GDScripts разработан как пользовательский язык сценариев для использования с игровым движком Godot.

GDScript — это объектно-ориентированный и императивный язык программирования, созданный для Godot. Его особенности включают в себя:

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

GDScript выглядит как Python, когда вы структурируете свои блоки кода с помощью отступов, но на практике он работает иначе. Он вдохновлен несколькими языками, включая Squirrel, Lua и Python.

Особенности

Каждый файл GDScript неявно является классом. Технически скрипт - не класс. Вместо этого он сообщает движку о последовательности инициализации, необходимой для запуска build-in классов. Внешние классы обращаются к собственной БД Godot, котоаря обеспечивает доступ к информации классов в рантайм. Эта дб хранит свойства, методы, константы и сигналы. Прикрепление скриптов к объектам расширяет эти осущности.

Ключевое слово extends определяет класс, которому этот сценарий наследует или расширяет. К примеру для Sprite2D.gd запись в начале файла extend Sprite2D означает, что наш скрипт получит доступ ко всем свойствам и функциям ноды Sprite2D, включая классы, которые он расширяет, такие как Node2D, CanvasItem и Node.

В GDScript, если вы опустите строку с extends, ваш класс будет неявно расширять RefCounted, который Godot использует для управления памятью вашего приложения.

К унаследованным свойствам относятся те, которые вы можете увидеть в доке Inspector, например, texture ноды.

По умолчанию Inspector отображает свойства ноды в «Заглавном регистре» с заглавными буквами, разделенными пробелом. В коде GDScript эти свойства находятся в «snake_case», то есть в нижнем регистре со словами, разделенными символом подчеркивания

func _init():
    print("Hello, world!")

Ключевое слово func определяет новую функцию с именем _init. Это специальное имя для конструктора нашего класса. Механизм обращается _init() к каждому объекту или ноде при их создании в памяти, если вы определяете эту функцию.

var speed = 400
var angular_speed = PI

Входящие переменные располагаются в верхней части скрипта после любых строк extends, но перед функциями. К примеру, каждый экземпляр ноды с прикрепленным к нему скриптом будет иметь собственную копию свойств speed и angular_speed. Ноды в Godot по умолчанию работают в радианах, но у вас есть встроенные функции и свойства, если вы предпочитаете вместо этого вычислять углы в градусах.

Игры работают, отображая множество изображений в секунду, каждое из которых называется кадром, и они делают это в цикле. Мы измеряем скорость, с которой игра создает изображения, в кадрах в секунду (FPS). Большинство игр нацелены на 60 кадров в секунду, хотя вы можете найти такие цифры, как 30 кадров в секунду на более медленных мобильных устройствах или от 90 до 240 для игр виртуальной реальности.

Разработчики движка и игры делают все возможное, чтобы обновлять игровой мир и рендерить изображения с постоянным интервалом времени, но всегда есть небольшие различия во времени рендеринга кадров. Встроенный параметр delta делает наше движение независимым от частоты кадров.

В примере ниже мы используем собственную функцию класса Node для вращения объекта, к которому прикреплен скрипт.

func _process(delta):
    rotation += angular_speed * delta

_process(), как _init(), начинается с подчеркивания. По соглашению, функции Godot, то есть встроенные функции, которые вы можете переопределить для связи с движком, начинаются с символа подчеркивания.

rotation - это свойство класса. Оно работает с радианами и управляет вращением. rotation += angular_speed * delta. В редакторе кода вы можете щелкнуть, удерживая клавишу Ctrl, любое встроенное свойство или функцию, например position, rotation, или _process и открыть соответствующую документацию на новой вкладке.

Переменные внутри функции определяют локальные переменные в области действия функции.

func _process(delta):
    rotation += angular_speed * delta

    var velocity = Vector2.UP.rotated(rotation) * speed

    position += velocity * delta

Мы определяем локальную переменную с именем velocity, двумерный вектор, представляющий как направление, так и скорость. Чтобы заставить узел двигаться вперед, мы начинаем с константы класса Vector2 Vector2.UP, вектора, направленного вверх, и поворачиваем его, вызывая rotated(). Это выражение Vector2.UP.rotated(rotation) представляет собой вектор, указывающий вперед относительно нашего оринетира. Умноженное на наше speed, он дает нам скорость, которую мы можем использовать для перемещения ноды. position так же имеет встроенный тип Vector2

Пользовательский ввод

У вас есть два основных инструмента для обработки ввода игрока в Godot:

  • Встроенные колбеки ввода, в основном _unhandled_input(). Например _process(), это встроенная функция, которую Godot вызывает каждый раз, когда игрок нажимает кнопку. Это инструмент, который вы захотите использовать для реагирования на события, которые не происходят в каждом кадре, например, нажатие Space для прыжка. Подробнее
  • Синглтон Input. Синглтон — это глобально доступный объект. Godot предоставляет доступ к нескольким сценариям. Это правильный инструмент для проверки ввода в каждом кадре.

Подробный пример с Input смотри тут.

Сцены

Сцены в Godot - это любой реюзабельынй обхект игры, от персонажа до обстановки уровня.

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

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

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

Эти лучшие практики ООП имеют несколько последствий для лучших практик в структуре сцены и использовании скриптов.

Если это вообще возможно, следует проектировать сцены без зависимостей. То есть надо создавать сцены, которые держат в себе все необходимое.

Если сцена должна взаимодействовать с внешним контекстом, рекомендуется использовать Dependency Injection. Этот метод предполагает, что высокоуровневый API предоставляет зависимости от низкоуровневого API. Зачем это делать? Потому что классы, которые полагаются на свой внешний контекст, могут непреднамеренно вызывать ошибки и неожиданное поведение.

Чтобы добиться этого необходимо предоставить данные, а затем полагаться на родительский контекст для их инициализации:

  • Подключиться к сигналу. Чрезвычайно безопасный способо, но его следует использовать только для того, чтобы «отреагировать» на поведение, а не запускать его. Обратите внимание, что имена сигналов обычно представляют собой глаголы в прошедшем времени, такие как «введенный», «навык_активированный» или «предмет_собранный».
  • Вызов метода. Используется для начала поведения.
  • Инициализация свойства Callable. Безопаснее, чем метод, поскольку владение методом не требуется. Используется для начала поведения.
  • Инициализировать ссылку на узел или другой объект.
  • Инициализировать NodePath.

Эти опции скрывают точки доступа от дочернего узла. Это, в свою очередь, удерживает Child в слабой связи с окружением. Его можно повторно использовать в другом контексте без каких-либо дополнительных изменений в его API.

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

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

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

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

Выбор структуры дерева узлов

Итак, разработчик может знать, что он хочет делать, какие системы он хочет иметь, но где их все разместить? Можно построить деревья узлов бесчисленным количеством способов, но есть общие рекомендации.

В игре всегда должна быть своего рода «точка входа»; место где все начинается. Это место также служит обзором всех остальных данных и логики программы. Для традиционных приложений это будет «основная» функция. В данном случае это будет основной узел Node "Main" (main.gd).

Затем у каждой игры есть свой настоящий внутриигровой «Мир» (2D или 3D). Это может быть дочерний элемент Main. Кроме того, для их игры потребуется основной графический интерфейс, который управляет различными меню и виджетами, необходимыми для проекта.

Node “Main” (main.gd) -> Node2D/Node3D “World” (game_world.gd) -> Control “GUI” (gui.gd)

При изменении уровней можно поменять местами потомков узла «World». Изменение сцен вручную дает пользователям полный контроль над тем, как меняется их игровой мир.

Следующим шагом будет рассмотрение того, какие системы геймплея требуются для вашего проекта. Если требуется система, которая:

  • отслеживает все свои данные внутри
  • должна быть глобально доступна
  • должна существовать изолированно

тогда нужно создать узел “singleton” автозагрузки.

Для небольших игр более простой альтернативой с меньшим контролем было бы наличие синглтона «Game», который просто вызывает метод SceneTree.change_scene_to_file() для замены содержимого основной сцены. Эта структура более или менее сохраняет World в качестве основного узла.

Любой графический интерфейс также должен быть одноэлементным; быть преходящей частью World; или быть добавленным вручную как прямой дочерний элемент корня. В противном случае узлы GUI также удаляли бы себя во время перехода между сценами.

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

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

Ключом к организации сцены является рассмотрение SceneTree в реляционных, а не пространственных терминах. Зависят ли узлы от существования своего родителя? Если нет, то они могут существовать сами по себе где-то еще. Если они зависимы, то само собой разумеется, что они должны быть детьми этого родителя (и, вероятно, частью сцены этого родителя, если они еще не являются).

Означает ли это, что сами узлы являются компонентами? Нисколько. Деревья узлов Godot формируют отношение агрегации, а не отношения композиции. Но хотя у вас все еще есть возможность перемещать узлы, все же лучше, когда такие перемещения не нужны по умолчанию.

Когда лучше использовать сцены, а когда скрипт

Скрипты определяют расширение класса движка с императивным кодом, сцены с декларативным кодом.

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

Анонимные типы

Можно полностью определить содержание сцены, используя только скрипт. Это, по сути, то, что делает редактор Godot, только в конструкторе C++ своих объектов.

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

const MyNode = preload("my_node.gd")
const MyScene = preload("my_scene.tscn")
var node = Node.new()
var my_node = MyNode.new() # Same method call
var my_scene = MyScene.instantiate() # Different method call
var my_inherited_scene = MyScene.instantiate(PackedScene.GEN_EDIT_STATE_MAIN) # Create scene inheriting from MyScene

Кроме того, скрипты будут работать немного медленнее, чем сцены, из-за различий в скорости кода движка и скрипта. Чем больше и сложнее узел, тем больше причин строить его как сцену.

Именованные типы

Скрипты могут быть зарегистрированы как новый тип в самом редакторе. Это отобразит его как новый тип в диалоговом окне создания узла или ресурса с необязательным значком. Таким образом, возможности пользователя по использованию скрипта значительно упрощаются. Вместо того чтобы:

  • Знать базовый тип скрипта, который вы хотели бы использовать.
  • Создать экземпляр этого базового типа.
  • Добавить скрипт в узел.

… с зарегистрированным скриптом можно создавать скрипты непосредственно из диалогового окна.

Существует две системы регистрации типов.

Пользовательские типы:

  • Только для редактора. Имена типов недоступны во время выполнения.
  • Не поддерживает унаследованные пользовательские типы.
  • Средство инициализации. Создает узел со скриптом. Больше ничего.
  • Редактор не распознает тип скрипта или его связь с другими типами движков или скриптами.
  • Позволяет пользователям определять значок.
  • Работает для всех языков сценариев, поскольку имеет дело с ресурсами сценария абстрактно.

Классы сценариев

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

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

Последний аспект, который следует учитывать при выборе сцен и скриптов, — это скорость выполнения.

По мере увеличения размера объектов размер скриптов, необходимый для их создания и инициализации, становится больше. Создание иерархий узлов демонстрирует это. Логика каждого узла может состоять из нескольких сотен строк кода.

Код скрипта, намного медленнее, чем код C++ на стороне движка. Каждая инструкция вызывает API-интерфейс сценариев, что приводит к множеству «поисков» на серверной части, чтобы найти логику для выполнения.

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

В итоге:

  • Если кто-то хочет создать базовый инструмент, который будет повторно использоваться в нескольких различных проектах и ​​который, вероятно, будут использовать люди всех уровней квалификации (включая тех, кто не называет себя «программистами»), то есть вероятность, что это скрипт, вероятно, с пользовательским именем/значком.
  • Если кто-то хочет создать концепцию, характерную для его игры, то это всегда должна быть сцена. Сцены легче отслеживать/редактировать, и они обеспечивают большую безопасность, чем сценарии.

Полезные ссылки:

Смотри еще: