はじめに
VRCSDKにはVRChat特有の機能が色々あり、UdonSharpは通常のC#にはない制限事項が色々ある。
結果、適当な指示で何か作らせようとしてもまあ全然うまくいかない。
しかしそれではもったいないし今後の開発モチベーションに悪影響大なので、ちょっと本気出して何か1つ通しで作ってみることにした。
どんな問題が発生しがちで、対策にはどのようなアノテーションが必要なのか?
同期・非同期をわからせることはできるのか?
実際にやってみたので、ここで得られた知見を共有することにする。
ClaudeCode課金勢なのでClaudeCodeのみで、Unity用MCPは前に色々試して全然良くなかったので今回は使っていない。
※追記:この記事は2025/11頃に書きかけてからしばらく放置していました。ので情報がちょっと古いです。寝かせてる間にSkillsとかいう概念が生まれてまた話が変わってきたので、続編記事を書きました。今から参考にするならこっち読んだ方がいいです。↓
作ったもの
これです。
「8番出口」の映画がとても良かったのと、VRChatに8番ライクなゲームワールドがあったら嬉しいなと思ったので、これを実現するための汎用ギミックを作ったら誰か遊んでくれるかもしれないと期待した。
しかしギミックの複雑さに対して売り上げ見込みは小さい。ゲームワールド作りに関心がある人自体少ないし、ホームワールドやイベントワールドといった高需要ワールドでの応用がほぼ不可能である。
普通に作ってたら工数に全然見合わない対価しか得られないだろう。それにワンチャンなんらかの権利に引っ掛かって取り下げになるリスクもあるし……。
しかし、手間をかけずにAIが書いてくれるなら話は変わってくる。
ノウハウ
ここでは実際に使用したPromptを交えつつ、ケース毎の攻略法などを書いていく。
U#スクリプトの新規作成
これは全然ダメだった。手動で準備してあげた方が早い。
U#はコンポーネントとしてはUdonBehaviourを使用するが、実際に動作するスクリプトは別のコンポーネントを作る、みたいなかなり特殊なことをやっており、これをわからせるうまいやり方は見つけられなかった。
どこのGameObjectにどんな機能を持ったコンポーネントを配置するか、という基本設計は人間が考えてやるべきで、そういう意味ではコーディング経験のない素人が完全にVibe Coding……というのはまだ無理っぽい。
ローカルパートの作成
普通にうまくいった。Trigger進入検知でテレポートするとか、その度にワールドの状態を更新するとか。
要するにVRCSDK特有の制限があんまりないパートであれば問題なく作れるようだ。
まあUnityのコードはネット上に豊富にあるので、今更驚くようなことではない。
異変の種類をInspectorに書けるようにして、それが現れたり消えたりする仕組みはもう全部書いてもらった。Vibesだ。
同期パートの作成
はっきり言って、全く役に立たなかった。なんとなくそれっぽいものを書いてはくれるが、よく見ると矛盾だらけで使い物にならない。
とりあえず最初はあんま分析しないで見たままのVibesを伝えてみるか、と思いこんな感じのことを言ってみた。
1人目がJoinした瞬間いきなりゴールから始まってしまいました。何かおかしいようです。
なんとなく直したような返事が来るが、やってみると別の箇所が壊れている。
正常にスタート地点から開始するようになりました。しかしクリア後にJoinした人のテレポートが動作しませんね。
そこはかとなくコードを覗いてみると、ほんの数回のやりとりなのに、もう使ってない変数や誰からも呼ばれないメソッドが登場している。
もうちょっとVibesしようと思っていたが手が滑ってこんなことを言う。
クリアかどうかの判定にIsClearedを使わないなら、この変数自体不要なんじゃない?
これは良くなかった。この応答で一言では説明しようがないほど壊れてしまった。
全体的に、同期変数が書き換わる契機とか、決定論的状態遷移とかいうものが全然わかっていないようだった。
仕方ないので自分で中身を読んで実装方法を提案してみる。この時点でもうあんまりVibesではない。
まだ動いていないですね。私の方でコードを確認しました。
問題は、あとからJoinした人にNetworkRequestTeleportToEscapeのリクエストが届いた時、同期変数が同期完了していることが保証されていない、
つまりSyncedCurrentCorrectStreakCountが正しくないことがあるからですね。
なのですみませんがやっぱり各々のローカルでIsClearedを持っておく必要がありますね。
そしてIsClearedの判定は他の同期変数には依存しない必要があります。
脱出地点にテレポートしたタイミングでtrueにするのがよいと思います。
そしてOwnerがIsClearedの時だけNetworkRequestTeleportToEscapeを送信し、
各々のローカルなIsClearedを見てテレポートを実行するかどうか判定するようにしましょう。
これで大体直ったが、まだJoin直後の挙動に問題があって、また自分で調べなければいけなかった。
この提案には見落としがあって、AIの書いたコードがOnEnableの中で変数の初期値を送信するとかいうしょうもないことをしていたのに気付いていなかった。
ここでもうめんどくさくなって自分で直すことにした。
ダメだったので私の方でログを追加して調査しました。
本当の原因はOnEnableの中で自分にSetOwnerしているからのようです。
最初にJoinした人はOwnerだし、それ以降の人は同期変数の初期化はOnDeserialization()が実行されるまで待つことになるのでこのSetOwnerは不要ですね。
消したら直りました。ありがとうございました。
編集済みのソースコードを確認しておいてください。
結論:同期はまだ早い。
エディタ拡張
エディタ拡張を作らせるのはかなり良かった。いわゆる拡張インスペクタを書かせたのだが、ここはVRCSDKによる制限がほとんど及ばない部分なので本領発揮できていた。
異変ループメーカーでは設定したい異変毎にオンオフするオブジェクトやAnimatorのパラメタなどを設定するが、U#では構造体の配列を扱えないため、それらを全部1次元の配列にバラしてやらないといけない。
そんなもんを人間が目で見て編集するのは無理なので、拡張インスペクタでそれらの配列を構造体の配列に変換してやり、UI上はそれぞれを構造体のように扱い、編集結果はまた配列に変換して格納してやる。
設定できるオブジェクトは可変長なので、それぞれの要素の個数を別途保存しておく必要もある。
理屈としてはそんなに難しくないが、実装は果てしなくめんどくさい……。
以前は何とも思わずに自分で書いていたかもしれないが、AI以後はいちから自分で書くのはもうやる気全然出ない内容だ。
AnomalyLoopMakerなんですが、public struct AnomalyInfoに異変の情報を設定する前提で進めていたのですが、
VRChatのUdonSharpコンパイラではstructが使えませんでした。
そこでこれをstructを使わないような実装に改めてほしいです。
ただし、Inspector上はstructのarrayを編集するという体裁をキープしておきたいです。
つまり、エディタ上はstructの配列として扱いつつ、内部的にはGameObjectまたはプリミティブな型のArrayのみで構成するようにするということです。
UdonSharpコンパイラでは他にもListやDictionaly、Reflectionなどが全て使用できないという点も留意してください。
最初はU#スクリプトの方にコンパイル条件付きの構造体メンバを宣言してそこにシリアライズしようとしてきた。
しかしこれは当然実行時にエラーとなるため、以下のように軌道修正することで解決した。
だめだな。Field of type 'AnomalyInfo[]' does not exist any longer, compile U# scripts then allow Unity to compile assemblies to fix thisが引き続き出ている。
思うに、AnomalyInfosをシリアライズする必要がそもそも無いのでは?
だって全く同じ意味の情報がUdonSharp compatible arraysに既に格納されているのだから。
君がやるべきことは、AnomalyInfoをシリアライズすることではなく、
カスタムインスペクタ側でUdonSharp compatible arraysとAnomalyInfo[]の相互変換メソッドを記述して、
AnomalyLoopMakerのインスタンスにはあくまでUdonSharp compatible arraysのみをシリアライズし、
UI上でだけそれを構造体配列に見せかけることだ。それでどうだろう?
Gizmo
異変ループメーカーにはテレポート機能があり、テレポート先の半径を指定して範囲内でバラつきを持たせることで複数プレイヤー同時テレポート時にプレイヤー同士が重なる問題を軽減している。
この半径を設定しやすくするためにGizmoを追加した。
いいですね。せっかくカスタムインスペクタを用意したので、このテレポート半径をGizmoで可視化しましょう。
ループ地点は赤、脱出地点は青でそれぞれテレポート範囲に円を描いてください。
面のないワイヤーフレームでよいです。
AnimalyLoopMakerが選択されている時だけ表示されるものとします。
しかしこのGizmoは線が細くて全然見えなかったので、何とかしてもらおうとしたのだが、それはあまりうまくいかなかった。
次のステップなのですが、テレポート先地点の範囲を示すGizmoの線が細すぎて見えないことがあるので、もうちょっとだけ太くできませんか?
線を何本も重ねて描画する案を実装してくる。しかしこれは見た目が悪いので却下。
いや、ワイヤーフレームの線を何本も引くのはいいアイデアではないようです。見た目が落書きのようにグチャっとした線になってしまいました。何か他のアイデアはありませんか?
ワイヤーフレームをやめて球にしようとか言ってくる。もちろん却下。
塗り潰しの円盤で、かつ半透明にすることはできますか? 球は実際の効果を反映していないのでやめておきましょう。
しかしこれは結局ワイヤーフレームの中に補助線を引いただけで、全然ダメだった。
ここで諦めて自分でぐぐった結果、DrawLineをやめてHandles.DrawBezierを使うというハックを発見。これをそそのかしたところ、やっとうまくいった。
うーん、全然だめですね。見た目がよくないです。
当初のワイヤーフレーム円に戻して、DrawLineの代わりにHandles.DrawBezierを使えませんか?
こっちはthicknessを調整できたはずです。
リファクタリング
大体形になったところで、ソースコードをざっと眺めてみた。だいぶツギハギ感があり、ゴミも残っている。
そこで以下のように指示してみた。
Assets\Yodokorochanフォルダにある.csファイルを全部チェックして、リファクタリングしてください。特に以下の観点については注意深くお願いします。
・コメントは日本語で書く。
・単に何をしているかしか書いていないようなコメントは消す。コードを一瞥しただけでは意図がわからないような箇所に限ってコメントを残すようにする。
・変数の命名規則を統一する。
・エラー以外のDebug.Logは全部消す。
・メソッド名を文字列で指定している箇所はnameof式に直す。
コメントが日英混在していて統一感がない。どうせ作者しか読まないので日本語にしておいた方がよい。
また、ClaudeCodeの癖として、一度書いたコードにレビュー等で指摘・修正が入ると、変更内容についてコメントを残すというのがある。
これが蓄積してコードとコメントが全然一致してないというケースがありまくるので一掃してほしかった。
しかし、意外にもこれがうまくいかなかった。まずコンパイルエラーが出てしまったので直してもらい、次に改めてコードを目視するとコメントは英語だらけだし、変数に値を入れるだけの箇所に「××に○○の値をセット」みたいなしょうもない内容がいっぱいある。
仕方ないので以下の指示を改めて出すことで日英混在の問題だけ対処した。
英語コメントがまだ大量に残っていますね。UdonSharpとかNetwork Eventみたいな固有名詞は英字のままでよいですが、文章は日本語にしてください。
で、最初の指示でその辺ばかり注記したせいか、構造的なリファクタリングが全然されていなかった。
全く同じ処理が同期モード毎に別メソッドとして用意されていたりしてよくない。
そこで以下の指示を出してみた。
確認しました。いい感じです。次は構造的なリファクタリングに注力してください。冗長な処理はモジュール化し、ソースコード全体の見通しを良くしましょう。また制作中の試行錯誤の過程で不要になった処理や、命名が現在の実情を反映していない変数名やメソッド名がないかも重要観点としてください。無ければ無いが結論でもよいのでとにかくソースコードの品質向上をお願いします。
これは結構効いた。と思う。少なくとも内容丸被りメソッドみたいな分かりやすい箇所はゴッソリ修正された。
デグレも特に起きなかったので、これで最終的なコードとした。
人間の目でちゃんとレビューしたらまだまだ問題があるのかもしれないが、今回はさっさと作ってさっさと出すのが目的なので気にしないことにした。動けばよかろうなのだ。
シェーダー
U#からは少し話がそれるが、フェード処理のために視界ジャックシェーダーが必要だったのでこれも書いてもらうことにした。
視界ジャックの仕組み自体は以前ChatGPTに作らせて全然ダメだったという結果がある。
ので今回はもっと多機能なやつをこちらで用意して、それを改変してもらうという方式で作った。
ありがとうございます。ちょっと話がそれるのですが、このフェードに使用するはずだったシェーダーが諸事情で使えなくなったので別途Assets/Yodokorochan/AnomalyLoopMaker/Shaders/FadeOverlay.shaderを持ってきました。
これはフェード機能の他にマスクとかコントラストとか要らない機能が入ってるので、_BlackOutの部分を_Fadeに置き換えて、他の機能を全部削除してください。
こういう改変はうまくやってくれた。ただ元の設計を尊重してくれるので、わざとやめさせたい時は明示的に指示してやる必要がある。
素晴らしいです。ソースを見て気になったのですが、GrabPassを使う必要ももう無いのではないでしょうか?背景を歪ませたりする処理も消してしまったので。GrabPassを使う代わりに、単純な透明度制御のみでフェードを表現できませんか?
SPS-I対応も以下の指示で一発でやってくれた。
期待動作しました。いい感じです。このシェーダーをSPS-I(Single Pass Stereo-Instancing)対応にすることはできますか?
以下に対応例となるサンプルを示します。
https://raw.githubusercontent.com/lilxyzw/Shader-MEMO/refs/heads/main/Assets/SPSITest.shader
まとめ
結局、多くの人が言うように、完全なVibesだけで完成させるのはまだ無理ですね。
webアプリなんかみたいにAI自身がテストサイクルを回せるようになればもっと任せられるのだろうけど、Unityはそれができないので短いサイクルでやり取りを繰り返すことになる。
それにVRCSDK特有の制限や仕様は全然わかってくれない。ネット上に情報が少ないので当然なのだけど……。
不具合が出た時に、それをVibesではなくエンジニアの言葉で伝える必要がある。つまり、作業者がエンジニアの言葉を使えなければならない。
誰もが気軽にギミック作成!ってなるのはもーちょっと先の話っぽい。