Unity3DとF#を友達にした方法


最近、私は関数型プログラミングにますます興味を持ち、言語を選択するとき、 HaskellF#という 2つの言語から選択しました。
F#では、MSILアセンブリにコンパイルできるようになりました。これにより、他のMicrosoft .Net言語でF#クラスライブラリを使用できるようになり、彼自身もそれらを使用できるようになりました。 それに加えて、私はUnity3Dの初心者でもあり、MSILにコンパイルする場合、UnityでF#スクリプトを使用できますか? グーグルは答えを与えた:人間には不可能です。 クラスライブラリを作成し、プロジェクトにUnityEngine.dllライブラリへのリンクを配置し、それをアセットとしてコンパイルおよびインポートし、ライブラリから直接Mono-behaviorコンポーネントを追加できますが、これはあまり便利ではありません。 ただし、Google、Reflection、およびUnityのヘルプを確認した後、エディター内でのF#スクリプトの作業を、組み込み言語のスクリプトを使用した作業に近づける(ただし正確に繰り返すことはできません)ことはできました。 詳細はhabrakatの下にあります。




パートゼロ


私は何か間違ったことをしたことを認めます(そして、それがそうである可能性が最も高い)。そして、重大な間違いとそれほど大きな間違いはありません(それでも、私はそれらについて、またはこれを行う方法を知りたい別のアクションの方が良い/きれいです)。 テレマスター効果もある可能性があり、この記事で説明するエラー/グリッチは発生しません。 私は自分の行動と観察を説明するだけです。 理解して扱ってください。 最後に、それは私と初心者が学び、間違いを犯し、それらを修正するために必要なものです!

Habrahabrのモデレーターに感謝します。私のリクエストで、 ランダムに送信されたが公開の準備ができていないトピックをドラフトに置いてくれたことに感謝します。 これからは、もっと注意します。

パート1
箱から出してどのように見えますか


それはパンなしでそれがどのように見えるかです。 既にコンパイルされたライブラリを1つの簡単なスクリプトで使用します。
(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 


このスクリプトは元々、私の編集者 -スクリプトによって作成されました。 これは、コメントの存在と名前空間の明示的な表示によるものです(以下でその役割について詳しく説明します)が、少し変更しました。
ライブラリをコンパイルしてプロジェクトにインポートすると、次の結果が得られます。



これは、記事の冒頭で不便さについて話していたときの意味です-複数のライブラリを作成し、それらを異なる場所に保持する必要があります(少なくとも何らかの可視性を作成するため)または長いリストで何かを見つけようとする場合プロジェクトは成長します。

気配りのある匿名habrauserはすでに気づいているため、松葉杖がいくつかありました-System.TypeLoadException例外がコンパイラによってスローされることなくインポートされるように、 ライブラリの隣にFSharp.Core.dllライブラリを配置する必要があります
そして正確に言うと、FSharp.Core.dllだけでなく、すべての依存関係のライブラリーは、Unityコンパイラーが単独で解決できない競合を引き起こします。 F#を使用したゲームはろうそくに値しないことが明らかになり、Unity3dの下に何かを書くことは、 動物の好奇心からのみ受け入れられ、それでも大したボリュームではありません。

ここでの明らかな解決策は、すべてのF#ソースをヒープに配置し、それらをコンパイラに直接送信できるエディタースクリプトを使用することです。 ちなみに、イベントが説明される少し前に、広大な広大な広大さについて、私はこの記事に出会い、それが私の研究の出発点になりました。

次の部分の説明では、2つの場合を除き、どういうわけかジャングルに深く入りません。
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はその後軟膏にハエを投げます。どこで、以下で説明します)。

パート2
じゃじゃ馬ならし
F#のCodeDomProviderを受け取って操作します


しかし、2番目の「BUT」をまだ回避できる場合、最初の「BUT」はどうでしょうか。
ここでは、グーグルの過程で見つかったF#Power Packが役に立ちました。これは、何らかの方法でF#を使用するすべての人に役立つと思います。 引数(代わりに)「FSharp」(C#のCodeDomProviderの作成方法に似ています-引数「CSharp」)および「F#」(すり抜けますか?)

