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プラグインで利用する予定です。

UnrealEngine4.12でAndroidアプリ作成とインストール

フィーリングで探すと File メニューからパッケージ化するというものが見つかります。

パッケージ化メニューからAndroid(すべて)を選べばインストールバッチファイルが作成されます。
これを実行すればAndroid端末にUnreal Engineのゲームがインストールされます。

やった、インストールできた!なんて喜んでいるあなた
はい、アウト~
実はすでに落とし穴x3にハマっていますので、一つずつ抜け出しましょう。

落とし穴1.スタートアップコンテンツ有りにしているとパッケージ化に時間を要します。(30~40分くらい?)
落とし穴2.ディフォルトのレベルを指定していないとブラックスクリーンになります。(エディタのアクティブなレベルは表示されないのですよ。)
落とし穴3.すべてのAndroidでパッケージ化すると、ファイルがデカ過ぎてゲームを実行できなくなります。

最初の落とし穴から抜け出すのは簡単ですね、プロジェクト作成時に余計なコンテンツを入れなければよいだけです。(コンテンツブラウザから削除してもよい)
二つ目の落とし穴から抜け出すのは難しいですね。Unityの感覚でシーンを指定していなくても、エディタでアクティブだったらそれが表示されると思っていたら大間違いです。
Unreal Engine は Blank プロジェクトを実行すると Black Screen になるのが正しい動作なのです。
適当なこと書いているブログだと Mobile HDR のチェックを外すとかありますが、そんな情報には惑わされないでください!

具体的な解決方法は次の図の通り、プロジェクト設定にてディフォルトのレベルを指定します。
f:id:simplestar_tech:20160703115846j:plain
これでゲーム起動時のブラックスクリーン問題は解決します。

最後の落とし穴もまた抜け出すのが難しい。
スタートアップコンテンツ有りですべてのAndroid向けにビルドすると2.3GBほどのファイルサイズになってしまい
Androidにインストールが成功しても、ストレージに余裕がないなどの理由でOBBファイルは一緒に入らず、ファイルにアクセスできずNo Google Play Store Keyのダイアログが出て先に進めなくなります。

同じような問題はこちらに示されています。
No Google Play Store Key - UE4 AnswerHub

具体的な解決方法としては Blank プロジェクトにして、スタートアップコンテンツは無しにして、パッケージ化の項目で Android(ETC1) を選びます。
パッケージ化も高速で終わり、OBBファイルも小さく(それでも50MBあったけど)インストールで入りきらないなんてこともなくなります。

はい、そんな落とし穴x3にハマったのは私です!
で、すべての落とし穴から抜け出すと次の図で示すように、お手持ちのAndroid端末にてUnreal Engineで作ったゲームが遊べるようになります。

f:id:simplestar_tech:20160703121445j:plain

ちなみに、fps は 60 出てました。
fps を表示したいときは指4本でタップして、コンソールコマンドに stat fps を打ち込むと表示されるようになります。

ゲームエンジン初心者が一番最初に行うだろう作業に
空のプロジェクトをビルドしてAndroid端末で動作チェックするという作業がありますが
Unreal Engineには、こんな落とし穴があるということをここに記録しておきましょう。

プラグイン作成中ですが、つい共有したくなったので書きました。
では、プラグイン作成の作業に戻ります。

UnrealEngine4.12のC++プラグイン機能を利用する

オブジェクト(UE4だとActor)のTransformをC++のコードから編集したいだけなんです。

私がどんな落とし穴にハマっていくか、記録していきます。

Visual Studio 2015 を Community からインストールしました。
あ、ここまでは大丈夫です。
C++がディフォルトで入っていないことは下調べして知っているので、ちゃんとC++の項目にチェックを入れてインストールしました。

7/2 追記:
公式ドキュメント入門書もバッチリ読んでます。
間違いようがないです。

そしてその作業フローを実行すると

コンパイル失敗」

Unreal Engine を 4.12, 4.11, 4.10 と試しても同じエラーが出ました。
これは…Visual Studio の Update に問題があるのかな?

と調べると、確かにそんな記述が数時間前から公式フォーラムにちらほら上がりだしてきた。

When i start UE4 with C++ it gives me compile failed every time - UE4 AnswerHub
Unreal Error "This project could not be compiled. Would you like to open it in Visual Studio?"
Compile Failed when creating Cpp project - UE4 AnswerHub

どうも、Unreal Engine 4 のビルドエラー祭りにナイスタイミングでハマったようだ。
4.12.4 の次期バージョンでは直る予定とのことで
お急ぎの方はひとまず Visual Studio 2015 の Update 3 を Update 2 に戻して使ってほしいとのことです。

