nizima LIVEのスクリプト機能(β)の特徴・注意点

(追記 2024/02/16: ポイント数の追記と色、onMouseMoveイベントについての項目の追加を行いました。)
# はじめに
記事の作成時点(2024/02/08)でのnizimaLIVEのバージョンは1.7.0です。
この記事では、同バージョンでのスクリプト機能を使っていた上で気づいた点などをまとめています。
ただしベータ版の機能なので、今後内容が変更される可能性は十分にありますのでご注意ください。

# プラグイン機能との違い
## プラグイン機能

https://docs.live2d.com/nizimalive/tutorials/plugins/

WebSocketを通じてJSONのデータをnizimaLIVEとやり取りし、内容の取得や設定ができる機能。
localhost内でのみ通信可能。
基本的にはアプリケーション形式での配布で、アプリケーションの保存場所は(おそらく)自由。

## スクリプト機能(β)

https://docs.live2d.com/nizimalive/tutorials/scripts/

nizimaLIVE内でJavaScriptのファイルを動作させる機能。
保存場所によって、アプリの起動時に実行されアプリ全体に影響を与えるアプリケーションスクリプトと、特定のモデルが読み込まれたときにのみ実行させるモデルスクリプトに振り分けられる。
.js形式での配布で、保存場所はモデルなどのデータを置くnizima LIVEフォルダ(Windowsの場合、C:\Users\ユーザー名\AppData\Roaming\Live2D\nizima LIVE)内のscriptsフォルダに入れるとアプリケーションスクリプトに、models\モデル名\scriptsフォルダに入れるとモデルスクリプトになる(モデルスクリプトの動作は未検証)。

# スクリプト機能の特徴
- 基本はJavaScriptなので、Mathとか普通に使える
- 円、楕円、点、線、長方形、画像、文字列といったものをモデルのウィンドウ内に描画できる
- キー入力に対応
- モデルの情報などの取得ができる
- 簡素なドキュメントが整備されている

https://live2d.github.io/nizimaLIVEScriptAPI/index.html

# カテゴリ別の注意点
## 準備
### nizimaLIVEを過去のバージョンからのアップデートしてもscriptsフォルダが生成されない
nizimaLIVEは基本的にこんな感じがするのですが、ユーザーデータを格納するフォルダをアップデートのときにいじってくれません。
例に倣って、今回のscriptsフォルダもアップデートでは作成されないため、自分で作る必要があります。

ちなみに作成しないと、nizima LIVEを起動した後のスクリプト一覧からフォルダを開くボタンを押しても、うんともすんとも言いません。
せめてフォルダが存在しないなどのエラーメッセージを出してくれればなぁ……とは思いますが、とりあえずフィードバックは返しておきましたので今後変わるかもしれません。

あと、nizima LIVEのアプリケーションのインストール先とユーザーデータ格納フォルダは全く別なのですが、nizima LIVEのインストール先にもscriptsフォルダやmodelsフォルダなどが存在します(おそらく初回インストールの際のテンプレートかと)。
紛らわしいので間違えないようにしましょう。インストール先のscriptsフォルダにjsをぶち込んでも反応しません。

### スクリプトを追加した場合はアプリを再起動しなければ反応しない
現時点でスクリプトの追加はnizima LIVEの起動時に検知され、スクリプト一覧はアプリを再起動するまで固定されます。
新しいjsファイルを置いた場合、必ずnizima LIVEを再起動する必要があります。

### ファイルの内容を更新した場合は再起動せずとも反映される
既にスクリプト一覧に存在するスクリプトを上書き保存した場合、自動で最初から読み込み直してくれます。
読み込み時→onEnabled→paintというように再起動後の初回実行と同様に最初からやり直してくれます。
いちいち再起動する必要はありません。

## 基本機能

### ログには一部のエラーしか出力されない
結構やりにくいのですが、何かしらエラーがある場合、何も出力されずに実行が停止されることが多いです。
たまに出してくれますが、その差異は良くわかっていません(関数内でのエラーは表示されない……？)。
うまく動かなかったら地道にデバッグしましょう。

### console.log()がとてつもなく重い
触っててびっくりしましたが、ブラウザでの開発の感覚でconsole.log()をすると痛い目にあいます。
ログのウィンドウを表示していようがなかろうが、1フレームあたりの`console.log()`の数が数個増えるだけでfpsがガタ落ちします。
1フレームに10個程度でそんなに出力してないつもりでも余裕で10 FPSを下回ります。これでもi7 12700Fなんですがね……。
後述の通り画面上への文字の描画は軽量なので、簡単な変数の値の確認などにはそちらを使用しましょう。

### スクリプト同士は完全に独立する
複数のスクリプトを同時に動作させている場合でも、同じ名前の変数や関数が複数のスクリプトに存在したとしても、それらは独立します。
**よって競合を心配する必要はありません。**

