UE4のスクリプトをC#で書きたい(実験編)
C#でスクリプトを書きたい
私はUE4のC++は好きです。マーケットプレイスにもいくつかC++プラグインを出してます。ですがC#もたくさん書いているので、時々C#だったらもっと楽に書けるのになあと思うことはあります。
そんな事を考えていて、ふと次のような書き込みをTwitterでしました。
ふとUE4のスクリプトをC#で書きたいなあ。どうしたらいいかなあと考えていたんだけど、なかなかいい実装が思いつかない。
— ayuma (@ayuma_x) 2019年8月24日
すると@kekyo2さんからこんなお誘いがあり、今日実際に1日かけてトライしてきました。
IL2C
今回の主役となるツールは@kekyo2さんの作成されているIL2Cです。
IL2Cを用いるとC#をコンパイルしたILを含むDLLやEXEからC言語のソースコードが生成できます。( 凄い)
これを使ってC#のコードをUE4のC++から呼んでみようというのが今日行った実験です。
開発環境
今回行った実験は以下の環境でおこなっています。
- Visual Studio 2019
- Unreal Engine4 Ver. 4.22
また、以下に今回行った手順を書いていきますが色々試行錯誤した結果のため、手順を見ただけでは何故それをやるのかわからない箇所もあるかと思います。
今回の試みはまだ実験段階のためご了承ください。
今回作成したプロジェクト一式は以下に置いてあります。(UE4, C#全部入ってます) github.com
C#プロジェクトの作成
まずC#のクラスライブラリのプロジェクトを作成します。今回は.NET Standardのクラスライブラリを選択しました。
次にプロジェクトファイル(*.csproj)のPropertyGroupに
この記述は通常.cで出力されるファイルを.cppで出力します。
これはUE4でこの後ビルドを正常に通すために必要な設定です。
<PropertyGroup> <TargetFramework>netcoreapp2.1</TargetFramework> <IL2CEnableCpp>true</IL2CEnableCpp> </PropertyGroup>
次にデフォルトで作成されているClass1.csに以下の記述を行います。
using System; namespace UE4Il2CSample { public class Class1 { public static int Add(int a, int b) => a + b; } }
staticなメソッドを1個用意しました。 単純にint型の引数2つを足し算する関数です。
今回はこのAdd関数をUE4から使ってみる事を目標にします。
次にNuGetから「IL2C.Buid」をプロジェクトに追加します。
追加が完了したらビルドを行います。
C#プロジェクト側でやることは以上です。簡単!!
Cのコードの確認
C#プロジェクトでのビルドが成功すると、[*.dllの出力ディレクトリ]-[IL2C]に変換されたCのコード一式ができています。
この出力されたCコード一式をUE4側で使っていきます。
UE4プロジェクトの作成
C#を変換したCのコードを使うUE4のプロジェクトを使います。 どんな設定でも構わないですが、今回はC++コードを使うのでC++プロジェクトでIL2CTestという名前のプロジェクトを作成しました。
(ブループリントプロジェクトで作成しても問題はないです)
IL2Cランタイムのビルド
C#から出力されたCコードを実行するために必要なランタイムを追加します。
今現在は自分でランタイムをビルドする必要があります。
まず以下のnugetのページから手動でパッケージをダウンロードします。
NuGet Gallery | IL2C.Runtime 0.4.70
次にダウンロードしたil2c.runtime.0.4.70.nupkgをil2c.runtime.0.4.70.zipにリネームしたあと解凍します。
解凍したディレクトリのil2c.runtime.0.4.70\lib\native\ディレクトリをUE4のプロジェクトディレクトリにコピーします。
nativeディレクトリ内にC++のスタティックライブラリのプロジェクトを新規作成し、中のsrcとincludeをプロジェクトに追加します。
次にプロジェクトのプロパティから[構成プロパティ]-[C/C++]-[全般]-[追加のインクルードディレクトリ]に以下のディレクトリパスを追加します。 $(ProjectDir)/include
ここでIL2CTest\native\src\Core\il2c_allocator.cの中身を一部修正します。
void* il2c_cleanup_at_return__(void* pReference) { IL2C_THREAD_CONTEXT* pThreadContext = il2c_get_tls_value(g_TlsIndex__); if (pThreadContext != NULL) { pThreadContext->pTemporaryReferenceAnchor = pReference; } return pReference; }
設定が完了したらRelease, x64でビルドします。
native\x64\Release\にIL2C.Runtime.libができているのを確認します。
UE4プロジェクトへ変換されたCファイルを追加
以下のディレクトリにC#から出力されたincludeとsrcディレクトリをコピーします。
IL2CTest(UE4のプロジェクトディレクトリ)\Source\IL2CTest
これでUE4のC++プロジェクトに必要なファイルが追加されました。
ちょっとしたおまじない
現状ビルドを通すために以下のおまじないをします。
- Class1.cppと同じディレクトリにClass1.hを追加します。(中身は空でよい)
Class1.cppの先頭行に
#include "Class1.h"
を追加します。[C#ライブラリ名]_internal.cppの先頭行にも
#include "[C#ライブラリ名]_internal.h"
を追加します。
このおまじないをしないと現状ビルドを通すことができないです。
プロジェクトの更新
IL2CTest.uprojectを右クリックし、「Generate Visual Studio project files」を選択し、プロジェクトファイルを更新します。
以下のように必要なファイル一覧がプロジェクトに並びます。
Cファイルへのインクルードパスの追加とlibファイルの参照追加
追加したCのファイルとランタイムへのインクルードパスをビルド設定ファイル(IL2CTest.Build.cs)に記述します。
また合わせてランタイムライブラリへの参照設定も記述します。
// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. using UnrealBuildTool; public class IL2CTest : ModuleRules { public IL2CTest(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay" }); PublicIncludePaths.AddRange(new string[] { // IL2C.Runtimeのincludeディレクトリの追加 ModuleDirectory + "/../../native/src/", ModuleDirectory + "/../../native/include/", // C#から生成されたCファイルのincludeディレクトリの追加 ModuleDirectory + "/src/", ModuleDirectory + "/include/" }); // IL2C.Runtime.libの参照追加 PublicAdditionalLibraries.Add(ModuleDirectory + "/../../native/x64/Release/IL2C.Runtime.lib"); } }
UE4プロジェクトのビルド
以上で設定は完了したのでビルドします。
設定がうまくいっていれば以下のように成功するはずです。
UE4 C++からCの関数を呼ぶ
実際にC#から変換されて出力されたCの関数を呼んでみます。
今回はActorを継承したIL2CSampleActorを作成し、BeginPlayの中から呼んでみました。
// Fill out your copyright notice in the Description page of Project Settings. #include "IL2CSampleActor.h" #include "include/UE4Il2CSample.h" // Sets default values AIL2CSampleActor::AIL2CSampleActor() { // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. PrimaryActorTick.bCanEverTick = true; } // Called when the game starts or when spawned void AIL2CSampleActor::BeginPlay() { Super::BeginPlay(); auto addRet = UE4Il2CSample_Class1_Add__System_Int32_System_Int32(1, 2); UE_LOG(LogTemp, Log, TEXT("IL2C add ret = %d"), addRet); }
上記cpp内のUE4Il2CSample_Class1_Add__System_Int32_System_Int32
がC#で作成したAdd関数です。
関数名が長いのは関数名をユニークにするために、ネームスペース、クラス名、関数名、引数の型(オーバーロード対策)が繋がっているからだそうです。
今回は1 + 2を計算しているのでうまくいけば3が返ってくることになります。
実行
先程作成したIL2CSampleActorをレベルに配置してPlayしてみます。
ログからちゃんと足し算されている結果が確認できました。
まとめ
@kekyo2さんに丸1日付き合っていただき、C#で書いた関数をUE4のC++から呼ぶことができました。 今後色々探っていく中でこの仕組みの可能性を探っていけたらいいなあと思ってます。
次はIL2C.RuntimeをUE4のプラグインにすると、UE4側での実装量がだいぶ減るような気がするのでチャレンジしてみようかと思います。