Как я подружил Unity3D и F#

в 21:29, , рубрики: .net, game development, Mono, unity3d, Visual Studio, Компиляторы, Программирование, функциональное программирование, метки: , , , , , , ,

Как я подружил Unity3D и F#
В последнее время я стал все больше и больше интересоваться функциональным программированием, и при выборе языка предо мною пал выбор среди двух очень понравившихся мне языков — Haskell и F#.
В F# меня соблазнило то, что его можно компилировать в MSIL сборки, что обеспечивает возможность использования библиотек классов F# в других языках Microsoft .Net, а также то, что он и сам может их использовать. Ко всему прочему, я ещё и начинающий разработчик Unity3D, и мне в голову пришла мысль: если компилируется в MSIL, то может можно использовать F# скрипты в Unity? Гугление дало ответ: по-человечески нельзя. Можно создать библиотеку классов, поставить в проекте ссылки на библиотеку UnityEngine.dll, компилировать и импортировать как ассет, после чего добавлять компоненты Mono-behaviour напрямую из библиотеки, но это не слишком удобно, согласитесь. Однако, пройдя гугл, Reflection и справку по Unity, мне все таки удалось приблизить(но не повторить в точности) работу с F# скриптами внутри редактора к тому виду, в котором производится работа со скриптами на встроенных языках. Подробности — под хабракатом.

Часть нулевая

Я допускаю что сделал что-то не так (и скорее всего так и есть), и тут могут быть как грубые, так и не очень, ошибки (тем не менее я буду рад узнать о них или о том, как можно осуществить то или иное действие лучше/красивее). Также может иметь место эффект телемастера и вы просто не получите тех ошибок/глюков, которые я описываю в этой статье. Я просто описываю свои действия и наблюдения. Прошу отнестись с пониманием. В конце концов, на то я и начинающий чтобы учиться, делать ошибки и исправлять их!

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

Часть первая
Как это выглядит «из коробки»

