simplestarの技術ブログ

目的を書いて、思想と試行、結果と考察、そして具体的な手段を記録します。

UnrealEngine4.12のC++プラグインでスタティックリンクライブラリを利用する

入門書や公式リファレンスを読んでいてびっくりしたのですが
Unreal Engine におけるプラグイン機能は、次の図で示す通りゲームのプロジェクトにC++ソースコードが配置される感じです。

f:id:simplestar_tech:20160703175311j:plain

プラグインは独立したプロジェクトにならないことに衝撃を受けました。

こんな感じのドキュメントがあるほど
けっこう面倒な処理を手で加えないと
スタティックリンクライブラリを使うプラグインというものを作れないそうなのですよ。

私に限ってかもしれませんが
自分の書くコードにはほとんど価値なんてなくて、世にある優れたライブラリの力を借りつつプラグインって作るものですよね?
そんな作り方が簡単にできない仕組みになっているとは、どういうことなのか!

と、また落とし穴にハマったので抜け出してみたいと思います。

OpenCVをUnreal Engine4で使用する、というドキュメントに参考になるコードがあったので利用します。

追記;
で、実際にスタティックリンクライブラリを介してDLLの実装内容を利用できるところまで確認しましたので要点を記載します。

1.Win32コンソールアプリのテンプレートを利用してDLLを作成します。(シンボルのエクスポートを忘れずに行うこと)
今回は単純に動作確認したいだけだったので、SimpleMathという適当なDLLプロジェクトを用意し、関数を次の図に示す通りに作りました。
f:id:simplestar_tech:20160704081254j:plain
落とし穴がさっそくあります。Unreal Engine 4はx64プロジェクトですので、このDLLもx64でビルドしなければいけません。(DLL機能を使うコードを書いてリンクする時にエラーが出ます。)
作った成果物のうち .lib ファイル(スタティックリンクライブラリ)は次の通りの場所に配置します。
f:id:simplestar_tech:20160704081901j:plain
ThirdParty製ライブラリファイルの配置時にも落とし穴があります。.libとセットで.dllファイルも配置したいところですが、プロジェクトが.dllの配置を見ている箇所は一つしかありません。
次に示すBinaries\Win64フォルダです。(ここにThirdPartyの.dllファイルをすべて配置します。試していませんがたぶんサブフォルダ切れると思います。私が設計担当ならそう作りますので)
f:id:simplestar_tech:20160704082417j:plain

