AmebientとUdonプログラミング

Amebient Advent Calendar 19日目。Udon周りの話をします。
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## 初期実装

今までの記事で長々と実装方針を書いてきたわけですが、これらをナイーブに実装すると**とても無駄が多い**です。ナイーブさにも依りますが。

<blockquote class="twitter-tweet" data-conversation="none"><p lang="ja" dir="ltr">かわいいね <a href="https://t.co/cHTK53xnu6">pic.twitter.com/cHTK53xnu6</a></p>&mdash; phi16 (@phi16_) <a href="https://twitter.com/phi16_/status/1284098874248851457?ref_src=twsrc%5Etfw">July 17, 2020</a></blockquote>

18.1msは大分やばいです。そうだね。

<blockquote class="twitter-tweet" data-conversation="none"><p lang="ja" dir="ltr">いまここ <a href="https://t.co/rUDbmRcfLq">pic.twitter.com/rUDbmRcfLq</a></p>&mdash; phi16 (@phi16_) <a href="https://twitter.com/phi16_/status/1284158665478467584?ref_src=twsrc%5Etfw">July 17, 2020</a></blockquote>

諸々弄ってそこそこ減らしました。完璧ではないとは言え、まぁ努力がある程度報われたくらいの領域です。

**一般に**、高速化をする際には何らかの「仮定」が必要です。こういう条件が成り立つから処理を省いて良い、という考え方をします。

今回Amebientで行うこととなった「仮定」は以下です。

- 楽器に当たってない雨・雨に当たってない楽器は何もしなくて良い
- 楽器や水面(水量)はpickupされてなければ動かない
- 楽器と雨の組み合わせが固定なら鳴るリズムも同じ
    - これはバルブから出る水滴に関しては成り立ちません
- 多くの楽器の移動が同時に発生することはあまり無い
- バケツや飯盒や水面の揺れはそのうち収束する
- 世界遷移は滅多に起きない

こういう**暗黙知を実際に顕在化させることが、エンジニアリング**です。多分。

## Updateの削減

前述の仮定は「常時行わなければならない処理はそう多くない」ことを導きます。各UdonBehaviourでは`Update`内で延々処理をしていたんですが、それらを削減することを考えてみることにします。

例えば**バケツの取っ手**。率直に書くと毎フレーム物理計算をすることになりますが、概ね静止しているので処理は削って良いはず。

そして動き出す瞬間は**pickupされたタイミング**です。とは言え、他人から見るとそれは検知できない (イベント送れば別だけど) ので「楽器の位置が変化したタイミング」を使うことにします[^rot]。

[^rot]: 回転は検知していないんですが、手で動かす限り必ず移動が発生する、ことを仮定しています (Desktopで回転させるとやばいかも)

```cs
Vector3 moveDirection = center - nextCenter;
if(stability > 10 && moveDirection.magnitude < 0.0001f) {
    return;
}
center = nextCenter;
```

安定性パラメータ`stability`を導入して、「動いてるときは`0`、静止していると増加」させることにしました。バケツの取っ手が静止していることは「角度変化がほぼ無いこと」で判断できます…が、どうやら私は「角度が限界まで来ていること」で判断しているようです。なんでだろう[^st]。

[^st]: つまり空中に置いてぶら下げたりすると静止判定にならないということなんですが、まぁ数多く無いからいいのかな…

静止してそうなら`stability`を`0`に。そうでないなら`1`増やす、ことで静止時に`Update`内の処理の殆どがスキップできるようになります。物理演算系だとsleepとか言ったりしますね。バルブ (`ValveHandle`) にも同じ様な処理が入っています。

&nbsp;
他にも鉄パイプ (`Mallet`) は`pickup`されてなければ何も処理しない[^pipe]とか、静止した金属 (`Instrument`) は「音を発音する予定が無い」なら何も処理しないとか (鉄パイプで叩かれたときの`SendCustomNetworkEvent`で起きます) やっています。

