An incursion under C#. Протаскиваем F# в Godot
Вы когда-нибудь бывали на боевом задании?
Что вы имеете ввиду?
Вторжение под водой с целью взятия крепости, захваченной элитным подразделением, имеющим в своём распоряжении 15 управляемых снарядов с газом VX.
Godot
— игровой движок, который имеет нативную поддержку dotnet
. К сожалению, эта поддержка до такой степени заточена под C#, что F# она выходит боком. Почти все проблемы разрешимы, но при недостатке опыта они скатываются в большой пластилиново-волосатый валик у самого входа в подземелье, который иногда приводит к преждевременной и бессмысленной гибели. Чтобы избежать этого в данной статье я дам программу-минимум, которая позволит выжить в Godot, но не выжать из него максимум. Это не значит, что у сочетания F# + Godot
нет своих плюшек. Просто мне хотелось съесть вначале сосредоточить всех мух в одном месте, а котлетами заняться потом и в более свободной манере. Также я предполагаю, что на данную статью будут натыкаться как новички в F#, так и новички в Godot, поэтому местами я буду дублировать базовые руководства.
В область любительского геймдева я залез от скуки в попытке развлечь себя, сменив роль игрока на роль разработчика игр. Это хобби без далеко идущих целей, и автономно уходить в геймдев я пока не планирую.
Мне нравится иногда моделировать механики из настольных игр, так как это даёт свои плоды при описании бизнес-логики. Если речь идёт о каком-нибудь филлере, то для управления состоянием игры хватает встроенного в F# REPL. В более сложных сценариях пригождается UI в виде Avalonia / WPF. Я шапочно знаком с Unity и сильно больше с Monogame, но при их использовании никогда не ощущал в себе сил доделать пет-проект до конца. От Godot я ожидал чего-то аналогичного, но оказалось, что этот движок обладает необходимым мне набором готовых компонентов с очень высокой долей проницаемости. Да, есть проблемы с интеграцией F#, но это около-константные величины, которые всё меньше и меньше волнуют меня с ростом кодовой базы. В итоге Godot оказался по сложности где-то между REPL и Avalonia, а по выразительности правее любого UI-фреймворка.
Поддержка .NET в Godot
Ранее Godot использовал Mono
, которого я сторонюсь больше, чем UI-проектов под WinXP. С версии 4.0 Godot переехал на полноценный .NET 6+
, но названия некоторых инструментов по инерции продолжают говорить о Mono
. В .NET
Godot встраивается в виде SDK, который можно скачать с сайта nuget-а целиком или по пакетам. Godot.NET.Sdk
заменяет дефолтный Microsoft.NET.Sdk
, после чего вы получаете доступ ко всем функциям Godot. Механизм напоминает подключение WPF в самых ранних его версиях (ещё для .NET Core
). Однако в отличие от WPF, вы не сможете написать Godot-приложение, используя только dotnet
-проект.
В Godot есть свой язык программирования GDScript
, и он считается основным, в то время как dotnet
(и C#, в частности) поставляется лишь как опция-плагин. Я пока не встретил каких-то существенных различий между API для GDScript
и для dotnet
, и не вижу идеологических предпосылок к их появлению. У нас тот же доступ, что и у родного языка, однако и там, и там мы не в полной мере контролируем внешнюю среду исполнения. Мы вольны бегать по файловой системе, использовать Hopac
, Garnet
, Hedgehog
и всё, что душе угодно, пока это позволяет ось. Однако движок не даст себя запустить через условный App.Run()
, что не является проблемой для конечного продукта, но у нас очередной раз отобрали бесшовный REPL. Формально мы можем подключить все необходимые пакеты в интерактиве, но большинство обращений к Godot-объектами будут заканчиваться ошибками доступа и прочим nativeptr
.
В 4.0 версии в связи с переездом временно пропала возможность собирать .NET
-приложения под мобильные устройства, но с выходом 4.2 она вернулась с экспериментальной звёздочкой. Это не было банальной перестраховкой, так как я уже напоролся на ряд сетевых проблем на Android, которые вынудили полностью переехать на Godot.HttpClient
. Разработчики обещали исправить это позднее, но и текущего workaround-а хватает. Веб-версии приложений нам недоступны, и со слов разработчиков движка, этот момент вряд ли изменится, пока в недрах dotnet
не научатся заводить WASM
в качестве неосновного модуля.
Развёртывание проекта и настройка редактора
Godot доступен здесь. По умолчанию он не включает поддержку dotnet
и её вроде как нельзя догрузить потом, поэтому лучше сразу скачать версию Godot Engine - .NET
. Установщика у Godot нет, скачанные 150 мб можно запустить непосредственно из папки. На всех своих компах я располагаю движок и репозитории godot-проектов одинаковым относительно друг друга способом (причины даны ниже).
Запустив редактор, вы увидите стандартную панель с ранее открывавшимися проектами и т. п. В отличие от VS Godot не создаёт отдельную папку под новый проект, так что это лучше сделать самостоятельно до того, как он загадит общую директорию. Встроенная поддержка gitignore
ничего не знает о специфике dotnet
или Visual Studio, так что от её галочки проще отказаться, взять стандартный dotnet new gitignore
и дополнить его строкой .godot/*
.
После открытия проекта можно будет подправить общие настройки редактора. Например, имеет смысл сразу переключить редактирование C#-скриптов на Visual Studio / Rider выставив опцию в Редактор -> Настройки редактора -> Dotnet -> Редактор -> External Editor
в нужное состояние. На самом деле в половине случаев Godot продолжит использовать внутренний редактор, так что проблема параллельного редактирования исходников с вопросами про Сохранить
VS Перезагрузить
останется. Если вы планируете собирать проект под Android, то в тех же настройках в Экспорт -> Android
можно подружить Godot с уже установленными SDK и "debug.keystore"
. Например, так они выглядят, если на компе уже стоит Visual Studio с расширениями под мобильную разработку:
export/android/android_sdk_path="C:/Program Files (x86)/Android/android-sdk"
export/android/debug_keystore="C:/Users//AppData/Local/Xamarin/Mono for Android/debug.keystore"
В Godot есть собственный формат для описания проектов (project.godot
), ресурсов (.tres
), сцен (.tscn
), настроек самого редактора и, наверное, чего-то ещё, о чём я пока не знаю. Этот формат ориентирован на человека, и при необходимости данные файлы могут быть подправлены руками. Мне он нравится тем, что его можно осмысленно читать в дифах к коммитам. Однако, если я правильно понял, Godot не даёт готового API для работы с этим форматом. Внятных публичных пакетов я также не нашёл.
Условно основным файлом в проекте является project.godot
, в нём преимущественно хранятся глобальные настройки проекта, типа разрешения экрана, основной сцены, настроек dotnet
и т. п. Списка используемых файлов в нём нет, подразумевается, что всё, что лежит в папке с проектом, принадлежит ему. Godot добавит .csproj
только после того, как будет добавлен хотя бы один .cs
-файл или прямо дана команда Проект -> Инструменты -> C# -> Create C# Solution
. .csproj
и .sln
файлы окажутся в корневой папке, рядом с project.godot
. Однако перед этим очень рекомендую поправить имя dotnet
-проекта (это же имя используется для солюшена). Сделать этом можно либо через меню Проект -> Настройки проекта -> Dotnet -> Проект -> Assembly Name
, либо через прямое указание в исходниках .godot
:
[dotnet]
project/assembly_name="SomeName.WithoutSpaces"
Если этого не сделать, то Godot
по умолчанию накидает пробелов между словами и вы получите .sln
и .csproj
файлы с пробельными именами и фрагментарным camelCase
. Одновременно с этим пространства имён в проектах будут без пробелов, и разницу в именах на devops-этапе придётся разруливать руками.
Подключение F#
Далее необходимо открыть .sln
в своей IDE и добавить в решение библиотеку классов F# (я предпочитаю называть его
). У нового проекта надо поменять SDK, для этого достаточно открыть .csproj
от Godot и скопировать шапку C#-проекта в F#:
net6.0
net7.0
net8.0
После нужно будет сослаться из C#-проекта на
.
Здесь сразу стоит запомнить, что Godot как-то нестандартно подтягивает зависимости. Обычно dotnet
без чьей-либо помощи неявно включает пакеты из проектов-зависимостей, но Godot в этом процессе что-то сломал. Все пакеты, что будут добавлены в F#-проект, должны быть в явном виде добавлены в C#-проект, иначе сборка будет падать. Не проверял данную особенность в отношении связки C# -> C#
, но нас она точно касается.
Отладка и настройка запуска
Обычно я редко использую дебаг, однако Godot недоступен из REPL-а, да и в целом является для меня новой средой, так что здесь отладка может быть полезной. Редактор Godot подключается к запускаемой сцене, но F# он дебажить не умеет. Тем не менее он позволяет копаться в актуальном древе сцены (через Панель "Сцена" -> Вкладка "Удалённый"
), и я очень рекомендую обратить внимание на эту фичу. Она предназначена для Godot.Node
(и её наследников) и покрывает лишь экспортируемые свойства (видные в редакторе). Но при желании можно абузить технологию, формируя собственные сугубо отладочные ноды, и откладывать в них необходимую информацию.
F# надо дебажить при помощи основной IDE. Сначала запускаете сцену в Godot, а потом из IDE подключаетесь к запущенному процессу. Конкретно в VS этот пункт прячется в Отладка -> Присоединиться к процессу...
. Это не самый удобный способ, но он полезен, если вы неожиданно загнали какой-то компонент в невалидное состояние и надо, не останавливаясь, изучить его внутренние данные.
Если вы знаете, что собираетесь дебажить, то можно заблаговременно подготовить информацию для подключения. Если пройти в VS по Отладка -> Свойства отладки для проекта
, то откроется окно, в котором можно будет добавить новый профиль с запуском внешнего исполняемого файла и т. д. Переводя на русский, мы можем настроить консольный вызов некоего .exe
-файла вместо стандартного запуска (который не работает для библиотек), и по нажатию F5
запустится сцена, а не сообщение об ошибке. От нас требуется указать путь к исполняемому файлу, аргументы вызова и рабочую директорию. Все эти настройки VS сохраняет в файле Properties/launchSettings.json
, который можно заполнить самостоятельно.
Если вызывать редактор с аргументом --help
(т. е. Godot_v4.2.1-stable_mono_win64.exe --help
), то можно получить здоровенную справку и убедиться в том, что Godot вполне могёт в devops. Нас, правда, больше волнуют настройки запуска. Скажем, через --editor
можно открыть проект в редакторе, а через Relative/Path/To/Scene.tscn
запустить конкретную сцену. Опций там действительно много, и в них имеет смысл погружаться, лишь имея конкретную задачу. Я пока ограничиваюсь небольшим скриптом, который генерирует:
профиль для запуска редактора Godot,
дефолтной сцены,
и каждой сцены в проекте.
[
Profile.RunEditor
Profile.RunGame
let scenes =
System.IO.Directory.EnumerateFiles(
godotProjectDirectory
, "*.tscn"
, System.IO.SearchOption.AllDirectories
)
for fullPath in scenes do
System.IO.Path.GetRelativePath(godotProjectDirectory, fullPath)
|> Profile.RunScene
]
Профили интерпретируются следующим образом:
[]
type Profile =
| RunEditor
| RunGame
| RunScene of ScenePath : string
with
member this.CommandLineArgs = [
"--path"
"."
match this with
| Profile.RunEditor ->
"--editor"
//"--rendering-engine"
//"opengl3"
| Profile.RunGame -> ()
| Profile.RunScene path ->
path
"--verbose"
]
member this.Name =
match this with
| Profile.RunEditor -> "Godot Editor"
| Profile.RunGame -> "Godot Game"
| Profile.RunScene path -> $"Godot {path}"
Этот код элементарен, и думаю, что его нетрудно будет модифицировать, если вам захочется отобразить зоны / пути навигации, и т. д. Остальной код более рутинен, и с ним лучше ознакомиться прямо в источнике.
У скрипта есть изъян, связанный с тем, что он генерирует профили, которые зависят от расположения Godot_v4.2.1-stable_mono_win64.exe
на конкретной машине. Я использую одну и ту же схему взаимного расположения проектов и движка, но в профили попадают абсолютные пути, которые не позволяют безболезненно шарить Properties/launchSettings.json
между компами, из-за чего последний попадает в .gitignore
. При желании эта кустарщина может быть вылечена регистрацией Godot в Environment.Path
.
Godot:
- Engines: // Папка с различными версиями godot.
- Godot_v{godotVersion}-stable_mono_win64
- Godot_v{godotVersion}-stable_mono_win64.exe
- Projects: // Папка с проектами.
- ProjectName:
- ProjectName.csproj
- ProjectName.sln
- ProjectName.Core:
- ProjectName.Core.fsproj
- PrepareLaunchSettings.fsx
- Properties: // Изначально может отсутствовать.
- launchSettings.json
В итоге открытие проекта у меня выглядит следующим образом:
Скачиваем репозиторий;
Открываем солюшен в VS;
Запускаем
PrepareLaunchSettings.fsx
(пропускаем, если профили не протухли с прошлого запуска);Выбираем
Godot Editor
в качестве запускаемого профиля;Запускаем проект без отладки!
В результате данных действий у меня одновременно оказываются запущенными готовые к использованию VS и Godot. Следует учитывать, что по умолчанию сцена отлаживается только в одном из редакторов, а не сразу в двух. Если сцена запускается из VS, то она недоступна для отладки в Godot, и наоборот, запущенное в Godot не отлаживается в VS (если не считать кейса с присоединением к процессу, что был дан парой экранов выше). Последовательный отладочный запуск Godot Editor
из VS и сцены из Godot не даст ничего, так как VS не будет слушать внука. Этот сценарий нужен только для тестирования самописных Godot-тулов.
Первый скрипт сцены и механика проброса
В категориальном аппарате Godot скрипт — это любой файл с кодом класса сцены/ноды на GDScript
/C#. То есть речь идёт об обычном аналоге .fs
, а не .fsx
. Так как в описываемом сценарии .fsx
-скрипты больше упоминаться не будут, я решил сохранить терминологию Godot.
По умолчанию сцены в Godot не имеют скриптов («по-русски» Code Behind
). Их можно подключить, самостоятельно нажав на соответствующую кнопку. Причём скрипт прикрепляется не к сцене вообще, а к любой ноде в сцене. Скриптов в сцене может быть много при условии, что на каждую ноду приходится не более одного скрипта. Я предпочитаю один скрипт на всю сцену, который прикреплён к корневой ноде, что делает процесс похожим на классическое XAML-based приложение, но с точки зрения Godot, данный подход не является каноном.
Далее, прикрепляемый скрипт может быть на любом языке из поддерживаемых (базово это GDScript и C#, но ещё есть некий GDExtensions
). То есть в одной сцене может быть множество скриптов, прикреплённых к разным нодам и написанных на разных языках. Это требует определённых усилий со стороны авторов Godot, но это также позволяет пользователям стравливать сложность по мере роста проекта. Надо понимать, что интеграция F# на данный момент настолько громоздка, что весь этот багаж по большей части будет потерян и его надо будет компенсировать своими силами. Меня этот факт не напрягает, а даже радует, так как таскать инфу с места на место у F# получается гораздо лучше, чем у конкурентов.
Мой алгоритм (акцент на субъективности, а не авторских правах) создания сцены и подключения F#:
Создаём сцену.
В сцене создаём корневую ноду необходимого типа, именуем её
MySceneName
.Сохраняем её под тем же именем в одноимённой папке (
MySceneName/MySceneName.tscn
).Прикрепляем к корневой ноде на C# (
MySceneName.cs
), от шаблонов отказываемся, сохраняем в той же папке.Открываем VS, в F#-проекте создаём
MySceneName.fs
файл.В файле объявляем модуль верхнего уровня
ProjectName.Core.MySceneName
.Открываем
Godot
.Объявляем тип
Main
, который наследуем от типа из пункта 2.Оверрайдим метод
_Ready
заглушкой (ну илиHello world!
).Собираем проект, чтобы в C# прогрузились новые созданные модули, типы и методы.
Открываем в VS
MySceneName.cs
, заменяем тип предка наProjectName.Core.MySceneName.Main
.Оверрайдим метод
_Ready
дефолтной реализацией, которая вызывает метод предка.Готово.
Итоговый код F#:
module ProjectName.Core.MySceneName
open Godot
type Main () =
inherit Node2D()
override this._Ready () =
GD.Print "Hello world!"
Итоговый код C#:
using Godot;
using System;
public partial class MySceneName : ProjectName.Core.MySceneName.Main
{
public override void _Ready() => base._Ready();
}
C#-тип помечен как partial
. Это обязательное требование со стороны Godot, без его соблюдения проект не компилируется. Вызвано это тем, что Godot.SourceGenerators
(идут вместе с Godot.NET.Sdk
) должны создать ещё несколько файлов, где указанный тип через partial
будет расширен методами, отвечающими за интероп с экземпляром (вида HasMember
/CallMember
).
Godot принимает решения о вызове на основе своей проекции мира. Если в этой проекции ничего не сказано про методы _Ready
, _Process
и т. д., то с точки зрения движка их не существует. Это не значит, что, вызвав _Ready()
в dotnet
-коде, вы словите какой-нибудь MissingMemberException
. Такой вызов пройдёт без ошибок. Это значит, что, когда движок поместит данную ноду в древо, он не увидит у неё метода _Ready
и сэкономит наносеки, проигнорировав его вызов. То же самое должно произойти, если вызвать метод _Ready
из GDScript
. Где-то в недрах HasGodotClassMethod
и InvokeGodotClassMethod
вызов завернётся на ошибку.
Godot ничего не знает об F#. Генераторы не умеют писать .fs
-файлы, да к тому же их никто не вызывает. В резульатте F#-типы оказываются без интероп-методов. Движок в рантайме не распознаёт методы в нодах и не утруждает себя заходом в dotnet
.
Выходит, что связка C# -> F#
вызвана не тем, что мы не можем привязать к ноде F#-скрипт (хотя это тоже правда), а тем, что мы не можем воспользоваться встроенным генератором в отношении F#-типа. Я пробовал воспроизвести генератор самостоятельно, но оказалось, что пара низкоуровневых фич в F# просто не заводятся на уровне языка. И это положение не измениться, так как официальная позиция F# (выраженная Светочем Нашим Саймом в ряде ишуев) сводится к: «Вот с этим? Идите наC#.».
С результатом генерации можно ознакомиться через ProjectName -> Зависимости -> Анализаторы -> Godot.SourceGenerators -> _ -> <Имя типа>_<Суффикс генератора>.generated.cs
. Код там довольно тупой, и если всё работает штатно, то системно заглядывать в него смысла нет. Сводится всё к описанию типов в альтернативной нотации, как если бы мы отрефлексировали типы и выразили их в каких-нибудь json
-схемах. Я не заметил там точек, за которые можно было бы зацепиться и наваять что-нибудь эдакое. Эта штука сильно проще, чем DependencyProperty
.
Кроме этого, даже если бы мы смогли переписать генерацию на F#, то возникла бы проблема с отсутствующим partial
. Хитро сшивать рукописные исходники с машинными в рамках одного файла я не хочу, ибо слишком высок риск ввода дополнительных ограничений. Генерировать потомка (или предка, на основе «послезнания») я тоже не хочу, ибо в этом случае у нас всё равно появляется дополнительный тип, который проигрывает C#-наследнику, так как проще сгенерировать тип-заглушку на C#, а дальше официальный генератор Godot настрогает все необходимые обвязки самостоятельно.
После всего этого, когда Godot возьмёт C#-тип в оборот, он даст его интероп-описание, опираясь на описание его предка. Проблема в том, что если предок был определён в F#, то описание предка будет неактуальным. Можно сказать, что все F#-типы в цепочке наследования будут пропущены, пока мы не столкнёмся со следующим C#-типом. Пока что самый простой способ обойти это ограничение — повторно переопределить «F#-члены» в C#. Выглядит неэстетично, лично меня коробит, но задачу свою решает.
Подводя итог, на данный момент лучше всего использовать C# как DSL на стероидах. Мы не пишем никакой логики в .cs
-файлах, только обозначаем имеющиеся члены. Максимально механистичный подход, который в перспективе может быть заменён уже нашим генератором кода.
Общие принципы интеропа
Godot допускает, что иногда будет вызывать обычные ненаследуемые методы и свойства наших типов, если этого потребует GDScript
или сцена (например, в результате подписки на сигнал). Чтобы было быстрее, вызовы производятся не через рефлексию, а через заранее подготовленный контракт. Часть контракта собирается через сумму контрактов предков. Остальную формируют генераторы на основе свойств и методов, определённых непосредственно в типе. Генераторы создадут обёртки для каждого встреченного члена, если посчитают, что его сигнатуру можно описать в доступных движку категориях. Этот список несколько шире, чем совокупность примитивов и наследников GodotObject
, так как движок умеет работать с абстрактными enum
-ами и, наверное, с чем-то ещё.
Судя по всему, генератор подхватывает переопределённые методы просто из-за того, что они есть и они Godot friendly
, а не потому, что он ждёт именно их. Во всяком случае я не увидел разницы в контракте переопределённого метода и одноимённого ему перекрытия. Генератор также игнорирует атрибуты private
, так что инкапсуляция хромает. Способов указать на нежелательность экспорта Godot не даёт.
Забавно, но в нашем случае мы получаем больше контроля от наличия C#-прокладки, чем от её отсутствия. Если мы не продублируем метод, то он не появится в контракте, а, значит, будет недоступен для движка:
member this.VisibleMethod (input : int) = ()
member this.InvisibleMethod (input : int) = ()
// Обращаем внимание на слово `new`.
public new void VisibleMethod(int input) => base.VisibleMethod(input);
GDScript
активно использует дактайпинг. Вполне возможно, что из него вызовут некий метод нашей ноды, не будучи уверенными, что он действительно у неё есть. Для этих целей генератор готовит несколько интероп-методов экземпляра, суть которых сводится к следующему:
match methodName, args.Count with
| "SomeMethod", 1 ->
Vartiants.toInt32 args.[0]
|> this.SomeMethod
|> Variants.fromUnit
// | .. -> ..
| _ ->
base.InvokeGodotClassMethod(method, args)
В оригинале всё это описывается на if
-ах, но обе конструкции много не жрут. Однако в каком-то вырожденном случае в классе с сотней служебных методов и тысячей вызовов матчинг может ударить по производительности. В том числе по этой причине я склонен пробрасывать только те члены типа, что действительно будут использоваться.
Важно различать системные вызовы от диких. _Process
, _Draw
и т. п. методы вызываются напрямую, но какой-нибудь таймер каждый раз дёргает _on_enemy_spawn_timer_timeout
именно через InvokeGodotClassMethod
. В этом можно убедиться через дебагпоинты в .generated.cs
-файлах.
Экспортируемые свойства и атрибуты Godot
Godot позволяет маркировать некоторые свойства типа как доступные для редактора. Для этого свойство должно быть промаркировано атрибутом Export
. Вешать атрибуты в F# бесполезно, генератор слеп, так что рабочей схемой является объявление нового одноимённого свойства с этим же атрибутом. Сначала объявляем его в исходнике:
member val Value = 42 with get, set
А потом перекрываем его в проекции:
[Export]
public new int Value
{
get => base.Value;
set => base.Value = value;
}
Лишний Export
в F# никак не скажется на результате, так что писать его или нет, решаете сами. С учётом гипотетического генератора я завёл отдельный MustBeExportAttribute
, который ставлю на свойства в F#.
Также следует знать, что Export
не модифицирует исходный код, так что никаких чужих NotifyPropertyChanged
никто не вставляет. Нашу дополнительную логику также никто не трогает, так что оттуда можно триггерить события / сигналы.
С другой стороны, интероп не поддаётся какой-либо кастомизации, так что дублирование свойств позволяет слегка шаманить с конвертацией DU
, option
и list
. Это избавляет нас от некоторого мусора, но усложняет логику в C#. Для таких случаев я предпочёл бы использовать адаптеры с отдельной описательной моделью. Это особенно актуально ввиду дополнительных опций кастомизации видимости и группировки свойств в редакторе, которые в данный момент растянуты между атрибутами и оверрайдом.
Сцены, исчерпывающиеся скриптами
В случае корневых нод сцен создание экземпляра происходит на стороне Godot (ну либо через PackedScene
), что почти автоматически заменит дикого степного F#-предка на огодотнутого C#-потомка. В этом случае нам не надо беспокоиться о конструкторе или фабрике. Однако в моей практике, типы с .tscn
-файлами — это скорее исключение, чем норма. Я спавню большое количество типов, которые существуют только в виде скриптов. Так что между module ProjectName.Core.MySceneName
и type Main
может быть определено ещё десяток или два Godot-типов поменьше. Где-то пятая часть из них может содержать переопределённые методы.
Создавать их напрямую нельзя, так как для движка такие экземпляры будут пустыми. Нам снова нужны потомки, пропущенные через жернова генераторов. В C# в этом месте прикрутили бы DI, но я вряд ли буду его использовать где-либо ещё, так что лично мне хватит и обычного IVar
мутабельного поля с фабрикой непосредственно в типе:
type Minor () =
inherit Node2D()
static member val Factory =
// Func только из-за того, что C# слишком вербозно описывает FSharpFunc и Unit.
System.Func(fun () -> failwith "Factory is empty!")
with get, set
static member create () = Minor.Factory.Invoke ()
override this._Ready () =
GD.print "Hello from Minor!"
Предполагается, что вместо конструктора Minor
будет использоваться метод create
:
this.AddChild ^ Minor.create()
Далее надо определить инициализацию фабрики в потомке:
public partial class Minor : GodotFSharp.Core.Main.Minor
{
public static void Initialize()
{
GodotFSharp.Core.Main.Minor.Factory = () => new Minor();
}
public override void _Ready() => base._Ready();
}
И, наконец, вызвать Minor.Initialize
в Program.Main
. Проблема в том, что никакого Program.Main
не существует, так как Godot вообще не даёт нам привычного EntryPoint
-метода. Вместо него, в Godot есть механизм Autoload
, который предназначен для инициализации глобальных singleton
-ов. В контексте движка это ноды, которые существуют независимо от открытой сцены, но к которым всегда есть доступ через this.getNode "root/SingletonName"
. Они предназначены для хранения глобального состояния, типа игровых настроек, профиля игрока и т. д. И этим объясняется некоторая неказистость фичи применительно к нашей задаче.
Нам нужно создать ещё один тип ноды, но теперь только на стороне C#. Описать в ней процедуру инициализации общую для всего проекта и вызвать её в методе _Ready()
:
public partial class EntryPoint : Godot.Node
{
private bool Initialized = false;
private static void Initialize()
{
// Упоминаем все типы.
Minor.Initialize();
// Либо заводим DI классическим способом.
}
public override void _Ready()
{
if (!Initialized)
{
Initialized = true;
Initialize();
}
}
}
Далее в Godot-редакторе следует открыть Проект -> Настройки проекта...
, выбрать вкладку Автозагрузка
, открыть файл EntryPoint.cs
и Добавить
его в список глобальных переменных. Теперь, независимо от выбранной сцены, будет создаваться EntryPoint
, который настроит Minor
и другие типы.
Вместо сигналов
Сигналы в Godot выполняют роль событий, но их реализация и использование достаточно сильно отличаются от привычных нам IEvent<_,_>
. Вместо того, чтобы выразить сигналы в выделенных объектах (как в F#), создатели размазали их по экземпляру в виде архаических конструкций. Из-за этого F# нельзя использовать как первичный источник данных, приходится обмазываться абстрактными методами и реализовывать их в потомке. Выглядит всё жутковато, а попытки облагородить оборачиваются библиотечным кодом, чего в рамках данной статьи я всячески пытался избежать.
Я вернусь к этой теме в следующих статьях, а сейчас могу лишь сказать следующее:
Можно и нужно использовать сигналы, которые уже есть в существующих типах.
Свои сигналы имеет смысл определять только в том случае, если предполагается интероп с
GDScript
.Во всех остальных случаях лучше использовать обычные события,
Hopac
,ECS
и т. д.
Заключение
Основные моменты подключения F# к Godot я осветил. Получилось C#-ориентированно, но этого должно хватить, чтобы пройти туториалы, не изобретая велосипед. Небольшой пример проекта на основе этих данных находится здесь.
Дальше в моих планах поговорить об F#-специфичных подходах и фичах. Пока не знаю, как быстро это произойдёт, так что дам напутствие:
Godot изобилует конструкциями, рассчитанными на ООП-шный стиль. Благодаря мультипарадигмальности F# их можно и нужно эксплуатировать пока это выгодно, и по необходимости отбрасывать, когда количество или сложность бизнес-логики превысят комфортные объёмы.
Godot намертво скрепляет собственный контракт с синтаксическими конструкциями. Это C#-way, это хроническое, и оно не лечится. К счастью, у нас есть ниша в виде смычки между F# и C#, которая позволяет держать этот дефект в загончике. Если чувствуете, что какая-то часть контракта (не логики) начинает уходить в отдельный объект, не сопротивляйтесь. Скорее всего, это оправдано естественным порядком вещей.
Godot ратует за композицию, но ни в официальных, ни в фанатских руководствах эта линия практически не развивается. Если вы новичок и слабо понимаете, о чём идёт речь, то рекомендую первое время сводить данную концепцию к одной практической максиме: Создавайте гораздо больше типов, чем привыкли. Этого хватит, чтобы усреднённая сцена начала распадаться на 5–6 крупных компонентов с набором связей между ними, которые все вместе оперируют ворохом типов поменьше. Подчеркну, что речь не идёт о разбитии сцены в Godot. Все эти компоненты спокойно размещаются между
module
иtype Main
и обретают полную самостоятельность лишь при необходимости.Если вы в состоянии сформировать подходящий DSL, то редактор Godot нужен будет только для визуального позиционирования объектов в пространстве, и то далеко не всех. Всё остальное можно делать, не выходя из F#.
Автор статьи @kleidemos
НЛО прилетело и оставило здесь промокод для читателей нашего блога:
— 15% на заказ любого VDS (кроме тарифа Прогрев) — HABRFIRSTVDS