この記事は裏 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 入門
エディタ拡張テンプレ
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 に追加される。
生成されるのはタブが一つあるウィンドウ。
これをひな型に作るのが容易。
※エディタ拡張制作中はbootstrap.js
を持ってきて、live-reload対応の処理を入れておくとなお便利。
// 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。
- ファイル名を
extension-*.js
で作成 - GUI部品は同梱ライブラリの
UMG.js
を使って設定 - UMG.jsで作ったものは
instantiator.js
で有効化 - ウィンドウやタブ、メニュー登録などは
editor-maker.js
で設定 -
JavascriptEditorLibrary
やJavascriptEditorEngineLibrary
などのAPIでエディタの機能にアクセス - 処理自体は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フォーマットをパラメータに与える形になる。詳細は後述。
テキスト
UMG.text({}, "text")
TextBlock
を生成する、UMG.jsのメソッド。ショートハンドメソッドになっており、UMG(TextBlock,{Text:"text"})
のシュガーシンタックスである。
UMG.div(
{},
UMG.text({},"text"),
UMG(TextBlock,{Text:"text"})
)
なお、エディタ拡張用にはデフォルトのフォントが大きすぎるので小さいフォントを設定しておくとよい。
//エディタ組み込みのフォントにアクセスするために取得
const GEngine = Root.GetEngine();
//フォント:SmallFont、サイズ10が大体エディタ拡張の通常のフォントサイズ
UMG.text({Font:{FontObject:GEngine.SmallFont,Size:10}},"text");
なお、文字入力系はEditableTextBox
などを使用する。
画像
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")}})
ボタン
いわゆる普通のボタン。
UMG(Button,{},"default button")
何も設定しないとフォントがでかく、パネルいっぱいに広がるので実際には調整することになるだろう。
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
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項演算子で設定している個所があるのは、最初はリストのヘッダー部分でitem
がundefined
で返ってくるからである。
PropertyEditor
Detailsパネルの中身。
表示内容はプロパティを見たいものを直接渡すか、見せたい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();
UMG(PropertyEditor,
{
OnChange:(propertyName) => {
//...
},
$link:(elem)=>{
elem.SetObject(ushowProp)
elem.updateData = _ => {
elem.SetObject(ushowProp)
}
},
$unlink:(elem) =>{
//...
}
})
タブ
タブ自体は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として使うこともできる。
JSで設定するのすら面倒!な場合はこの方法が使えるかも?
ただし、イベント周りの処理は工夫が必要になるだろう(未検証)。
その他
JS側にAPIが出されているUI(ue.d.ts
でVisual
を継承する子クラス)であれば基本使用可能。
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自身となる。
UMG<T extends Visual>(
T,
{
$link?:(elem:T)=>void,
$unlink?:(elem:T)=>void
},
//...
UI毎のトリガーイベント
UIの部品ごとに設定できるイベントがあるので、
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(タブ・メニュー・コマンドなど)を便利に作れるライブラリ。
まとめ
- 基本的な作り方 をもとにJSでエディタ拡張を作れる
- UE4のAPI群とUnreal.js同梱のライブラリを活用できる
- 今回のサンプルをgithubにあげてあるので参考に
Next Day
明日(6日目)は @alwei さんによる 「UE4 VR空間で手を飛ばす方法について」です。