📜 ⬆️ ⬇️

How I made friends with Unity3D and F #


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.




Part zero


I admit that I did something wrong (and most likely it is), and there may be both gross and not very much mistakes (nevertheless, I will be glad to know about them or how to do something other action is better / more beautiful). There may also be the effect of a telemaster and you simply won’t get those errors / glitches that I describe in this article. I simply describe my actions and observations. Please treat with understanding. In the end, that's what I'm starting to learn, make mistakes and correct them!

Special thanks to the Habrahabr moderators for putting a randomly sent, but not ready for publication topic, at my request, in drafts. From now on, I will be more careful.

Part one
How it looks out of the box


That is, how it looks without any buns. I will use the already compiled library with one simple script. Here it is:
(here and below Haskell highlighting is used, but know that this is 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 

')
This script was originally created by the means of my Editor - script. This also determines the presence of a comment and an explicit namespace (I will tell you more about its role below), I just changed it a little.
After compiling the library and importing it into the project, we get the following result:



This is what I meant when I talk about the inconvenience at the beginning of the article — you will either have to create several libraries and keep them in different places (in order to create at least some sort of order) or try to find something in a long list if the project will grow.

As the attentive anonymus habraiser has already noticed, it was not without crutches - you have to put the FSharp.Core.dll library next to each one , smash me thunder, EACH library, so that it is imported without a System.TypeLoadException being thrown by the compiler.
And to be precise, not only FSharp.Core.dll, but ALL dependencies libraries, which the Unity compiler cannot resolve conflicts on its own . Here it becomes obvious that the game with F # is not worth the trouble, and writing something under Unity3d is acceptable except from animal curiosity , and even then in not very large volumes.

The obvious solution here is to use such an Editor-script, which would be able to collect all the F # source in a heap and send them straight to the compiler. At an opportune moment, some time before the events described, in the vastness of the immense I had met this article, which became the starting point in my research.

I would not in the description of the next part to somehow delve into the wilds, if not two BUT:
1. The System.CodeDom.Compiler.CodeDomProvider class cannot provide a compiler for F #.
2. All classes from the System.CodeDom.Compiler namespace are simply “invisible” to the Unity compiler. Even if there is a link in the project and Visual Studio says that everything is in order, then the compiler will simply show a big and thick log in which he will tell and show that it is impossible to do this and that he doesn’t even dream about the contents of System.CodeDom.Compiler . Even if you connect a library that in one way or another refers to System.CodeDom.Compiler, the compiler will still strongly disagree with you - in the log you will receive a message similar to this:

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'.


The story would have ended if not for one thing: classes from this namespace can be found in an assembly already in progress by simply iterating over the collection of assemblies available in the current domain and outputting all types of names to the log, which means you can use Reflection to create Objects and methods call (by the way, Reflection will later throw a fly in the ointment, and I will explain where, but below).

Part two
The Taming of the Shrew
We receive and operate CodeDomProvider for F #


But if the second "BUT" can still be circumvented, then what about the first?
F # Power Pack which was found in the process of googling, which, I think, will be useful to anyone who somehow uses F #. At this step, I was pushed by a throw-in of the ConfigurationErrorsException exception by the CodeDomProvider.CreateProvider method when called with arguments (alternately) “FSharp” (just as CodeDomProvider is created for C # - the argument “CSharp”) and “F #” (maybe skipped over?).

F # Power Pack and provides the coveted Microsoft.FSharp.Compiler.CodeDom.FSharpCodeProvider, which has almost no differences in use compared to the usual CodeDomProvider from the System.CodeDom.Compiler namespace.

It would seem that things are easy - connect and use, but there are some pitfalls here as well:
1. Due to the fact that this library is not included in the .Net Framework package, it should be put into the project.
2. FSharpCodeProvider itself refers to the types from System.CodeDom.Compiler and therefore it is possible to get a persistent System.TypeLoadException at compilation, and Studio IntelliSense will not lead with a nose.
By Chaos’s will, I did just that, and so I did everything using Reflection and Activator . Perhaps, after all, I somewhere made a mistake, but it is already too late to change something. It should also be noted that the library I originally wrote for this purpose (for all the same F # volume), which I did not bring to mind, was compiled without such tricks (that is, I did not have to run methods in FSharpCodeProvider through Reflection), but had all the same problems with the long-suffering namespace System.CodeDom.Compiler, which was solved (again) through Reflection. Who is curious, can get acquainted with a short paste here .

And why the author did not add the library on F #?
And because I was going to write in F # not only the code that would be able to compile and compile everything, but also the code of the Editor-scripts. But here one interesting detail was revealed - Editor-classes on F # send Unity3D editor to unconditional knockout. No error messages, no pop-ups, kindly offering to send error data (along with all your project and your comments) to the engine developers, neither the hung process, nor MessageBox. Nothing! Emptiness. This behavior is usually observed when the stack overflows or errors in some Unmanaged libraries, which was detected with the help of OllyDbg , but, alas, I now need it a little less than anything and only download it in order to try (!) to elicit the cause of the fall was corny laziness.
For the curious, I provide the paste with the code of the Editor-class on F #. ATTENTION! It is possible that after adding a library with this class, what happens as a result of the compilation will fall into the Library directory, from where it will be problematic to erase it, as a result of which you will get a persistent “neotkryvak” (due to the editor’s failure due to the Editor-class curve) when you try to turn on the editor. It is necessary to delete the Library folder, which usually entails the most severe consequences . I warned.

Returning to our CodeDomProvider, I remind you that a lot is being done through Reflection. And here is the promised fly in the footsteps of statistics - when I try to get the type (!) System.CodeDom.Compiler.CompilerParameters with the help of Activator, I get the exception NotSupportedException due to " the type being created does not derive from MarshalByRefObject ", which is quite sad, because you still need to set the compiler parameters. To do this, you have to act not too elegantly - you need to find the System.dll in the assembly collection in the current domain and get the type we need from the assembly:

 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"); } } 

But getting an instance of both CompilerParameters and FSharpCodeProvider is easy:

 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); 

Important note
I should note that in the same way you can compile C # scripts from asset packages (so-called AssetBundle). The fact is that in the bundle all the scripts are in the form of TextAssets. Given this fact, their use is problematic. However, using CodeDomProvider they can be compiled and used.

An instance of CompilerParameters is created with only three necessary dependencies (yes, this is a minus, which will be corrected later, but this will be enough for compilation) as an argument. The third member of the array returns a property that collects the full path to the UnityEngine.dll library and looks like this:

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


Things are easy - to collect a list of files and send to the mercy of the compiler. But here there is one more nuance, the explanations for which I first give this screenshot:

image

"Something is not so," thought Stirlitz. And I thought correctly - I showed a screenshot of the project, which already uses the resulting Editor-script. Nor do I run ahead. I had to do this so that the essence of further actions was clear. And it is this: in order to click on the Script element, it is the asset that is highlighted, and not the script in the library, you need to create associations such as <=> asset, and therefore there is no point in adding the script to the list of scripts for compiling, if not imported. In order to check this, you need to use AssetDatabase. At the same time, “is located in the Assets! = Folder is imported into assets”. But this will all be a little later. What was required to be obtained at this step has already been done - we have obtained and prepared for use instances of the compiler and its parameters.

Speaking of icons
It should be added that the F # script icon will not always be the same as mine. Why? From the help from the help of the EditorGUIUtility.ObjectContent method (which is called inside the UnityEditor.DoObjectField method, or rather one of its overloads, which draws a window based on the value of the current Event.Current.type), there is little to understand, but it seems that the editor first tries to find an icon in the cache, then get the icon from the system and only then assigns the icon to DefaultAsset, if the appropriate icon is not found.


Part three
From the gun on the sparrows. Drag & Drop and naughty inspector


It is very handy to add components to the object by dragging it. But the fact is that by showing the screenshot I did not tell the whole truth - the editor did not understand that what he highlights is still a script. Unrecognized assets (for example, a file with an unknown extension, just like our .fs script) are imported with the UnityEngine.DefaultAsset type (which seems to be Internal, therefore it is not available in the Editor script, but this doesn’t bother us at all). But since it is impossible (in any case, I did not find a way) to directly obtain an asset object from its location (you can do just the opposite ), I had to, figuratively speaking, make a barrel :

 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; } } 


