Help us understand the problem. What is going on with this article?

[UE4]Unreal.jsでUE4のエディタ拡張を作る

More than 3 years have passed since last update.

この記事は裏 Unreal Engine 4 (UE4) Advent Calendar 2016の5日目の記事です。

はじめに

UnrealEngine4(UE4)はUnity3D(Unity)に比べるとエディタ拡張が作りにくい。Unityの場合、通常のゲームのコードを書くときと同じC# or UnityScriptで特別な環境がなくても書ける。一方、UE4の場合はVisualStudioかXcodeでUnreal C++で書かないといけない。

UE4でエディタ拡張をやりたい場合、「エディタに新機能を付ける」というよりも「 効率の良い処理を便利なUIでやりたい 」という需要が多いのではないか 1 。そしてその需要があるのはプログラマはもちろん、アーティストやゲームデザイナなどのノンプログラマも同じである。

ノンプログラマにVisualStudioやXcodeの環境はふつうはまずない。C++というのも敷居が高い。BlutilityはBPで書けるって?いやー、あれUI自由に作れないし、そもそもしばらくメンテされてないよね~。

そんなあなたに福音となるかもしれないのが、Unreal.jsでエディタ拡張を書く というアプローチだ。

参考:Unreal.js 入門

エディタ拡張テンプレ

エディタ拡張のテンプレcode
module.exports = function main(){
    const UMG = require("UMG");
    const I = require('instantiator');
    const EMaker = require("editor-maker");

    //menu group settings
    if(!global.editorGroup){
        global.editorGroup = JavascriptWorkspaceItem.AddGroup(JavascriptWorkspaceItem.GetGroup("Root"), "Samples");
    }

    //Editor window with tab simple template
    EMaker.tabSpawner(
        {
            DisplayName:"Simple Tab Win",
            TabId: "SimpleTabWin@",
            Role: EJavascriptTabRole.MajorTab,
            Group: global.editorGroup
        },
        () => I(UMG.text({},"test"))
    )

    //deconstructor
    return () => {

    };
}

メニュー > Window > Samples > Simple Tab Win に追加される。

Template_Tabspawner_SampleTabWin.png

生成されるのはタブが一つあるウィンドウ。

SampleTabWin.png

これをひな型に作るのが容易。

※エディタ拡張制作中はbootstrap.jsを持ってきて、live-reload対応の処理を入れておくとなお便利。

コードの最後に追加してmain関数をここでexportするように書き換えておく
// bootstrap to initiate live-reloading dev env.
try {
    module.exports = () => {
        let cleanup = null

        // wait for map to be loaded.
        process.nextTick(() => cleanup = main());

        // live-reloadable function should return its cleanup function
        return () => cleanup()
    }
}
catch (e) {
    require('bootstrap')('extension-Button')
}

基本的な作り方

Unreal.jsにはエディタ拡張作成に便利なライブラリが同梱されているのでそれを使う2

  1. ファイル名を extension-*.js で作成
  2. GUI部品は同梱ライブラリの UMG.js を使って設定
  3. UMG.jsで作ったものは instantiator.js で有効化
  4. ウィンドウやタブ、メニュー登録などは editor-maker.jsで設定
  5. JavascriptEditorLibraryJavascriptEditorEngineLibraryなどのAPIでエディタの機能にアクセス
  6. 処理自体はJSで書く

ドキュメントは不足しているので、Unreal.jsの型定義ファイルを再生成してAPIを調べやすくするで紹介した方法でue.d.tsを更新し、コード補完が効くようにしておくのがおすすめ。

UI編

UE4のエディタUIはSlateでできているが、UMGはSlateを継承しているので、UMG.jsでSlateもUMGも扱える。

公式Wiki: UMG widgets

レイアウト

UMG.div()

VerticalBoxを作成する。HTMLのdiv要素のように3、子要素を縦方向に持つことができる。

使い方
UMG.div({【オプションのObject}, ...children)

オプションに指定できるのはVerticalBoxおよびそれの先祖クラスのプロパティが指定できる。

UMG.span()

HorizontalBoxを作成する。HTMLのspan要素のように3、子要素を横方向に持つことができる。