F#Power Packは、System.CodeDom.Compiler名前空間の通常のCodeDomProviderと比べて使用上の違いがほとんどない、切望されているMicrosoft.FSharp.Compiler.CodeDom.FSharpCodeProviderを提供します。

問題は小さいようです-プラグアンドプレイですが、ここにも落とし穴があります:
1.このライブラリは.Net Frameworkパッケージに含まれていないため、プロジェクトに配置する必要があります。
2. FSharpCodeProvider自体がSystem.CodeDom.Compilerから型にアクセスするため、コンパイル中に永続的なSystem.TypeLoadExceptionを取得する可能性があり、スタジオIntelliSenseも先導しません。
Xaosの意志で、私はそれをやったので、ReflectionとActivatorを使用してすべてをしました。 おそらく、結局のところ、私はどこかで間違いを犯しましたが、何かを変更するには遅すぎます。 また、これらの目的のために(同じF#用に)私が最初に書いたライブラリは、そのようなトリックなしでコンパイルされた(つまり、Reflectionを介してFSharpCodeProviderで実行するメソッドはありませんでした) System.CodeDom.Compiler名前空間の問題を抱えた同じ問題は、リフレクションによって解決されました。 好奇心one盛な人なら誰でもここで短いペーストを知ることができます

著者がライブラリをF#に追加しなかったのはなぜですか?
そして、すべてをコンパイルしてコンパイルできるコードだけでなく、エディタースクリプトのコードもF#で書くつもりだったからです。 しかし、ここで興味深い詳細が明らかになりました-F#のエディタークラスは、Unity3Dエディターを無条件のノックアウトに送ります。 エラーメッセージ、ポップアップ、 エラーデータ (すべてのプロジェクトとコメントと共に)をエンジンの開発者、ダングリングプロセス、またはMessageBox に送信することを親切に提供します 。 何もない! 空虚。 この動作は通常、 OllyDbgを使用して検出されたいくつかのアンマネージライブラリでのスタックオーバーフローまたはエラー中に観察されますが、 残念ながら、少なくとも何らかの方法でそれを必要とし、試してみるだけでダウンロードします(!)転倒の原因を試すのは、ささいな怠lazでした。
好奇心の強い人のために、F#のエディタークラスコードを貼り付けます。 注意! このクラスでライブラリを追加した後、コンパイルの結果として何が起こるかがライブラリディレクトリになり、そこから消去するのが問題になることがあり、その結果、永続的な「未開封」が得られます(エディタクラスが湾曲しているためにエディタがクラッシュするため)エディターを有効にしようとしたとき。 Libraryフォルダを削除する必要がありますが、これは通常最悪の結果をもたらします。 警告した。

CodeDomProviderに戻ると、多くのことがReflectionを介して行われていることを思い出します。 そして、ここに統計軟膏で約束されたフライがあります -アクティベーターを使用してタイプ(!)System.CodeDom.Compiler.CompilerParametersを取得しようとすると、「 作成されるタイプはMarshalByRefObjectから派生しない 」ため、 NotSupportedExceptionを取得しますコンパイラーはまだパラメーターを設定する必要があるためです。 これを行うには、あまりにもエレガントに行動する必要はありません-現在のドメインのアセンブリコレクションで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); 

重要なお知らせ
同じように、アセットパック(いわゆるAssetBundle)からC#スクリプトをコンパイルできることを言わなければなりません。 実際には、バンドルではすべてのスクリプトがTextAssetsの形式になっています。 この事実を考えると、それらの使用には問題があります。 ただし、CodeDomProviderを使用すると、コンパイルして使用できます。

CompilerParametersインスタンスは、3つの必要な依存関係(はい、マイナスです。これは後で修正されますが、コンパイルには十分です)のみを引数として作成されます。 配列の3番目のメンバーは、UnityEngine.dllライブラリへのフルパスを収集するプロパティを返し、次のようになります。

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


小さなことは、ファイルのリストをコンパイルしてコンパイラに送信することです。 しかし、もう1つ微妙な違いがあります。最初にこのスクリーンショットを提供します。

画像

