TypeScript
UE4
UnrealEngine
Unreal.js

[UE4]Unreal.jsをTypeScriptで書く

はじめに

  • Unreal.jsとは
    • UnrealEngine上でJavaScriptを動かすことのできるプラグイン
    • Unreal.js 入門
  • TypeScriptとは
  • 👍 Unreal.jsをTypeScriptで書くメリット
    • Unreal.jsの出力する型定義ファイルを使って実行前型チェックができる
      • ※簡単なエラーはこの時点で弾ける
    • 型定義ファイルによる大量のAPIの補完が効く
      • ※Unreal.jsは型定義ファイルが実質的なAPIドキュメント
    • Unreal.js推奨のVSCodeとの親和性が高い
      • ※VSCodeはTypeScriptで書かれたアプリケーション
    • Unreal.jsのV8が未対応の新しいECMAScript1の記法が一部使える
      • ESNext2で提案中の仕様など
    • 書いたコードから型定義ファイルを生成できる為ライブラリの作成にも向く
      • Unreal.jsは型を意識する必要性が高い為

この記事の想定対象者

  • UE4は知っているが、JavaScriptはあまり詳しくない人
  • Unreal.jsを書き始めたが、やっぱりUE4の世界は型がほしいという人
  • Unreal.jsの吐き出すue.d.tsを見てTypeScriptが気になった人

比較

  • UnityのUnityScriptと同じように型があるが、最適化には用いられない
  • C#と言語設計者が同じなので似ている
    • Microsoftが一推し中の言語で環境が整備されている
  • ES2015のスーパーセットなので謎仕様・歴史的経緯はそのまま

準備

既にUnreal.js導入済みの場合はNode.js環境の準備までスキップ。

UEのプロジェクトの準備

  • Unreal.jsプラグインのインストール
  • Unreal.jsの有効化
    • 既存のプロジェクトでも新規でもどちらでもOK

詳細は Unreal.js 入門#導入 を参照のこと。

テキストエディタの準備

TypeScriptの補完に対応したテキストエディタであればVSCode以外でもOK。以降はVSCode前提とする。

Node.js環境の準備

Node.jsのインストール

環境に合わせてNode.jsを導入しておく。

Unreal.jsはNode.js自体が使われるわけではないが、以下の目的で環境整備に必要になる。

  • TypeScriptコンパイラの導入と実行
  • 型定義ファイルの導入
  • 外部ライブラリの導入

Node.jsをインストールするとパッケージマネージャのnpmもインストールされる。

package.jsonの作成

  1. プロジェクトのContent/Scripts/以下に移動してコンソールから以下のコマンドを打つ
    • npm init
    • VSCodeの場合、統合コンソールから打つこともできる
  2. いろいろと質問されるので適当に答えておく
    • 後から書き換えればよいので適当でOK

TypeScriptの設定

tscのインストール

TypeScriptのコンパイラであるtscをインストールする。
コンソールに次のコマンドを入れる。

  • npm i typescript -g -D
  • パッケージ(プロジェクト)毎にインストールする場合(TSのバージョンを分ける時など)
    • npm i typescript -D

tsconfig.jsonの設定

TypeScriptの設定ファイルを作成する。コンパイラ以外にも、VSCodeがこのファイルを自動で読み込んでエラーなどを表示するので必須。

  1. まずContent/Scripts/でコンソールに以下のコマンドを入れる
    • tsc --init
  2. 質問に答えてゆくとtsconfig.jsonファイルが生成される
  3. tsconfig.jsonファイルを開き以下のように設定する
tsconfig.json
{
    "compilerOptions": {
        //デフォルトの設定(略)
        "target": "es2016",  
        "lib": [
            "es2016"
        ],
        "skipLibCheck": true,
        "allowJs": true,
        "moduleResolution": "node", 
        //etc...
    },
    "exclude": [
      "aliases.js"
    ]
}

参考: TypeScript errors #164

必須系型定義ファイルの導入

上記までの設定では console.log() などのEcmaScript1のコアAPI以外のAPI呼び出しがコンパイルエラーになる。
そこでNode.js用の型定義ファイルを代用で導入する。

npm i @types/node -D

Unreal.jsはNode.jsの互換APIを持つのでこれでコンパイルエラーにならず補完が効くようになる。ただし、全くイコールではないので注意。

(オプション)エディタの補完時にAPIの説明コメントを追加する

