3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

UnityでTypeScriptを使ってみる(TypeScriptToLua)

Last updated at Posted at 2022-06-19

概要

Unityで何が何でもTypeScript使いたい…というより、LuaとC#(C系言語)がいろいろ違いすぎてイライラしてくるのでスクリプト言語でもC系っぽい感じの言語が使いたいという気持ちになったので試してみた。

インストール・設定

インストール

node.jsで動くのでnode.jsをインストールする必要があります。
node.jsをインストール(してパスを通)した環境で以下のコマンドをUnityプロジェクトのルートフォルダ上で実行するだけです。
VSCodeの場合はUnityから開いた状態でコマンドラインから実行します。

npm install -D typescript-to-lua lua-types ts-node @typescript-to-lua/language-extensions typescript-tstl-plugin

typescript-to-luaだけでトランスパイルできますが、そのままではlua標準関数が使えないのでlua-typesも一緒に入れておくのがおすすめです。

設定ファイル

インストールは簡単ですが、設定はちゃんとしないと使いにくくなるので重要です。

package.json

npmからビルドできるようにルートフォルダのpackage.jsonを編集します。

package.json
{
  "private": true,
  "scripts": {
    "build": "tstl",
    "dev": "tstl --watch"
  },
  "devDependencies": {
    "lua-types": "^2.13.0",
    "typescript-to-lua": "^1.10.0"
  },
  "dependencies": {
    "@typescript-to-lua/language-extensions": "^1.0.0",
    "ts-node": "^10.9.1",
    "typescript-tstl-plugin": "^0.3.2"
  }
}

"devDependencies"の部分はインストールした時点ですでに記載されているので、
"private""scripts"を上のような感じで追加します。

tsconfig.json

この設定ファイルが重要になります。

tsconfig.json
{
    "$schema": "https://raw.githubusercontent.com/TypeScriptToLua/TypeScriptToLua/master/tsconfig-schema.json",

    "compilerOptions": {
        "target": "esnext",
        "lib": ["esnext"],
        "moduleResolution": "node",
        "strict": true,
        "types": ["lua-types/5.3", "typescript-to-lua", "@typescript-to-lua/language-extensions"],
        "outDir": "./Assets/Resources",
        "baseUrl": "./",
        "paths": {
            "*": ["TypeScript/*"]
        }
    },

    "exclude": ["TypeScript/plugins/*"],

    "tstl": {
        "noHeader": true,
        "luaTarget": "5.3",
        "luaLibImport": "require",
        "extension": ".lua.txt",
        "noResolvePaths": [],
        "luaPlugins": [
            { "name": "./TypeScript/plugins/unityplugin.ts" },
        ]
    }
}

上のtsconfig.jsonはTypeScriptフォルダにTypeScriptソースを置き、fungusで使う場合に適した設定になっているので状況によってここから変更します。

  • "types": lua-types/5.2を使うバージョンに合わせて変更します、ちなみにMoonsharpは5.2準拠で、xLuaはデフォルト(ver2.1.15 zip内のビルド済み共有ライブラリ)は5.3のようです。
  • "outDir": 変換したluaファイルを出力するフォルダを設定します。Fungusの場合は./Assets/Resources/luaがデフォルトです。MoonSharpを直接使う場合は./Assets/Resources/MoonSharp/Scripts、xLuaは./Assets/Resourcesがデフォルトになります。
  • "paths"内の"*": TypeScriptソースを置いておくフォルダを設定します。ルートフォルダだと上手くいかないので何かしらTypeScript用のフォルダを作っておきましょう。
  • luaTarget: 出力するLuaのバージョンを設定します。"types"で設定したバージョンに合わせましょう。
  • "luaLibImport": クラスなどを実現するために使われるTypeScriptToLua独自関数をどのように追加するか指定します。"require"は全関数が記載されているファイルlualib_bundle(73kb)を一緒に出力してrequireします、"inline"はファイルの先頭に使用する独自関数を追記します、"none"は独自関数を使うように変換はしますが、requireも追記もしません、Lua上でエラーになります。
  • "extension": 変換したファイルの拡張子を設定します。Unity上では工夫しないと拡張子.luaは使えないのでFungus/MoonSharpなら.txt、xLuaなら.lua.txtに設定します。
  • "noResolvePaths": モジュールを使う時にどうしてもエラーが出る(変換はしてくれる)のでそのエラーを何が何でも抑止したい場合に使います。
  • "LuaPlugins": プラグインを設定します。unity用にプラグインを設定しています。

変換の自動実行(VSCode)

VSCode起動時に自動的にタスクを起動することができます。この機能を利用してwatchモードでトランスパイラを起動するタスクを設定すれば、TypeScriptファイル更新時にトランスパイルが実行されるので変換忘れを防ぐことができます。

やり方は以下を参考に

メニューの「ターミナル」→「タスクの構成」から「npm: dev」を選んでtasks.jsonを作ってもらいます。
tasks.jsonを開いて「npm: dev」のタスクに下のような感じで"runOptions"を追加します。