しかし同時に、自分の確認できた範囲では**依存関係をもたせる術もありません。**
importやrequireを試しましたが、importは未知のトークン、requireはundefined扱いでした。
よって**ライブラリ・モジュール・コアとなるスクリプト作成して使い回すことができません。**
(自分はこのimport/requireの辺りに疎いのでもしできた方が居ましたら教えてください)

### functionの外に書いたスクリプトは一度しか実行されない

変数宣言などのスクリプトは基本的に関数定義の外に書くことになります。
その部分はnizimaLIVEの起動後、初めてそのスクリプトが有効になった際、あるいはそのファイルが更新された際のみに実行されます。
一度無効にして再度有効にしてもその部分は実行されません(変数の値は保持されます)。
有効にするたびに変数の初期化を行いたい場合はonEnabledなどを使用しましょう。

### アプリケーションスクリプトとモデルスクリプトの区別がつきにくい(想像)
アプリケーションスクリプトの場合もモデルスクリプトの場合も、同じ数の引数を持つonEnabled、paint関数が実行されます。
しかし第一引数のタイプがアプリケーションスクリプトの場合はApplication、モデルスクリプトの場合はModelであり互換性がありません。
双方でも動くようにするには、特定の変数の存在の有無などでどちらのスクリプトなのか確認する必要がありそうです。

### 実行の順番は関数外→onEnabled→paint→paint→……
UnityのMonoBehaviourで言うなら、onEnabled関数がUnityでのStart関数、paint関数がUnityでのUpdate関数のようなものです。
変数の初期設定などはどこで行うべきか必ず確認しましょう。

## 画面描画

### painterクラスはpaint関数の引数でのみ取得できる
文字色など、画面描画系の設定にはpainterクラスをいじる必要があります。
しかしonEnabledではpainterは渡されません。
paint関数の引数でのみ取得できるのですが、paint関数は毎フレーム呼び出されます。
初期設定のようにしたい場合は、onEnabledで初回実行フラグを立ててpaint関数内で条件分岐することになります。

### painterでの描画の反映はアプリ・モデルの描画と同期しない
**これ最重要です。マジで引っかかります。**
paint関数はアプリケーションが描画されるたびに実行されるので、描画を60 FPSに設定していれば1秒間に60回ほど実行されます。
描画の命令も1フレームごとに行えます。
**しかしそれが即座に反映されるわけではありません。
painterクラスでの描画の結果が映りは体感的に10 FPSかというほどカクつきます。**
現時点でリアルタイムに描画するのは難しそうです。

### テキスト描画(painter.drawText)のクセがすごい
#### 基準は左下
文字のアンカーポイントは左下に設定されています。画面の座標は左上原点なので、y=0で文字を描画すると画面の上に隠れます。

#### pixel基準とpoint基準のフォントサイズを指定できるが、片方は必ず-1になる(追記：2024/02/16)
文字描画の際、文字のサイズはpixelSize、pointSizeのどちらかで指定できます。
point指定の場合は画面のdpiを考慮してサイズが調整されるのかもしれません(未検証)。
しかし一番の問題は、**片方を指定した場合もう片方が-1になる**ということです。
~~そして現時点でpointの数値をpixelに変換する方法はおそらく用意されていません。~~
**(追記 2024/02/16)検証していませんが、おそらく12 pt = 16 pxです。**
イラストや文書などではptは現実の長さを基準とするため、それに慣れすぎて12pt=16pxの換算式を完全に失念していました……。

更に文字の座標はpixelで指定することになります。
そのためきれいに文字を配置したい場合、文字サイズはpixelを基準にすると良いでしょう。
**しかし文字サイズの初期値はpixelSize=-1、pointSize=12です。**
よって初期値を変更しなければpixelでフォントサイズを指定できません。
**そして文字サイズを設定するためにはpainterクラスが必要なので、前の記述の通りpaint関数が呼び出されなければ文字サイズを変更できません。**
onEnabledに書きたい気持ちを抑えて文字サイズ指定を書くことになります。

まあでもそんな綺麗に配置しなくてもいいんだが？と思うかもしれませんが、この問題は次に影響します。

#### 改行は無視される
**どうして？** と頭を抱えましたが、なんとこの関数、改行を完全に無視します。
改行を反映させるには自力で改行文字で文字列をスプリットし、1行ごとにずらしながら描画する必要があります。
面倒な方のために拙作のスクリプトを載せますが、**pixelSize基準のため初期設定のままでは動きません。pixelで指定してください。**
```javascript
function drawMultilineText(painter, x, y, text){
    let lines = text.split("\n");
    for(let i=0;i<lines.length;i++){
        painter.drawText(x, y+i*painter.pixelSize, lines[i]);
    }
}
```

```javascript
//使用例
function paint(app, painter){
    drawMultilineText(painter, x, y, 'hoge\nhogehoge\nhogehogehoge');
}

/*描画結果:
hoge
hogehoge
hogehogehoge
*/
```