То есть, как это выглядит без каких-либо плюшек. Я буду использовать уже компилированную библиотеку с одним простым скриптом.Вот он:
(здесь и ниже используется подсветка Haskell но знайте что это все-таки F#)

//Please, don't try to change namespace
namespace Assembly_FSharp_vs
open UnityEngine;
type public SphereMoving () = 
        inherit UnityEngine.MonoBehaviour()
        member public this.Start () = UnityEngine.Debug.Log("initialized") 
        member public this.Update() = 
            let mutable newpos:Vector3 = Vector3.zero 
            newpos.x  <- Mathf.Sin(Time.time) 
            newpos.y  <- Mathf.Cos(Time.time)
            this.transform.position <- newpos 

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

Как я подружил Unity3D и F#

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

Как уже успел заметить внимательный анонимус читатель, не обошлось и без костылей — приходится класть библиотеку FSharp.Core.dll рядом с каждой, разрази меня гром, КАЖДОЙ библиотекой, чтобы она импортировалась без генерации компилятором исключения System.TypeLoadException.
А если быть точным, то не только FSharp.Core.dll, а библиотеки ВСЕХ зависимостей, конфликты с которым компилятор Unity не в силах разрешить самостоятельно. Тут становится очевидно, что игра с F# не стоит свеч, и писать на нем что-то под Unity3d приемлемо разве что из животного любопытства, да и то в не очень то уж и больших объемах.

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

Я бы не стал в описании следующей части как-то углубляться в дебри, если бы не два НО:
1. Класс System.CodeDom.Compiler.CodeDomProvider не может предоставить компилятор для F#.
2. Все классы из пространства имен System.CodeDom.Compiler попросту «невидимы» для компилятора Unity. Даже если есть ссылка в проекте и Visual Studio говорит о том что все в порядке, то компилятор просто покажет большой и толстый лог, в котором расскажет и покажет, что нельзя так делать и что он ни сном ни духом о содержимом System.CodeDom.Compiler. Даже если вы подключаете библиотеку, которая тем или иным образом ссылается на System.CodeDom.Compiler, то компилятор все-равно будет с вами категорически не согласен — в логе вы получите сообщение, подобное этому:

Internal compiler error. See the console log for more information. output was:
Unhandled Exception: System.TypeLoadException: Could not load type 'System.CodeDom.Compiler.CompilerResults' from assembly 'System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.

На этом история бы и закончилась если бы не одно но: классы из этого пространства имен можно обнаружить в сборке уже в процессе выполнения путем простого перебора коллекции доступных в текущем домене сборок и вывода имен всех типов в лог, а значит, можно использовать Reflection для создания объектов и вызова методов(впрочем, Reflection впоследствии подкинет ложку дёгтя, и я обьясню где, но ниже).

Часть вторая
Укрощение строптивого. Получаем и эксплуатируем CodeDomProvider для F#

Но если второе «НО» ещё можно обойти, то как быть со первым?
Как нельзя кстати тут оказался F# Power Pack который был найден в процессе гугления, который, думаю, пригодится всем, кто так или иначе использует F#. На этот шаг меня натолкнул вброс исключения ConfigurationErrorsException методом CodeDomProvider.CreateProvider при вызове с аргументами(поочерёдно) «FSharp» (подобно тому как создается CodeDomProvider для C# — аргумент «CSharp») и «F#»(авось проскочит?).

F# Power Pack и предоставляет желанный Microsoft.FSharp.Compiler.CodeDom.FSharpCodeProvider, который по использованию почти не имеет отличий по сравнению с обычным CodeDomProvider из пространства имен System.CodeDom.Compiler.

Казалось бы, дело за малым — подключай и пользуй, но и тут есть свои подводные камни:
1. По причине того, что эта библиотека не входит в пакет .Net Framework, то её следует класть в проект.
2. Сам FSharpCodeProvider обращается к типам из System.CodeDom.Compiler и поэтому можно получить стойкое System.TypeLoadException при компиляции, а студийный IntelliSense не поведет и носом.
Волею Xаоса у меня получилось именно так и поэтому я все сделал с использованием Reflection и Activator'а. Возможно, все-таки я где-то и допустил ошибку, но уже слишком поздно что-то менять. Следует также заметить, что изначально написанная мною для этих целей библиотека (на все том-же F#), которую я до ума не довел, компилировалась без таких фокусов(т.е. не приходилось методы в FSharpCodeProvider запускать через Reflection), но имела все те-же проблемы с многострадальным пространством имен System.CodeDom.Compiler, что решалось (опять таки) через Reflection. Кому любопытно, может ознакомиться с коротенькой пастой тут.

А почему автор не дописал библиотеку на F#?

А потому, что я собирался на F# написать не только код, который бы смог все собрать и компилировать, но и код Editor-скриптов. Но тут раскрылась одна интересная деталь — Editor-классы на F# отправляют редактор Unity3D в безоговорочный нокаут. Никаких ни сообщений об ошибке, ни всплывающих окон, любезно предлагающих отправить данные об ошибке(вместе со всем вашим проектом и вашими комментариями) разработчикам движка, ни повисшего процесса, ни MessageBox'а. Ничего! Пустота. Такое поведение обычно наблюдается при переполнении стека или ошибки в каких-то Unmanaged библиотеках, что обнаруживалось с помощью OllyDbg, но, увы, он мне сейчас нужен чуть менее, чем хоть как-нибудь, и качать его только для того чтобы пытаться(!) выпытать причину падения было банально лень.
Для любопытствующих предоставляю пасту с кодом Editor-класса на F#.ВНИМАНИЕ! Возможно, что после добавления библиотеки с этим классом, то, что получится в результате компиляции попадет в каталог Library, откуда вытравить его будет проблематично, в результате чего вы получите стойкий «неоткрывак» (по причине падения редактора из-за кривого Editor-класса) при попытке включить редактор. Придется удалять папку Library, что обычно влечет за собой наипечальнейшие последствия. Я предупредил.

Возвращаясь к нашему CodeDomProvider'у напоминаю, что многое делается через Reflection. А вот и обещанная ложка дегтя в бочке статистики — при попытке получить тип(!) System.CodeDom.Compiler.CompilerParameters с помощью Activator'а я получаю исключение NotSupportedException по причине "the type being created does not derive from MarshalByRefObject", что является весьма печальным, ибо параметры компилятору задать все-таки нужно. Для этого приходится действовать не слишком изящно — нужно найти System.dll в коллекции сборок в текущем домене и уже из сборки получить нужный нам тип:

IEnumerator asmEnum = System.AppDomain.CurrentDomain.GetAssemblies().GetEnumerator();
while (asmEnum.MoveNext())
{
	Assembly asm = asmEnum.Current as Assembly;
	if (asm.FullName.Contains("System, V"))
	{
		comparamtype = asm.GetType("System.CodeDom.Compiler.CompilerParameters");
	}
							
	if (asm.FullName.Contains("FSharp.Compiler.CodeDom"))
	{
		compilertype = asm.GetType("Microsoft.FSharp.Compiler.CodeDom.FSharpCodeProvider");
	}
}

Зато получить экземпляр как CompilerParameters так и FSharpCodeProvider'а не составляет труда:

object _params = System.Activator.CreateInstance(comparamtype, new object[] { new string[] { "System", "System.Core", UenginePath } });
comparamtype.GetProperty("IncludeDebugInformation").SetValue(_params, true, new object[] { });
comparamtype.GetProperty("OutputAssembly").SetValue(_params, @"Assets/Assembly/Assebly-FSharp-vs.dll", new object[] { });
object compiler = System.Activator.CreateInstance(compilertype);

Важное замечание

Должен заметить что таким-же образом можно компилировать и C# скрипты из паков ассетов(т.н. AssetBundle). Дело в том, что в бандле все скрипты находятся в виде TextAsset'ов. Учитывая этот факт, их использование проблематично. Однако с помощью CodeDomProvider их можно будет компилировать и использовать.

Экземпляр CompilerParameters создается только с тремя необходимыми зависимостями(да, это минус, который будет исправлен в последствии, но и этого будет достаточно для компиляции) в качестве аргумента. Третий член массива возвращает свойство, которое собирает полный путь к библиотеке UnityEngine.dll и выглядит оно вот так:

static string UenginePath
	{
		get
		{
			return UnityEditor.EditorApplication.applicationContentsPath + "/Managed/UnityEngine.dll";
		}
	}

Дело за малым — собрать список файлов и отправить на растерзание компилятору. Но тут есть ещё один нюанс, разъяснения для которого я сначала приведу этот скриншот:

image

«Что-то тут не-так», подумал Штирлиц. И правильно подумал — я показал скриншот из проекта, в котором уже используется получившийся в итоге Editor-скрипт. И при этом я не забегаю наперёд. Это я должен был сделать чтобы была понятна суть дальнейших действий. А она заключается вот в чем: для того, чтобы при нажатии на элемент Script подсвечивался именно ассет, а не скрипт в библиотеке, необходимо создать ассоциации тип <=> ассет, и поэтому нет никакого смысла добавлять скрипт в перечень скриптов для компиляции, если он не импортирован. Для того, чтобы это проверить, нужно использовать AssetDatabase. При этом «находится в папке Assets!=импортирован в ассеты». Но об этом всём будет чуть позже. То, что требовалось получить на этом шаге уже сделано — нами получены и подготовлены к использованию экземпляры компилятора и его параметров.

К слову об иконках

Следует добавить, что иконка F# скрипта будет не всегда такая, как у меня. Почему? Из справки из справки по методу EditorGUIUtility.ObjectContent (который вызывается внутри метода UnityEditor.DoObjectField, а точнее одной из его перегрузок, которая рисует окно базируясь на значении текущего Event.Current.type) мало что можно понять, однако похоже что сначала редактор пытается найти иконку в кэше, потом получить иконку у системы и уже потом назначает иконку DefaultAsset'а, если подходящая иконка не нашлась.

Часть третья
Из пушки по воробьям.Drag & Drop и непослушный инспектор

Весьма и весьма сподручно добавлять компоненты на объект обычным перетаскиваем. Но дело в том, что показав скриншот я не рассказал всей правды — редактор так и не понял, что то что он подсвечивает — это все-таки скрипт. Нераспознанные ассеты(например, файл с неизвестным расширением, прямо как наш .fs скрипт) импортируются с типом UnityEngine.DefaultAsset(который по всей видимости, Internal, потому недоступен в Editor-скрипте, но это нам ничуть не мешает). Но так как нельзя(во всяком случае, я не нашел способа) получить напрямую объект ассета из его расположения (можно сделать только наоборот), мне пришлось, образно выражаясь, сделать бочку:

string[] _files = Directory.GetFiles("Assets/", "*.fs");
typenameToObject = new Dictionary<string, UnityEngine.Object>();
foreach (string file in _files)
	{
		UnityEngine.Object o = AssetDatabase.LoadAssetAtPath(file, typeof(UnityEngine.Object));

		if (CollectCompileDeploy.typenameToObject != null)
		{
			CollectCompileDeploy.typenameToObject.Add(o.name, o);
		}
		else
		{
			return;
		}
	}

Тут как раз создается то что нам нужно — из списка путей к .fs файлам я получаю список ассетов, под которыми они импортировались. У таких ассетов есть одна особенность, которая проявила себя при создании CustomInstector'а для F# скриптов, а именно: UnityEngine.DefaultAsset загружаются в AssetDatabase только после выделения в окне инспектора или другого действия с ними. Поэтому тут они загружаются вручную, используя путь, полученный с помощью Directory.GetFiles.
CustomInspector для ассетов F# скриптов является чисто косметической приблудой и выглядит вот так:

imageИсходя из того, что в Editor-скрипте тип UnityEngine.DefaultAsset недоступен, приходится создавать CustomEditor для UnityEngine.Object и проверять, является ли данный UnityEngine.Object именно .js файлом, а не, например, префабом или текстурой, после чего читается содержимое файла чтобы отобразить код (который, кстати, можно выделить и скопировать и даже отредактировать, но результат специально не будет сохранен).
Код этого CustomInspector'а:

[CustomEditor(typeof(UnityEngine.Object))]
public class FSharpScriptInspector : Editor
{
	public SerializedProperty test;
	string text;

	void OnEnable()
	{
		Repaint();
	}

	public override void OnInspectorGUI()
	{
		GUI.enabled = true;

		if (!AssetDatabase.GetAssetPath(Selection.activeObject).EndsWith(".fs"))
		{
			DrawDefaultInspector();
		}
		else
		{
			if (text == null)
			{
				StreamReader sr = File.OpenText(AssetDatabase.GetAssetPath(Selection.activeObject));
				text = sr.ReadToEnd();
				sr.Close();
			}

			GUILayout.Label("Imported F# script");
			EditorGUILayout.TextArea(text);
		}
	}
}

Параметр атрибута CustomEditor указывает на тип, для которого мы хотим создать CustomInspector. Т.к. подавляющее большинство типов Unity3D наследуют от UnityEngine.Object (в т.ч. ассеты) то нельзя допустить, чтобы то, что должно рисоваться только для .fs скриптов в окне инспектора, рисовалось для всех ассетов. Для этого получается путь к ассету через AssetDatabase.GetAssetPath и проверяется расширение. Отдельного внимания заслуживает метод DrawDefaultInspector. Как можно было догадаться из названия, этот метод включает прорисовку инспектора по умолчанию.
Для реализации же этого D&D нужно отслеживать события, последнее из которых всегда можно получить из свойства Event.current, и которое всегда возвращает одно из этих значений. Нужные нам Draw & Drop события возвращаются свойством тогда, когда мы тащим объект в окне инспектора.Но объекты скриптов можно добавлять только на геймобьекты, поэтому, нужен CustomInspector, который бы смог отследить эти события D&D на окне инспектора геймобьекта. Как ни странно, но для осуществления этого существует аж два варианта:
1. Использовать CustomInspector для класса UnityEngine.GameObject
2. Использовать CustomInspector для класса UnityEngine.Transform

Примечание

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

imageВторое предпочтительнее. И на картинке справа видно почему.
Вот и немного головной боли от использования DrawDefaultInspector. Ведь мне необходимо было только ловить события, но дело в том что CustomInspector должен что-то нарисовать! Или вызвать DrawDefaultInspector, но на скриншоте видно к чему это привело.
Но не всё потеряно, т.к. окно довольно простое и не составит труда его восстановить. Достаточно создать три поля для структуры Vector3:
1. Для вращения объекта в виде Эйлерового угла
2. Для позиции объекта
3. Для значения масштаба обьекта
Значение угла из эйлерового значения в кватернион переводится методом Quaternion.Euler.
Код этого CustomInspector'а:

using UnityEngine;
using System.Collections;
using UnityEditor;
using System.IO;
using System.Collections.Generic;

[CustomEditor(typeof(UnityEngine.Transform))]
public class ComponentCI : Editor
{
	Vector3 position;

	void OnEnable()
	{
		Repaint();
	}

	public override void OnInspectorGUI()
	{
		EditorGUILayout.BeginVertical();
		(this.target as Transform).localRotation = Quaternion.Euler(EditorGUILayout.Vector3Field("Local Rotation", (this.target as Transform).localRotation.eulerAngles));
		(this.target as Transform).localPosition = EditorGUILayout.Vector3Field("Local Position", (this.target as Transform).localPosition);
		(this.target as Transform).localScale = EditorGUILayout.Vector3Field("Local Scale", (this.target as Transform).localScale);
		EditorGUILayout.EndVertical();

		if (Event.current.type == EventType.DragPerform)
		{
			if (AssetDatabase.GetAssetPath(DragAndDrop.objectReferences[0]).EndsWith(".fs"))
			{
				(this.target as Transform).gameObject.AddComponent(DragAndDrop.objectReferences[0].name);
			}
		}
	}
}
А что с UnityEngine.GameObject?

А если использовать UnityEngine.GameObject в качестве целевого типа то получается вообще каша — ComboBox'ы превращаются в обычные поля ввода, и маску слоев уже нужно вводить как целое число (вместо ComboBox'ов с CheckBox'ами внутри)

Внимательно взглянув на код можно заметить одну не совсем понятную деталь — какой компонент добавляется при событии Drop?
Одна из перегрузок GameObject.AddComponent принимает в качестве аргумента имя скрипта, хотя на самом деле это имя типа, а требуемое пространство имен, в котором этот скрипт находится, определяется уже на ходу.

Подробнее о пространствах имен

Как и обещал, разъяснения по поводу всей суматохи вокруг пространства имен: Исходя из документации явно указывать пространство имен(во всяком случае, для C#) сейчас банально нельзя. Но работая с F# еще на уровне самодельной библиотеки я заметил такую деталь: пространство имен и название класса могут быть любыми. Т.е. знакомая, наверное всем, пользователям Unity3D ошибка Script file name does not match the name of the class в F# скрипте не появляется. Можно создать несколько классов — наследников MonoBehaviour в одном файле. Но есть один нюанс — они будут доступны только из библиотек (если делать так, как делал я). При этом от использования namespace никак не уйти — компилятор требует явного указания пространства имен или модуля. В случае невыполнения требований хладнокровно вбрасывает ошибку FS0222.

Теперь D&D для скриптов готов. Только вот выглядеть это будет не слишком изящно. Для того, чтобы сделать окно инспектора таким, мне придется создавать CustomInspector для каждого F# скрипта. Код инспектора (на примере класса SphereMoving):
(используется автоформатирование VisualStudio, но на самом деле код создается без всякого форматирования и отступов, ибо не рассчитывалось что это результат будет кто-то читать)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
using UnityEditor;
using System.IO;
[CustomEditor(typeof(Assembly_FSharp_vs.SphereMoving))]
[CanEditMultipleObjects]
public class ins_SphereMoving : Editor
{
	public SerializedProperty prop1;
	public List<SerializedProperty> props;
	void OnEnable()
	{
		props = new List<SerializedProperty>();
		System.Reflection.FieldInfo[] fields = typeof(Assembly_FSharp_vs.SphereMoving).GetFields();
		foreach (System.Reflection.FieldInfo field in fields)
		{
			SerializedProperty mp = serializedObject.FindProperty(field.Name);
			if (mp != null)
			{
				props.Add(mp);
			}
		}
		Repaint();
	}
	public override void OnInspectorGUI()
	{
		if (UnityEditor.EditorApplication.isCompiling)
		{
			EditorGUILayout.LabelField("Can't show anything during compilation");
			//I don't want to live on this scope anymore!
			return;
		}
		try
		{
			EditorGUILayout.ObjectField("Script", CollectCompileDeploy.typenameToObject.ContainsKey("SphereMoving") ? 

CollectCompileDeploy.typenameToObject["SphereMoving"] : null, typeof(UnityEngine.Object), false);
			EditorGUILayout.BeginVertical();
			foreach (SerializedProperty p in props)
			{
				EditorGUILayout.PropertyField(p);
				EditorGUILayout.Space();
			}
			this.serializedObject.ApplyModifiedProperties();
			EditorGUILayout.EndVertical();
		}
		catch { }
	}
}

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

1346840941-clip-21kb

Но иногда бывало и похуже.
Почему так происходит и что я сделал неправильно? А пёс его знает. Посему — снова Reflection и получение имен всех полей с последующим получением SerializedProperty с помощью метода SerializedObject.FindProperty. Атрибут CanEditMultipleObjects необходим для того, чтобы можно было отобразить инспектор для нескольких одинаковых скриптов, находящихся на одном объекте, тут он необязателен, но на всякой случай я его оставлю. Также стоит заметить, что я специально добавил поле Script которое является ObjectField'ом, и в которое нужно отправить именно объект UnityEngine.DefaultAsset, который представляет собой наш импортированный .fs файл. После этого при нажатии на это поле будет подсвечен именно .fs файл, а не скрипт внутри библиотеки.
Итак, результат выглядит так:

Как я подружил Unity3D и F#

Взглянув на эту картинку, внимательный читатель воскликнет: что за диво в моем саду?
Да, это еще далеко не всё.

Часть четвёртая
Дальше — глубже. Расширяем меню, создаем скрипты и обновляем файл решения Visual Studio

image
Далее, для того чтобы ещё приблизить работу с F# скриптами к работе с C#/Boo/UnityScript нужно добавить пункт «F# Script» в меню «Asets->Create» и в контекстное меню, а также добавить генерацию файла проекта и включение проекта в файл решения (об этом чуть ниже). Как оказалось, контекстное меню инспектора проекта это тоже меню
Assets->Create, поэтому все обошлось одним несложным методом:

	[MenuItem("Assets/Create/F# script")]
	public static void CreateFS()
	{
			string path = AssetDatabase.GetAssetPath(Selection.activeObject);
			string addNum = "";

			if (Selection.activeInstanceID <= 0)
			{
				path="Assets";
			}

			if (path.Contains("."))
			{
				path =Directory.GetParent(path).ToString();
			}

			while (File.Exists(path + "/NewBehaviourScript" + addNum.ToString() + ".fs"))
			{
				addNum = addNum == "" ? (1).ToString() : (int.Parse(addNum) + 1).ToString();
			}
			
			path = path + "/NewBehaviourScript" + addNum.ToString() + ".fs";
			StreamWriter sw = File.CreateText(path);
			sw.WriteLine("//Please, don't try to change namespace");
			sw.WriteLine("namespace Assembly_FSharp_vs");
			sw.WriteLine("type public " + "NewBehaviourScript" + addNum.ToString() + " () =");
			sw.WriteLine("        inherit UnityEngine.MonoBehaviour()");
			sw.WriteLine("        [<DefaultValue>] val mutable showdown1 : UnityEngine.Vector3");
			sw.WriteLine("        [<DefaultValue>] val mutable showdown2 : UnityEngine.Vector3");
			sw.WriteLine("        [<DefaultValue>] val mutable showdown3: int");
			sw.WriteLine("        member public this.Start () = UnityEngine.Debug.Log("initialized")");
			sw.Flush();
			sw.Close();
			AssetDatabase.LoadAssetAtPath(path,Type.GetType("UnityEngine.DefaultAsset,UnityEngine"));
			AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
	}

Как я подружил Unity3D и F#Атрибут MenuItem создает пункт меню, который указан в качестве аргумента, и вызывает функцию, к которой этот атрибут применен, при нажатии на этот пункт меню. Результат виден на картинке справа.Выбранным объектом (Selection.activeObject) может быть как ассет, так и каталог, поэтому производится проверка полученного пути на наличие точек (которые указывают на то, что это скорее всего именно файл) и вычисляется родительский каталог в случае если в пути есть расширение (должен заметить, что если выделен ассет, файл которого не имеет расширения, то будет ошибка. Но кто, чёрт возьми, работая с Unity3d, использует ассеты без расширения?). Также, выделенный объект всегда имеет свой activeInstanceID, который меньше нуля в случае, если выбран объект сцены. А если выбран ассет, то activeInstanceID больше нуля. В случае, если не выбрано ничего, то этот activeInstanceID равен нулю. В случае, если ничего не выбрано или выбран обьект сцены, то скрипт создается в каталоге Assets(точно также создаются скрипты встроенных языков). Также, путем перебора, проверяется существование файла, чье имя удовлетворяет регулярному выражению NewBehaviourScript[d]*.fs, и создается файл с первым попавшимся таким именем, куда записывается такой вот незамысловатый F# код, цель которого показать что и как нужно записать, чтобы оно и работало, и показало в инспекторе всё что нужно:

//Please, don't try to change namespace
namespace Assembly_FSharp_vs
type public NewBehaviourScript1 () =
        inherit UnityEngine.MonoBehaviour()
        [<DefaultValue>] val mutable showdown1 : UnityEngine.Vector3
        [<DefaultValue>] val mutable showdown2 : UnityEngine.Vector3
        [<DefaultValue>] val mutable showdown3: int
        member public this.Start () = UnityEngine.Debug.Log("initialized")

Все что остается — это теперь дать человеческое название, открыть и начинать писать! Но при попытке открыть .fs файл двойным кликом по ассету будет создан новый экземпляр Visual Studio, которое создаст новый проект и новое решение, что приведет к тому, что ВСЕ другие классы, созданные в проекте, будут недоступны в IDE (равно, как и пространства имен UnityEngine и прочие). Благо, формат файла проекта довольно простой:

Файл проекта

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
     <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProductVersion>8.0.30703</ProductVersion>
    <SchemaVersion>2.0</SchemaVersion>
    <ProjectGuid>{ACFBFD03-C456-E983-5028-ACC6C3ACEA62}</ProjectGuid>
    <OutputType>Library</OutputType>
    <RootNamespace>Assembly_FSharp_vs</RootNamespace>
    <AssemblyName>Assembly_FSharp_vs</AssemblyName>
    <TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
    <Name>Assembly-FSharp-vs</Name>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <Tailcalls>false</Tailcalls>
    <OutputPath>binDebug</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <WarningLevel>3</WarningLevel>
    <DocumentationFile>binDebugAssembly_FSharp_vs.XML</DocumentationFile>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <Tailcalls>true</Tailcalls>
    <OutputPath>binRelease</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <WarningLevel>3</WarningLevel>
    <DocumentationFile>binReleaseAssembly_FSharp_vs.XML</DocumentationFile>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="mscorlib" />
    <Reference Include="FSharp.Core" />
    <Reference Include="System" />
    <Reference Include="System.Core" />
    <Reference Include="System.Numerics" />
    <Reference Include="D:/Program Files/Unity3.5/Editor/Data/Managed/UnityEngine.dll" />
  </ItemGroup>
  <ItemGroup>
<Compile Include="Assets/NewBehaviourScript.fs" />
<Compile Include="Assets/SphereMoving.fs" />
</ItemGroup>
  <Import Project="$(MSBuildExtensionsPath32)FSharp1.0Microsoft.FSharp.Targets" Condition="!Exists('$(MSBuildBinPath)Microsoft.Build.Tasks.v4.0.dll')" />
  <Import Project="$(MSBuildExtensionsPath32)..Microsoft F#v4.0Microsoft.FSharp.Targets" Condition=" Exists('$(MSBuildBinPath)Microsoft.Build.Tasks.v4.0.dll')" 

/>
  <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
	     Other similar extension points exist, see Microsoft.Common.targets.
	<Target Name="BeforeBuild">
	</Target>
	<Target Name="AfterBuild">
	</Target>
	-->
</Project>

Как я подружил Unity3D и F#Как можно заметить, предоставленный XML — это уже сгенерированный скриптом код файла проекта. За основу был взят файл Assembly-CSharp-vs.csproj. На язык, используемый в проекте, указывает только расширение файла (csproj для C# и fsproj для F#). Все, что нужно менять — это содержимое блоков зависимостей (ReferenceInclude) и содержимое блоков исходных файлов (CompileInclude). На картинке слева можно наблюдать то, каким решение станет после обновления. Т.о… можно даже не вникать в классы для работы с XML, достаточно обычной записи нескольких строковых значений друг за другом. За исключением значения ProjectGUID. Для каждого проекта оно должно быть уникальным.
Получить его можно с помощью класса UnityEditor.VisualStudioIntegration.SolutionGuidGenerator (который по неизвестным причинам не задокументирован, а запрос " SolutionGUIDGenerator Unity3d" (без кавычек) выдает только 1 результат, который ведет на эту пасту. Впрочем, это вполне понятно, ибо мало кто этим пользуется). Этот класс создает за меня GUID и для проекта, и для решения. Единственный аргумент — это название проекта или решения. Кстати, для того, чтобы присоединить проект к решению, достаточно в начале файла(после заголовка) добавить нечто подобное:

Project("ACFBFD03-C456-E983-5028-ACC6C3ACEA62") = ".FSharp-csharp", "Assembly-FSharp-vs.fsproj", "{9512B9D0-6FAE-8F85-778C-B28C5421520D}"
EndProject

Первое число — это GUID для проекта. Второе число — это GUID для решения.
Обновление происходит автоматически после ручной компиляции, а также завязано на пункт меню «F#->Update soltuion file».
Метод, который обновляет информацию в файле решения:

	static string xmlSourceFileDataEnter = "<Compile Include="";
	static string xlmSourceFileDataEnding = "" />";

[MenuItem("F#/Update solution file")]
	public static void UpdateSolution()
	{
		if(File.Exists("Assembly-FSharp-vs.fsproj"))
		{
			File.Delete("Assembly-FSharp-vs.fsproj");
		}

		StreamWriter sw = File.CreateText("Assembly-FSharp-vs.fsproj");
		sw.WriteLine(xmlEnterData);

		foreach (UnityEngine.Object file in typenameToObject.Values)
		{
			sw.Write(xmlSourceFileDataEnter);
			sw.Write(AssetDatabase.GetAssetPath(file));
			sw.WriteLine(xlmSourceFileDataEnding);
		}

		sw.WriteLine(xmlFinishData);
		sw.Flush();
		sw.Close();
		sw.Dispose();
		string[] slnfiles = Directory.GetFiles(".","*-csharp.sln");
		
		if (slnfiles != null && slnfiles.Length>0)
		{
			StreamReader sr = File.OpenText(slnfiles[0]);
			List<string> lines = new List<string>();

			while (!sr.EndOfStream)
			{
				string readenLine = sr.ReadLine();
				lines.Add(readenLine);

				if (readenLine.Contains("Assembly-FSharp-vs.fsproj"))
				{ 
					sr.Close();
					sr.Dispose();
					return;
				}
			}

			sr.Close();
			sr.Dispose();
			sw = File.CreateText(slnfiles[0]);
			List<string>.Enumerator linesEnum = lines.GetEnumerator();
			linesEnum.MoveNext();
			sw.WriteLine(linesEnum.Current);
			linesEnum.MoveNext();
			sw.WriteLine(linesEnum.Current);
			string slinname = slnfiles[0].Remove(slnfiles[0].LastIndexOf(".sln"));
			sw.WriteLine("Project("" + UnityEditor.VisualStudioIntegration.SolutionGuidGenerator.GuidForProject("Assembly-FSharp-vs") + "") = "" + slinname + "", "Assembly-FSharp-vs.fsproj", "{"+UnityEditor.VisualStudioIntegration.SolutionGuidGenerator.GuidForSolution(slinname)+"}"");
			sw.WriteLine("EndProject");

			while (linesEnum.MoveNext())
			{
				sw.WriteLine(linesEnum.Current);
			}

			sw.Flush();
			sw.Close();
			sw.Dispose();
		}
	}

Как я подружил Unity3D и F#В xmlEnterData и xmlFinishData находится все, что до блока «Compile Include» и после него соответственно.
Зависимость к библиотеке UnityEngine устанавливается в инициализаторе поля, в котором также устанавливается GUID проекта. После компиляции получаем меню с пунктом «Update solution file»…
Как видно из рисунка, имеется еще и пункт «Manual rebuild». Я специально сделал так, чтобы приходилось перестраивать скрипты F# по команде, ибо в моем исполнении процесс перестройки занимает довольно долгое время, и было бы весьма и весьма печально, если бы большую часть времени работы с проектом я ждал окончания автоматически включившейся компиляции. Ко всему прочему, операции с ассетами, а также все операции с экземплярами, что наследуют UnityEngine.Object, должны выполняться в главном потоке. Ещё нельзя забывать, что после того, как компилятор Unity закончит свою работу, все поля вернуться к исходному значению, ибо будет перезагружена сборка.Но и ввиду того, что моя реализация не блещет скоростью работы, окно редактора во время компиляции скриптов F# выглядит повисшим.
Чтобы дать пользователю хоть какую-то информацию, нужно добавить какое-нибудь информационное окошко. У меня это такое вот маленькое окошко с ProgressBar'ом:

Как я подружил Unity3D и F#

А если нельзя, но очень хочется?

Как я подружил Unity3D и F#
такую ошибку можно получить при попытке произведения операций с UnityEngine.Object вне главного потока. И способов получить красный лог при работе вне главного потока чуть более чем достаточно.

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

P.S. Если использовать ExecuteInEditMode для MonoBehaviour то обновление сцены (вызов Monobehaviour.Update) будет вызвано только если в ней что-то изменится, а корутины вообще работать не будут. По заверениям сообщества, встроенные корутины в Edit-time могут работать только в unity script, но мы ведь работаем в C#, верно?

Часть пятая
Корутины в Edit-time и информационное окно

Как я подружил Unity3D и F#Немного баянистой может показаться работа с информационным окном, но есть один интересный момент, о котором я хочу упомянуть, поэтому я не опущу работу с ним а просто уберу в спойлер. Стоит упомянуть то, о чем я не рассказал в предыдущих частях, а именно атрибут InitializeOnLoad (который по неизвестным причинам не упомянут в справке по скриптингу). Атрибут, примененный к любому классу, инициализирует его как только, так сразу, при этом нужен статический конструктор для правильной его работы. Благодаря этому можно собирать информацию для правильной работы CustomInspector'ов после завершения компиляции проекта в целом, ибо после этого происходит перезагрузка всех сборок и данные полей теряются.
Итак, корутины. Как видно из примеров, которые находятся в странице справки, ссылку на которую я давал выше, корутина выполняет какой-то код, после чего выполняет yield-инструкцию, что позволяет вернуть управление в метод который её вызвал. Однако львиная доля кода была написана ДО того как я понял необходимость использования корутин, и поэтому, каким-то образом завязать всё на yield-инструкцию по-человечески не получилось. Посему я решил разделить код на кучу анонимных типов, которые содержаться в вот таком вот классе:

public class UniversalEnumerator : IEnumerator
	{
		List<Func<object>> allCodeFrames = new List<Func<object>>();
		IEnumerator codeEnum;
		Func<object> finishAction = null;

		public Func<object> FinishAction
		{
			get { return finishAction; }
			set { finishAction = value; }
		}
		
		public void Add(Func<object> code)
		{
			allCodeFrames.Add(code);
		}

		public void _Finalize()
		{
			codeEnum = allCodeFrames.GetEnumerator();
		}

		public object Current
		{
			get { return codeEnum.Current; }
		}

		public bool MoveNext()
		{
			bool res = codeEnum.MoveNext();

			if (res)
			{
				(codeEnum.Current as Func<object>)();
			}
			else
			{
				if (FinishAction != null)
				{
					return FinishAction()!=null;
				}
			}

			return res;
		}

		public void Reset()
		{
			codeEnum = null;
			allCodeFrames.Clear();
			FinishAction = null;
		}
	}

Как видно, метод Add принимает в качетсве аргумента Func и ставит его в очередь. В методе MoveNext мы перемещаемся по полученному в _Finalize энумератору и выполняем codeEnum.Current. FinishAction — это некоторое действие, которое выполняется тогда, когда мы достигли верхушки коллекции. Возвращаемое значение рассматривается для того чтобы понять, нужно ли вернуть False, которое сигнализирует о том, что больше для выполнения инструкций нет (когда я покажу код, именно собранный в последовательность таких корутин, я объясню зачем это нужно). Сам же редактор имеет свой UpdateLoop и в него можно добавить свой анонимный метод. Это делается в статическом конструкторе моего класса с атрибутом InitializeOnLoad:

	static UniversalEnumerator currentRoutine;

	static CollectCompileDeploy()
	{
		UnityEditor.EditorApplication.update += new EditorApplication.CallbackFunction(() =>
		{
			if (currentRoutine != null)
			{
				if (!currentRoutine.MoveNext())
				{
					currentRoutine = null;
				}
			}
		});
		Initialize();
	}

Под спойлером находится код метода Recompile как раз собирает в кучу отрезки кода и возвращается полученный UniversalEnumerator.

Развернуть

public enum CompilationState
	{ 
		GATHER =1,
		COMPIL,
		VALIDATION,
		SOLUTION,
		DONE,
		NONE
	}

public static UniversalEnumerator Recompile()
	{
		UniversalEnumerator myEnum = new UniversalEnumerator();
		_current = CompilationState.GATHER;
		CompilationProgressWindow.Init();
		myEnum.Add(() =>
			{
				
				files.Clear();
				ReassingTypes();
				return null;
			});

		bool exitall = false;

		myEnum.Add(() =>
			{
				if (File.Exists("Assets/Assembly/Assembly-FSharp-vs.dll"))
				{
					AssetDatabase.DeleteAsset("Assets/Assembly/Assembly-FSharp-vs.dll");
					File.Delete("Assets/Assembly/Assembly-FSharp-vs.dll");
				}

				if (files.Count == 0)
				{
					Debug.Log("seems like no any F# file here.terminating");
					_current = CompilationState.NONE;
					exitall = true;
				}
				return null;
			});

		System.Type comparamtype = null;
		System.Type compilertype = null;

		myEnum.Add(() =>
			{
				if (exitall)
				{
					return null;
				}

				UniversalEnumerator bufferedRoutine = currentRoutine;
				UniversalEnumerator nroutine = new UniversalEnumerator();
				IEnumerator asmEnum = System.AppDomain.CurrentDomain.GetAssemblies().GetEnumerator();

				while (asmEnum.MoveNext())
				{
					Assembly asm = asmEnum.Current as Assembly;

					nroutine.Add(() =>
						{
							if (asm.FullName.Contains("System, V"))
							{
								comparamtype = asm.GetType("System.CodeDom.Compiler.CompilerParameters");
							}
							
							if (asm.FullName.Contains("FSharp.Compiler.CodeDom"))
							{
								compilertype = asm.GetType("Microsoft.FSharp.Compiler.CodeDom.FSharpCodeProvider");
							}
							return null;
						});
				}

				nroutine.FinishAction = () => { currentRoutine = bufferedRoutine; return new object(); };
				nroutine._Finalize();
				currentRoutine = nroutine;
				return null;
			});

		myEnum.Add(() =>
			{
				if (exitall)
				{
					return null;
				}

				UnityEditor.EditorApplication.LockReloadAssemblies();

				try
				{
					object _params = System.Activator.CreateInstance(comparamtype, new object[] { new string[] { "System", "System.Core", UenginePath } });
					comparamtype.GetProperty("IncludeDebugInformation").SetValue(_params, true, new object[] { });
					comparamtype.GetProperty("OutputAssembly").SetValue(_params, @"Assets/Assembly/Assebly-FSharp-vs.dll", new object[] { });
					object compiler = System.Activator.CreateInstance(compilertype);
					List<string> __fls = new List<string>();
					foreach(UnityEngine.Object asset in typenameToObject.Values)
					{
						__fls.Add(AssetDatabase.GetAssetPath(asset));
					}
					_current = CompilationState.COMPIL;
					object _output = compilertype.GetMethod("CompileAssemblyFromFile").Invoke(compiler, new object[] { _params, __fls.ToArray() });
					compiled = _output.GetType().GetProperty("CompiledAssembly").GetValue(_output, new object[] { }) as Assembly;

					foreach (object message in _output.GetType().GetProperty("Output").GetValue(_output, new object[] { }) as System.Collections.Specialized.StringCollection)
					{
						Debug.Log(message);
					}

					foreach (object error in (_output.GetType().GetProperty("Errors").GetValue(_output, new object[] { }) as System.Collections.CollectionBase))
					{
						Debug.LogError(error);
					}

					if (compiled != null)
					{
						_current = CompilationState.VALIDATION;
						UniversalEnumerator bufferedRoutine = currentRoutine;
						UniversalEnumerator nroutine = ValidateInspectors();

						nroutine.Add(() =>
						{
							_current = CompilationState.SOLUTION;
							UpdateSolution();
							_current = CompilationState.DONE;
							CompilationProgressWindow.Remove();
							AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
							return null;
						});

						nroutine._Finalize();
						nroutine.FinishAction = () => { currentRoutine = bufferedRoutine; return new object(); };
						currentRoutine = nroutine;
					}
					else
					{
						Debug.LogError("compiled assembly is still not visible!");
					}
				}
				catch { }

				UnityEditor.EditorApplication.UnlockReloadAssemblies();
				
				return null;
			});

		myEnum._Finalize();
		return myEnum;
	}

Этот метод и производит все необходимые операции для компиляции и отображения.
По поводу корутин. Объясняю смысл этой конструкции:

UniversalEnumerator bufferedRoutine = currentRoutine;
UniversalEnumerator nroutine = new UniversalEnumerator();
nroutine.Add(() =>
				{
					//Что душе угодно.
					return null;
				});
nroutine.FinishAction = () => { currentRoutine = bufferedRoutine; return new object(); };
nroutine._Finalize();
currentRoutine = nroutine;

Тут я в отдельной переменной я буферизую старый объект UniversalEnumerator, после чего переменной currentRoutine назначается новая очередь вызовов в лице класса UniversalEnumerator, после чего старое значение восстанавливается в FinishAction. new object() возвращается для того, чтобы MoveNext не вернул false, ибо мы помним что FinishAction вызывается тогда, когда достигается верхушка коллекции, а в EditorUpdateLoop метод MoveNext происходит до тех пор, пока он не вернет false, после чего currentRoutine присваивается значение null. Таким образом необязательно добавлять методы в один и тот-же экземпляр UniversalRoutine, что, на мой взгляд, весьма сподручно.

А почему не потоки?

Использовать потоки нету ни малейшего жаления по ряду причин:
1. Т.к. используется Mono, то значение свойства IsBackground попросту игнорируется. Это значит что процесс не завершится до тех пор, пока не завершатся все потоки.
2. Первое было бы не так страшно если бы не одно но: крайне скверно работает Thread.Abort и в свое время работая над многопоточным приложением на Unity3D я получал пренеприятнейшую картину: какие-то потоки попросту зависали и браузерное приложение не закрывалось. Это приводило к очень нехорошим последствиям — попросту зависал браузер. Также имел место довольно интересный момент — поток доходил до последней инструкции (Debug.Log, которая потокобезопасна) и… приложение не закрывалось, т.е. поток вел себя так, будто бы он продолжает работу. Воспроизвести не получается, я просто рассказываю как было.
По этим причинам я не использую ни потоки ни таски, а изобретаю велосипед.

Также стоит уделить внимание enum'у CompilationState, чья роль является сугубо информационной и используется уже в информационном окне. Соответствующее значение в нужный момент присваивается свойству _current:

private static CompilationState __current = CompilationState.NONE;

	public static CompilationState _current
	{
		get 
		{
			return CollectCompileDeploy.__current; 
		}
		set 
		{
			CollectCompileDeploy.__current = value;

			if (CompilationProgressWindow.me != null)
			{
				CompilationProgressWindow.me.Repaint();
			}
		}
	}

Как видно, CompilationProgressWindow — это класс информационного окна, базовым классом которого является EditorWindow. вызов Repaint является банальным костылем, ибо после какого-то изменения, по неизвестным мне причинам, прекратило автоматическую перерисовку. Код и описание — под спойлером.

Информационное окно

Код класса информационного окна:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEngine;
using System.Runtime.InteropServices;

public class CompilationProgressWindow : EditorWindow
{
	public static CompilationProgressWindow me;

	[StructLayout(LayoutKind.Sequential)]
	public struct WndRect
	{
		public int Left;
		public int Top;
		public int Right;
		public int Bottom;
	}

#if UNITY_EDITOR && UNITY_STANDALONE_OSX

	public static Vector2 GetWNDSize()
	{
		return Vector2.zero;
	}
#endif

#if UNITY_EDITOR && UNITY_STANDALONE_WIN
	[DllImport("user32.dll")]
	static extern IntPtr GetForegroundWindow();

	[DllImport("user32.dll", SetLastError = true)]
	static extern bool GetWindowRect(IntPtr hWnd, out WndRect rect);

	public static Vector2 GetWNDSize()
	{
		WndRect r = new WndRect();
		GetWindowRect(GetForegroundWindow(), out r);
		return new Vector2(r.Right - r.Left, r.Bottom - r.Top); ;
	}
#endif

	public static void Init()
	{
		if (me == null)
		{
			Vector2 mainWND = GetWNDSize();
			CompilationProgressWindow window = (CompilationProgressWindow)EditorWindow.GetWindow(typeof(CompilationProgressWindow), true, "F# Compilation progress", true);
			window.position = new Rect(mainWND.x/2-200, mainWND.y / 2 - 50, 400, 100);
			window.title = "F# compilation progress";
			window.Focus();
			me = window;
		}
		else
		{
			me.Focus();
		}
	}

	void OnGUI()
	{
		string msg = "";
		switch (CollectCompileDeploy._current)
		{ 
			case CollectCompileDeploy.CompilationState.GATHER:
				msg = "Gathering data";
				break;
			case CollectCompileDeploy.CompilationState.NONE:
				this.Close();
				break;
			case CollectCompileDeploy.CompilationState.COMPIL:
				msg = "Compiling F# assembly";
				break;
			case CollectCompileDeploy.CompilationState.DONE:
				msg = "Done! Wait for assembly import and enjoy";
				break;
			case CollectCompileDeploy.CompilationState.SOLUTION:
				msg = "Preparing solution files for usage";
				break;
			case CollectCompileDeploy.CompilationState.VALIDATION:
				msg = "Validating editor scripts";
				break;
		}

		EditorGUI.ProgressBar(new Rect(0, 0, 400, 100), ((float)CollectCompileDeploy._current) / 5f, msg);
	}


	public void OnLostFocus()
	{
		this.Focus();
	}

	public static void Remove()
	{
		if (me != null)
		{
			me.Close();
		}
		else
		{

		}
	}
}

Сердцем этого окна является switch, в котором определяется нужное сообщение в окошке. Отдельного внимания стоит метод GetWNDSize. Вместо GetForegroundWindow можно было бы использовать и System.Diagnostics.Process.GetCurrentProcess но MainWindowTitle возвращает null, а если я вызываю GetWindowRect с аргументом, полученным из MainWindowHandle, то в результате центр моего информационного окошка приходится на (0,0). Это прискорбно, поэтому приходится получать идентификатор активного окна. В конце концов, мое нутро мне подсказывает, что когда мы вызовем этот пункт меню, активным будет именно окно редактора, а не какое-то другое, и я думаю что ему (нутру) в этом плане можно верить. Ну, и директивы условной компиляции я расставил для того, чтобы обеспечить хотя-бы теоретическую возможность запуска этого детища на компьютерах производства компании Apple, но если и все сработает хорошо, то окно появится не посередине окна редактора, как я того хотел. Также в Unity3D отсутствует встроенных механизм модальных окон. Приходится переназначать фокус каждый раз, когда окно этот фокус теряет. Ну, и как видно из кода, окно закрывает само себя когда CollectCompileDeploy._current примет значение CompilationState.NONE.

Часть шестая
Финиш

А вот и финиш. Насладиться результатами можно на картинке под спойлером а также при содействии UnityPackage файла с маленьким демо. Надеюсь что всё будет работать у вас не хуже чем у меня (вам ведь наверняка знаком «Эффект телемастера»?).

3 большие картинки

Как я подружил Unity3D и F#

Как я подружил Unity3D и F#

Как я подружил Unity3D и F#

P.S. Во время и после компиляции F# скриптов часть окна может стать серой. Чтобы восстановить его в прежний вид достаточно просто пошевелить его. В случае, если после компиляции пропадут инспекторы самым верным решением будет просто закрыть проект и открыть его снова.

Автор: 6opoDuJIo

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js