「ここで何かがおかしい」とスターリッツは思った。 そして、私は正しく考えました-結果のエディタースクリプトを既に使用しているプロジェクトのスクリーンショットを示しました。 また、私は自分自身に先んじることはありません。 さらなる行動の本質が明確になるように、私はこれをしなければなりませんでした。 そして、ここにあります:ライブラリ内のスクリプトではなく、スクリプト要素をクリックしたときにアセットが強調表示されるためには、タイプ<=>アセットの関連付けを作成する必要があります。したがって、コンパイル用のスクリプトのリストにスクリプトを追加しても意味がありませんインポートされません。 これを確認するには、AssetDatabaseを使用する必要があります。 同時に、「アセットにあります!=アセットにインポートされたフォルダー」。 しかし、それについては後で。 このステップで取得する必要があったものはすでに行われています-コンパイラーとそのパラメーターのインスタンスを受け取って使用する準備をしました。

アイコンといえば
F#スクリプトアイコンが常に私のものと同じになるとは限らないことに注意してください。 なんで? EditorGUIUtility.ObjectContentメソッド(現在のEvent.Current.typeの値に基づいてウィンドウを描画するオーバーロードの1つであるUnityEditor.DoObjectFieldメソッド内で呼び出されます)のヘルプからのヘルプからは、ほとんど理解できませんが、エディターは最初に見つけようとしているようですキャッシュ内のアイコン、次にシステムからアイコンを取得し、適切なアイコンが見つからなかった場合にDefaultAssetアイコンを割り当てます。


パート3
スズメの大砲からドラッグアンドドロップといたずらな検査官


通常のドラッグアンドドロップでコンポーネントをオブジェクトに追加することは非常に便利です。 しかし、実際には、スクリーンショットを表示した後、私は完全な真実を語りませんでした-編集者は、彼が強調するものがまだスクリプトであることを理解しませんでした。 認識されないアセット(たとえば、.fsスクリプトと同様に未知の拡張子を持つファイル)は、UnityEngine.DefaultAssetタイプでインポートされます(明らかに、Internalであるため、エディタースクリプトでは使用できませんが、これはまったく気にしません)。 しかし、資産オブジェクトをその場所から直接取得することは不可能であるため(いずれにしても、私は方法を見つけられませんでした)、それは正反対に行うことができます、比fig的に言えば、 樽を作る必要がありました

 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ファイルへのパスのリストから、それらがインポートされたアセットのリストを取得します。 このようなアセットには、F#スクリプト用のCustomInstectorの作成時に現れる1つの機能があります。つまり、UnityEngine.DefaultAssetは、ウィンドウでインスペクターまたは別のアクションを選択した後にのみ AssetDatabaseにロードされます。 したがって、ここではDirectory.GetFilesを使用して取得したパスを使用して手動でロードされます。
F#スクリプトアセットのCustomInspectorは、純粋に表面的なものであり、次のようになります。

画像 UnityEngine.DefaultAssetタイプがエディタースクリプトで使用できない場合、UnityEngine.ObjectのCustomEditorを作成し、このUnityEngine.Objectが、たとえばプレハブやテクスチャではなく.fsファイルであるかどうかを確認する必要があります。コードを表示します(ちなみに、選択してコピーしたり、編集することもできますが、結果は特別に保存されません)。
この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イベントがプロパティから返されます。ただし、スクリプトオブジェクトはゲームオブジェクトにのみ追加できるため、ゲームオブジェクトインスペクターウィンドウでこれらのD&Dイベントを追跡できるCustomInspectorが必要です。 奇妙なことですが、これを実装するためには、すでに2つのオプションがあります。
1. UnityEngine.GameObjectクラスにCustomInspectorを使用します
2. UnityEngine.TransformクラスにCustomInspectorを使用します
ご注意
UnityEngine.Transformは、オブジェクトの変換(位置、回転、スケール)を示します。 それはすべて 、例外なくゲームオブジェクトにあります。
画像 後者が好ましい。 そして、右の写真はその理由を示しています。
DrawDefaultInspectorを使用することによる頭痛の種があります。 結局、イベントをキャッチするだけで済みましたが、実際には、CustomInspectorは何かを描画する必要があります。 または、DrawDefaultInspectorを呼び出しますが、スクリーンショットはこれが何につながったかを示しています。
しかし、すべてが失われるわけではありません。なぜなら ウィンドウは非常にシンプルで、復元することは難しくありません。 Vector3構造の3つのフィールドを作成するだけで十分です。
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をターゲットタイプとして使用する場合、通常は混乱します。コンボボックスは通常の入力フィールドになり、レイヤーマスクは整数として入力する必要があります(チェックボックスを内部に持つコンボボックスの代わりに)