[^pipe]: 音発生するのは`Percussion`と`Instrument`なので所有者以外は本当に何もしなくて良い

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">配管が鳴らなかった原因 <a href="https://t.co/F1JS3cVtKk">pic.twitter.com/F1JS3cVtKk</a></p>&mdash; phi16 (@phi16_) <a href="https://twitter.com/phi16_/status/1284557779638693890?ref_src=twsrc%5Etfw">July 18, 2020</a></blockquote> 

この辺の処理は条件の優劣関係をそこそこ考えないといけないので大変です (ちゃんと考えればいいだけですけど)。これは全ての初期化を行う前に「音発音予定かどうか」をチェックしていて、音発音は初期化が行われないと実行されないようになっているので、何も行われなくなったというやつです。かなしいね。

&nbsp;
リズムに乗った環境音 (`RhyAmbience`) でも似たようなことをします。これは**発音タイミングが確定している**ので、次の発音直前まで何もしなくて良いんです。

```cs
if (minDelay >= 1.0f) {
    waitTime = Time.time + Mathf.Min(2, minDelay) - 0.9f;
    return;
}
```

次回の発音が1秒以降である場合、**最大1秒**待機します。…なんでだろう。変化が発生するとしたら世界状態が変わる時、ではあるんですけど雷の音は「世界状態が変わった64拍後」くらいに起きるはず。恐らく`Time.time`の精度がちょっと怖かったんだと思います (ラグる度に音声系とズレるので)。もうちょっと長めに時間取っても良かった気はする。

&nbsp;
まぁ最大のボトルネックは勿論`Percussion`です。今までの処理軽減系が大体入っています。

- 世界が終わってたら処理終了
- 雨に当たっておらず、鉄パイプの衝突も無ければ処理終了
- 次に発音するタイミングの直前[^pr]まで処理をしない

[^pr]: 0.1秒前までsleepしているっぽいんですが、Amebientでラグる (10fpsを下回る) と音が飛ぶ原因はこれです

しかし**`Percussion`は突然状況が変わることがあります**。顕著なのがバルブの水量変化。または「上方にある`Percussion`が抜かれて突然自身に雨粒が衝突するようになるケース」とか。

丁寧に「関わりそうなGameObjectに対してイベントを飛ばす」のもありですが、面倒だったので**シーンに存在する`Percussion`全てにイベントを飛ばす**ことにしました。ついでに`Rain`にもイベントが飛んでいます。…軽減した方が良かったかもしれない。まぁ実装した時にはそほど問題ないと判断したんだと思います。

雨に当たっている`Percussion`は、自身の移動を検知すると`Metronome`にイベントを飛ばし、それはいろんな対象にブロードキャストされます。`Percussion`を移動している時間はさほど多くないという判断です。

少なくともこの辺の処理をする前よりは大分軽くなりました。

&nbsp;
実際に最終的なプロファイルを見るとこんな感じになってます。…ひどいですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88634/1d2f1639-4096-9bc5-e60d-82d6c8048673.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88634/cc9fdb7a-abc6-ac41-fca9-2a421f9dedde.png)
極稀に動くUdonBehaviourでも、**まずUdonを起動するコストがある**のでそれすら無いことが勿論望ましいです…。
`Update`を各々に書くのではなく誰かにまとめて発行してもらう (ことでオンオフ制御を丁寧にやる) ことも出来はすると思うんですけど…。「起きなければならない条件」みたいなものを丁寧にまとめて上げないといけないのでちょっと複雑です。

まぁ緩めにしても大きな削減にはなりそうだな…。ちょっと今度やろうかな…。

## 相互作用

複数のUdonが協調動作をすることがそこそこ求められている本作ですが、一般にUdonから他Udonを呼ぶのはちょっと悩ましいものです。

```cs
public class A : UdonSharpBehaviour {
    public int F(int x) {
        return x;
    }
}

public class B : UdonSharpBehaviour {
    private A a;
    private int u, v;
    private int F(int x) {
        return x;
    }
    public void G() {
        u = a.F(0);
        v = F(0);
    }
}
```