めっちゃ時間がかかっていますが Visual Studio のバージョン差し戻し作業しています。
なんか…Visual Studioのバージョン戻しても同じエラーが起こりそうな予感はしているのだけど、とりあえずやってみます。

具体的には、先に Update 3 が適用された Visual Studio をアンインストールして
次のリンクから Update 2 のインストーラ(Visual Studio Community 2015 with Update 2)を取得してインストールするだけです。
Visual Studio Release Notes | Visual StudioVisual Studio 2015 Update 2 now available for download


追記:
ぎゃあぁあああ!
update-2 のインストーラでインストールしても update-3 になってしまった!
終わった。
どうやったら update-2 のまま使えるのか…戻す方法とか無いの!?
何よりここまで時間を無駄にしたことに腹が立っている

追記:
と、ここで転機がおとずれる
実はリンク先で取得した Visual Studio は英語版だったので、今までUE4が文字化けして表示していたエラー内容が表示されるようになった
そこをキーワードに検索をかけて解決策の提示にたどり着く

forums.unrealengine.com

具体的な解決方法は
C:\Program Files (x86)\Epic Games\4.12\Engine\Source\Runtime\Core\Public\Windows\WindowsPlatformCompilerSetup.h
ファイルの末尾に#pragma warning(disable : 4599)を追記して Unreal Engine 4.12 をC++で起動してみると良いとのこと。

うおぉおお!初めてコンパイルエラーが起きずにプロジェクトが立ち上がったぞ!
私の環境では解決した!
やっと入門書の通りに作業が開始できる。

できた!
f:id:simplestar_tech:20160702184120j:plain

Unity の MonoBehaviour を継承したスクリプトコンポーネントに感覚が似ていました。
Unreal Engine だと Actor の Location をいじることで Actor の位置を変えることができました。
以下は実装コードの一部です。

#include "GameFramework/Actor.h"
#include "FloatingAnimActor.generated.h"

UCLASS()
class HEADERREPLACEPROJECT_API AFloatingAnimActor : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	AFloatingAnimActor();

	// Called when the game starts or when spawned
	virtual void BeginPlay() override;
	
	// Called every frame
	virtual void Tick( float DeltaSeconds ) override;

	float RunningTime;
	
};

// 以下は実装ファイルの記述

// Called every frame
void AFloatingAnimActor::Tick( float DeltaTime )
{
	Super::Tick( DeltaTime );

	FVector NewLocation = GetActorLocation();
	float DeltaHeight = (FMath::Sin(RunningTime + DeltaTime) - FMath::Sin(RunningTime));
	NewLocation.Z += DeltaHeight * 20.0f;       //Scale our height by a factor of 20
	RunningTime += DeltaTime;
	SetActorLocation(NewLocation);
}

いや、驚きましたよ。
エディタのコンパイルボタンを押したら、即上記のコードの編集内容が反映されてゲームが実行できました。
エントリの目標達成はまだですが、ひとまず一行目に書いたやりたいことまでは確認できました。

リンクメモ

この記事は、無視してください!


まずここ
OpenGL+Objective-C編 - 勉強

理解したらここ
livedoor Techブログ : 猫でもわかるiPhoneで画像にフィルターをかける方法

あと、ここのオプティカルフローの説明良い
https://courses.engr.illinois.edu/cs543/sp2012/lectures/Lecture%2008%20-%20Feature%20Tracking%20and%20Optical%20Flow%20-%20Vision_Spring2012.pdf

うーむ、これ?
http://blog.negativemind.com/2015/05/15/point-cloud-for-unity/
からの
床井研究室 - シェーダで Point Sprite

ここ、いい
画像処理 — OpenCV-CookBook

ヒストグラム平坦化の原理について
ヒストグラム平坦化のアルゴリズム

関数の説明はここ
ヒストグラム — opencv 2.2 documentation


それでもうまくいかなかったら試す
ゴリラになる知識: ガンマ補正



これ試してみるか
【iOS】UIImageとNSString(Base64)の相互変換 - Qiita


まずは動くかみとけ
Unity C# JSONObjectを使って簡単なJSON文字列をパースする - Qiita

Windowsでキーボード割り当てを変更

先日、GPUが壊れてPCが起動できなくなったことがあり
問題の切り分けのためOSの再インストールを行いました。

そのため、使っている英字キーボードのカスタマイズ情報が無くなりました。
再度割り当てるので、次の記事を参考に再割り当てしました。

www.gigafree.net

設定はこちら
f:id:simplestar_tech:20170717140025j:plain

うまくいきました。
環境は Windows 10 64bit