tasks.json
{
	"version": "2.0.0",
	"tasks": [
		{
			"type": "npm",
			"script": "dev",
			"problemMatcher": [],
			"label": "npm: dev",
			"detail": "tstl --watch",
			"runOptions": {
				"runOn": "folderOpen"
			}
		}
	]
}

この設定だけでは動かないので自動タスクを許可する必要があります。 (1敗)
F1やCtrl+Shift+Pでコマンドパレットを開き、「タスク: フォルダー内の自動タスクの管理」または「tasks: Manage Automatic Tasks in Folder」を選んで、
「フォルダーで自動タスクを許可する」または「Allow Automatic Tasks in Folder」を選ぶとVSCode(でこの設定をしたフォルダ)を開いた時に自動でタスクが実行されるようになります。

モジュールについて

なぜそうなるのか謎なんですが、Unityの仕様の関係で拡張子を.txtで出力する設定で相対パスでモジュール(外部のTypeScriptファイル)を指定すると、
.txt拡張子付きでrequireするコードに変換してしまいます(拡張子.luaで出力するとそうならないのに)。
どこかの環境を想定しているのかもしれませんが、とりあえずUnityのAssets/Resources内にLuaスクリプトを配置する環境では.txt付きでrequireされても動きません。
試行錯誤した結果、tsconfig.jsonの"paths"内の"*"でTypeScriptフォルダを設定した上で、./をつけずにimportすることでやっと.txt拡張子がつかない状態でrequireしてくれるようになりました。

//1.10.0では結果が違います。
import {a} from "abc";            // local ____abc = require("abc")
import {def} from "test/def";     // local ____def = require("test.def")
import {ghi} from "./test/ghi";   // local ____ghi = require("test.ghi.txt")
2022/9/8 追記: 上記の挙動はバグだったようで、最近のバージョン(1.10.0)では上記設定をしても.txt付きでrequireするようになっています。

TypeScriptToLuaにはプラグインという仕組みがあり、Luaに変換されたコードを特定のタイミングで書き換えることができる仕組みがあるので、それを利用してrequire内の.txtを除去することで対応します。

unityplugin.ts
import * as ts from "typescript";
import * as tstl from "typescript-to-lua";

const plugin: tstl.Plugin = {
  beforeEmit(program: ts.Program, options: tstl.CompilerOptions, emitHost: tstl.EmitHost, result: tstl.EmitFile[]) {
    void program;
    void options;
    void emitHost;

    for (const file of result) {
      //file.code = "-- Comment added by beforeEmit plugin\n" + file.code;
      file.code = file.code.replace(/require\("(.*?)\.txt"\)/g, 'require("$1")')
    }
  },
};

export default plugin;

これをここではTypeScript/Pluginに作成します。tsconfig.jsonにpluginの設定とexcludeを設定してプラグインのコードがLuaへのトランスパイルの対象にならないようにしておきます。(上記のjsonはそういう設定に修正済み)
追記ここまで。

トランスパイル時には下のようなエラーが出ますが、変換はしてくれるのでスルーしても大丈夫です。

error TSTL: Could not resolve lua source files for require path 'abc' in file TypeScript\test.ts.

error TSTL: Could not resolve lua source files for require path 'test/def' in file TypeScript\test.ts.

どうしても上のエラーを抑止したい場合はtsconfig.jsonの"noResolvePaths"にモジュール名を追加します

tsconfig.json
        "noResolvePaths": ["abc", "test/def"]
    }

これってどうやるの?

変数の型判定

typeof(変数)で取得できます。ただしLuaのtype()と違い"table"は"object"、"nil"は"undefined"と比較する必要があります。

if(typeof(a) === "number")     // if type(a) == "number" then
if(typeof(a) === "object")     // if type(a) == "table" then
if(typeof(a) === "nudefined")  // if type(a) == "nil" then

また、比較以外でtypeof()を使うと__TS__TypeOfというTypeScriptToLua独自の関数を参照するように変換されるのでそういうのが困る場合は注意しましょう。

let a: any
let b: string

b = typeof(a)     // b = __TS__TypeOf(a)

配列の長さ

.lengthで取得できますが、変数の型が配列であることが推論できないと変換してくれません。
通常の型なら.lengthが存在しない場合は警告されますが、any型の場合は何も警告なくそのまま.lenghを参照するようなコードになるので注意が必要です。

let a: any;
a.length;             // a.length

let b: any[] = [];
b.length;            // #b

型は不明なものの長さを取得したい変数が配列であることがわかる・確認する場合は最初からanyの配列(any[])として扱うかanyの配列にキャストして長さを取得します。

if (typeof(a) === "object") {
    (a as any[]).length;     // #a
} 

Luaの多値

Luaは関数の戻り値を複数返して受け取れるんだけどググった感じ多値って言うらしい。
TypeScriptには(多分)ないものなので、それ用に特別に型や関数が用意されてます。

//定義
function multireturn(): LuaMultiReturn<[boolean, any]>
{
    return $multi(false, "多値のテスト"); // return false, "多値のテスト"
}