2つの異なる`F`の呼び出しは、次のようにコンパイルされます。

```rb
# u = a.F(0);
PUSH, a
PUSH, __0_const_intnl_SystemString
PUSH, __0_const_intnl_SystemInt32
EXTERN, "VRCUdonCommonInterfacesIUdonEventReceiver.__SetProgramVariable__SystemString_SystemObject__SystemVoid"
PUSH, a
PUSH, __1_const_intnl_SystemString
EXTERN, "VRCUdonCommonInterfacesIUdonEventReceiver.__SendCustomEvent__SystemString__SystemVoid"
PUSH, a
PUSH, __2_const_intnl_SystemString
PUSH, __0_intnl_SystemObject
EXTERN, "VRCUdonCommonInterfacesIUdonEventReceiver.__GetProgramVariable__SystemString__SystemObject"
PUSH, __0_intnl_SystemObject
PUSH, __0_intnl_SystemInt32
EXTERN, "SystemConvert.__ToInt32__SystemObject__SystemInt32"
PUSH, __0_intnl_SystemInt32
PUSH, u
COPY
        
# v = F(0);
PUSH, __0_const_intnl_SystemInt32
PUSH, __0_x_Int32
COPY
PUSH, __0_const_intnl_exitJumpLoc_UInt32
JUMP, 0x00000008
PUSH, __0_intnl_returnValSymbol_Int32
PUSH, v
COPY
```

変数の場合はこう。

```rb
# u = a.x;
PUSH, a
PUSH, __0_const_intnl_SystemString
PUSH, __0_intnl_SystemObject
EXTERN, "VRCUdonCommonInterfacesIUdonEventReceiver.__GetProgramVariable__SystemString__SystemObject"
PUSH, __0_intnl_SystemObject
PUSH, u
EXTERN, "SystemConvert.__ToInt32__SystemObject__SystemInt32"
        
# v = x;
PUSH, x
PUSH, v
COPY
```

**Udonは唯のVM**で、最適化がばりばり行われるとかもありません。**命令数が速度に直結します。**加えて、関数呼び出しから発生する`SendCustomEvent`は「相手のUdonBehaviourの起動」も行います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/88634/2a22516a-7abc-6495-7446-82bddfd0e56a.png)

つまりは**相互作用にはランタイムのコストがめちゃ掛かる**ということです。でもそれは**適切に分割されたソースコードの生む開発効率の向上**との相関です。私は最適化の為に構造を壊すようなことはしたくないので、これらのコストは受け入れた上で、できる限り軽減することを考えました。…そんなにちゃんとはしてないけど。

例えば「全ての初期化」。Amebientでは`Metronome`が全Udonの総括をしていますが、UdonBehaviourの初期化順は制御可能なものではありません。よって各Udonは`Metronome`が起きたことを検知する必要があります。

まずそれぞれのUdonは単純に`GameObject.Find("Metronome").GetComponent<Metronome>()`によって`Metronome`の参照を拾っています。わざわざ手で割当したくはなかったし、もうちょっと厳格にチェックするべきかと考えてもまぁどうせ`Start`時に読むだけだからアバターの影響は恐らく少ない。いまのとこ動いてるし。

そして`Update`内で次のコードが動いています。

```cs
if(!initialized) {
    if(metronome.initialized) {
        initialized = true;
    } else return;
}
```

