0
Help us understand the problem. What are the problem?

posted at

updated at

Puerts を使って TypeScript で Unity する方法

こんなつぶやきを見つけて気になったので入門した。

どうやら中華人民共和国の大手企業テンセントが開発中の、
TypeScript で Unity や Unreal Engine などを使えるようにするライブラリらしい。

ドキュメントが殆ど中国語で大変だったけど、とりあえず基本的な使い方が分かったので書く。

プロジェクトのサンプル

今回学習するにあたり、
「このリポジトリをクローンして改造すれば、普通に1からやるよりはそれなりに楽」な
サンプルプロジェクトを作った。

環境構築

Unity 側の設定

  1. Unity プロジェクトを用意する
  2. git clone https://github.com/Tencent/puerts.git する
  3. リポジトリの puerts/unity/Assets/ の中身を Unity プロジェクトの Assets にコピー
  4. このページ から Plugins_Nodejs_*** を解凍し、中の Plugins をプロジェクトの Assets/Plugins に上書きする
  5. このファイルAssets/Editor に保存
  6. Unity を開き、トップバー Puerts -> Generate index.d.ts をクリック。すると Assets/Gen/Typing/csharp/index.d.ts が生成される

TypeScript 側の設定

  1. Assets の親ディレクトリに TypeScript ディレクトリを作成
    (まあ別にどんな名前でもいいけど、今回は便宜上このように名づける)
  2. このファイルTypeScript に保存
  3. 後述の 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-tsnpm 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/Resourceshello.js.txtworld.js.txt が作られているはず。

Unity 側の作業

適当な GameObject を作り、Add Component -> New Scripthoge.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 も書きたいときの方法。

これに関しては、自分でそういうスクリプトを書いたので、
それを使ってもらったほうが早いと思う。

使い方

まず TypeScriptWrapper.ts を置き、AssetsJSLoader.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 して動かすための関数を生成してるっぽいので、
エラってる関数をまるまる消してもあんまり動作に支障が出なさそう?
応急処置でそうしてみるといいかもしれない。

自信なし。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
0
Help us understand the problem. What are the problem?