//取得
let a: boolean;
let b: string;
[a, b] = multireturn();                 // a, b = multireturn(nil)

気をつけたほうがいいこと

うっかりしているとやらかしそうな罠に気づいてしまう場面がちょいちょい見られます。
都度都度変換してみて出力されたLuaコードを確認したりPlaygroundでどのように変換するか確認したほうがいいです。

グローバル変数の衝突に気をつけよう

なぜだかわかりませんが、トップレベルステートメントに定義した変数はグローバル変数になるので定義する場合は衝突に気をつけましょう。

let a: number = 9;          // a = 9

function test()
{
    let b: number = 5;      // local b = 5
}

while (true) 
{
    let c: number = 1;      // local c = 1
    break;
}

配列の扱いには気をつけよう

TypeScriptの配列は0オリジンですが、Luaの配列は1オリジンです。
変換時に自動的に調整するのですが、配列の長さと同様に変数の型が配列であることが推論できないと変換してくれません。
any型の変数を配列でアクセスしようとすると変換してくれずに要素がズレてわかりにくいバグに悩まされるハメになるので特に気をつけましょう。

let a: any;
let b: any[] = [];

let c = a[0];                // c = a[0]
let d = b[0];                // d = b[1]

let e: number = 0;

let f = a[e];                // f = a[e]
let g = b[e];                // g = b[e + 1]

if (typeof(a) === "object")
{
    let h = (a as any[])[e]; // local h = a[e + 1]
}

文字列結合時にtostrigは不要

変数と文字列結合する際に文字列型と推論される変数ではなかった場合は自動的にtostring()関数をラップしてくれるようです。
唯一レベルでありがたい仕様。

let a: any;
let b: number = 0;
let c: string = "";
let d: string = "";

d = a + "";         // d = tostring(a) .. ""
d = b + "";         // d = tostring(b) .. ""
d = c + "";         // d = c .. ""

クラスを使う時は覚悟しよう

クラスを使用すると必ずTypeScriptToLuaの独自関数を使うようになります。

export class test
{
    testval: number = 5;

    static test1(): number
    {
        return 1;
    }

    test2(): number
    {
        return this.testval;
    }
}
/*
local function __TS__Class(self)
    local c = {prototype = {}}
    c.prototype.__index = c.prototype
    c.prototype.constructor = c
    return c
end

local ____exports = {}
____exports.test = __TS__Class()
local test = ____exports.test
test.name = "test"
function test.prototype.____constructor(self)
    self.testval = 5
end
function test.test1(self)
    return 1
end
function test.prototype.test2(self)
    return self.testval
end
return ____exports
*/

好みの問題ではありますが、独自関数を使ってほしくはないがクラスみたいなことをしたい場合は。
オブジェクト型にフィールドを追加していくJavaScriptっぽいやり方にするといいと思います。
当たり前ですがインテリセンスは効かなくなるので注意。

let test = {
    test1: function(): number
    {
        return 1;
    },

    New: function()
    {
        return {
            testval: 5,
            test2: function(): number
            {
                return this.testval;
            },
        };
    },
};
/*
test = {
    test1 = function(self)
        return 1
    end,
    New = function(self)
        return {
            testval = 5,
            test2 = function(self)
                return self.testval
            end
        }
    end
}
*/

noSelfは信用するな

TypeScriptToLuaはデフォルトでコロンを使って関数を呼び出そうとします。なんならメソッドじゃない場合はnilを渡してでも第一引数にオブジェクトを渡しに行こうとします。
noSelfアノテーションやnoImplicitSelfオプションなんかで抑止できると言っていますが、any型のプロパティや変数に入っている関数を呼び出した時はどうあがいても抑止できません。ひどい時はバグでうまく抑止できないこともあるらしい(怒)
とにかく第一引数にオブジェクトを渡されると動かない関数を使う時はどういう形で変換するのかに常に気を払う必要があります。

自分で関数やクラスを作る場合はそういう前提で作ればいいですが、誰かが作ったライブラリの場合はそうもいかないし規模によっては型定義ファイルを作ってられない場合もあるので、
どうしても上記のような挙動を避けたい場合は下のような感じで第一引数をスルーするラッパー関数を通じて呼び出すしかなさそう。
UnityのGameObjectはMoonSharpでもxLuaでもコロン呼び出しで動くのでホッとしてます。

function invoke (this: any, fn: any, ...args: any[]) : any
{
    if(typeof(fn) === "function") 
    {
        let [success, result] = pcall(fn, ...args);
        return result;
    }
    return null;
}
let obj: any;

obj.get("test");            // obj:get("test")
invoke(obj.get, "test");    // invoke(nil, obj.get, "test")

感想

変換後のLuaが読めないと謎のバグに悩まされそうな罠がいろいろ見られるので、Luaの習得は必須です。
結局Luaが使えるなら最初からLua使ったほうがいいという結論になりそう。

ちなみに

別のアプローチでTypeScriptを使えるソリューションがあるらしい、V8/QuickJS/Node.jsのいずれかで動かすようだ。
見た感じこれはこれで大変そう。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?