こんなつぶやきを見つけて気になったので入門した。
どうやら中華人民共和国の大手企業テンセントが開発中の、
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 して動かすための関数を生成してるっぽいので、
エラってる関数をまるまる消してもあんまり動作に支障が出なさそう?
応急処置でそうしてみるといいかもしれない。
自信なし。