**まず毎フレーム動かしたくはないです**。でもそれをしないのは結構難しいと思う。いつMetronomeの初期化が終わるかというのはUdonの初期化以上に「[いつmasterから開始信号が送られてくるか](https://qiita.com/phi16/items/bebc6671b109bff07044#%E6%99%82%E9%96%93%E3%82%92%E6%B8%AC%E3%82%8B)」でもあるので、やるなら`Metronome`が各Udonに向かって信号を発信しなきゃいけない。

でも誰に発信すべきか、というのは指定が難しくて…　まぁ**シーン上に存在する全てのUdonBehaviourにイベントを飛ばす**のはありかもしれない。それ以外は出来ないと思います、各Udonは自分自身の存在を`Metronome`に伝えることが出来ないのです (`Metronome`の初期化が遅い可能性がある為)。

…あ、でも、未初期化のUdonのpublic変数にちゃんと書き込みができるなら ([これ](https://vrchat.canny.io/vrchat-udon-closed-alpha-bugs/p/588-heap-values-are-not-initialized-in-some-cases-so-getprogramvariable-returns) がちゃんと治ってるなら)、外部から`Metronome`の持つ「初期化対象リスト」をがしがし編集しちゃう、っていうのもありなのかもしれない…。

まぁ、それは次何か作る時に考えるとして。

毎フレーム動かすとなると、当然そのコストが小さいほど嬉しいです。`metronome.initialized`には**一度trueになったら以後true**という法則があります。この仮定によって、「自前で`initialized`という変数を持っておいて以後それを参照する」ことができます。命令数は半分(5/14)くらいになります[^tr]。

[^tr]: 否定を付けなければ2命令なのでそこまでやるべきでは？？？(`notInitialized`って名前になるけど)

これは「全てのUdonにわざわざ`initialized`変数と更新検知機構を入れる労力」との対価なので、まぁ見合うのならやるといいのだと思います。

&nbsp;
そしてもう一つ。`Metronome`の初期化**以外**の状態検知に関しては、Udonの実行順とかは関係無い話になります。

「世界状態の変化を検知する」のは別Udonの変数を読みに行っても可能ですが、それ以上に**変化させた主体が検知を通達する**のが自然と言えます。

要はpull型とpush型というやつです。特にpollingするとコストになるのでやっぱりpush型が望ましいと思うのです。

というわけで、起動した各Udonは`Metronome`に「自身をListenerとして登録」してもらいます。

```cs
public void AddListener(UdonSharpBehaviour b) {
    if(listenerCount >= listeners.Length) {
        bool[] us = new bool[listenerCount*2];
        UdonSharpBehaviour[] bs = new UdonSharpBehaviour[listenerCount*2];
        for(int i=0;i<listenerCount;i++) {
            bs[i] = listeners[i];
        }
        listeners = bs;
    }
    int ix = listenerCount;
    listeners[ix] = b;
    listenerCount++;
}
private void Emit(string name) {
    for(int i=0;i<listenerCount;i++) {
        listeners[i].SendCustomEvent(name);
    }
}
```

これは可変長配列の実装も含んでおりますね。まぁこうして好きな相手に好きなタイミングでメッセージを送ることができます。

例えば「楽器の配置が変わった」「バルブの水量が変わった」「世界遷移が発生した」「世界が終わった」「水没した」とか色々あります。各Udon側は好きにpublicメソッドを定義しておくだけです。

**まぁ今考えると`SetProgramVariable`を直接叩いたほうが速い気がしますけど。**実行速度的な意味で。

それこそデータ渡せるしな…。次に作る場合の反省点がどんどん溜まってきますね。

というか高速化もうちょっと頑張っていいんじゃないか？という気がしてきた。まだまだやれますね…。

全体的に**丁寧にアセンブリを見る**か**丁寧にprofilerを見る**のが何よりも正しいと思います。

## まとめ

大枠の話に留まってしまいましたが、まぁ思想面の話として…。

`Rain`の内部処理では「複数の楽器が同時に動くことは少ない」ことから「1F中に更新チェックする楽器は1つにする」みたいなことをやってたりもしてますが、まぁ小さな話…。それが高速化に寄与しているかは正直わかりません。

ギミック屋 (？) としてはギミックの重さで体験が阻害されるのは大分悲しいと思うのでもうちょっと軽くしてあげたかった気持ちはすごいあります…。

自分でこの文章書いてて色々改善点見つけてしまったのでいつか治したいです。まぁ**描画コストも相当ある**のでこういうところだけではないんですけどね…。どっちも作ったの私だが。

おわりです。