使い方
UMG.span({【オプションのObject}, ...children)

オプションに指定できるのはHorizontalBoxおよびそれの先祖クラスのプロパティが指定できる。

スロットのサイズルール

UMG/Slateには「スロット」があり、Unreal.jsでUIを設定する際にもUMG.jsのオプションやプロパティ経由で設定できる。

レイアウトにかかわるスロットのサイズルールはESlateSizeRuleというEnumがあるのでそれで指定可能。

UMG.div(
  {
    //オプションでの設定
    Slot:{Size:{Value:10, SizeRule:ESlateSizeRule.Fill}},
    //入れ子が面倒な時は以下の方法でも設定可能
    "slot.size.size-rule":ESlateSizeRule.Fill,
    //...
  },
  //...
)

タブ・パネルのレイアウト

ある程度複雑な複数のタブやパネルをもつエディタ拡張を作る場合には、上記までの簡易な方法ではなくJavascriptEditorTabManagerに対してJSONフォーマットをパラメータに与える形になる。詳細は後述

テキスト

sample code

UMG.text({}, "text")

TextBlockを生成する、UMG.jsのメソッド。ショートハンドメソッドになっており、UMG(TextBlock,{Text:"text"})のシュガーシンタックスである。

UMG_text.png

UMG.div(
    {},
    UMG.text({},"text"),
    UMG(TextBlock,{Text:"text"})
)

なお、エディタ拡張用にはデフォルトのフォントが大きすぎるので小さいフォントを設定しておくとよい。

//エディタ組み込みのフォントにアクセスするために取得
const GEngine = Root.GetEngine();

//フォント:SmallFont、サイズ10が大体エディタ拡張の通常のフォントサイズ
UMG.text({Font:{FontObject:GEngine.SmallFont,Size:10}},"text");

UMG_fonts.png

なお、文字入力系はEditableTextBoxなどを使用する。

画像

Image_sample.png

UMG.span(
    {},
    UMG(UImage,
    {
        Brush:{
            ImageSize:{X:248,Y:138},
            ResourceObject:UObject.Load("/Game/Image")
        }
    })
)

VerticalBox直下に置くと、エディタUI上のデフォルトだと引き延ばされるので注意。

なお、UMG.img()というショートハンドメソッドもある。

UMG.img({Brush:{ResourceObject:UObject.Load("/Game/Image")}})

ボタン

いわゆる普通のボタン。

Button_default.png

UMG(Button,{},"default button")

何も設定しないとフォントがでかく、パネルいっぱいに広がるので実際には調整することになるだろう。

Button_decotext.png

const GEngine = Root.GetEngine();
const btnFColor = new LinearColor()
    btnFColor.R = btnFColor.G = btnFColor.B = 0;
    btnFColor.A = 100;

//simple button with font decoration
UMG(Button,{ColorAndOpacity:btnFColor},UMG.text({Font:{FontObject:GEngine.SmallFont,Size:10}},"decorated text button"))

リスト

JavascriptListView

ListView_Sample.png

OnGenerateRowEventプロパティの中でリスト項目を設定する。リスト項目はUObjectである必要があるのでuclass.jsでStructを作って渡す。

リストの表示データ
    class A/* Struct */{
        ctor(){
            this.desc = "A desc"
            this.name = "A"
        }
        properties(){
            this.desc /* String */
            this.name /* String */
        }
    }
    class B/* Struct */{
        ctor(){
            this.desc = "B desc"
            this.name = "B"
        }
        properties(){
            this.desc /* String */
            this.name /* String */
        }
    }

    let data = [
        new (UClass()(global, A)),
        new (UClass()(global, B)),
    ]

公式のサンプルにはJSONからUObjectのデータをまとめて作成する json2u.js というライブラリもあるので数がある場合はこれを使うのもよいだろう。

表示設定箇所
OnGenerateRowEvent: (item, column) => {
    let s = ""
    switch(column){
        case "Name":
            s = item ? item.name : column;
            break;
        case "Desc":
            s = item ? item.desc : column;
            break;
        default:
            s = "default"
    }

    let design = 
        UMG(JavascriptTextBlock,
            {
                Font : {
                    FontObject : GEngine.SmallFont,
                    Size : 10
                },
                Text : s
            }
        )
    return I(design)
},

