こんなつぶやきを見つけて気になったので入門した。
どうやら中華人民共和国の大手企業テンセントが開発中の、
TypeScript で Unity や Unreal Engine などを使えるようにするライブラリらしい。
ドキュメントが殆ど中国語で大変だったけど、とりあえず基本的な使い方が分かったので書く。
プロジェクトのサンプル
今回学習するにあたり、
「このリポジトリをクローンして改造すれば、普通に1からやるよりはそれなりに楽」な
サンプルプロジェクトを作った。
環境構築
Unity 側の設定
- Unity プロジェクトを用意する
-
git clone https://github.com/Tencent/puerts.git
する - リポジトリの
puerts/unity/Assets/
の中身を Unity プロジェクトのAssets
にコピー -
このページ から
Plugins_Nodejs_***
を解凍し、中のPlugins
をプロジェクトのAssets/Plugins
に上書きする -
このファイル を
Assets/Editor
に保存 - Unity を開き、トップバー
Puerts
->Generate index.d.ts
をクリック。するとAssets/Gen/Typing/csharp/index.d.ts
が生成される
TypeScript 側の設定
-
Assets
の親ディレクトリにTypeScript
ディレクトリを作成
(まあ別にどんな名前でもいいけど、今回は便宜上このように名づける) -
このファイル を
TypeScript
に保存 - 後述の package.json と tsconfig.json を
TypeScript
に追加してnpm i
package.json
{
"scripts": {
"watch-ts": "tsc -w -p tsconfig.json",
"watch-cp": "nodemon --exec npm run postbuild",
"postbuild": "node copyJsFile.js output ../Assets/Resources"
},
"nodemonConfig": {
"watch": [
"./output"
]
},
"dependencies": {
"nodemon": "^2.0.15",
"typescript": "^4.5.4"
}
}
-
npm run watch-ts
を実行するとTypeScript
内の TS が変更される度にTypeScript/output
に JS がコンパイルされる -
npm run watch-cp
を実行するとTypeScript/output
が変更される度にAssets/Resources
に JS がコピーされる
tsconfig.json
{
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"baseUrl": ".",
"paths": {
"csharp": ["../Assets/Gen/Typing/csharp/index.d.ts"],
"puerts": ["../Assets/Puerts/Typing/puerts/index.d.ts"]
},
"outDir": "output"
}
}
- 最低限の設定。欲しい機能は適宜追加すること。
簡単な使い方
TypeScript 側の作業
上述の環境構築を終えたら、まず TypeScript
ディレクトリ自体を VSCode で開く。
そしてターミナルを2つ開いて npm run watch-ts
と npm run watch-cp
をそれぞれ起動する。
次に、 TypeScript/hello.ts
を作り、以下をコピペ。
import { UnityEngine } from "csharp";
import world from "./world";
console.log("母が恋しいか");
UnityEngine.Debug.Log("じっと我慢の子であった。");
console.log(world());
今度は TypeScript/world.ts
を作り、以下をコピペ。
export default () => "ボン" + "ボンカレー♪".repeat(2);
上手くいけば、Assets/Resources
に hello.js.txt
と world.js.txt
が作られているはず。
Unity 側の作業
適当な GameObject を作り、Add Component
-> New Script
で hoge.cs
を作成。
以下のように書き換える
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
+ using Puerts;
public class hoge : MonoBehaviour
{
+ JsEnv jsEnv;
// Start is called before the first frame update
void Start()
{
+ jsEnv = new JsEnv();
+ jsEnv.Eval("require('hello')");
}
// Update is called once per frame
void Update()
{
}
+
+ void OnDestroy()
+ {
+ jsEnv.Dispose();
+ }
}
この require('hello')
が、要するに hello.js.txt
を呼び出すという意味になる。
さあ、Unity で実行してみよう。
Console タブに、以下のようなログが出ていれば成功。
[XX:XX:XX] 母が恋しいか
UnityEngine.Debug.Log (object)
[XX:XX:XX] じっと我慢の子であった。
UnityEngine.Debug.Log (object)
[XX:XX:XX] ボンボンカレー♪ボンカレー♪
UnityEngine.Debug.Log (object)
MonoBehaviour 自体を操作したい場合
TypeScript で Start も Update も OnCollisionEnter も書きたいときの方法。
これに関しては、自分でそういうスクリプトを書いたので、
それを使ってもらったほうが早いと思う。
使い方
まず TypeScript
に Wrapper.ts
を置き、Assets
に JSLoader.cs
を置こう。
TypeScript 側
書きたい処理が決まったら、適当な名前の TS ファイルを作る。
(今回は hoge.ts
を作ったことにする。)
import { UnityEngine } from "csharp";
import Wrapper from "./Wrapper";
const { Vector3, Time } = UnityEngine;
export = (...args: unknown[]) =>
new (class extends Wrapper() {
constructor(...args: unknown[]) {
super(args);
}
_start(): void {
// ここに Start() 時の処理
}
_update(): void {
// ここに Update() 時の処理
// gameObject や transform には this.$ でアクセスできる。
this.$.transform.Rotate(new Vector3(0, 100, 0));
}
// VSCode を使用しているなら、他のイベント関数のインテリセンスが効く。
})(...args);
このように hello.ts
を書くことによって、MonoBehaviour を操作可能なコードが出来上がる。
対応してるイベント関数は JSLoader.cs
のソースコードを見ればわかると思う。
Unity 側
先ほど作った TypeScript (hoge.ts
) をアタッチしたい GameObject に、
JSLoader.cs
をコンポーネントとして追加する。
File Name
には Assets/Resources
ディレクトリの中にある *.js.txt
ファイルの「拡張子なしの名前」を入力。
もしディレクトリを挟む場合は path/to/filename
というように指定することも可能。
(今回の場合は hoge
と入力)
Tick Timing
は、メインで使用するループ関数を指定。
(日本語化すると 更新
になっているものは、Update
のことです)
Props
(日本語化時は プロパティー
) は、JSON 形式で JS 側に値を渡せる。
特に決まってなければ {}
と入力。
何も問題が無ければ、再生しても問題なく動くはず。
Props を設定する場合の書き方例
import Wrapper from "./Wrapper";
// Props を定義
interface Props {
name: string;
extCount?: number;
}
export = (...args: unknown[]) =>
new (class extends Wrapper<Props>() { // <-
constructor(...args: unknown[]) {
super(args, {
// デフォルトの Props を設定
defaultProps: {
name: "World",
},
});
}
_start(): void {
console.log(
// this.props からアクセス可能
`Hello, ${this.props.name}${"!".repeat(this.props?.extCount || 1)}`
);
}
})(...args);
<< Props が未指定のとき >>
[XX:XX:XX] Hello, World!
UnityEngine.Debug.Log (object)
<< Props が {"name": "悪魔博士"} のとき >>
[XX:XX:XX] Hello, 悪魔博士!
UnityEngine.Debug.Log (object)
<< Props が {"name": "(ここでコブラのテーマ) こんちは――――――――――っ", "extCount": 10 } のとき >>
[XX:XX:XX] Hello, (ここでコブラのテーマ) こんちは――――――――――っ!!!!!!!!!!
UnityEngine.Debug.Log (object)
より詳しい使い方
公式ドキュメントを読もう。
トラブルシューティング
個人的に詰まったところを書く。
GetComponent ってどうすんの
型定義を見てみると System.Type
を引数に入れろと書かれていて、
なんのこっちゃ分からんかったが、こうすればいいらしい。
import { UnityEngine } from "csharp";
import { $typeof } from "puerts";
const { MeshFilter, Component } = UnityEngine;
/* 以下、Wrapper のイベント関数内で */
// 自身の GameObject から MeshFilter コンポーネントを取得
const meshFilter = this.$.GetComponent($typeof(MeshFilter)) as MeshFilter;
// すべてのコンポーネントを取得
const allComponents = this.$.GetComponents($typeof(Component));
// その 3 番目を取得
console.log(allComponents.get_Item(2));
JS 側に値を渡したい場合はどうすんの
明らかに型が足りない
Configure ファイルを新たに作る必要がある。
Assets/Editor
ディレクトリに、正しい書式で書かれた *.cs
ファイルを置けば動く。
すでにあるファイルを書き換えるもよし、設定ごとに分けて書くもよし。
追加・編集したら、トップバー Puerts
-> Generate index.d.ts
をクリックして型定義ファイルを更新するのを忘れずに。
GetComponents で取得した複数のコンポーネントを処理するために、
GetLength
などの関数が使いたかったので、新たに作ったファイル (CustomCfg.cs
) はこんな感じ。
using System.Collections.Generic;
using Puerts;
using System;
[Configure]
public class CustomCfg
{
[Binding]
static IEnumerable<Type> Bindings
{
get
{
return new List<Type>()
{
typeof(System.Object),
typeof(System.Array),
};
}
}
}
オリジナルの型定義などもこれでいろいろ出来る模様。
System.Array を JS の配列に変換したい
System.Array 型の配列はそのままだと map, filter, reduce などが使えない。
なので TypeScript で扱いやすい配列型に変換したい場合はどうすればいいのか。
こういう関数を作ればよい。
import { System, UnityEngine } from "csharp";
import { $typeof } from "puerts";
/** System.Array 型を JS で扱いやすい配列にする */
export const arrayToArray = <T>(array: System.Array): T[] =>
[...new Array(array.GetLength(0)).keys()].map((i) => array.GetValue(i));
// 使用例: 自身の GameObject のすべてのコンポーネントの型名リストを取得
const types: System.Type[] = arrayToArray<UnityEngine.Component>(
this.$.GetComponents($typeof(UnityEngine.Component))
).map((c) => c.GetType());
これで TypeScript でも LINQ のようなものができる。
Generate Code で生成されたコードがエラってる
Generate Code すると処理が軽くなるらしいが、たまに生成コードにエラーが発生する。
直し方は不明。
ただ、単純に Wrap して動かすための関数を生成してるっぽいので、
エラってる関数をまるまる消してもあんまり動作に支障が出なさそう?
応急処置でそうしてみるといいかもしれない。
自信なし。