This is exactly what we need - from the list of paths to the .fs files, I get a list of assets under which they were imported. Such assets have one feature that manifested itself when creating a CustomInstector 'for F # scripts, namely: UnityEngine.DefaultAsset is loaded into AssetDatabase only after selecting in the inspector window or another action with them. Therefore, they are loaded here manually using the path obtained using Directory.GetFiles.
CustomInspector for assets F # scripts is purely cosmetic and looks like this:

image 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).
The code for this CustomInspector is:

 [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 attribute parameter CustomEditor indicates the type for which we want to create a CustomInspector. Because the overwhelming majority of Unity3D types inherit from UnityEngine.Object (including assets), then we cannot allow what should be drawn only for .fs scripts in the inspector window, drawn for all assets. To do this, we obtain the path to the asset through AssetDatabase.GetAssetPath and check the extension. Special attention is given to the DrawDefaultInspector method. As the name suggests, this method involves drawing a default inspector.
To implement this D & D, you need to track events, the last of which can always be obtained from the Event.current property, and which always returns one of these values . The Draw & Drop we need is returned by the property when we drag an object in the inspector window. But you can add script objects only to game objects, so you need a CustomInspector to track these D & D events on the game object inspector. Oddly enough, but for the implementation of this there are already two options:
1. Use CustomInspector for the UnityEngine.GameObject class
2. Use CustomInspector for the UnityEngine.Transform class
Note
UnityEngine.Transform specifies the transformation of the object - its position, rotation, scale. He is at all , without exception, gameplay
image The second is preferable. And in the picture on the right you can see why.
That's a bit of a headache from using the DrawDefaultInspector. After all, I only needed to catch events, but the fact is that CustomInspector has to draw something! Or call DrawDefaultInspector, but in the screenshot you can see what it led to.
But all is not lost. The window is quite simple and it is not difficult to restore it. It is enough to create three fields for the Vector3 structure:
1. To rotate an object in the form of an Euler angle
2. For object position
3. For the scale of the object
The angle value from the Eulerian value to the quaternion is translated by the Quaternion.Euler method.
The code for this CustomInspector is:

 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); } } } } 


