UnrealLIFE!

UE4やプログラミング言語に関するブログです。

UE4用mrubyプラグインを作ってみる。

UE4AdventCalender其ノ弐の11日目の記事です。

小ネタのつもりで書いたらめっちゃ長くなっちゃいました。

今回の記事は、UE4でmrubyを使えるようにするプラグインを一通り作ってみたという記事です。
UE4ではブループリントとC++の2つの言語を使ってプログラミングをしていきますが、ブループリントはビジュアルプログラミング言語で、C++は低レイヤーのプログラミング言語なので、その中間のテキストのスクリプト言語が無いかなぁと思ったので、即興で作ってみました。(正直言ってしまうとLispにしようかとも思ったんですが、ブループリントでC++な皆さんに全力で逃げられそうだったので断念しました)

先に断っておきますと、ブループリントやC++の置き換えではなく、設定ファイルとして利用するという一発ネタに近い感じの記事です。完全なスクリプトとして実装するためにはAPIリファレンス読み込まなければいけないのでめんどくさかった。 ただ、Ruby自体はVagrantやChef、Metasploit Frameworkなどのソフトウェアで設定ファイルや拡張言語として利用されているので、しっかり実装すれば有用なものになると思います。

今回の主な目的は、組み込み向けRubyの実装であるmrubyをUE4に組み込んで設定ファイルとして利用するというものです。
当初はプラグインの利用についてのみの解説だったのですが、少し有用性に欠けていたと思ったので、UE4にCやC++で書かれた外部ライブラリ(今回は.lib形式の静的ライブラリ)を組み込んでみるという記事にしました。
その際に必要となるCとUnrealC++の連携やライブラリのリンクなどを主に解説して、mrubyの細かい部分は省きながらいきたいと思います。(mrubyに関しては後日別記事を書くかもしれません)
一応実装の全てを見れるようにgithubにコードを上げています。詳細はそちらをご確認ください。
https://github.com/hsssnow23/MRubySetting

UE4のバージョンは4.10を、mrubyは1.2.0を使っていきます。

mrubyのビルド

mrubyはバイナリが配布されておらず、自力でgithubからソースコードを取得してコンパイルする必要があります。
ここでの注意は、UE4のエディタは64bit版しかないので基本的に64bit版を使用するようにすることです。 32bit版に切り替える際も自動で行えるようにしています。(たぶん) 今回は64bitのみをビルドして使います。
ちなみにUE4とmrubyでVisualStudioのバージョンをあわせないとシンボル未解決のエラーが出たりするのでそれも注意ですね。(3時間ぐらいハマった)

今回使った依存ソフトウェア

mrubyをgithubからダウンロードして展開したフォルダ中にbuild_config.rbがあるので、8行目付近の

toolchain :gcc

toolchain :visualcpp

に書き直し、 "VS2015 x64 NativeTools コマンドプロンプト"をスタートメニューから起動し、

cd mruby-[version]/
ruby ./minirake

を実行します。
すると、"mruby-[version]/build/host/"の中にlibmruby.libができます。
このlibmruby.libとmrubyのルートディレクトリにあるincludeディレクトリを、UE4用のC++プラグインを実装するモジュールのフォルダのThirdParty/MRubyディレクトリに、libmruby.libはLibraries/libmruby.x64.lib、includeディレクトリはIncludesにリネームして置きます。

これでmrubyの準備は完了です

mrubyをUE4のモジュールにリンクする

.libのリンクにはモジュールの設定ファイルである*.Build.csを改変する必要があります。 追記したコードを載せます。

public class MRubySetting : ModuleRules
{
    private string ModulePath
    {
        get { return Path.GetDirectoryName(RulesCompiler.GetModuleFilename(this.GetType().Name)); }
    }

    private string ThirdPartyPath
    {
        get { return Path.GetFullPath(Path.Combine(ModulePath, "./ThirdParty/")); }
    }

    public MRubySetting(TargetInfo Target)
    {
        // 省略

        LoadMRubyLIB(Target);
    }

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

        if ((Target.Platform == UnrealTargetPlatform.Win64) || (Target.Platform == UnrealTargetPlatform.Win32))
        {
            isLibrarySupported = true;

            string PlatformString = (Target.Platform == UnrealTargetPlatform.Win64) ? "x64" : "x86";
            string LibrariesPath = Path.Combine(ThirdPartyPath, "MRuby", "Libraries");

            PublicAdditionalLibraries.Add(Path.Combine(LibrariesPath, "libmruby." + PlatformString + ".lib"));
        }

        if (isLibrarySupported)
        {
            PublicIncludePaths.Add(Path.Combine(ThirdPartyPath, "MRuby", "Includes"));
        }

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

        return isLibrarySupported;
    }

モジュールパスとサードパーティパスの取得関数を定義し、それらを利用してlibmruby.x64.libをリンクする関数を定義して、それをコンストラクタで呼び出しています。
重要なのは、PublicAdditionalLibraries.AddとPublicIncludePaths.Addです。名前の通り.libファイルのリンクとインクルードパスの追加を担当します。 地味に肝なのが、string PlatformStringで32bitと64bitの判別をし、ビルド時にリンクするライブラリを切り替えている部分です。こうすることにより32bitでも64bitでも動くアプリケーションをUE4と連携して作ることができます。

