scenario-unity-architecture について
ゲームプレイプログラマーやアーキテクト向けの Unity スキルで、複数の Play セッション、オブジェクト破棄、セーブ、リファクタリング、マージに耐える設計を扱います。Unity 6.3 向けに、検証済みの構成、スクリプト、ジョブ、品質ゲートを備えています。
主な機能
- 01テスト可能な Core・Runtime・Editor・Tests 構成
- 02ScriptableObject の定義とプレーンな C# 状態
- 03移行チェック付きバージョン管理 JSON セーブ
- 04非同期ライフサイクルと静的状態の管理
- 05入力リバインドとシリアライズデータの監査
活用シーン
- 01Unity ゲームプレイプロジェクトの土台作成
- 02プレイヤー再起動後のセーブ検証
- 03オブジェクト破棄後の非同期処理の残留防止
- 04入力リバインドとアセット変更の検証
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ scenario-labs/ skills --skill scenario-unity-architectureインストール後、Claude Code で/scenario-unity-architectureを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/critique.mdexpert-notes.mdgui-paths.mdprocedures.mdsources.mdscripts/AgentKit/Architecture/ArchInputAudit.csArchJobs.csArchPlayMode.csscripts/templates/Core/Game.Core.asmdefHealthModel.csHealthStats.csInventory.csSaveData.csscripts/templates/Editor/EventChannelEditor.csGame.Editor.asmdefItemDatabaseValidator.csscripts/templates/Runtime/Game.Runtime.asmdefscripts/templates/Runtime/Async/AwaitableExtensions.csLifetime.csRegeneration.csscripts/templates/Runtime/Composition/GameRoot.csscripts/templates/Runtime/Events/EventChannel.csFloatEventChannel.csStringEventChannel.csVoidEventChannel.csVoidEventListener.csscripts/templates/Runtime/Health/Health.csHealthConfig.csscripts/templates/Runtime/Input/BindingStore.csInputContext.cs詳細
Unity のアーキテクチャ(ゲームプレイプログラマー/アーキテクト)
Unity の高度なアーキテクチャは、境界条件を超えて機能し続けるかどうかで評価されます。2 回目の Play セッション、await 中のオブジェクトの破棄、ビルド済みプレイヤーの再起動、クラス名の変更、2 つのブランチのマージ、空のシーンへのプレハブの配置などです。データはアセットに、状態はプレーンな C# とファイルに保持し、システム間では所有していないイベントを介して通信します。また、すべての主張は、エージェントがマウスなしで実行できるテストによって裏付けます。対象:Unity 6000.3.21f1、Input System 1.20.0、Test Framework 1.6.0、macOS。ここに記載された関連スキルが利用可能なスキルにない場合は、ユーザーにインストールを依頼してください(npx skills add scenario-labs/skills --skill <name>)。無人で作業する場合は、ツールスキーマに基づいて進め、不足している点を明記してください。
必須の背景知識: scenario-unity-expert(チャンネル、レビューループ、6.3 の落とし穴)。ツールキット:
ut_env、ut_run、ut_live、およびこのスキルの scripts/ut_arch.py、
scripts/AgentKit/Architecture/ ジョブと
scripts/templates/(テスト済みの Game.Core/Game.Runtime/Game.Editor/tests の構成)。
ステータス(2026-09-24): すべての手順を、この Mac の Unity 6000.3.21f1 で実行済みです。
tests/code/unity-architecture/run_all.sh --live(結果は
archive/tests/unity-architecture/live_results.jsonl)。
スタンス(専門家との違い)
- ScriptableObjects は定義であり、セーブデータではありません。 プレイモード中に SO に書き込んだ値は Editor に保持され、ビルドを起動するたびにリセットされます(Code Monkey、5a-ztc5gcFw [00:04:17])。さらに詳しく観察したところ、書き込みは Editor のメモリ内にのみ存在し(
IsDirtyは false、SaveAssetsの後も.assetは変更されません)、ファイルハッシュでは検出できません。そのため、検出できるのはプレイ中のプロセス内にあるメモリ上のガードだけです(so_guard、プレハブ SO テスト)。macOS のプレイヤーを 2 回実行すると、セーブ FILE は引き継がれましたが、起動時にはベイク済みの値が読み込まれました。ランタイムのモディファイアは構造体のコピーに適用されます(config.Stats、Tarodev、tE1qH8OxO2Y [00:11:18])。ライブレジストリはランタイムのセットであり、シーン検索は使いません(Hipple、raQ3iHhE_Kk [00:40:06])。セーブには、ID を含むバージョン管理された JSON、アトミックな書き込み、バックアップ、マイグレーションを使います。 - Play テストを信頼する前に、Enter Play Mode の設定を確認してください。 ドメインリロードを無効にした場合(6.3 ではオプション、6.6 の新規プロジェクトではデフォルト)、2 回目の Play で static カウンターが 2 を示し、static イベントには 2 つのサブスクライバーが登録されていました(観察済み)。
ExitingPlayModeで static ハンドラーの登録を解除してください(Manual のベストプラクティス。単独でも問題なく動作することを確認済み)。また、SubsystemRegistrationで値をリセットしてください。DoublePlayで両方を確認できます。シーンリロードを無効にすると Editor の起動時の動作は実際の環境を反映しなくなります。また、スクリプトを変更するとドメインは引き続きリロードされます(6.3 Manual)。 - すべての await には所有者が必要です。 await は Destroy や Play の終了後も継続します(Unity、eXkFPoEp3BA [00:13:28])。トークンのないループは、
Destroy後の 0.3 秒間にさらに 13~14 回実行されました(観察済み)。MonoBehaviour ではdestroyCancellationTokenを渡します(IO も終了時に停止させる必要がある場合はLifetime.Linked(this))。通常の C# サービスではApplication.exitCancellationTokenを渡します。Awaitable を 2 回 await してはいけません(プールされるため、AsTask()を使います)。Task をブロックしてはいけません。リリースプレイヤーではメインスレッドチェックが省略されます。開発プレイヤーではメインスレッド以外からpersistentDataPath、Time.time、transform、new GameObjectにアクセスすると例外が発生しましたが、リリースプレイヤーでは何も発生しませんでした(観察済み)。開発ビルドでスレッド処理をスモークテストしてください。 - デザイナーが配線し、コードが判断します。 シリアライズされた SO 参照は、デザイナーが確認できるインジェクターです(Hipple [00:14:39])。チャンネルで送信元と受信先を疎結合にし、リスナーを後ろから反復します([00:33:23])。デバッグ計画も用意します。件数、ログの切り替え、ジョブからの
RaiseDebug()、インスペクターボタンです([00:36:07])。UnityEvent はシリアライズされた関数呼び出しです([00:29:26])。デザイナーが配線する応答にのみ、まれに使用してください(6.3 で呼び出しごとに 0 B と測定されましたが、対象はメソッド名で解決されるハード参照です)。コンポジションルートは 1 つだけを最初に実行し([DefaultExecutionOrder(-1000)]、git-amend、PJcBJ60C970 [00:12:01]。これがないとコンシューマーが何も見つけられませんでした)、問題があれば明確に失敗させます。各プレハブは空のシーンで単独実行できるようにします([00:16:29])。static は小規模な個人プロジェクトでのみ適しています(Weimann、hQE8lQk9ikE [00:01:28]、Tarodev [00:03:00])。その場合もリセット処理を用意します。列挙型を使うのは「何らかの規則によって」固定されているセットのみです。デザイナーが拡張するカテゴリには SO アセットを使います([00:46:39])。 - リバインドは 6 つの落とし穴があるステートマシンです。 アクションを無効にする必要があります(そうしないとパッケージが例外をスローします)。デフォルトでは Escape を押しても Button のリバインドはキャンセルされません。コンポジットの先頭はリバインドできません。重複は
effectivePathで判定され、コンポジットのパーツはそれより前のパーツとのみ比較されます。修飾キーの組み合わせはshortcutKeysConsumeInputに従います(デフォルトではオフのため、Shift+B を押すと B も発火します)。Reset All は永続化されたオーバーライドを削除します。LoadBindingOverridesFromJsonはfalseを渡さない限り、既存のオーバーライドを消去します(samyam、csqVa2Vimao [00:12:51]、[00:27:44]、[00:33:22]。1.20 のマニュアル。すべてテスト済み)。UI には Rebinding UI サンプルをベースにしてください。1.20 では無効化と破棄はすでに実装されていますが、4 つの修正が不足しています(ut_arch.rebind_audit)。生成された入力クラスをnewするたびに別のアセットが作られるため、1 つを共有してください。 - アセンブリは契約です。
noEngineReferencesを設定した Core には、ミリ秒単位でテストできるルールを格納します。ランタイムの asmdef から Editor の asmdef を参照してはいけません(Editor ではコンパイルできますが、プレイヤービルドは失敗します。観察済み)。テスト用 asmdef からはUnity.InputSystem.TestFrameworkを参照します。6000.3.21f1 ではtestablesは不要でした(観察済み)。 - シリアライズされたデータはリファクタリングを越えて保持され、意図した場合にのみマージされます。 フィールドには
[FormerlySerializedAs]、名前を変更した[SerializeReference]クラスには[MovedFrom]を使います(これがないとエントリーは null として読み込まれました。観察済み)。すべての[SerializeReference]型に[Serializable]を付けます(6.3 ではなくても許容されましたが、バージョンノートによると 6.4 では必須です)。UnityYAMLMerge は、-l/-rを指定しても、競合したフィールドがあると終了コード 2 を返します。また、これらを指定しない場合は BASE の値を書き込みます(観察済み)。終了コードを確認してから、Unity で結果を開いてください。
最初に確定すること
チームの構成(動作をつなぐのはデザイナーかプログラマーか)、プロジェクトの期間、対象プラットフォーム(Web には管理対象スレッドがありません)、Enter Play Mode の設定、入力デバイス(キーボードとマウス、ゲームパッド、タッチ、ローカルマルチプレイヤー)と修飾キーショートカット、セーブの要件(スロット、クラウド、移行)、VCS(デフォルトは Git + LFS)です。デフォルト設定:プロジェクト全体のアクション、Game.Core/Runtime/Editor/Tests の asmdef、SO 定義 + プレーンな C# 状態、システム間のイベントチャネル、レジストリ用ランタイムセット、GameRoot は 1 つ、persistentDataPath 配下の JSON セーブ、UnityYAMLMerge ドライバーを使った Git。ArchJobs.ProjectFacts を実行し、その回答を計画に記載します。
ワークフロー
- スキャフォールドを作成してコンパイルします。 目標:純粋な C# でルールを定義します。方法:
ut_arch.install(P)、ut_arch.scaffold(P)(A1)、次にArchJobs.ProjectFacts(Unity -batchmode -nographics -quit -executeMethod AgentKit.Architecture.ArchJobs.ProjectFacts)を実行します。 GATE:コンパイルエラーと警告がゼロであること。ArchJobs.AssemblyGraph:Game.Core のengine_refs == 0、violations == []。 - データをアセットとして扱います。 目標:デザイナーが編集する定義を用意します。方法:
ut_arch.item_specs()を使ってArchJobs.CreateAssetsを実行します(SerializedObjectによる編集、[SerializeReference]のリストにはmanaged_refs、変更時のみ SetDirty)(A2、A13)。GATE:2 回目の実行ではすべてunchangedであること。ItemDatabaseValidatorに問題がないこと。 - テスト付きのコアシステムを用意します。 目標:インベントリ、ヘルス、イベント、セットを実装します。方法:テンプレートを使い、
ut_run.run_tests(P, "EditMode", assemblies="Game.Tests.EditMode")を実行します。GATE:すべて成功すること。ut_arch.code_lintでランタイムコードに警告もエラーもないこと(A10)。 - セーブとロードを実装します。 目標:再起動後も保持される状態を実現します。方法:
SaveService(A3)を使います。GATE:EditMode でのラウンドトリップ、破損ファイルのフォールバック、v1 の移行を確認すること。2 つのエディタープロセス間でCrossProcessSaveを実行すること。永続化に関する主張を確認するため、プレイヤーを 2 回実行すること(A4)。セーブでスレッドに触れる場合は開発用プレイヤーを使うこと(A12)。 - 入力を実装します。 目標:あらゆるデバイスに対応し、永続化されてリセット可能な再バインドを実現します。方法:プロジェクト全体のアクション、
InputContext、RebindFlow、BindingStoreを使い、.inputactionsを JSON として編集します。ArchInputAudit.Auditを実行します(A5、A15)。GATE:各トラップについてInputTestFixtureテストを行うこと。missing_schemeもduplicate_bindingもない状態で監査が成功すること。rebind_auditで見落としがないこと。 - 非同期処理とライフサイクルを実装します。 目標:孤立した処理をなくします。方法:トークンを渡す
async Awaitable Start()、AsTask、PlayMode スイートを使います。非同期テストは、内部の Awaitable を返す[UnityTest]またはasync Taskとします(A6、A7、A14)。GATE:Destroy 後にティックが発生しないこと。プレハブの分離とプレハブ SO ガードを通過すること。 - プレイモードを堅牢化します。 目標:セッション 2 がセッション 1 と一致し、イベントが応答に届くようにします。方法:
ArchJobs.StaticStateAuditを実行し、続いてプロジェクトのモードでso_guardとinvoke(各チャネルでRaiseDebug)を使ってArchPlayMode.DoublePlayを実行します(A8、A11)。GATE:独自のアセンブリにarch.static_without_resetがないこと。監視対象の値が各セッションで一致すること。so_changed_in_memory == []であること。すべての invoke がokとなり、その応答がカウントされていること。 - バージョン管理を設定します。 目標:マージや移動で参照が壊れないようにします。方法:
ArchJobs.SetVcsSettings、ut_arch.write_vcs_files、git_setup_unity_merge、meta_audit、yaml_mergeの順に実行し、その後ArchJobs.InspectAssetを実行します(A9)。GATE:meta_audit.okであること。.metaが LFS に含まれていないこと。問題のないマージが終了コード 0 で完了し、デシリアライズできること。競合はマージされず、レポートが出力されること。 - 根拠を添えて引き継ぎます(scenario-unity-expert のレポート形式):各ステップの Unity 呼び出し、Verified、Assumed。
数値
| 値 | 比較対象 | 出典 |
|---|---|---|
| 11~13 s | このプロジェクトのバッチジョブ 1 回(エディターの起動を含む) | 実測 |
| 約 11 s/16 s | EditMode スイート(33 テスト)/PlayMode スイート(17 テスト)、最終実行 | 実測 |
| 13~14 ティック | Destroy 後 0.3 s の間の、トークンなしの 0.02 s Awaitable ループ | 実測 |
| 2/2 | ドメインリロード無効時の Play セッション 2 における静的カウンター/静的イベントのサブスクライバー | 実測 |
| 4/6/0/6 | 開発用/リリース用 macOS プレイヤーで、メインスレッド外から呼び出して例外をスローした Unity 呼び出し | 実測 |
| 0 B | ウォームアップ後の 10,000 回のチャネル呼び出し、または UnityEvent の永続的な呼び出しによるヒープ増加量 | 実測 |
| 約 37,000~61,000 fps | バッチ -nographics Play モード(上限なし):フレームではなく時間を待機 | 実測 |
| 0.05 s;false | PerformInteractiveRebinding の OnMatchWaitForAnother;shortcutKeysConsumeInput のデフォルト値 | パッケージソース 1.20;実測 |
次の Update(30 fps で 33 ms) | メインスレッドで開始された Task の継続処理 | 6.3 Manual |
| 約 500 KB | Git LFS の導入に見合うバイナリサイズ | Pettersen 2016 |
| exit 0/2 | UnityYAMLMerge の競合なし/競合するフィールドあり(-l、-r を指定した場合も同様) | 実測 |
| 114.7 MB/311.2 MB | プローブシーンの macOS Mono リリース用/開発用プレイヤー | 実測 |
品質ゲート
- 測定可能な項目: コンパイルエラーと警告がゼロであること;
AssemblyGraph.violations == []であること;単体テストだけでなく、完全なスイートで EditMode と PlayMode のfailed == 0であること;ランタイムコードでcode_lintに警告もエラーもないこと;so_guardとプレハブ SO テストに問題がないこと(SnapshotAssetsのchanged_on_disk == []が証明するのは、エディターツールが何も保存しなかったことだけです);StaticStateAuditに問題がないこと;DoublePlayの各セッションが一致すること;ArchInputAuditの警告が 0 件であること;rebind_auditが完了していること;meta_audit.okであること;マージの終了コードを確認すること;永続性またはスレッドについて主張する場合は、プレイヤー再起動レポート(A4)と開発用プレイヤーレポート(A12)があること。 - 視覚的な確認(少数): シーンのマージ後に、変更前後のカメラキャプチャを確認します(scenario-unity-expert の
AgentCapture);再バインド画面に新しいバインドテキストが表示されることを確認します(scenario-unity-ui)。
よくある間違い
| 間違い | 発生する問題 | 修正 |
|---|---|---|
| SO に保存データを格納する | Editor では動作しますが、ビルドを再起動すると失われます | SaveService ファイルを使います。SO は読み取り専用にします(A3、A4) |
| ファイルハッシュで SO をガードする | Play 中に SO を書き込んでも検出されません | 再生中のプロセスで so_guard/プレハブ SO テストを行います(A11) |
| 参照されていない SO にセッション状態を格納する | シーンのロード後、ビルドでは値がベイク時の状態に戻ります | 参照を維持するか、C# で保持します |
| ドメインリロード無効時の static | 2 回目の Play が 2 から始まり、ハンドラーが 2 回実行されます | ExitingPlayMode で登録解除し、SubsystemRegistration でリセットします |
| トークンなしで await する | オブジェクトの破棄後や Play の終了後にもロジックが実行されます | destroyCancellationToken、サービスには Lifetime.App を使います |
| Awaitable を 2 回 await する | 例外またはハングが発生します | AsTask() を 1 回だけ使います |
| リリースビルドだけでスレッドをチェックする | 今は問題なくても、後で動作が未定義になります | 開発用プレーヤーでスモークテストを行います(A12) |
| リスナーを順方向に反復する | 自身を削除する応答が次のリスナーをスキップします | 逆方向に反復します |
| 実行順序が設定されていないコンポジションルート | コンシューマーの Awake でサービスが見つかりません | [DefaultExecutionOrder(-1000)] |
| 有効なアクションを再バインドする | InvalidOperationException が発生します | RebindFlow が無効化し、完了時とキャンセル時に再度有効化します |
WithCancelingThrough なしで Button を再バインドする | Escape がバインドになります | .WithCancelingThrough("<Keyboard>/escape") |
| Reset All でファイルが残り、読み込みがレイヤー化されない | 古い再バインドが戻り、以前のオーバーライドが消えます | BindingStore.ResetAll、Load(..., removeExisting: false) を使います |
テスト中または PlayerInput 使用時に InputSystem.actions を使う | フィクスチャーテスト後に null になり、ペアリングが壊れます | EditorBuildSettings.TryGetConfigObject(...)、playerInput.actions を使います |
| コンシューマーごとに生成済み入力クラスを用意する | 再バインドが 1 つの画面にしか反映されません | 1 つの共有インスタンスを使います |
| ランタイム asmdef がエディター asmdef を参照する | Editor では問題ありませんが、プレーヤービルドに失敗します | AssemblyGraph で明示的な参照チェックを行います |
[SerializeReference] の背後にあるクラス名を変更する | エントリーが null として読み込まれます | 同じ変更内で [MovedFrom] を指定します |
| マージ出力をそのまま信頼する | 競合したフィールドが警告なくベースの値に戻ります | 終了コード 2 を確認し、解決してから InspectAsset を実行します |
引き継ぎ
- 受け取るもの:scenario-unity-expert から、概要、チャンネル、プロジェクトの事実。scenario-unity-gameplay から、どのシステムにヘルス、ダメージ、ステートマシンが必要か(FSM パターンはそこにあります)。
- 引き渡すもの:scenario-unity-ui には、HUD と設定画面用の
RebindFlow、BindingStore、InputContext、チャンネル(RaiseDebugを含む)とテストを渡します。scenario-unity-gameplay には、IDamageable、Health、Services、GameRoot、ランタイムセットを渡します。scenario-unity-pipeline-automation には、asmdef のレイアウト、run_tests(..., assemblies=...)、開発ビルド、.gitignore/.gitattributes、マージドライバーを渡します。scenario-unity-performance には、フレームごとの GC(scenario-unity-expert のAgentProfile)、オブジェクトごとの Awaitable ループを渡します。scenario-unity-web には、マネージスレッドを使用しないこと(SaveAsync は同期のまま、Task.Delayは使用しない)を伝えます。scenario-unity-mobile には、タッチ方式、persistentDataPathを渡します。パケットには、書き込んだファイル、合計値付きのテスト、件数付きの監査、Verified と Assumed を含めます。
Unity 6.3 の注意事項
- Enter Play Mode はドロップダウン("When entering Play Mode")です。6.6 の新規プロジェクトでは既定で "Reload Scene only" になります。6.5 ではコードのライフサイクル属性と
AutoStaticsCleanupが追加されます。 FindObjectsByType<T>(FindObjectsSortMode.None)(ソートモードは 6.4 から非推奨です)。[SerializeField]はフィールドにのみ使用できます。GetInstanceIDは 6.5 でコンパイルエラーになります(EntityId)。[SerializeReference]の型は 6.4 から[Serializable]である必要があります(バージョンノートには記載されていますが、そこでの検証は未実施です)。- テンプレートでは Input System のみが実行されます(レガシーの
Input.*は例外をスローします)。Input System 1.20 がバンドルされています。OnMouseDownがこれと連携するのは 6.4 からです。 Object.Destroy:6000.3.21f1 で実動作を検証した結果(LifecycleTests、2026-09-24)、OnDisableはすべての子孫(4 階層)に到達します。6.4 アップグレードガイドとバージョンノートでは、6.4 より前のエディターは「直接の子のみ」と説明されているため、古いエディター向けには冪等なOnDestroyの登録解除を維持してください。- Force Text と Visible Meta Files は 6.3 の既定値です(観測済み)。6.6 では YAML のワードラップがオフになり、「Reduce Version Control Noise」が削除されます。
参考資料
references/procedures.md:A1 から A15 までの完全なコード、実動作テスト、記録された結果。references/expert-notes.md:エキスパート別の原則、タイムスタンプ、判断表、すべての観測結果。references/critique.md:自己レビューの評価基準。references/gui-paths.md:同じタスクに使用するメニューとウィンドウ。references/sources.md:ソース、認証情報、URL、最適なタイムスタンプ、改訂履歴。scripts/ut_arch.py、scripts/AgentKit/Architecture/*.cs、scripts/templates/:テスト済みのコード。