
Recently, I became more and more interested in functional programming, and when choosing a language before me, I chose among two languages I really liked - Haskell and F # .
In F #, I was seduced by the fact that it can be compiled into an MSIL build, which makes it possible to use the F # class libraries in other Microsoft .Net languages, and also that it can use them. Everything else, I am also a beginning Unity3D developer, and a thought came to my mind: if I compiled into MSIL, can I use F # scripts in Unity? Googling gave the answer: humanly it is impossible. You can create a class library, put in the project links to the UnityEngine.dll library, compile and import as an asset , and then add Mono-behaviour components directly from the library, but this is not very convenient, agree. However, after going through Google, Reflection and Help on Unity , I still managed to bring (but not repeat exactly) the work with F # scripts inside the editor to the form in which the work with scripts in the built-in languages is performed. Details - under habrakat.
//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 
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'. 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"); } } 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); static string UenginePath { get { return UnityEditor.EditorApplication.applicationContentsPath + "/Managed/UnityEngine.dll"; } } 
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; } }
Assuming that the UnityEngine.DefaultAsset type is not available in the Editor script, you must create a CustomEditor for UnityEngine.Object and check whether the given UnityEngine.Object is an .fs file, and not, for example, a prefab or texture, and then the contents of the file are read. to display the code (which, by the way, can be selected and copied and even edited, but the result will not be specifically saved). [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); } } }
The second is preferable. And in the picture on the right you can see why. 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); } } } } 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 { } } } 


[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); }
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") <?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>bin\Debug\</OutputPath> <DefineConstants>DEBUG;TRACE</DefineConstants> <WarningLevel>3</WarningLevel> <DocumentationFile>bin\Debug\Assembly_FSharp_vs.XML</DocumentationFile> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <DebugType>pdbonly</DebugType> <Optimize>true</Optimize> <Tailcalls>true</Tailcalls> <OutputPath>bin\Release\</OutputPath> <DefineConstants>TRACE</DefineConstants> <WarningLevel>3</WarningLevel> <DocumentationFile>bin\Release\Assembly_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)\FSharp\1.0\Microsoft.FSharp.Targets" Condition="!Exists('$(MSBuildBinPath)\Microsoft.Build.Tasks.v4.0.dll')" /> <Import Project="$(MSBuildExtensionsPath32)\..\Microsoft F#\v4.0\Microsoft.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>
, 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 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(); } }
xmlEnterData xmlFinishData , «Compile Include» .

, , , . , , InitializeOnLoad ( ). , , , , . CustomInspector' , . 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; } } static UniversalEnumerator currentRoutine; static CollectCompileDeploy() { UnityEditor.EditorApplication.update += new EditorApplication.CallbackFunction(() => { if (currentRoutine != null) { if (!currentRoutine.MoveNext()) { currentRoutine = null; } } }); Initialize(); } 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; 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(); } } } 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_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); ; } #else public static Vector2 GetWNDSize() { return Vector2.zero; } #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 { } } } 


Source: https://habr.com/ru/post/151009/
All Articles