And what about UnityEngine.GameObject?
And if you use UnityEngine.GameObject as the target type, it turns out to be porridge in general - ComboBoxes become normal input fields, and the layer mask should already be entered as an integer (instead of ComboBoxes with CheckBoxs inside)

Carefully looking at the code you can see one not quite clear detail - which component is added at the Drop event?
One of the GameObject.AddComponent overloads takes the name of the script as an argument, although in fact it is the name of the type, and the required namespace in which this script is located is determined on the fly.

More about namespaces
As promised, the explanation for all the confusion around the namespace: Based on the documentation, it is not possible to explicitly specify the namespace (in any case, for C #). But while working with F # at the self-made library level, I noticed such a detail: the namespace and class name can be anything. Those. The familiar, probably to all, Unity3D users error Script file name does not appear in the F # script. You can create several classes - the heirs of MonoBehaviour in one file. But there is one caveat - they will be available only from libraries (if done in the way I did). In this case, the use of the namespace does not go away - the compiler requires an explicit indication of the namespace or module. In the event of failure to comply with the requirements in cold blood, throws an error FS0222 .

Now D & D for scripts is ready. Only now it will not look too elegant. In order to make the inspector window like this , I will have to create a CustomInspector for each F # script. Inspector code (using the SphereMoving class as an example):
(VisualStudio's automatic formatting is used, but in fact the code is created without any formatting and indentation, because it was not calculated that this result will be read by someone)

 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 { } } } 


When I started talking about the inspector, I did not specify one important detail - the Unity3D inspector classes save the values ​​entered in the inspector window using the SerializedObject and SerializedProperty classes and there is no need to save these values ​​on my own. As you can see from the help you can get
an iterator for these most SerializedProperty, but I still do not understand how to use this construct correctly, and as a result I got such a mess:

1346840941-clip-21kb


But sometimes it was worse.
Why is this happening and what have I done wrong? And the dog knows it. Therefore - again Reflection and getting the names of all the fields and then getting SerializedProperty using the SerializedObject.FindProperty method. The CanEditMultipleObjects attribute is required so that the inspector can be displayed for several identical scripts that are on the same object, here it is not necessary, but I will leave it for any case. It is also worth noting that I specifically added the Script field which is an ObjectField, and to which you need to send the UnityEngine.DefaultAsset object, which is our imported .fs file. After that, when you click on this field, the .fs file will be highlighted, and not the script inside the library.
So, the result looks like this:



Looking at this picture, an attentive reader exclaims: what a marvel in my garden?
Yes, that's not all.

Part four
Further - deeper. Extend the menu, create scripts and update the Visual Studio solution file.



image
Further, in order to bring the work with F # scripts closer to working with C # / Boo / UnityScript, you need to add the “F # Script” item in the “Asets-> Create” menu and context menu, and add the generation of the project file and include the project in the file decisions (about this below). ,
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); } 

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>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 


— 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(); } } 


xmlEnterData xmlFinishData , «Compile Include» .
UnityEngine , GUID . «Update solution file»…
, «Manual rebuild». , F# , , , . , , , UnityEngine.Object, . , , Unity , , . , , F# .
- , - . ProgressBar':



, ?

UnityEngine.Object . .

, . , , Unity3d . , .

PS ExecuteInEditMode MonoBehaviour ( Monobehaviour.Update ) - , . , Edit-time unity script, C#, ?

Part five
Edit-time



, , , . , , 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.

Expand
 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_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 { } } } 

switch, . GetWNDSize. GetForegroundWindow System.Diagnostics.Process.GetCurrentProcess MainWindowTitle null, GetWindowRect , MainWindowHandle , (0,0). , . , , , , - , () . , , - Apple, , , . Unity3D . , . , , CollectCompileDeploy._current CompilationState.NONE.



. UnityPackage . ( « »?).
3







PS F# . . , .

UPD1 : , ( Flash — ). UnityPackage . gnoblin ' Android.

UPD2 : Google Docs

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


All Articles