実装

実装についてはかなりざっくりいきます。 主にヘッダファイルを載せます。

まずmrubyのヘッダファイルをインクルードし、(C言語なので念の為にextern "C" {}で囲む)
MRubySettingBPL.h

extern "C" {
   #include "mruby.h"
   #include "mruby/variable.h"
   #include "mruby/hash.h"
   #include "mruby/compile.h"
   #include "mruby/string.h"
}

mrubyファイルをインポートできるようにアセットを実装し、
MRubySettingBPL.h

UCLASS()
class UMRubyAsset : public UObject
{
    GENERATED_UCLASS_BODY()

public:
    UPROPERTY(EditAnywhere)
    FString Source;
};

UCLASS()
class UMRubyAssetFactory : public UFactory
{
    GENERATED_UCLASS_BODY()

    virtual bool DoesSupportClass(UClass* Class) override;
    virtual UClass* ResolveSupportedClass() override;
    virtual UObject* FactoryCreateText(
        UClass* InClass,
        UObject* InParent,
        FName InName,
        EObjectFlags Flags,
        UObject* Context,
        const TCHAR* Type,
        const TCHAR*& Buffer,
        const TCHAR* BufferEnd,
        FFeedbackContext* Warn
        ) override;
};

設定ファイルを表現するクラスとmrubyの値のラッパーの実装をして完成です!
MRubySettingBPL.h

USTRUCT()
struct FMRubyState {
    GENERATED_USTRUCT_BODY()

public:
    mrb_state* MRB;

    FMRubyState();
    ~FMRubyState();
};

UCLASS(Blueprintable)
class UMRubyValue : public UObject
{
    GENERATED_BODY()

public:
    mrb_value Value;

    UFUNCTION(BlueprintPure, Category = "MRuby")
    bool ToBool();
    UFUNCTION(BlueprintPure, Category = "MRuby")
    int32 ToInt();
    UFUNCTION(BlueprintPure, Category = "MRuby")
    float ToFloat();
    UFUNCTION(BlueprintPure, Category = "MRuby")
    FString ToString();
};


UCLASS(Blueprintable)
class UMRubySetting : public UObject
{
    GENERATED_UCLASS_BODY()

public:
    TSharedPtr<FMRubyState> MRubyState;

    UFUNCTION(BlueprintCallable, Category = "MRuby")
    void SetConfigString(FString Key, FString Val);

    UFUNCTION(BlueprintCallable, Category = "MRuby")
    UMRubyValue* GetConfig(FString Key);

    UFUNCTION(BlueprintCallable, Category = "MRuby")
    bool ExecuteSource(UMRubyAsset* MRubyAsset);
};

雑すぎんだろ!( ‘д‘⊂彡☆))Д´) パーン

というだけではあんまりなので、.cppの方でよく使った関数やマクロについて少し書こうと思います。
こういったC言語C++のライブラリは独自の文字列型や値型を持っていることが多いので、そこの連携を調べるのが少し面倒かと思います。
mrubyは値をmrb_valueという型で持っており、Cの値に変換する関数がありますが、一番めんどうなのが文字列です。
まずmrubyの文字列をUE4の文字列に変換するにはmrb_value->char*->TCHAR*->FStringというステップを踏みます。
mrb_valueからcharを取り出すにはRSTRING_PTRを使いました。(が、どうにもこれを使うのはまずいらしいのですがとりあえず簡単なこれでいきます) charをTCHAR*に変換するのにANSI_TO_TCHARを使い、あとはFStringのコンストラクタに渡すだけです。

FString(ANSI_TO_TCHAR(RSTRING_PTR(Value))); // こんな感じです。

では逆にUE4の文字列をmrubyに渡す方法は、単に逆のプロセスを踏むだけです。 FStringは*を付けることによって内部のTCHAR*文字列が取れるので、ANSI_TO_TCHARの逆のTCHAR_TO_ANSIでchar*に変換したらmrubyに渡せます。

TCHAR_TO_ANSI(*Str);

使ってみる

ではこのプラグインの使用例を載せてみます。 例えばゲームらしくドラゴンの設定ファイルを題材にすると、
blueprint f:id:shsnow23:20151211000212p:plain unreal_dragon.rb

config do |info|
    hp = 100
    if info["difficulty"] == "normal"
        hp = 200
    end
    {
        "name" => "Salamander",
        "hp" => hp,
        "mp" => 50
    }
end

これを実行すると、画面に
Salamander
200
50
と表示されます。
Rubyを設定ファイルとして利用する場合、情報を与えて設定ファイル側でその情報を元に分岐ができたりするのが強みです。(ここではhpを難易度によって変えています)

というようなネタでした。

お疲れ様でした。

明日はdabura6さんの「TobiiEyeXとUe4」です!よろしくお願いします!