2.プラグイン側でThirdPartyのスタティックリンクライブラリ、ダイナミックリンクライブラリを参照します。
さっそく落とし穴があり、ハマりました。例で示したOpenCVを利用するものを参考にするとコンパイルできなくなります。
Unreal Engine 4.12 からは次のコードにするとコンパイルできるようになるそうです。(ファイルはプラグインのSimplePlugin.Build.csファイルです。C#ファイルなので、C++プロジェクトから参照が正しく機能せず、Unreal Engine特有のクラス定義がどんなフィールドを持っているか、ランタイムで解析するなども行いました。しかし、簡単にプロジェクト名を取得する方法が無いようなので、おかしな文字列操作コードを仕込んでいます。TargetInfoにプロジェクト名とかあると良かったのですけどね、なかったです。)

// Copyright 1998-2016 Epic Games, Inc. All Rights Reserved.
using System;
using System.IO;
using System.Reflection;

namespace UnrealBuildTool.Rules
{
	public class SimplePlugin : ModuleRules
	{
		public SimplePlugin(TargetInfo Target)
		{
			PublicIncludePaths.AddRange(
				new string[] {
					// ... add public include paths required here ...
				}
				);

			PrivateIncludePaths.AddRange(
				new string[] {
					"Developer/SimplePlugin/Private",
					// ... add other private include paths required here ...
				}
				);

			PublicDependencyModuleNames.AddRange(
				new string[]
				{
					"Core",
					// ... add other public dependencies that you statically link with here ...
				}
				);

			PrivateDependencyModuleNames.AddRange(
				new string[]
				{
					// ... add private dependencies that you statically link with here ...
				}
				);

			DynamicallyLoadedModuleNames.AddRange(
				new string[]
				{
					// ... add any modules that your module loads dynamically here ...
				}
				);


            LoadThardParty(Target);
        }

        private string ModulePath
        {
            get {
                FileReference CheckProjectFile;
                string ProjectNameModuleRules = System.Reflection.Assembly.GetExecutingAssembly().GetName().Name;
                string ProjectName = ProjectNameModuleRules.Substring(0, ProjectNameModuleRules.Length - "ModuleRules".Length);
                UProjectInfo.TryGetProjectForTarget(ProjectName, out CheckProjectFile);
                RulesAssembly r = RulesCompiler.CreateProjectRulesAssembly(CheckProjectFile);
                FileReference f = r.GetModuleFileName(this.GetType().Name);
                return Path.GetDirectoryName(f.CanonicalName);
            }
        }
        private string ThirdPartyPath
        {
            get { return Path.GetFullPath(Path.Combine(ModulePath, "../../ThirdParty/")); }
        }

        public bool LoadThardParty(TargetInfo Target)
        {
            bool isLibrarySupported = false;

            // Create Plugin Path 
            string SimpleMathPath = Path.Combine(ThirdPartyPath, "SimpleMath");

            // Get Library Path 
            string LibPath = "";
            bool isdebug = Target.Configuration == UnrealTargetConfiguration.Debug && BuildConfiguration.bDebugBuildsActuallyUseDebugCRT;
            if (Target.Platform == UnrealTargetPlatform.Win64)
            {
                LibPath = Path.Combine(SimpleMathPath, "Libraries", "Win64");
                isLibrarySupported = true;
            }
            else if (Target.Platform == UnrealTargetPlatform.Win32)
            {
                LibPath = Path.Combine(SimpleMathPath, "Libraries", "Win32");
                isLibrarySupported = true;
            }
            else
            {
                string Err = string.Format("{0} dedicated server is made to depend on {1}. We want to avoid this, please correct module dependencies.", Target.Platform.ToString(), this.ToString()); System.Console.WriteLine(Err);
            }

            if (isLibrarySupported)
            {
                //Add Include path 
                PublicIncludePaths.AddRange(new string[] { Path.Combine(SimpleMathPath, "Includes") });

                // Add Library Path 
                PublicLibraryPaths.Add(LibPath);

                // Add Dependencies 
                if (!isdebug)
                {
                    //Add Static Libraries
                    PublicAdditionalLibraries.Add("SimpleMath.lib");

                    //Add Dynamic Libraries
                    PublicDelayLoadDLLs.Add("SimpleMath.dll");
                }
                else
                {
                    //Add Static Libraries (Debug Version)
                    PublicAdditionalLibraries.Add("SimpleMath.lib");

                    //Add Dynamic Libraries (Debug Version)
                    PublicDelayLoadDLLs.Add("SimpleMath.dll");
                }
            }

            // Definitions.Add(string.Format("SOME_PREPROCESSOR={0}", isLibrarySupported ? 1 : 0));

            return isLibrarySupported;
        }
    }
}

3.プラグインコードでThirdPartyのライブラリを利用する。
f:id:simplestar_tech:20160704083700j:plain
この赤い波線が気になるのですよね。
実行時はIncludesへのパスが設定されているのでコンパイルエラーは起こりませんが、Visual Studio でコーディング中はパスの参照が切れているのでヘッダファイルが見つからない旨を教えてくれます。
直接ゲームプロジェクトのインクルードディレクトリをいじれば解決しそうですが、試してはいません。(たぶん解決しますが、プロジェクトを自動生成している関係でいつか誰かがクリアしちゃうものなのだと覚悟する必要があるでしょう)
プラグイン側のテスト実装は次の通りです。(ここにあるコメントの通り、相対パスでIncludesにあるヘッダファイルを指定するとプロジェクト設定に非依存で参照問題を解決できます。)

// Copyright 1998-2016 Epic Games, Inc. All Rights Reserved.

#include "SimplePluginPrivatePCH.h"
#include "SimpleMath.h"
// #include "../../../ThirdParty/SimpleMath/Includes/SimpleMath.h"

class FSimplePlugin : public ISimplePlugin
{
	/** IModuleInterface implementation */
	virtual void StartupModule() override;
	virtual void ShutdownModule() override;
};

IMPLEMENT_MODULE( FSimplePlugin, SimplePlugin )



void FSimplePlugin::StartupModule()
{
	// This code will execute after your module is loaded into memory (but after global variables are initialized, of course.)

	CSimpleMath a;
	int sum = a.Sum3Int(1, 2, 3);
	return;
}


void FSimplePlugin::ShutdownModule()
{
	// This function may be called during shutdown to clean up your module.  For modules that support dynamic reloading,
	// we call this function before unloading the module.
}

デバッグ実行時に落とし穴がありました。
エディタを起動しながらステップ実行してプラグインのコードを動作確認をしたいなら、ビルド時の構成は次のものを利用します。(初回ビルドに5分ほど時間を要しますよ。)
f:id:simplestar_tech:20160704084404j:plain
ディフォルトではDevelopment Editorになっているようで、そのときの挙動をみるとリリースビルドに近かったです。
Sippingはさらに最適化が施される形なのかなと予想(選んで実行はしていない)

はい、ここまでの1.2.3.で示したステップを踏むことで私の環境ではThridParty製のライブラリをプラグインで利用できることが確認できました。
自分は自分で独自にOpenCVなどのThridParty製のライブラリをUnreal Engineプラグインで利用する予定です。