コードをよく見ると、完全に明確ではない詳細が明らかになります-ドロップイベント中に追加されるコンポーネントはどれですか?
GameObject.AddComponentのオーバーロードの1つは、 スクリプト名を引数として取りますが、実際にはタイプ名であり、このスクリプトが配置される必須ネームスペースはすでに決定されています。

名前空間の詳細
約束どおり、名前空間をめぐるすべての混乱の明確化: ドキュメントに基づいて 、名前空間(少なくともC#の場合)を明示的に指定することは不可能になりました。 しかし、その場しのぎのライブラリのレベルでさえF#で作業して、この詳細に気付きました。名前空間とクラス名は任意です。 つまり スクリプトファイル名がクラスの名前と一致しないというエラーは、F#スクリプト内のすべてのUnity3Dユーザーによく知られていますが表示されません。 1つのファイルにMonoBehaviourの子孫である複数のクラスを作成できます。 しかし、1つの警告があります-それらはライブラリからのみ利用可能になります(あなたが私と同じようにした場合)。 同時に、名前空間を使用して逃げることはできません-コンパイラには明示的な名前空間またはモジュールが必要です。 準拠に失敗した場合、落ち着いてエラーFS0222をスローします。

これで、スクリプトのD&Dの準備ができました。 今だけそれはあまりにもエレガントに見えません。 このようなインスペクターウィンドウを作成するには、F#スクリプトごとにCustomInspectorを作成する必要があります。 インスペクターコード(例として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 { } } } 


インスペクターについて話し始めたとき、1つの重要な詳細を示しませんでした-Unity3Dインスペクタークラスは、 SerializedObjectクラスとSerializedPropertyクラスを使用してインスペクターウィンドウに入力された値を保存し、これらの値を自分で保存する必要はありません。 あなたが得ることができるヘルプから見ることができるように
これらの同じSerializedPropertyのイテレータですが、このデザインを正しく使用する方法を理解できませんでした。その結果、次のような混乱が生じました。

1346840941-clip-21kb


しかし、時にはそれが悪化しました。
なぜこれが起こっているのか、何を間違えたのか? しかし、犬は彼を知っています。 したがって、再度リフレクションを行い、すべてのフィールドの名前を取得し、 SerializedObject.FindPropertyメソッドを使用してSerializedPropertyを取得します。 CanEditMultipleObjects属性 、同じオブジェクトにある複数の同一スクリプトのインスペクターを表示できるようにするために必要です。ここではオプションですが、 念のために残しておきます。 また、ObjectFieldであるスクリプトフィールドを具体的に追加し、インポートされた.fsファイルであるUnityEngine.DefaultAssetオブジェクトを送信する必要があることも注目に値します。 その後、このフィールドをクリックすると、ライブラリ内のスクリプトではなく、.fsファイルが強調表示されます。
したがって、結果は次のようになります。



この写真を見ると、気配りのある読者は叫ぶでしょう: 私の庭での奇跡は何でしょうか?
はい、それだけではありません。

パート4
さらに-より深い。 メニューを展開し、スクリプトを作成し、Visual Studioソリューションファイルを更新します



画像
さらに、F#スクリプトでの作業をC#/ Boo / UnityScriptでの作業により近づけるために、メニュー「Asets-> Create」とコンテキストメニューに項目「F#Script」を追加し、プロジェクトファイルの生成とファイルへのプロジェクトの追加も追加する必要があります決定(これについては以下で詳しく説明します)。 判明したように、プロジェクトインスペクターのコンテキストメニューもメニューです
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#, ?


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.

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


All Articles