### 楕円全体の幅は指定したwidth, heightの倍
楕円描画関数`painter.drawEllipse()`のwidth、heightの値は、中心点から上下・左右の辺までの距離を示します(円の半径のようなイメージです)。
よって、ドキュメントにはwidth、heightと書いてありますが、最終的な幅・高さはその倍になります。

### 描画系は結構たくさん置いても大丈夫
painterでの描画は、大量の描画の指示を送っても軽量に動作します。
楕円の描画という重そうな関数を1フレームあたり130個とか表示してもアプリケーションのフレームレートには全然影響しませんでした。
高いフレームレートでの表現ができないものの、**1フレーム中に数個入れただけでアプリ全体のフレームレートを大きく落とすconsole.log()より数億倍マシです。**
動作中の変数の値の確認にはdrawTextを使用しましょう。

### painterでの描画はスクリプトを無効にすると消える
`painter.clear()`を実行せずとも、描画結果はスクリプトが無効になると自動で削除されます。
逆に、スクリプトが有効になっている間は`painter.clear()`を実行しないと前の描画は残り続けます。

### 色は16進数で指定可能だがα値は使えない(追加：2024/02/16)
painterに色を渡す際は文字列を渡すことになります。
公式のドキュメントでは`"red"`や`"black"`など、色の名前を入れた文字列を渡す例しか提供されていませんが、`"#6c3524"`などのように16進数カラーコードでの色の指定にも対応しているようです。
逆に、`"rgb(0, 0.5, 0.5)"`のように指定することは**できません**。

また半透明を描画しようとして`"#6c352480"`のようにしても、半透明で描画されることはありません。
α値が指定された場合、00であろうがffであろうが、その値によらず常に描画されず透明になります。

## キーイベント
### 引数のkeyは装飾キーを含めた文字列
例えばAキーが押されたら`onKeyDown(app, key)`のkeyは'A'に、
上矢印キーが押されたら'Up'に、
CtrlキーとZキーが同時に押されたら'Ctrl+Zになります。'

### モデルの表示画面がアクティブでないと読み取れない
キーの読み取りには、nizima LIVEを表示しているだけでなく、nizima LIVEの中でもモデルを表示するメイン画面をアクティブにした状態でなければキーは読み取られません。
少し状況が限られますが、逆に言えばキーロガーのようなことをされる心配はなさそうです。

### 装飾キーを単体で押した場合の文字列は特殊
Shiftキーを単体で押すと'Shift+Shift'に、Ctrlキーを単体で押すと、'Ctrl+Control'となりました。
おそらく装飾キーを押していたら無条件で'Ctrl+'などを先頭につけるようになっているのでしょう。

### 全てのキーが反応するわけではない
多くのキーが反応するしましたが、少なくともTabキーは反応しませんでした。

### onMouseMoveではキーを受け取れない(追加：2024/02/16)
公式のドキュメントでは第4引数にbuttonがありどのキーが押されているかを取得できそうですが、試したところonMouseMoveの場合第4引数にはundefinedが入れられてしまうようです。
おそらくドキュメントの書き間違いで、おま環ではないと思うのですが……。
押し続けているかどうかは、onMouseDownとonMouseUpイベントで変数に書き込んで管理すべきのようです。

# おわりに
スクリプト機能はまベータ版で、これからも変化していくことでしょう。
様々なクセがありますが、それでも夢の広がる大きな機能だと思います。

余談ですが、買い切りのVTube Studioと違い、nizima LIVEはサブスク契約になります。
1年で見ればそこまで高くはないとはいえ、サブスクとなるとどうしても敷居を感じてしまうもの。
通話でスタッフの方とお話をする機会がありましたが、VTSを意識して安い買い切りにしてはどうかという話も出ていました。
しかしスタッフの方によればnizima LIVEはLive2D社謹製のトラッキングソフトのため、そういうことをしてしまうと独禁法に触れる恐れがあるんだとか……。

一方で最近のVTSはnizima LIVEを潰しにかかってきているというか、nizima LIVEの実装した機能を~~パクって~~積極的に実装している気がします(コラボ機能やエフェクト機能など、nizima LIVEの後追いで機能をひたすら付け足しています)。
それにVTSにはNVIDIA Broadcastを使用した高度なトラッキングなど、多数の魅力があります……が、僕はnizima LIVEのほうが好きです。
コミュニティの空気が和やか。UIがシンプル。動作が非常に軽量。
そして何より、Live2D社が手掛けていることもあってか**物理演算の動作がCubismエディタに近い**(VTSはやけに揺れが小さい)。
Live2Dモデラーでもある身として、自分の意図した挙動をしてくれるのが嬉しいのです。

つまるところ、僕はnizima LIVEが大好きです。
この記事ではスクリプト機能の注意点をたくさん挙げてきましたが、それは批判したいのではなく、多くの人がよりnizima LIVEに触れやすくなりますようにとの想いで書きました。
僕はこれからも財布が許す限りnizima LIVEを応援していきます。