[UE4]Unreal.jsをTypeScriptで書く

# <i class="fa fa-cube" />はじめに

* <i class="fa fa-question-circle" /> **[Unreal.js](https://github.com/ncsoft/Unreal.js)とは**
    * UnrealEngine上でJavaScriptを動かすことのできるプラグイン
    * [Unreal.js 入門](http://qiita.com/ConquestArrow/items/bb81ad5e63cf6bc4e549)
* <i class="fa fa-question-circle" /> **[TypeScript](https://www.typescriptlang.org/)とは**
    * JavaScriptの新規格先行取り込み＋静的型付けをしたAltJS
    * http://qiita.com/tags/TypeScript
* 👍 **Unreal.jsをTypeScriptで書くメリット**
    * Unreal.jsの出力する型定義ファイルを使って実行前型チェックができる
        * ※簡単なエラーはこの時点で弾ける
    * 型定義ファイルによる大量のAPIの補完が効く
        * ※Unreal.jsは型定義ファイルが実質的なAPIドキュメント
    * Unreal.js推奨のVSCodeとの親和性が高い
        * ※VSCodeはTypeScriptで書かれたアプリケーション
    * Unreal.jsのV8が未対応の新しいECMAScript[^1]の記法が一部使える
        * ESNext[^2]で提案中の仕様など
    * 書いたコードから型定義ファイルを生成できる為ライブラリの作成にも向く
        * Unreal.jsは型を意識する必要性が高い為

# <i class="fa fa-cube" />この記事の想定対象者

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

# 比較

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

# <i class="fa fa-cube" />準備

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

## <i class="fa fa-cubes" />UEのプロジェクトの準備
* <i class="fa fa-plug" />Unreal.jsプラグインのインストール
* Unreal.jsの有効化
    * 既存のプロジェクトでも新規でもどちらでもOK

詳細は [Unreal.js 入門＃導入](http://qiita.com/ConquestArrow/items/bb81ad5e63cf6bc4e549#%E5%B0%8E%E5%85%A5) を参照のこと。

## <i class="fa fa-cubes" />テキストエディタの準備
* 推奨: [VSCode](https://code.visualstudio.com/)のインストール
    * 参考：[Visual Studio Codeの使い方、基本の「キ」](http://www.atmarkit.co.jp/ait/articles/1507/10/news028.html)

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

## <i class="fa fa-cubes" />Node.js環境の準備
### Node.jsのインストール
環境に合わせてNode.jsを導入しておく。

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

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

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

### package.jsonの作成

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

## <i class="fa fa-cubes" />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`ファイルを開き以下のように設定する

```json:tsconfig.json
{
    "compilerOptions": {
        //デフォルトの設定(略)
        "target": "es2016",  
        "lib": [
            "es2016"
        ],
        "skipLibCheck": true,
        "allowJs": true,
        "moduleResolution": "node", 
        //etc...
    },
    "exclude": [
      "aliases.js"
    ]
}
```
参考: [TypeScript errors #164](https://github.com/ncsoft/Unreal.js/issues/164#issuecomment-289242192)

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

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

`npm i @types/node -D`

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

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

お好みで以下の設定をしておく。
[Unreal.jsの型定義ファイルを再生成してAPIを調べやすくする](http://qiita.com/ConquestArrow/items/fa7560d8fa7aa03553e5)

# <i class="fa fa-cube" />コーディング

## <i class="fa fa-cubes" />型定義ファイルのインストール

`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/

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

[一部だけだが存在する](https://github.com/ConquestArrow/BP_Obfuscator/tree/master/Content/Scripts/typings)のでDLするなりして導入。

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

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

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

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

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

### `require` -> `import`

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

```ts
//CommonJS
const UMG = require("UMG")
```
↓

```ts
//import
import * as UMG from "UMG"
```

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

なお、[ver. 2.4以降では ES.next のDynamic import式にも対応している](http://qiita.com/vvakame/items/289555dcc9aa888ce588#dynamic-import%E3%81%8Cstage-3%E3%81%AB%E3%81%AA%E3%81%A3%E3%81%9F%E3%81%AE%E3%81%A7%E5%85%A5%E3%81%A3%E3%81%9F)ため、`require()`が必要な場面が少なくなっている。

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

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

```js:tscが`import`文をCommonJS式でトランスパイルすると出力する一文
Object.defineProperty(exports,"__esModule", {value: true});
```
Unreal.jsの世界には`exports`オブジェクトが存在しないのでこの一文がエラーになる。古いバージョンのtsc（2.1等）を使うか、`require()`のままでいくか（型情報が失われる）、出力後のJSファイルからGulp等で自動でその一文を削除するか、工夫が必要。

### uclass.js

```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();
```

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

TypeScriptで記述する場合は[`uclass.d.ts`の型定義](https://github.com/ConquestArrow/BP_Obfuscator/blob/master/Content/Scripts/typings/uclass.d.ts)と **ジェネリクス** を使うことで、上記プロパティの型チェックや補完が効くようになる[^3]。

```ts: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();
```

<img width="145" alt="uclassified_declaration.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/54764/a390bdce-db8a-10f7-b4c1-61c528ce485c.png">

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

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

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

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

### `UObject.GetOuter()`,`UObject.GetOutermost()`

UE4のAPIでは[outerオブジェクト](https://answers.unrealengine.com/questions/292594/how-can-i-understand-the-data-member-outer-in-the.html)を引数に指定するものが多く、`UObject.GetOuter()`,`UObject.GetOutermost()`あたりはUnreal.jsでも使用頻度が高い。ただし、標準の`ue.d.ts`で定義されているように、戻り値の型は`UObject`になってしまう。

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

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

```ts:UObjectクラスの定義を同名インターフェイスで拡張
declare interface UObject{
}
```
元の`UObject.GetOuter()`,`UObject.GetOutermost()`はそのままに、関数オーバーロードの形で、ジェネリクス版を定義する。

```ts:ジェネリクス版のメソッドをオーバーロード定義
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`オブジェクトを呼べる。

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

## <i class="fa fa-cubes" />コンパイル

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

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

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

### bootstrap.ts

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

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

    require('devrequire')(filename)
}
```

```ts:追加コード
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

# <i class="fa fa-cube" />テンプレートプロジェクト

* [UnrealTSTemplate (github)](https://github.com/ConquestArrow/UnrealTSTemplate)
    * ※サンプルコードで使用している型定義ファイルの一部は含まれないのでコピーして使うとよい

## ソース管理

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

# まとめ

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



---
[^1]: JavaScriptの標準言語仕様のこと。JSの世界ではJSはEcmaScriptの１実装という扱い。
[^2]: 次世代EcmaScriptの提案仕様。
[^3]: 厳密には`ctor?:()=>void`や`properties?:()=>void`などの不要なものも含まれてしまうが。