なお、3項演算子で設定している個所があるのは、最初はリストのヘッダー部分でitemundefinedで返ってくるからである。

PropertyEditor

Detailsパネルの中身。

PropertyEditorSample.png

表示内容はプロパティを見たいものを直接渡すか、見せたいUPROPERTYを定義したUClassを作って渡す必要がある。
→参考: [UE4]Unreal.jsでJSのコードに型とフラグ情報を付与する

見せたいプロパティを定義したクラス
//PropertyEditor showing props
class ShowProps{
    ctor(){
        //default value set 

        this.myInt = 123;
        this.myFloat = 987.6;
    }
    properties(){
        this.myBoolean /* EditAnywhere+bool */;
        this.myInt /* EditAnywhere+Int */;
        this.myFloat /* EditAnywhere+float */;
        this.myIntArray /* EditAnywhere+Int[] */;
        this.myString /* EditAnywhere+String */;
        this.myVector2d /* EditAnywhere+Vector2D */;
        this.myVector /* EditAnywhere+Vector */;
        this.myActor /* EditAnywhere+Actor */;
        this.myColor /* EditAnywhere+Color */;
        this.someProp /* EditAnywhere+Category:MyCategory+DisplayName:My Prop Name+int */;
        this.advancedProp /* EditAnywhere+Category:MyCategory+AdvancedDisplay+Color */
    }
}

let UShowProps = UClass()(global, ShowProps)
let ushowProp = new UShowProps();
PropertyEditorのコア処理
UMG(PropertyEditor,
{
    OnChange:(propertyName) => {
        //...
    },
    $link:(elem)=>{
        elem.SetObject(ushowProp)
        elem.updateData = _ => {
            elem.SetObject(ushowProp)
        }
    },
    $unlink:(elem) =>{
        //...
    }
})

タブ

Tabs_sample.png

タブ自体はJavascriptEditorTabのインスタンス。editor-maker.jsに簡単に生成できるtab()メソッドがあるのでそれを使うと便利。

最低限のタブのサンプル
const EMaker = require("editor-maker")
let tab = EMaker.tab(
            {
                TabId:"Tab",
                Role:EJavascriptTabRole.NamadTab,
                DisplayName:"Tab"
            },
            ()=>{
                return I(UMG.text({},"text"))
            }
);

タブの種類

EJavascriptTabRoleには4種類あるが、見た目上は2種類しかない。

  • MajorTab : 基本的なタブで、パネルとしてドッキングはできない。
  • NomadTab : 他のNomadTabの四隅にドッキングしたり、タブを隠してパネル化できる。
  • DocumentTab, PanelTab : 見た目・機能的にはNomadTabと同じ?意味的に使い分けるのかもしれない。

複数のタブ

複数タブを表示させるにはそのままではなくJavascriptEditorTabManagerを使い、レイアウトやタブの設定等を行う。

    let tabManager = new JavascriptEditorTabManager(JavascriptLibrary.CreatePackage(null,'/Script/Javascript'))
    tabManager.Tabs = [tab, /* ... */]
    tabManager.Layout = JSON.stringify(tabLayout)

    //...

    $link:elem => {
         elem.AddChild(tabManager).Size.SizeRule  = "Fill"
   }

タブレイアウト

tabManager.LayoutにはJSONを設定する。このJSONはエディタのレイアウトを保存したときにiniファイルの中に保存されるものと同じフォーマットで、/Saved/Config/Windows/Editor.iniなどを見るとわかる。

パラメータの詳細は以下の公式ドキュメントを見ると参考になる。

メニュー

デフォルトのエディタの左上のメニューバーや画面上部のツールバーを拡張することができる。

公式サンプル

なお、現時点ではUnreal.jsはデフォルトのウィンドウ(Level Editor)のメニューの拡張にしか対応していない

ユーザーウィジェットをUIとして使う

WidgetBPをDesinerでWysiwygに作ったものをエディタ拡張のUIとして使うこともできる。