お好みで以下の設定をしておく。
Unreal.jsの型定義ファイルを再生成してAPIを調べやすくする

コーディング

型定義ファイルのインストール

Content/Scripts/typings以下にはUnreal.jsが生成したUnrealEngineの型定義ファイルが既に存在するが、Unreal.js同梱の内部ライブラリや他のJSの外部ライブラリの型定義ファイルはない。そこで外部から型定義ファイルを取得してくることになる(なくても書けないことはない)。

npmで型定義ファイルを導入する

TypeScriptの型定義ファイルは(存在すれば)npmで導入することができる。
外部ライブラリの例として、Unreal.jsの内部に同梱されているlodashの型定義ファイルをインストールしてみる。
npm i @types/lodash -D

これでlodashを使うときに補完や型チェックが効くようになった。

使いたいライブラリの型定義ファイルがあるかは 以下のサイトで検索可能。
https://microsoft.github.io/TypeSearch/

内部ライブラリ用の型定義ファイルを導入

一部だけだが存在するのでDLするなりして導入。

Unreal.jsの型定義出力で出力されない一部の定義も追加してある。

型定義ファイルがない場合

tsconfig.jsonにallowJsオプションを追加して有効にする。こうすることで素のJSファイルもざっくりとした補完が効くようになるが型情報は多くの場合補完されない。

また、allowJsを設定するだけではjsファイルがtscのコンパイル対象になることがあるため(同階層にファイルがあるなど)、excludeオプションも設定しておく。

Unreal.jsむけTypeScriptコードの書き方

require -> import

Unreal.jsで外部ライブラリなどをCommonJS形式で読み込む箇所はTypeSciptではimport文で置き換えることができる。

//CommonJS
const UMG = require("UMG")

//import
import * as UMG from "UMG"

import文で記述し、型定義ファイルを導入すると型チェックと補完が効くようになる。
CommonJSはimport以前の歴史的な古い"工夫"なのでUnreal.jsむけコードでもimport文で置き換えられる箇所は積極的に置き換えていきたい。

なお、ver. 2.4以降では ES.next のDynamic import式にも対応しているため、require()が必要な場面が少なくなっている。

【追記】※最新版では問題あり

最新のTypeScriptではimport文をCommonJS式でトランスパイルすると、次の一文が出力される。

tscが`import`文をCommonJS式でトランスパイルすると出力する一文
Object.defineProperty(exports,"__esModule", {value: true});

Unreal.jsの世界にはexportsオブジェクトが存在しないのでこの一文がエラーになる。古いバージョンのtsc(2.1等)を使うか、require()のままでいくか(型情報が失われる)、出力後のJSファイルからGulp等で自動でその一文を削除するか、工夫が必要。

uclass.js

jsでの書き方
const UClass = require("uclass")
class ShowProps{
    ctor(){
    }
    properties(){
        this.myInt /* EditAnywhere+Int */;
        this.myIntArray /* EditAnywhere+Int[] */
        this.blueprints /* Category:Select Blueprints+EditAnywhere+Blueprint[] */;
    }
}
let UShowProps = UClass()(global, ShowProps)
let ushowProp = new UShowProps();

上記のushowPropue.d.tsClassクラスの各種プロパティとShowProps.properties()メソッド内で定義されたプロパティを持つ。さすがにそこまではTypeScriptのコンパイラは型推論できないため、通常(内部的にtscを利用する)エディタの補完は期待できない。

TypeScriptで記述する場合はuclass.d.tsの型定義ジェネリクス を使うことで、上記プロパティの型チェックや補完が効くようになる3

tsでの書き方
import UClass from "uclass"
class ShowProps{
    myInt:number;
    myIntArray:number[];
    blueprints:Blueprint[]
    properties(){
        this.myInt /* EditAnywhere+Int */;
        this.myIntArray /* EditAnywhere+Int[] */
        this.blueprints /* Category:Select Blueprints+EditAnywhere+Blueprint[] */;
    }
}
let UShowProps = UClass<Class, ShowProps>()(global, ShowProps)
let ushowProp = new UShowProps();

uclassified_declaration.PNG

globalに生やしたオブジェクト

メニュー用の設定をglobalに生やす
    //menu group settings
    if(!global.editorGroup){
        global.editorGroup = JavascriptWorkspaceItem.AddGroup(JavascriptWorkspaceItem.GetGroup("Root"), "BP Tools");
    }