あと、日本語キーボードを接続したのに、英字キーボードとして認識されたときの問題に直面
Windows 10 環境では 設定→地域と言語 からキーボード配列を設定する項目があるので、そこで英字→日本語にして
再起動すると期待通りに日本語キーボードで日本語を打てた(あたりまえだけど)

ナイーブベイズとは

基本から確かめてみたくなったので
もう一度ナイーブベイズについて確認します。

確率の加法定理と乗法定理の式を使うと、ベイズの定理が導けます。

確率の加法定理とは、ベン図を使うと次のイメージです。
f:id:simplestar_tech:20160424133626j:plain
 {
P(A \cup B) = P(A) + P(B) - P(A \cap B)}

確率の乗法定理とは、ベン図を使うと次のイメージです。
f:id:simplestar_tech:20160424134919j:plain
 {
P(A \cap B) = P(A) \times P(B)
}

ん?
これだけではベイズの式は導けませんね。

次の条件付き確率のイメージを使います。
f:id:simplestar_tech:20160424141831j:plain
 {
P(A \cap B) = P(A|B) \times P(B)
}
同様に、次のイメージも使って
f:id:simplestar_tech:20160424140248j:plain
 {
P(A \cap B) = P(B|A) \times P(A)
}

どちらも  {
P(A \cap B)} についての式なので

次の等式は組めます。
 {
P(A|B) \times P(B) = P(B|A) \times P(A)
}

式を変形すると
 {
P(A|B) = \frac{P(B|A) \times P(A)}{P(B)}
}
この式が出ます。
事後確率 {P(A|B)}の計算が、尤度 {P(B|A)}と事前確率 {P(A)}と周辺確率 {P(B)}で行えます。
これがベイズの定理です。

まだ、ナイーブベイズではないので、以降、注意して読んでいってください。

具体的な問題を扱います。

ここで、事象 A を、迷惑メールである、とします。( {P(A)}は迷惑メールである確率)
事象 B はメールタイトルに「hi」,「% off」が同時に含まれる、とします。( {P(B)}はメールタイトルに「hi」,「% off」が同時に含まれる確率)

ん?

事象 B は、二つの事象を含んでいますので、事象 B, C と分けないと文章が作れませんね。

するとベイズの式は次の形となります。

 {
P(A|B \cap C) = \frac{P(B \cap C|A) \times P(A)}{P(B \cap C)}
}

実際問題を解こうとすると、このベイズの式の尤度 {P(B \cap C|A)}の計算が現実時間で解けなくなります。(組み合わせ爆発により)

そこで、次の考え方を取り入れて問題を単純(ナイーブ)にします。

B, C の事象が本当は互いに独立じゃないのに、独立であるとみなす考え方(単純化、ナイーブにする)を使います。
すると、次の無意味な式が導けます。

 {
P(B \cap C|A) = P(B|A) \times P(C|A)
}

この式からは無意味な値しか得られないということを認識してください。
しかし、尤度の計算は極めて高速に終了します。

このような膨大な組み合わせ計算の単純化(ナイーブにする)手法をナイーブベイズと呼びます。

つまり、この問題は
迷惑メールにおけるメールタイトルに「hi」が含まれる確率 {P(B|A)}を集計によって求め
迷惑メールにおけるメールタイトルに「% off」が含まれる確率 {P(C|A)}を集計によって求め
その二つの確率をかけた値に、そもそもの迷惑メールが送られてくる確率 {P(A)}をかけて
メールタイトルに「hi」,「% off」が同時に含まれた時の、迷惑メールである確率 {P(A|B \cap C)}の分子の部分が求まります。

分子の部分?
分母の値が 1 ではない点に注意です。
つまり、次の迷惑メールではない確率も求めなければ、分類問題は解けません。

標準メールにおけるメールタイトルに「hi」が含まれる確率 {P(B|\bar{A})}を集計によって求め
標準メールにおけるメールタイトルに「% off」が含まれる確率 {P(C|\bar{A})}を集計によって求め
その二つの確率をかけた値に、そもそもの標準メールが送られてくる確率 {P(\bar{A})}をかけて
メールタイトルに「hi」,「% off」が同時に含まれた時の、標準メールである確率 {P(\bar{A}|B \cap C)}の分子の部分が求まります。

分母は共通の値であり、この二つの分子の合計値なので、迷惑メールなのか、標準メールなのかの判定は分子だけでも行えます。
分母を出して、迷惑メールである確率 {P(A|B \cap C)}を求めて、0.5以上かどうかを見るというのも、同じ判定結果を得ます。

これが最も単純な問題であり、似たような解説は他のサイトや記事でも多いです。
ナイーブベイズについて、理解が深まったのでこの記事を書いてよかったです。

以上。