はじめに
この記事は以下の記事の続きです。
前回、Claude Sonnet4.5にUdonSharpを書かせるということをした。
VRCSDKの作法は十分に学習されておらず、あんまりうまくいかなかったと結論した。
その後になってSkillsという概念が生まれ、プロジェクト特有の事情や作法を予め教えておくことが簡単にできるようになった。
加えて、Antigravityの登場により誰でも簡単にPlanモードの恩恵に与れるようになった。
Claude Codeはコマンドプロンプト上で動作するので、履歴が読みにくい、長文のペーストに失敗する、などの細かい不満がたくさんあったが、Antigravityはその辺を全部解決してくれたので使わない手がない。
そういうわけで、この2点を活用して再度ギミック作りをしてみることにした。
環境構築
普通のVRCSDK環境に、これを追加するだけ。
https://github.com/niaka3dayo/agent-skills-vrc-udon
作者のNiAkaちゃんに、公開前のプレビュー版を提供してもらった。
なので開発初期の挙動は現行バージョンと異なるのでご了承いただきたい。
ファイルを配置したらAntigravityでプロジェクトフォルダを選択して準備OK。
作ったもの
これです。
ちょっと前にオリジナル版にドハマりして5000レベルほどやり込んでいたのだが、あまりにも時間泥棒なので封印していた。
オリジナル版は水を注ぐのが遅くて待ちが発生する、難易度を選べない、ブラインドモード的な運ゲーが挿入され回避できない、などの不満があり、この辺を解決したやつを遊びたかった。
それにVRで実際に瓶を手に持って動かしながら遊べたら楽しいだろうと思って題材にした。
超かわよくてちょっと怪レいPVも撮ったので見てね。
Skillsの内容
使われるSkillsはunity-vrc-world-sdk-3とunity-vrchat-udon-sharpの2つに分けられている。
内容は概ね以下の通りだ。
| Skills | 内容 |
|---|---|
| unity-vrc-world-sdk-3 | ・VRCSDKの独自コンポーネントの説明 ・レイヤーやCollistionMatrixなどのUnity側環境セットアップ内容 |
| unity-vrchat-udon-sharp | ・UdonSharpの独自仕様や制約事項の説明 ・同期/非同期の仕様とベストプラクティス |
検証記録
ここでは実際に使用したPromptを交えつつ、ケース毎に試したことなどを書いていく。
基本方針としては、なるべく「UnityやUdonについて何も知らない人」を演じてなんでもAI側に決めさせるようにし、うまくいかなかった場合にのみ救済を試みる。
自分でコードを編集することが一切ないまま完成までもっていくことを目標とする。
作成開始
一番最初に入力したpromptはこんな感じ。
ウォーターソートパズルってあるじゃないですか。
カラフルな液体が瓶に入ってて、同じ色の液体をマージしていくやつ。
あれのVRChatバージョンを作りたいんですよね。
3Dモデルの瓶があって、Pickupで掴んで注ぐことができるやつ。
なのでまずは要件定義から始めていただけますか。
実現方法を考えて、決めが必要なことをリストアップして私に訊いてください。
応答を見ると、真っ先にSkillsを読みに行ってくれている。いい感じだ。
要件定義パートの全文
### Planner Response
なるほど、VRChatでウォーターソートパズルを作るんですね!面白いプロジェクトです。
まず、技術的な実現方法を整理して、決めるべき事項をリストアップしますね。
## 🎯 VRChat ウォーターソートパズルの実現方法
### 核心的な技術要素
1. **瓶オブジェクト (VRC_Pickup + UdonSharp)**
- 瓶を掴んで傾けることができる
- 傾き角度に応じて液体が注がれる演出
2. **液体の表現方法**
- シェーダーベース(液体の高さをシェーダーで表現)
- メッシュベース(液体を3Dモデルで表現)
- パーティクルベース(流体シミュレーション)
3. **液体の移動ロジック**
- 瓶Aから瓶Bへの液体移動判定
- 色のマージ処理
- ゲームクリア判定
4. **ネットワーク同期**
- マルチプレイヤー対応の有無
- 液体の状態同期
---
## ❓ 決めていただきたいこと
以下の質問にお答えいただけますか?
### 1. **ゲームの基本仕様**
- **瓶の数**: 何本の瓶を使いますか?(例: 5本、10本)
- **液体の色数**: 何色の液体を使いますか?(例: 4色、6色)
- **瓶の容量**: 1本の瓶に何層まで液体を入れられますか?(例: 4層)
### 2. **液体の表現方法**
以下のどれが希望に近いですか?
- **A. シェーダーベース** (軽量、見た目は平面的)
- 瓶の中に色付きの平面が浮いているように見える
- パフォーマンス◎、実装難易度△
- **B. メッシュベース** (立体的、リアル)
- 液体が3Dモデルとして存在
- パフォーマンス△、実装難易度○
- **C. パーティクルベース** (超リアル、重い)
- 実際に液体が流れる演出
- パフォーマンス×、実装難易度×
### 3. **注ぐ操作方法**
- **A. 自動判定**: 瓶を掴んで別の瓶に近づけると自動で注がれる
- **B. 手動傾け**: 瓶を実際に傾けて、角度に応じて注がれる
- **C. ボタン操作**: Useボタンを押すと注がれる
### 4. **マルチプレイヤー対応**
- **シングルプレイ**: 各プレイヤーが独立してパズルを解く
- **協力プレイ**: 複数人で同じパズルを共有して解く
- **対戦プレイ**: 誰が早くクリアできるか競う
### 5. **難易度設定**
- **固定**: 1つの難易度のみ
- **選択式**: Easy/Normal/Hardなど選べる
- **ランダム生成**: 毎回異なる配置
### 6. **UI/フィードバック**
- **3D UI**: ワールド内に配置されたボタンやパネル
- **VR UI**: 手元に表示されるメニュー
- **音声/エフェクト**: 液体が注がれる音、クリア時のエフェクトなど
---
## 💡 私からの推奨案
初回実装としては以下をお勧めします:
- **液体表現**: シェーダーベース(軽量で実装しやすい)
- **操作方法**: 自動判定(VRで直感的)
- **マルチプレイ**: シングルプレイ(同期の複雑さを回避)
- **仕様**: 瓶5本、色4種、容量4層(スマホゲームの標準的な難易度)
この方向性で良ければ、すぐに要件定義を開始できます!
どの項目についてもご希望があれば教えてください 🙌
### User Input
瓶の数は柔軟に変更できるようにしたいですね。
段階的な難易度を設定して、いくつかの本数から選べるようにしたいです。
液体の色数についても同様で、実際にプレイしながら微調整したいです。
瓶の容量は一律4層としてよいです。
液体の表現はメッシュベースとするのがよいと思います。
「流れる」必要はないですが、注いでいる間液面が上昇・下降してほしいです。
透明だと表示がバグりやすいので不透明にしてしまいましょう。
色はマテリアルのプロパティで変えるのがよいと思います。オンデマンドに設定するか、色の数だけマテリアルを用意するかはお任せします。
注ぐ操作方法は、瓶の口を他の瓶に近付けて、かつ傾けると注がれる、としたいです。
マルチプレイヤーについては、単に全体の同期がとれていればよいです。
つまり、まさにその場にそのパズルが置いてあって、同じものを見ながら同じものに触れているという体験ができることが肝要です。
それをどのように遊ぶかはプレイヤーに委ねられます。
難易度は先述の通り何通りかから選べるようにし、瓶の本数が変わるようにします。
液体の内容は毎回ランダム生成するようにします。
完全なランダムだと解けない可能性があるので、初期状態からうまいこと逆算して生成する方法を考える必要がありますね。
UIは普通にWorld spaceなCanvasを置くのがよいと思います。
「1手戻る」、「リセット」が欲しいですね。
注がれる音は絶対欲しいです。これは私の方で用意するので再生する仕組みを作ってください。
クリア時には「クリア!」みたいな表示を出したいですね。これは単にGameObjectのActive切り替えでよいと思います。
そうすればパーティクルとかもスクリプトで個別に制御せずに表示できますよね。
### Planner Response
素晴らしい!詳細な仕様をありがとうございます。要件が明確になりましたので、これから要件定義フェーズを開始しますね。
まず、いただいた仕様を整理してから、段階的に要件定義を進めていきます。
*Edited relevant file*
*Edited relevant file*
*Edited relevant file*
### User Input
コメントしました。確認お願いします。
### Planner Response
コメントありがとうございます!フィードバックを反映して要件定義を更新しますね。
*Edited relevant file*
*Edited relevant file*
*Edited relevant file*
*Edited relevant file*
*Edited relevant file*
### User Input
ありがとうございます。内容はこれでよいと思います。
技術検証として挙げているのはなにかまだ不安要素があるということでしょうか?
私としては実装計画に進めてもよいと思っています。
詳細設計の図は、あなたが必要だと思うなら作ってもよいですが、私自身は必要ないです。
### Planner Response
了解しました!技術検証について補足しますね。
技術検証として挙げたのは、以下の点に**若干の不確実性**があるためです:
## 技術検証で確認したかった点
1. **メッシュの動的スケール変更のパフォーマンス**
- 10本の瓶 × 4層 = 40個のメッシュを毎フレーム更新する負荷
- ただし、実際には注ぐ瞬間だけなので問題ない可能性が高い
2. **ランダムパズル生成の解決可能性保証**
- 逆算アルゴリズムで本当に解けるパズルが生成できるか
- ただし、理論的には問題ないはず
3. **UdonSharpでの配列操作の制約**
- 多次元配列の扱いやリングバッファ実装
- ただし、基本的な配列操作なので問題ないはず
## 結論
おっしゃる通り、**実装計画に進んで問題ない**と思います。技術的に大きなリスクはなく、実装しながら調整できる範囲です。
詳細設計の図も、実装を進めながら必要に応じてコード内のコメントで補足する形で十分だと思います。
それでは**実装計画の策定**に進みますね!タスク分解と実装順序を決めていきます。
*Edited relevant file*
*Edited relevant file*
### User Input
すみません、1つ伝え忘れていました。
「WaterSort」の語は商標なので使わないでください。BottleSortと命名します。
今回の成果物は全て`Assets/Yodokorochan/BottleSortPuzzle`以下に配置してください。
クラス等も`Yodokorochan.BottleSort`名前空間に入れてください。
WaterBottleの語はドキュメントに残さないように注意してください。
### Planner Response
了解しました!商標の問題、重要な指摘ありがとうございます。すぐに修正します。
**変更内容**:
- `WaterSort` → `BottleSort`
- `WaterBottle` → `Bottle`
- 配置先: `Assets/Yodokorochan/BottleSortPuzzle/`
- 名前空間: `Yodokorochan.BottleSort`
すべてのドキュメントを更新しますね。
出力された要件定義書
VRChat ボトルソートパズル - 要件定義書
1. プロジェクト概要
VRChatワールド内で遊べるボトルソートパズルゲームを実装する。
プレイヤーはVRC_Pickupで瓶を掴み、傾けて液体を注ぎ、同じ色の液体を1つの瓶にまとめることを目指す。
プロジェクト配置: Assets/Yodokorochan/BottleSortPuzzle/
名前空間: Yodokorochan.BottleSort
2. ゲーム仕様
2.1 基本ルール
- 複数の瓶に異なる色の液体が層状に入っている
- 瓶を掴んで傾け、他の瓶に液体を注ぐことができる
- 同じ色の液体同士がマージされる
- すべての色が単色の瓶に分離されたらクリア
2.2 ゲームパラメータ
| パラメータ | 仕様 | 備考 |
|---|---|---|
| 瓶の本数 | 可変 (難易度選択) | 例: Easy=5本, Normal=7本, Hard=10本 |
| 液体の色数 | 可変 (難易度選択) | 瓶の本数に応じて調整可能 |
| 瓶の容量 | 固定4層 | すべての瓶で統一 |
| 初期配置 | ランダム生成 | 解決可能性を保証する逆算アルゴリズム |
2.3 難易度設定
プレイヤーは以下から選択可能:
- Easy: 瓶5本、色3種
- Normal: 瓶7本、色4種
- Hard: 瓶10本、色6種
[!IMPORTANT]
プレイヤーにはプリセット選択のみ提示。瓶数・色数の調整は**設計段階(Inspector)**でのみ可能とし、ランタイムでのカスタム設定UIは不要。
3. 技術仕様
3.1 液体の表現方法
メッシュベース を採用:
- 液体は不透明な3Dメッシュとして表現
- 注ぐ動作中、液面が上昇・下降するアニメーション
- 色はマテリアルのプロパティで制御
マテリアル管理方式
以下の2つの方式を検討:
| 方式 | メリット | デメリット |
|---|---|---|
| A. オンデマンド生成 | メモリ効率◎ | 実行時生成コスト |
| B. 事前準備 | パフォーマンス◎ | 色数分のマテリアル必要 |
推奨: 色数が限定的(10色以下)なら B. 事前準備 が安定
3.2 液体メッシュの動的制御
各瓶の液体状態:
- Layer 1 (底): Color A
- Layer 2: Color B
- Layer 3: Color C
- Layer 4 (最上層): Color D
メッシュ生成:
- 各層ごとに個別のメッシュ (Cylinder形状)
- Y軸スケールで高さ調整 (0.0 ~ 1.0)
- Y軸位置で層の積み重ね表現
3.3 注ぐ操作の判定
以下の条件をすべて満たすと液体が注がれる:
- 瓶Aを掴んでいる (VRC_Pickup.IsHeld)
- 瓶Bに近接している (距離 < 0.3m程度)
- 瓶Aが傾いている (角度 > 45度)
- 瓶Aに液体がある
- 瓶Bに空きがある (層数 < 4)
- 色が一致 (瓶Bの最上層と瓶Aの最上層が同色、または瓶Bが空)
3.4 ランダムパズル生成アルゴリズム
解決可能性を保証するため、逆算方式 を採用:
1. クリア状態を生成
- 色数分の瓶に単色液体を4層ずつ配置
- 残りの瓶は空
2. ランダムに「注ぐ」操作を逆再生
- 有効な移動を N 回繰り返す (N = 難易度係数)
- 移動履歴を記録しない (完全シャッフル)
3. 生成された配置を初期状態とする
[!WARNING]
UdonSharpではList<T>が使えないため、配列ベースの実装が必要
4. ネットワーク同期仕様
4.1 同期方針
完全共有型: すべてのプレイヤーが同じパズルを見て操作する
- 瓶の所有権は掴んだプレイヤーに移動
- 液体の状態は
[UdonSynced]で全プレイヤーに同期 - 複数人が同時に操作可能 (協力/対戦は自由)
4.2 同期データ
[UdonSynced] private int[] bottleColors; // 全瓶の全層の色データ (瓶数 × 4)
[UdonSynced] private int[] bottleLevels; // 各瓶の液体層数
データサイズ試算:
- 瓶10本 × 4層 × 4byte = 160 bytes
- 瓶10本 × 1層数 × 4byte = 40 bytes
- 合計 200 bytes → Manual Sync の制限内 (282KB)
4.3 同期タイミング
- 液体の移動完了時に
RequestSerialization() - Undo/Reset時に
RequestSerialization()
5. UI/UX仕様
5.1 UI構成
World Space Canvas を使用:
[UI Panel]
├── 難易度選択ボタン (Easy / Normal / Hard / Custom)
├── スタートボタン
├── Undoボタン (1手戻る)
├── Resetボタン (初期状態に戻す)
└── クリア表示 (GameObject、デフォルトで非表示)
5.2 カスタム設定UI
[!NOTE]
初期実装では不要。プリセット選択のみで運用し、フィードバックに応じて将来的に検討。
5.3 フィードバック
| イベント | フィードバック |
|---|---|
| 液体を注ぐ | 注ぐ音を再生 (AudioSource) |
| クリア | クリア表示GameObject.SetActive(true) + パーティクル自動再生 |
| 無効な操作 | (オプション) エラー音 |
[!NOTE]
音声ファイル(AudioClip)はユーザー提供。AudioSourceコンポーネントで再生する仕組みを実装。形式はUnityが自動変換するため設計上は考慮不要。
6. クラス設計 (概要)
6.1 主要クラス
BottleSortGameManager (UdonSharpBehaviour)
├── ゲーム全体の管理
├── パズル生成
├── クリア判定
└── UI制御
Bottle (UdonSharpBehaviour)
├── VRC_Pickup制御
├── 傾き検出
├── 近接瓶検出
├── 液体メッシュ制御
└── 注ぐ/受け取る処理
LiquidLayer (MonoBehaviour or データクラス)
├── 色情報
└── メッシュ/マテリアル参照
UIController (UdonSharpBehaviour)
├── ボタンイベント処理
└── クリア表示制御
6.2 データ構造
// 各瓶の状態 (配列で管理)
private int[][] _bottleStates; // [瓶番号][層番号] = 色ID
private int[] _bottleLevels; // [瓶番号] = 現在の層数
// 履歴管理 (Undo用)
private int[][] _historyStates; // 固定長配列でリングバッファ実装
private int _historyIndex;
[!CAUTION]
UdonSharpではList<T>,Stack<T>,Queue<T>が使用不可。配列ベースで実装。
7. パフォーマンス要件
7.1 ターゲット
| プラットフォーム | FPS目標 | 備考 |
|---|---|---|
| PC VR | 45+ FPS | 瓶10本時 |
| PC Desktop | 60+ FPS | 瓶10本時 |
| Quest | 72 FPS | 瓶5本推奨 |
7.2 最適化方針
- メッシュは静的Cylinderを使い回し (Scale/Position制御)
- マテリアルは事前生成 (色数分)
- Update()内での重い処理を避ける
- 不要なコライダーを削減
8. 実装上の制約 (UdonSharp)
8.1 使用不可機能
-
List<T>,Dictionary<T,K>→ 配列で代替 -
async/await→SendCustomEventDelayedSecondsで代替 -
try/catch→ null チェックで防御的プログラミング
8.2 ネットワーク制約
-
[UdonSynced]文字列は最大50文字 - Manual Sync の上限は 282KB
- 同期頻度に注意 (RequestSerialization の呼び出し回数)
9. 未確認事項・要検討事項
9.1 技術的検証が必要な項目
- メッシュの動的スケール変更のパフォーマンス影響
- 10本の瓶 × 4層 × アニメーション時の負荷
- ランダム生成アルゴリズムの解決可能性保証
- Undo機能の履歴保存方法 (配列サイズの決定)
9.2 ユーザー確認事項
[!IMPORTANT]
以下の仕様が確定しました:
- Undo機能の履歴保持数: 基本的に無制限。同期変数サイズから逆算して最大値を決定 (安全マージン考慮)
- カスタム設定UI: 初期実装では不要。プリセット選択のみ
- 音声ファイル: AudioClipとして提供される (形式は設計上考慮不要)
- 瓶のモデル: 作成が必要。階層構造・アンカー位置・メッシュ名を定義する
- クリア演出: PlayOnAwake=trueのパーティクルをクリア表示と同時にActive化 (スクリプト側は特別な制御不要)
10. 次のステップ
要件定義が承認されたら、以下を実施:
-
技術検証 (PoC)
- メッシュベース液体の実装テスト
- ランダム生成アルゴリズムの実装と検証
-
詳細設計
- クラス図の作成
- シーケンス図の作成
- データフロー図の作成
-
実装計画の策定
- タスク分解
- 実装順序の決定
- マイルストーンの設定
付録: 参考情報
A. ウォーターソートパズルのルール
- 同じ色の液体のみ注げる (または空の瓶に注げる)
- 注ぐ先の瓶に空きがある必要がある
- 最上層の液体のみ移動可能
B. VRChat制約
- VRC_Pickupは1人1つまで同時に持てる
- 所有権の移動は自動 (掴んだプレイヤーがOwnerになる)
- ネットワーク遅延を考慮した設計が必要
要件定義書の中に「実装上の制約」なる章が作られ、Skillsの中でも特に実装に関わる箇所が抜粋されている。
そして実装に着手した結果、実際その通りに作ってくれた。ここは良かった。
後はできたものを動かしながら不具合を順番に直していくだけである。
最初にお出しされたものをセットアップしてテストすると、それなりに動作してくれた。シンプルに素晴らしい。
瓶を傾けてくっつけると液体が移動する。色が違うと移動できない。空き瓶には何色でも入れられる。基本ルールがちゃんと動作している!
しかしマルチプレイをテストするとすぐに壊れてしまった。壊れ方はまちまちで、初期化ボタンが効かなくなったり、色がおかしいまま戻らなくなったり、さっきまで同期してたものがローカル動作みたいになったり……。
ここからが本番だ。
不具合:Ownerの取り合い
最初は当初の方針に従い「同期が全然できてない」「同期している時としてない時がある」みたいなありのままを伝えてみたが、見当違いな修正を当てずっぽうでするばかりで全然解決しなかった。
初手からこうなるのは悔しいが、自分でコードを見て原因を推測した。
振る舞い的にOwnerの取り合いっぽいなと思って同期タイミングを追っていったら、案の定それぞれのローカルで全部の判定処理をしていた。やはりこのレベルの同期は早すぎたか……?
迷走した結果を全部revertして、新しいセッションで以下のように提案することで解決を試みた。
BottleSortGameManagerとBottleについてなのですが、
Bottleの近接判定と中身を注ぐ処理が時々プレイヤー間で同期できていない状態になります。
確実な再現方法はまだ発見できていないのですが、現状のコードを見る限り、
BottleのUpdate()の中でOwner判定をせずに中身の移動処理を始めようとしているように見えます。
これだとインスタンス内のプレイヤー全員がそれぞれの見えているタイミングで注ぐ処理を実行しようとしておかしくなると思います。
まずはこの仮説について検証していただけますか。
ここまで線路を引いてやると、ようやく問題を理解してくれたようだった。
修正方針の提案
修正の方向性: 「注ぐ処理はGameManagerのOwnerが行い、他プレイヤーはリクエストを送るだけ」というパターンに変更します。
案A: Bottle.Update() に IsHeld + Owner判定を追加
vrcPickup.IsHeld が true のボトルを持っているプレイヤーだけが注ぎリクエストを送れる設計にします。ただし VRC_Pickup.IsHeld は 持ったプレイヤーのローカルでのみ true になるため、これを利用して実質的に「持っているプレイヤーのみがリクエストを出す」とすることが可能です。
(中略)
案B: GameManagerで注ぎロジックを管理
Bottle は状態変更を一切おこなわず、GameManagerの RequestPour() が変更の確定・同期まで担当するアーキテクチャ。より堅牢ですが変更量が多い。
案Aのほうが変更量が少なく、既存の IsHeld 判定の延長なので実装しやすいと思います。 どちらの方針で進めますか?また「GameManagerのOwnerでなくても注げる」仕様は維持する想定でよいでしょうか?
私の返事:
基本的には案Aがよいと思います。ただ、RequestPourの中では同期変数を編集するはずなので、GameManagerのOwnerに対するNetworkEventにするなどしてOwnerが処理をするようにしてやる必要があると思います。
BottleをPickupした人がGameManagerのOwnerを取るという手もありますが、これは複数人が一斉にBottleを動かそうとした時に混線のリスクが高まるのでやめましょう。
よって「GameManagerのOwnerでなくても注げる」仕様は維持します。Bottleを誰が動かしても注げる必要があります。
これで大体の挙動は正しく同期するようになった。
理解がいい加減だと迷走するが、ちゃんと理解した上で書かせるとちゃんと一発でいけるというのはコンテキストエンジニアリングの重要性を物語っている。
世間一般で正しいとされていることは大体正しいのだ。
OnDeserialization()がLateJoinerのJoin時にも発火するということは考慮していなかったようで、そこはデグレったので以下のように追加で修正指示を出して解決した。
ローカルでのテストはパスしました。いい感じです。
リモートのテストですが、液体の中身は今のところ正しそうですが、1点問題が発生しました。
ワールドにJoinした時、及び新規問題がリモートプレイヤーによって生成されるたびに、注ぐ効果音が再生され、液体も増減のアニメーションが再生されてしまいます。
OnDeserialization契機の遷移の中で、注いだ時と新規作問時でinstant及びSE再生を切り分ける必要があると思います。
不具合:Pickupableの同期ずれ
クリア後に瓶が持てる状態のままだとレイキャストの邪魔をすることがわかったので、Pickupableの制御を追加した。
しかしどこかに考慮漏れがあるようで、Pickupできないままになることがあった。
正確な再現手順がまだ特定できていないのですが、
クリア後に次のゲームを開始しているにもかかわらず、
リモートプレイヤーからBottleがPickupできないままになっていることがあるようです。
Pickupableを設定する契機を全体的に見直していただけますか。
これは一発で治った。
Pickupableの設定タイミングを見直し、以下の点に対応しました。
強制同期(フォールバック)の追加: 以前は「自分が新ゲーム状態を受信した時」かつ「まだローカル側がクリア済みと判定されている時」のみPickupableをオンにするという限定的な条件でしたが、これを「受信のたびに現在のゲーム状態(クリア中か否か)でPickupableを強制上書き」するように変更しました。これにより、リモート側で発生していた非同期ズレが解消されるはずです。
ログの強化: グローバルルールに基づき、それでも再現する場合に備えてログを仕込みました。GameManagerがPickupableを一括変更した契機や、各Bottleのプロパティが変更された時に詳細なログを出力します。
まずはこの状態でリモートプレイヤーと一緒にテストしてみてください。もしも再発した場合は、プレイヤーのログファイル(Client Log)をご確認いただき、「[Yodo][BottleSortGameManager] SetAllBottlesPickupable」や「[Yodo][Bottle] Bottle (番号) pickupable changed」といった出力の周辺ログをご提示いただければ、確実な原因特定が可能になります。
こういうのでいいんだよ、こういうので。
VRChatにおける同期のやり方をよくわかっている直し方である。
しかしどういうわけか、このセッションではSkillは別に読んでないようだった。なんでだよ!w
他にもUI関連の同期漏れがいくらかあったが、指摘すれば一発で直ることが多かった。
不具合:VRCObjectSyncとVRCConstraint
ギミック全体をPickupで持ち運びできるようにしたら、各瓶のObjectSyncと競合して位置がハチャメチャにズレてしまう不具合が発生した。
ObjectSyncのネストはあまりよろしくないようだ。
対策として子のObjectSyncはConstraintで拘束するとよいと教わったのでそのように修正することにした。
BottleSortPuzzlePickupでやっているBottleの位置リセット処理なのですが、VRChat側の不具合でリセット後にVRCObjectSyncが位置を動かしてしまうことがあるようです。
これに対策するため、現状のリセット処理に加えて、ParentConstraintを使ってより強制力の強い位置制御をしたいと思います。
BottleのPickupと同じGameObjectにParentConstraintを付けて、sourceに初期位置を設定しておきました。
Udon側では、このConstraintの参照を予めInspectorから持っておき、Pickup時に解除、Drop時に再有効化するようにしていただけますか。リモートプレイヤーでも同様の処理が行われるようにしてください。
私ははっきりParentConstraintと書いたし、実際にシーン上でそのようにセットアップしてから指示したのだが、Skillを読んだAIはこれをVRCParentConstraintだと解釈した。
VRC Constraintの使い方が確認できました。
`VRCParentConstraint`を使って`IsActive`プロパティで有効/無効化します。
ネットワークイベント(`SendCustomNetworkEvent`)でリモートプレイヤーにも同じ処理を適用します。
これはちょっと頭にきた。言う通りにしなさいよ。
しかしVRCConstraintがワールドSDKでも使えるようになったのを完全に忘れていたし、そっちの方が動作軽量な場面が多いと噂されているので従うことにした。
Constraintの設定全部やり直し。やはり人間が先走るべきではないな……。
不具合:シード値の同期をしていない
問題生成時に乱数のシードを決めて、問題生成の再現ができるようにした。
テストプレイでのフィードバックを受けて追加することにした新機能である。
それはすんなりやってくれたのだが、値が同期していないので同期処理の作り込みをさせた。
ここまでに「Skillを読んでくれない」という現象に何度も遭遇していたので、最初からそれを明示的にpromptに取り込むということを試みた。
シード値が同期されていないようなので同期するようにしてほしい。
同期のタイミングはそのシード値で作問が実行された時とする。
入力だけしてまだ開始ボタンを押していない段階では同期していなくてよい。
Udonの同期関連のSkillを読み込んでから作業すること。
これはかなりうまくいった。ベストプラクティス通りの実装計画が出てきたので承認し、そのまま期待動作したので採用とした。
実装方針をまとめました。
作問実行時に_currentSeedの値を[UdonSynced]の_syncedSeedに格納し、
他プレイヤーはOnDeserializationでこれを受け取った際にローカルのシード値とUIを更新するようにします。
自動で実装に進みますか?
不具合:リセットボタンの挙動
初期実装ではUndo履歴を200手くらい残していたので、リセットボタンの内容も「もっとも古い履歴の状態に戻す」という風になっていたらしい。
後からUndoを5手までに制限する仕様変更をしたら、リセットボタンを押しても5手前に戻るだけという不具合が起きた。
これを修正しようとして以下のように指示した。
ゲームが進んでいる状態でUIController.OnResetButtonClickedを押しても正しくリセットされずゲームの途中状態になってしまう不具合が出ています。
Undo機能改修時のデグレかと思いますので修正お願いします。
これに対する応答は、「初期状態を非同期の変数に保存しておき、リセット時はそれを取り出す」というものだった。
しかしこれは誤りだ。初期化時にはいなかったLate Joinerがリセットボタンを押したら、初期状態を知らないままなのでおかしくなる。
同期しているシード値に基づいて再生成するように指示したら直った。
応答の詳細を見ると、そもそもSkillsを読みに行っていないようだった。
またかい。
総評
ここで挙げた以外にも細かい修正や機能追加をたくさん実施したが、各セッションの流れは大体以上のパターンの類似になったためこのくらいにしておく。
当初の目標「自分でコードを編集することが一切ないまま完成までもっていく」は達成できた。これは本当にすごいことだ。
なんかコメントはごっちゃりしているし、使ってないプロパティはこちらから明示的にリファクタリングを指示しなければならなかったが、結局は動けばよかろうなので十分な成果だと思う。
Skillなしの時よりもはるかに複雑なギミックを人間による手直しなしで完成までこぎつけたのは個人的にエポックメイキングな出来事になった。
とはいえ……「なるべく"UnityやUdonについて何も知らない人"を演じてなんでもAI側に決めさせる」は、結局ほとんど達成できなかった。自分でコードを読んで不具合の原因を調べることが何度もあったし、そうしなければ永遠に迷走しそうだった。
しかし、明示的に意図を伝えた上での成果物の質はSkillsがあることによってかなり向上した。
前回はベストプラクティスのロジックを全部説明しなければいけなかったが、今回はもう一段抽象的な指示でなんとかなった。
相変わらずエンジニア目線の言葉で線路を敷いてやらねばならないという点は変わっていないが、求められる線路のクオリティはかなり下がった。
Unity用の新しいMCPがちょいちょい出ており、Claude Sonnetも4.5から4.6になったので、そろそろまた試してみてもいいかもしれない。
今後の課題
○ファイルの新規作成が不完全
通常コーディングといえばコードを書いたファイルを出力して終わりだが、UdonSharpの場合はUdonProgramAssetという中間ファイルをセットで生成してやる必要があり、ここがまだ自動化できていない。
空のテンプレみたいなものを用意してそれを出力するSkillでも作ればいいのだが、内部的にはソースコードのファイルのGUIDが必要になるので結局はMCPなどでうまいことUnityエディタ自身に働きかけられる仕組みが必要になってしまう。
これを知らないと「作ったスクリプトの使い方がわからない!」となってしまうので、これは別途Skillとして分離して、ユーザーに新規作成の操作を促すような設計にした方がいいかもしれない。
※2026/03/17 追記
こういうのがあるらしい:
https://gist.github.com/nemurigi/dea7c0a1fb94f7b9cf1c36481a459ded
新規ソースコードのインポート契機でUdonProgramAssetを自動生成してくれるスクリプトであるとのこと。
これを配置するだけのSkillがあってもいいかもしれない。
手動で配置するのとどっちが早いかという感じだが……。
○そもそもSkillを読みに行かないことが多い
セッションの最初のpromptでVRChatとかギミックとかのトリガーワードをうまいこと含めることができると読みに行ってくれるが、作りかけのギミックの修正を別セッションで始めるとそうしたワードが含まれないので読みに行ってくれない。
最初にコードを確認する段階でUdonSharpBehaviourの継承クラスであることが分かっているのだから、その後で読みに行ってくれてもいいと思うのに、なぜかしてくれない。
この問題はSkillsを提供してくれたNiAkaちゃんも認識していてdescriptionの試行錯誤をしてくれたのだが、結局まだ抜本的には解決していない。
誰かうまい書き方知ってたら教えてください。
○「公式ドキュメントを見ろ」は公式ドキュメント自体が微妙だとよくない
生成したコードで「The type or namespace name 'NetworkCallable' could not be found」のエラーが出た。
using文が無いだけなのでエラーをコピペして足してもらおうとしたが、迷走して定義が存在しないとか言い出して代替実装を始めようとした。
存在しないわけがないので結局Abortしてこれを使えと指示することで解決した。
Skillにどのように書いてあったか確認すると、NetworkCallableアトリビュートの説明はあるが、それがどこの名前空間に存在しているかの記載がない。この記載内容は公式ドキュメントをそのまま写してきたものだ。やれやれ。
今時のIDEは書いている途中でusingの抜けをサジェストしてくれたりするので自分の手で書いている時はさほど問題にならないが、AIにサジェストしてくれる人はいないのでこうなってしまったようだ。
今回のケースはたとえばSkill側の記載を手直しすることで対策できるかもしれないが、URLから公式ドキュメントを読ませに行くみたいなことをしているとちょっと面倒なことになるかもしれない。
まとめ
SkillsはUdonSharp環境でも結構有用っぽい。どんどん使おう。
でもまだ完璧って程ではない。寝てたら完成してくれる世界まではもーちょっとかかりそう。
あと本件についてSkillsを作っている側目線でのレポート記事が出てます。よかったら見てみてね。
ところでアセットの発売後らへんでAntigravityの改悪が入り、Proプランでもクオータが少なすぎてろくに開発できなくなってしまった。
モデルをGemini系に切り替えればまだ使えるが、コーディング能力はカスなので今から始めるならClaude Codeにするのがいいかも。
Claude Codeはそれはそれで扱いにくい部分があるのだが(通知が来ないので自分でHookを書かなきゃいけないとか)、しかし使えないことには始められないので仕方なく習熟を試みている。
VSCodeがどんどん嫌いになる毎日。せめてVS2022版プラグインがまともならなぁ……。
おまけ
Skillの話ではなくなるのでおまけ扱いにするが、このパズルの作問アルゴリズムをAIに書かせたらとんでもなく沼ったのでちょっとだけ言及しておく。
最初は完全ランダム配置にしたのだが「詰み状態」から始まってしまうケースが頻発したので、きちんと最終状態から逆算するように指示した。
最初はこんな感じのpromptでやらせた。
これはいわゆるウォーターソートパズルをVRChatでプレイできるようにしたものです。
で、現状1点問題があります。作問のアルゴリズムが仮のものなのです。
今は最終状態から何手かランダムに遡ってそれを初期状態とするという方法でシャッフルして作問しているのですが、いくつか問題があります。
・十分にシャッフルされないことがある。最初から完成してしまっているボトルがあることがある。
・解決不可能な問題が生成されることがある。おそらく1手遡る部分の判定に何らかのバグがある。
・初期状態で瓶に入っている液体の量、位置がランダムすぎる。
これらを解決するために、仮ではない本番アルゴリズムを今から考えようと思います。
要件を改めて書き出すと、以下の通りです。
・初期状態は「色と同じ数の瓶」+「空き瓶×2」とする。
・液体の入った全ての瓶が、少なくとも2色以上で構成されている。
・解決可能性が保証されている。つまり、パズルとして解法が必ず存在する。
・ランタイムで生成してもストレスにならない程度の処理時間で生成可能であること。有限時間内に生成完了またはエラー検出して終了することが保証されているアルゴリズムとする。
・論理的にパズルが不可能なケース(たとえば、2色+2瓶では入れ替えができないので解けない)については運用で回避するものとし、考慮しなくても良いものとする。
これについて実現可能なアルゴリズムが作成可能であるか検証して、内容を提案してください。 現状のアルゴリズムは仮のものなので考慮する必要はなく、入出力のインターフェース定義のためだけに参照するものとします。あらゆるアプローチを検討してください。
ウォーターソートパズルの基本ルールについても改めて記載しておきます。
・複数の瓶の中に色のついた液体が入っている。
・液体は最大4層まで重ねることができる。
・瓶の上層に空きがある限り、そこに同じ色の液体を注ぐことができる。
・異なる色の液体の上には新たに注ぐことはできない。
・空の瓶には何色の液体であっても注ぐことができる。
・全ての液体は4層分の量があり、手順を繰り返すことで色をソートしていき、単色で瓶を4層満たすことができる。
・全ての色の液体が単色で瓶を満たした状態になったらクリア。
これでも体感かなりの文章量なのでちょっとやりすぎかなと思ったが、全然ダメだった。
色々な間違いを次々お出ししてくる。
- 「ランダムに生成して、それの解法をBFS探索して解けた問題だけを出すことで解の存在を保証する」という方法を使おうとする。ランタイムでやることじゃない
- 実際のプレイ時と同じルールで液体を移動してシャッフルしようとする。最終状態から始めたら、ただただ満杯の瓶を空き瓶に移し替えるだけの虚無シャッフルになる
- 「どんな手なら合法手の逆になるか」が理解できていない。できる手をできないと考えて迷走する
全体的に、ゲームルールからシャッフルルールを導くということが全くできないようだった。
モデルやpromptを変えて何度も試行したが、おおむね全滅という感じの結果になった。
仕方ないのでこちらから「こういうアルゴリズムでシャッフルしてください」と全部説明することでようやくまとまった。
超長い最終prompt
これはいわゆるウォーターソートパズルをVRChatでプレイできるようにしたものです。
ウォーターソートパズルの基本ルールについてまず改めて記載しておきます。
・複数の瓶の中に色のついた液体が入っている。
・液体は最大4層まで重ねることができる。
・瓶の上層に空きがある限り、そこに同じ色の液体を注ぐことができる。
・異なる色の液体の上には新たに注ぐことはできない。
・空の瓶には何色の液体であっても注ぐことができる。
・全ての液体は4層分の量があり、手順を繰り返すことで色をソートしていき、単色で瓶を4層満たすことができる。
・全ての色の液体が単色で瓶を満たした状態になったらクリア。
で、現状1点問題があります。作問のアルゴリズムが仮のものなのです。
今は最終状態から何手かランダムに遡ってそれを初期状態とするという方法でシャッフルして作問しているのですが、いくつか問題があります。
・十分にシャッフルされないことがある。最初から完成してしまっているボトルがあることがある。
・解決不可能な問題が生成されることがある。おそらく1手遡る部分の判定に何らかのバグがある。
・初期状態で瓶に入っている液体の量、位置がランダムすぎる。
これらを解決するために、仮ではない本番アルゴリズムを考えます。
要件を改めて書き出すと、以下の通りです。
・初期状態は「色と同じ数の瓶」+「空き瓶×2」とする。
・液体の入った全ての瓶が、少なくとも2色以上で構成されている。
・解決可能性が保証されている。つまり、パズルとして解法が必ず存在する。
・ランタイムで生成してもストレスにならない程度の処理時間で生成可能であること。有限時間内に生成完了またはエラー検出して終了することが保証されているアルゴリズムとする。
私の考える基本的なアイデアは以下の通りです。
まず初期状態を作ります。完成状態、単色4層の瓶を色の数だけ用意し、残りの2瓶を空の状態にします。
そこから、シャッフル・ループに入ります。
1手分のシャッフルは、解法を1手遡ることに相当します。
つまり、まず、以下の条件の瓶を探します:「上層から数えて、2層以上同じ色が続いている」
この瓶の上層から、続いている層の数未満の層を取り出すことを考えます。
たとえば2層あったら、1層取り出せます。
3層あったら、1層または2層取り出せます。
言い換えると、一塊の層を2つに分けるようなイメージです。
分けるということは、その取り出した層がどこの瓶にあるにせよ、その一手を順方向に進めれば元に戻せるということです。
層の上に同じ色の層を注ぐということは、2つの瓶に分かれていたものが一つにまとまり一塊の層にマージされるということですから、これは正しいですよね。
例外は最後の1層を移す場合で、元の瓶が空き瓶になる=なんでも入れられるので、1層だけ取り出して別の瓶に移し替えても合法手を遡ったことになります。
これでどこの瓶から何層取り出すかを決めます。
ランダムに瓶を選択し、条件に合うか検証し、合えば何層取り出すかをまたランダムに決定し、次のステップに進む、とするのがよいと思います。
そういうわけで、どこの瓶から何層取り出すかを決めたら、次にどの瓶に戻すかを決定します。
以下の条件の瓶に戻すことができます:「戻したい層の数より多い空きがあり、かつ最上層の色が戻したい層の色と異なる」
たとえば赤1層を戻せるのは、空き瓶か、最上層の色が赤以外で、かつ1層以上の空きがある瓶です。
青2層を戻せるのは、空き瓶か、最上層の色が青以外で、かつ2層以上の空きがある瓶です。
瓶を無作為に選出し、条件に合う瓶が見つかったら実際に一手戻すことを適用します。 これで1手遡ることができました。
これを何度も繰り返すことでシャッフルが進行します。
繰り返す回数は、色の数に依存します。
なぜなら、1つの層を分割できるのは最大4回であり、それ以上は2層以上ある瓶が存在しなくなり探索する意味がないからです。
2色であれば2×4=8回以上のシャッフルは無意味になります。
よってそれくらいの回数ループすれば十分でしょう。
ここで問題が残ります。この方法だとシャッフル完了後は各瓶に液体の量がおよそ均一に分布してしまっており、「初期状態は「色と同じ数の瓶」+「空き瓶×2」とする」が満たせなくなります。
このループの前後に何かしらもうワンアクション入れて、出来上がったパズルの初期状態が「色と同じ数の瓶」+「空き瓶×2」となるように修正しなければなりません。
そこで、シャッフル中に予め「空にする予定の瓶」を2つ決めておき、その瓶には液体を注がないようにします。
その2つの瓶には2層だけ最後まで液体を残しておき、シャッフル終了後に残った液体を他の瓶の空いている上層に分配します。
2色ありますので、空いている上層が一方の色だったらもう一方を先に入れる、という方法で「戻したい層の数より多い空きがあり、かつ最上層の色が戻したい層の色と異なる」の条件を満たすことができます。
この最後の分配は、その2色2層を順次空き瓶に入れるという手順が可能ですので合法手を遡ったということが保証されます。
これで全ての要件を満たすシャッフルができました。
このアルゴリズムをあなたに実際のコードとして書き下していただきたいのです。
まず先に、アルゴリズムの妥当性の検証をするエディタ拡張を用意していただけますか。
指定回数生成して結果を検証し、問題があった場合はその詳細と、全体の統計を表示するようなものです。
結果はあなたにコピペでお伝えできるようにテキストエリアに出力してください。
アルゴリズム部分のメソッドはそのままBottleSortGameManagerに移植できるように、なるべくUdonの制約に則ったものとなるようにしてください。
移植しないエディタ拡張特有の部分はその限りではありません。
このアルゴリズムでは最終状態から1手ずつ遡って生成しますので、BFSなどの探索は検証には必要ないはずです。
単に1手遡る度にその手順を記録しておき、検証ではそれを順方向に再生して全てが合法手かどうか、そして最終状態がパズルが解かれた状態に戻ったかを検討するだけです。
あと、検証中にメインスレッドに張り付いているとUnityがフリーズしたようになってよくないので、適度にUnity側に処理を返すようにしてほしいです。
プログレスバーもあるとなお良いですね。
他に不明点があれば随時私に確認してください。
以上、お願いできますでしょうか。
これをきちんと言語化できない人間にはまだこのゲームをAIに作らせられないということである。
めんどくさくて死ぬかと思った。はやくシンギュラリティ来てくれ。
ちょうどSonnet4.6が出たタイミングだったのでそれにも助けられた。4.6はマジで賢い。
実際にはこのアルゴリズムではまだ十分に混ざらないケースが多くなるなどの問題があり、細かい修正を加えている。
最終的にどうなったか知りたい方は是非製品版をお買い求めください。(宣伝)