Unreal.jsのサンプルコードではglobalに設定等を記録しているコードがよくあるが、TSではそのままでは生やしたオブジェクトの定義がないのでエラーになる。以下のように事前に定義しておけばよい。

//extend global
declare var global: {
    editorGroup:JavascriptWorkspaceItem;
    //...
}

UObject.GetOuter(),UObject.GetOutermost()

UE4のAPIではouterオブジェクトを引数に指定するものが多く、UObject.GetOuter(),UObject.GetOutermost()あたりはUnreal.jsでも使用頻度が高い。ただし、標準のue.d.tsで定義されているように、戻り値の型はUObjectになってしまう。

そこで、ジェネリクスを使って戻り値の型を明確化する。

まずはUObjectの定義を拡張する。TypeScriptでは定義済みのクラスも、同名のインターフェイスで定義の拡張ができるのでこれを使う。

UObjectクラスの定義を同名インターフェイスで拡張
declare interface UObject{
}

元のUObject.GetOuter(),UObject.GetOutermost()はそのままに、関数オーバーロードの形で、ジェネリクス版を定義する。

ジェネリクス版のメソッドをオーバーロード定義
declare interface UObject {
    /**
     * Get a outer object with type parameter
     */
    GetOuter<T extends UObject = UObject>():T;
    /**
     * Get a outer most object with type parameter
     */
    GetOutermost<T extends UObject  = UObject>():T;
}

戻り値の型が明らかな場合はこちらを使用することで、型情報を失うことなくouterオブジェクトを呼べる。

戻り値の型が明らかな例
function renameAllFunctions(g:JavascriptGraphEdGraph){
    let bp = g.GetOuter<Blueprint>() //`bp`の型が`Blueprint`として保持される
}

コンパイル

TypeScriptは修正するたびにコンパイルコマンドを打つようなことはせず、tsc -wでwatch buildさせる(あるいは, gulpなどでも良い)。TSのソースコードが変更されるたびに自動でJSにコンパイルされる。

コンソール
cd [tsconfig.jsonの置いてあるディレクトリ]
tsc -w

また、Unreal.jsはUE4の Hot reloadに対応しているので、bootstrap.jsと組み合わせて通常のJSと同様の環境を作ることもできる。 TSファイルを修正→JSファイルに自動ビルド→Hot reloadで自動読み直し 、というパイプラインができるのでおすすめ。

bootstrap.ts

TypeScript版のbootstrap.jsは以下。こちらを使用することできちんと補完できるようになる。

bootstrap.ts
export default function (filename:string) {
    Context.RunFile('aliases.js')
    Context.RunFile('polyfill/unrealengine.js')
    Context.RunFile('polyfill/timers.js')

    require('devrequire')(filename)
}
追加コード
import bootstrap from "./bootstrap"

// bootstrap to initiate live-reloading dev env.
try {
    module.exports = () => {
        let cleanup:Function|null = null

        // wait for map to be loaded.
        process.nextTick(() => cleanup = main()); //この場合、main()が再読み込みされる

        // live-reloadable function should return its cleanup function
        return () => (<Function>cleanup)()
    }
}
catch (e) {
    // 追加するファイル名を記入
    bootstrap('【ファイル名】')
}

サンプルコード

https://github.com/ConquestArrow/BP_Obfuscator

テンプレートプロジェクト

  • UnrealTSTemplate (github)
    • ※サンプルコードで使用している型定義ファイルの一部は含まれないのでコピーして使うとよい

ソース管理

  • *ue.d.tsaliases.jsは自動生成なのでバージョン管理下に置かない
    • TypeScriptだけでなく共通
  • *.tsファイルもバージョン管理下に置く
  • 各自の環境でjsファイルをビルドする前提なら、jsファイルはバージョン管理から除外できる
  • *.tsファイルは/Content/Script/以下にある必要は必ずしもない
    • tsconfig.jsonのコマンドラインオプションで工夫することで分けることもできる

まとめ

  • Unreal.jsをTypeScriptで書くと、型チェックとコード補完の恩恵にあずかれるよ!
  • 出やすいエラーはいくつかの工夫で回避できるよ!
  • サンプルプロジェクトやテンプレを参考にしてね!


  1. JavaScriptの標準言語仕様のこと。JSの世界ではJSはEcmaScriptの1実装という扱い。 

  2. 次世代EcmaScriptの提案仕様。 

  3. 厳密にはctor?:()=>voidproperties?:()=>voidなどの不要なものも含まれてしまうが。