UserWidgetDesiner.png
UserWidget_sample.png

JSで設定するのすら面倒!な場合はこの方法が使えるかも?
ただし、イベント周りの処理は工夫が必要になるだろう(未検証)。

その他

JS側にAPIが出されているUI(ue.d.tsVisualを継承する子クラス)であれば基本使用可能。

呼び方
UMG(UI,UIのオプション】);

GithubのC++ソースを眺めるだけでも全体像はつかめる。JavascriptGraphEditorなど気になるものも用意されているようだ。

ここにないものは追加でプラグインに組み込むことになる。

機能編

UIのトリガーイベント

$link, $unlink

オプションのObjectプロパティに$link,$unlinkを設定する。

UMG(Button,
  {
     $link:elem => {},
     $unlink:elem => {}
  }
)

$linkはUIの構築時、$unlinkはUIの破棄時に呼ばれる?ようだ。

なお、$link,$unlinkの引数elemはイベントの設定されたUI自身となる。

TypeScriptの型定義で書くと以下のような感じ
UMG<T extends Visual>(
  T,
  {
    $link?:(elem:T)=>void,
    $unlink?:(elem:T)=>void
  },
//...

UI毎のトリガーイベント

UIの部品ごとに設定できるイベントがあるので、

_part_1_ue.d.ts
declare class Button extends ContentWidget { 
        //略
    OnClicked: UnrealEngineMulticastDelegate<() => void>;
    OnPressed: UnrealEngineMulticastDelegate<() => void>;
    OnReleased: UnrealEngineMulticastDelegate<() => void>;
    OnHovered: UnrealEngineMulticastDelegate<() => void>;
    OnUnhovered: UnrealEngineMulticastDelegate<() => void>;
//以後省略

$linkなどと同様にオプションのObject内で設定する。

UMG(Button,
  {
     OnClicked: _ => {
        //do something
     }
  }
)

選択中のアクターインスタンスを取得する

//エディタ(World Outlinerなど)上で選択したActorインスタンスの配列を取得
let usel = JavascriptEditorEngineLibrary.GetSelectedSet(Root.GetEngine(), Actor)
let sel = usel.GetSelectedObjects()

//一つ目のActorインスタンスを取得
let p = sel.Out[0];

ライブラリ編

UE4 API

JavascriptEditorLibrary

エディタの機能にアクセスするAPI群が定義されたライブラリクラス。

JavascriptEditorEngineLibrary

上記に似ているが、こちらはEditorEngineに関してのAPI群のライブラリクラス。

Javascript*クラス

Javascript*という名前のクラスは頭のJavascriptを消すと、Unreal C++の元の該当するAPIにたどり着けることが多い。

Unreal.js同梱

同梱ライブラリでエディタ拡張に使えそうなものを紹介。

fs.js

node.js互換のファイルIOライブラリ。

コード中でファイルの読み書きに使えるほか、npm経由で入れたjsライブラリでfsモジュールを呼んでいる場合に役に立つ。

なお、ファイルIO自体はUE4自体のAPIを使うこともできる。

UMG.js

UMG(UI,UIのオプション】,【子要素UI);

すでに紹介済みだが、UMG/Slateを便利に扱うためのクラス。いくつかのショートハンドメソッドも用意されている。

ショートハンドメソッドには実はUMG.list()UMG.TabManager()など上記で元のクラスAPIだけ紹介したものもあり、代表的なものは一通り用意されているようだ(今回は未検証)。時間があれば今後検証していきたい。

instantiator.js

UMG.jsで作成したUIを各APIに渡せる形にする。

editor-maker.js

エディタのUI(タブ・メニュー・コマンドなど)を便利に作れるライブラリ。

まとめ

Next Day

明日(6日目)は @alwei さんによる 「UE4 VR空間で手を飛ばす方法について」です。


  1. UE4の場合、機能追加はエンジンのソースに手を入れればよいので。 

  2. もちろん、APIを一つ一つたたくことで作成することもできるが 

  3. 正確にはもちろんHTMLのdiv/spanは縦横の並びは固定ではない。 

ConquestArrow
ノンプログラマです。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした