見えないものをデバッグする:イベントシステムに専用のオブザーバビリティが必要な理由
QAテスターがバグを報告する:「鍵を拾ってもドアが開かない。」
シンプルだろう? おそらく参照漏れか条件の間違い。プロジェクトを開き、鍵を拾うと...ドアは問題なく開く。自分の環境では再現する。テスターに再現手順を聞くと「30%くらいの確率で発生する。大抵セーブ/ロードサイクルの後」とのこと。
デバッグ地獄に突入だ。鍵ピックアップイベントからインベントリ更新、クエスト進行チェック、ドアのアンロック条件に至るチェインのどこかで、何かが間欠的に失敗している。しかしどのリンクだ? イベントが発火されなかった? 発火されたがリスナーがサブスクライブされていなかった? サブスクライブされていたが条件がfalseに評価された? 条件は正しかったがロード後にドアの状態が古かった?
分からない。そしてイベントシステムは教えてくれない。「fire and forget」——forgetの部分が強調されている。
これが、すべてのイベント駆動Unityプロジェクトがいずれぶつかるオブザーバビリティのギャップだ。単なるデバッグの不便さではない——リファクタリングを危険にし、パフォーマンスチューニングを不可能にし、新メンバーのオンボーディングを辛くするアーキテクチャ上の盲点だ。今日はこのギャップがなぜ存在するのか、実際に何がコストになっているのか、そして適切な解決策がどう見えるのかを話したい。
「発火した?」という問い
イベントシステムにおける最も基本的なデバッグの問いは、見かけよりずっと厄介だ:イベントは発火した?
PlayerCombatがonDamageDealt.Raise(42)を呼んだ時、イベントシステムはリスナーを走査し、ハンドラを呼び出し、リターンする。ログなし。トレースなし。発生した記録なし。Raiseが完了した瞬間に情報は蒸発する。
これは直接のメソッド呼び出しとは根本的に異なる。PlayerCombat.TakeDamage()がHealthBar.UpdateDisplay()を直接呼ぶなら、呼び出し元にブレークポイントを設定し、コードをステップ実行し、何が起こるか正確に確認できる。イベントでは、呼び出し元は誰がリッスンしているか知らない。リスナーは誰が呼んでいるか知らない。両者の接続はランタイムのイベントシステムのサブスクリプションリスト内にのみ存在し、デバッガからは見えない。
そこでDebug.Logを追加する:
private void HandleDamage(int amount)
{
Debug.Log($"HandleDamage called with amount={amount}");
// 実際のロジック...
}
1つのイベントなら機能する。次に、プロジェクト内のすべてのイベントのすべてのリスナーに対して掛け算する。フレームごとにコンソールに500行のログが流れ、読む速度より速くスクロールする。検索ワードでフィルタリングしようとするが、3つの異なるログ文で「Damage」のスペルが違う。タイムスタンプを追加し、呼び出し元名を追加し、スタックトレースを追加。各Debug.Logは3行のフォーマットコードで1行の実際のロギングを囲むようになる。
出荷時は? すべて削除する必要がある。あるいは#if UNITY_EDITORブロックで囲む。あるいは残して、フレームごとに500行の文字列フォーマッティングによるパフォーマンスヒットに誰も気づかないことを祈る。
Debug.Logはデバッグ戦略として、バケツが配管戦略であるのと同じだ。緊急時には機能するが、家の設計をそれに基づいてはしない。
Unityのプロファイラ:間違ったツール
Unityのプロファイラは「どのメソッドがどれだけ時間を使ったか」に答えるのに優秀だ。「どのイベントが、いつ、どんなデータで発火し、誰が応答したか」に答えるのには全く向いていない。
プロファイラにスパイクが見える。何かのコールバックメソッド——HandleDamage。コールスタックを掘る。呼び出し元は...イベントシステムのジェネリックなディスパッチ関数。どのイベント? プロファイラは知らない。ジェネリックなディスパッチ関数からのメソッド呼び出しが見えるだけ。どのリスナーが遅い? 個別にインストルメントする必要がある。どんなデータが渡された? プロファイラは引数をキャプチャしない。
プロファイラは時間がどこで費やされたかを教えてくれる。なぜイベントシステムがそのように振る舞ったかは教えてくれない。根本的に異なる問いだ。
OnPlayerDamagedの8つのリスナーのうち1つが4msかかっている。プロファイラはイベントシステムのディスパッチメソッドにスパイクを表示する。素晴らしい。8つのリスナーのどれが犯人? 各リスナーをStopwatchで囲んで結果をログに出せばいい。8リスナーに対して。1つのイベントに対して。60のイベントがある。480行のタイミングコード、そしてまだデバッグすら始めていない。
依存関係の問い
アーキテクトを夜も眠れなくする問い:「このイベントを誰が使っている?」
OnPlayerDeathをOnPlayerDefeatedにリネームしたい。ゲームデザインが変わって「死」はもう起きず、プレイヤーは「ノックアウト」されるようになったから。シンプルなリネームだろう?
プロジェクト全体でCtrl+F:OnPlayerDeath。12のコード参照が見つかる。すべてリネーム。出荷。
するとバグレポートが来る:アナリティクスシステムがプレイヤー敗北を追跡しなくなった。なぜ? アナリティクスのMonoBehaviourがインスペクターのシリアライズされたフィールドで旧OnPlayerDeath ScriptableObjectを参照していたから。Ctrl+Fはコード参照しか見つけない。インスペクターのバインディングは見つけない。Behavior Windowのサブスクリプションも見つけない。.csファイルではなくシリアライズされたUnityアセット内に存在する参照は見つけない。
これがイベントを削除する人がいない理由だ。リネームする人もいない。イベント階層をリファクタリングする人もいない。全体像が誰にも分からないから。「このイベントを誰が使っている?」は標準のUnityツールでは回答不能な問いだ。だからイベントは蓄積する。デッドイベントが永遠にプロジェクトに残る。イベントデータベースが膨らむ。新しい開発者が200のイベントを見て、どれがアクティブか見当もつかない。
リファクタリングへの恐怖は本物で、オブザーバビリティのギャップが直接的な原因だ。
ゲームをフリーズさせる再帰ループ
イベントAがBを発火。Bのリスナーがイベントcを発火。Cのリスナーがイベントaを発火。ゲームがフリーズ。エディタが応答しなくなる。Unityを強制終了し、未保存のシーン変更を失い、20分間コードを見つめてサイクルの始点を探す。
再帰イベントループはイベント駆動システムで最も厄介なバグの1つだ。デザインタイムには見えない——3つのイベントすべてが正しいリスナーで同時にアクティブになった時にのみ顕在化する。コードレビューにも出てこない。単一のファイルにはループが含まれていないから。各スクリプトは別のイベントに応答して1つのイベントを発火するだけ。単独では完全に合理的。組み合わさると壊滅的。
自動検出がなければ、これらのループはハードウェイで発見する:フリーズしたエディタとスタックオーバーフロー。
DevOpsが持っているもの(我々が持っていないもの)
バックエンド開発の世界はこの問題を何年も前に解決した。分散トレーシング(Jaeger、Zipkin)でリクエストを15のマイクロサービスにまたがって追跡し、どこで時間を費やしたかを正確に確認できる。メトリクスダッシュボード(Grafana、Datadog)がリクエストレート、エラーレート、レイテンシパーセンタイルをリアルタイムで表示する。ログ集約(ELKスタック、Splunk)で構造化クエリを使って数百万のログエントリを検索できる。アラートシステム(Prometheus、PagerDuty)がユーザーが苦情を言うBEFOREに通知する。
ゲームイベントはアーキテクチャ的にマイクロサービスメッセージと類似している。イベントが発火し(リクエストが送信され)、複数のリスナーが応答し(複数のサービスが処理し)、結果が下流に伝搬する(さらなるイベントをトリガーする)。同じオブザーバビリティ手法が適用できる。
しかしUnityのツールボックスが我々に与えるのは...プロファイラとDebug.Logだけだ。もっと良いものに値する。
GESの答え:2つの補完的ツール
GESはオブザーバビリティのギャップに、開発ライフサイクル全体をカバーする2つの専用ツールで対処する:エディットタイムの依存関係分析のためのEvent Finderと、プレイタイムのオブザーバビリティのためのRuntime Monitor。合わせて、標準のUnityツールでは答えられないすべての問いに答える。
Event Finder:このイベントを誰が使っている?
Event Finderはエディタウィンドウで、依存関係の問いに決定的に答える。任意のイベントアセットを選択し、Scanをクリックすると、そのイベントを参照するシーン内のすべてのMonoBehaviourを見つける——publicフィールド、privateシリアライズフィールド、ネストされた参照を通じて。リフレクションを使ってコンポーネントフィールドをスキャンするため、Ctrl+Fでは見つけられない参照もキャッチする。
リストビュー

リストビューはすべての参照をフラットリストで表示する。各エントリにはGameObject名、コンポーネント型、フィールド名、ステータスインジケータが表示される:
- グリーン — 参照が有効でコンポーネントがアクティブ
- レッド — 参照が壊れている(nullイベント、コンポーネントの欠損、または無効なオブジェクト)
任意のエントリをクリックしてヒエラルキーでPing(選択せずにハイライト)、Focus(選択してScene Viewでフレーム)、またはFrame(Sceneカメラをオブジェクト中心に移動)できる。
グループビュー

グループビューはコンポーネント型で参照を整理する。すべてのHealthSystem参照をまとめ、すべてのDamagePopup参照をまとめる。「どのオブジェクトがこのイベントを参照しているか」ではなく「どのシステムがこのイベントを使っているか」に答えたい時のビューだ。
安全なリファクタリングワークフロー
Event Finderが「このイベントを誰が使っている?」を回答不能な問いから30秒のルックアップに変える方法:
- Event Finderを開き、リネーム/削除/変更したいイベントを選択
- Scanをクリック——完全な参照リストを取得
- リストビューですべての参照をレビュー(予期しないコンシューマをチェック)
- グループビューに切り替えて影響を受けるシステムを把握
- 自信を持って変更を実行
- 再スキャンして何も壊れていないことを確認(すべてグリーンステータス)
推測なし。「全部カバーしたと思う」なし。インスペクターバインディングを見落として2週間後にバグレポートが来ることもない。Event Finderは依存関係を可視化することでリファクタリングを安全にする。
Runtime Monitor:専用のイベントオブザーバビリティ
Runtime Monitorは8つの専門タブを持つエディタウィンドウで、それぞれ特定のカテゴリのデバッグ質問に答えるよう設計されている。イベント、リスナー、条件、タイミング、フローグラフをネイティブに理解する——イベントシステムに組み込まれているから、後付けでボルトオンされたものではない。
Tools > TinyGiants > Game Event System > Runtime Monitorから開くか、GES Hubで見つける。モニターはPlay Mode中に最小限のオーバーヘッドでデータを収集する。エディタ専用コードであり、ビルドから完全に除去される。出荷ゲームへの影響はゼロ。
8つのタブすべてを見ていこう。
タブ1:Dashboard — ヘルスチェック
Dashboardは出発点だ。一目で、イベントシステムが健全かどうかがすぐに分かる。

メトリクスカードが画面上部に全体像を表示:プロジェクト内の総イベント数、このセッションのアクティブイベント数(1回以上発火したもの)、総リスナーサブスクリプション数、Play Mode開始以降の累計Raise回数。
パフォーマンスバーは色分けされている。グリーンはすべてのイベントの平均処理時間が1ms未満——問題なし。イエローは一部のイベントが1-10ms平均——確認の価値あり。レッドは何かが10msを超えている——止まって調査する。バーは平均ではなく最悪のパフォーマンスのイベントを反映する。1つの問題のあるイベントがバー全体をイエローにする。意図的だ——外れ値を知りたいから。
最近のアクティビティは直近のイベント発火がリアルタイムでスクロール表示:イベント名、タイムスタンプ、リスナー数、実行時間。ゲームプレイ中、イベントシステムが何をしているかのライブな脈拍が見える。
クイック警告は検出された問題を要約:高実行時間、多リスナー数、再帰Raise、メモリアロケーション。警告バッジをクリックすると関連する詳細タブにジャンプする。
Dashboardが答えるのは:「今、イベントシステムは健全か?」 はいなら、先に進もう。いいえなら、他のタブが理由を教えてくれる。
タブ2:Performance — 具体的な数値
何かが遅く感じる時、感覚ではなくデータが必要な時に行く場所。

プロジェクト内のすべてのイベントが以下の列で表示される:
- Event Name — ScriptableObjectアセット名
- Raise Count — このセッションで発火した回数
- Listener Count — 現在のアクティブサブスクライバ数
- Avg/Min/Max Time — 全リスナーでのRaiseあたりの実行時間
- GC Alloc — Raiseあたりのガベージコレクションアロケーション
時間セルは色分け:グリーン(<1ms)は正常、イエロー(1-10ms)は要注意、レッド(>10ms)は要緊急対応。任意の列でソート可能——「Max Time」でソートしてスパイクの犯人を見つけ、「GC Alloc」でアロケーションホットスポットを見つけ、「Raise Count」で高頻度イベントを特定。
Performanceタブをパワフルにしている洞察:イベント実行時間はすべてのリスナーの作業を含む。 50リスナーで平均5msなら、リスナーあたり約0.1ms——正常だ。2リスナーで5msなら、リスナーの1つが高コストな処理をしている。数値だけで「リスナーが多すぎる」のか「1つのリスナーが遅い」のかすぐに分かる。
タブ3:Recent Events — タイムライン
すべてのイベント発火の時系列ログ。イベントシステムのフライトレコーダーだ。

各エントリに表示されるのは:タイムスタンプ(ゲーム時間)、イベント名、引数値(文字列表示)、Raise()を呼んだスクリプトとメソッド、Raise時のリスナー数、実行時間。
任意のエントリをクリックするとフルコールスタックが表示される。「誰がこれをRaiseした?」に答えるためのゴールド情報——特に複数のシステムが同じイベントをRaiseできる場合:
PlayerCombat.TakeDamage() at PlayerCombat.cs:47
-> Int32GameEvent.Raise(42)
ダメージイベントがPlayerCombatシステムの47行目から、引数42で来たことが分かる。
イベント名でフィルタリングして特定のイベントをリアルタイムで監視できる。OnKeyPickedUpに設定して鍵ピックアップのシーケンスをプレイスルーする。存在する? いつ発火した? 何の引数? 見つからなければ問題は上流——Raiseを呼んでいない。正しいデータで存在していれば問題は下流——Listenersタブを確認しよう。
時間範囲でフィルタリング(直近N秒)や最小実行時間でフィルタリング(スパイクのみ表示)も可能。
Recentタブは「このイベントは本当に発火した?」を推測ゲームからルックアップに変える。
タブ4:Statistics — パターン
Recentが個別のイベントを表示する一方、Statisticsは時間経過の集計振る舞いを表示する。
頻度分析: 毎秒のイベント総数(リアルタイム)、イベントごとの頻度(毎秒・毎分のRaise回数)、分布ヒストグラム。
使用パターン: 最もアクティブなイベント(総Raise回数でソート)、最も非アクティブなイベント(0回発火——デッドコードの可能性)、最も忙しいタイミング(ピークアクティビティの期間)、セッション中のリスナー増加。
このタブはスポットチェックでは見つけられないことを明らかにする。例えばOnPositionUpdated——「たまに」発火するイベントだと思っていた——が実は20リスナーで毎秒60回発火していることを発見する。毎秒1,200回のリスナー実行。各0.01msでも、毎秒12msのCPU時間。1つのイベントに対して。モバイルでは重要だ。
あるいはOnBossDiedがボス戦を含むフルプレイスルー後もRaise回数がゼロであることを発見する。イベントのワイヤリングが正しくないか、デッドコードだ。いずれにせよ知りたい。
タブ5:Warnings — 自動ヘルスチェック
Warningsタブはイベントシステムを監視し、問題を自動的にフラグ付けする。何を探すべきか知る必要はない——Warningsタブが知っている。

パフォーマンス警告:
- 実行時間 > 10msのイベント(レッド)
- 実行時間 > 5msのイベント(イエロー)
- Conditionalリスナーなしで毎秒100回以上発火するイベント(イエロー)
リスナー警告:
- 50以上のリスナーを持つイベント(イエロー)
- 100以上のリスナーを持つイベント(レッド)
- DontDestroyOnLoadでないオブジェクト上のPersistentリスナー(イエロー)
メモリ警告:
- GCアロケーションを引き起こすイベントRaise(イエロー)
- GCアロケーション付きの高頻度イベント(レッド)
再帰警告:
- 処理中に発火されたイベント(レッド)
- 循環トリガー/チェイン依存の検出(レッド)
各警告にはイベント名、トリガーした具体的なメトリクス、推奨アクションが含まれる。「これは良くない」だけでなく「Conditionalリスナーの追加を検討して実行回数を減らしてください」や「RemoveListenerの呼び出し漏れを確認してください」のように。
再帰検出だけでも十分価値がある。イベントAがBを発火、BがAを発火、AがBを発火...はイベント駆動システムで最も厄介なバグの1つだ。自動検出がなければ、ゲームがフリーズしてスタックオーバーフローした時に発見する。Warningsタブは発生した瞬間にキャッチし、どのイベントが関与しているか正確に教えてくれる。
タブ6:Listeners — サブスクリプションマップ
このタブはすべてのアクティブなリスナーサブスクリプションを、イベントとリスナータイプ別に整理して表示する。

任意のイベントを展開してレイヤー別のリスナーを確認:
OnPlayerDamaged (12 listeners)
+-- Basic (4)
| +-- HealthSystem.HandleDamage
| +-- HitFlash.ShowFlash
| +-- CameraShake.OnDamage
| +-- SoundManager.PlayHitSound
+-- Priority (3)
| +-- [200] ArmorSystem.ReduceDamage
| +-- [100] HealthSystem.ApplyDamage
| +-- [25] HealthUI.RefreshBar
+-- Conditional (2)
| +-- [cond] BossModifier.ApplyBossMultiplier
| +-- [cond] CriticalHit.CheckCritical
+-- Persistent (1)
| +-- AnalyticsManager.TrackDamage
+-- Triggers (1)
| +-- -> OnScreenShake (delay: 0s)
+-- Chains (1)
+-- -> OnDamageNumber (delay: 0.1s, duration: 0.5s)
サブスクリプション監査: 期待するリスナーが実際にサブスクライブされているか確認。「なぜヒットサウンドが鳴らない?」 ここで確認——SoundManager.PlayHitSoundがリストにある? なければサブスクリプションが欠けている(おそらくライフサイクルの問題——オブジェクトが破棄されて再作成されたが再サブスクライブされていない)。
Priority確認: 実行順序が正しいか確認。UI更新(priority 25)がデータ変更(priority 100)より先に処理されていたら、priority値が逆転している。
リーク検出: 破棄されているはずのオブジェクトのリスナーが表示されていたら、サブスクリプションリークを発見。リスナーのターゲットが古く、OnDisableかOnDestroyでのRemoveListener呼び出しが欠けている。
タブ7:Automation — フローマップ
このタブはイベント間の接続——トリガーとチェイン——を依存関係グラフとして可視化する。

ツリービューでは各イベントがルートとして表示され、外向きの接続が子として表示される:
OnBossDefeated
+-- [trigger] -> OnPlayVictoryMusic (delay: 0s)
+-- [trigger] -> OnShowVictoryUI (delay: 1s)
+-- [chain] -> OnSaveProgress (delay: 2s)
+-- [chain] -> OnLoadNextLevel (delay: 0.5s)
「ボスが倒された時、他に何が起こる?」に答えるのに最適だ。ツリーをたどれば完全な伝搬パスが見える。
両方のビューで、Node Editorでビジュアルに設定された接続(「visual」マーク)とランタイムでプログラム的に作成された接続(「runtime」マーク)が表示される。フローが機能していない場合、期待する接続がそもそも存在するか確認する。「visual」は表示されるが「runtime」がなければ、設定は正しいがランタイム初期化を妨げる何かがある。
タブ8:Details — ディープダイブ
他のタブで任意のイベントをクリックすると、Detailsタブがその単一イベントの包括的なビューを表示する。
総Raise回数、平均/最小/最大実行時間、タイプ別の現在リスナー数、Raiseあたりのアロケーション、直近60秒の頻度、最後のRaiseタイムスタンプと引数。1つのイベントの振る舞いを一目で理解するために必要なものすべて。
重要な追加:リスナーごとのブレイクダウン。 Performanceタブがイベントごとの集計時間を表示する一方、Detailsタブは単一イベント内のリスナーごとの時間を表示する。
OnPlayerDamagedが10リスナーで平均3msの場合、DetailsタブはArmorSystem.ReduceDamageが2.5msかかり、残り9リスナーが各0.05msかかっていることを教えてくれる。最適化すべき場所が正確に分かる。推測なし、各ハンドラにStopwatchのインストルメンテーションを追加する必要なし、Debug.Logのタイミングコードなし。
リスナー履歴セクションは時間経過の追加と削除を表示する:
[0.0s] + AddListener: HealthSystem.HandleDamage
[0.0s] + AddPriorityListener: ArmorSystem.ReduceDamage (200)
[15.3s] - RemoveListener: HealthSystem.HandleDamage
[15.3s] + AddListener: HealthSystem.HandleDamage
[45.0s] + AddConditionalListener: BossModifier.Apply (100)
これは「ファントムリスナー」問題——オブジェクトのライフサイクルイベント(シーンロード、オブジェクトプーリング、有効化/無効化サイクル)によって出現・消失するリスナー——のデバッグに役立つ。
完全なデバッグワークフロー
イントロのドアバグを再訪しよう。両方のツールを使った調査の進め方:
ステップ1:再現。 セーブ/ロード後に鍵をピックアップ。ドアが開かない。
ステップ2:Recent Eventsを確認。 Runtime MonitorのRecentタブを開き、OnKeyPickedUpでフィルタリング。存在する? はい — タイムスタンプ23.4sに正しいキーIDで発火。Raiseは問題ない。問題は下流だ。
ステップ3:Listenersを確認。 Listenersタブに切り替え、OnKeyPickedUpを見つける。ドアのリスナーはサブスクライブされている? いいえ — 見つからない。セーブ/ロード前にはあったが、今はない。
ステップ4:根本原因の特定。 ドアのリスナーはOnEnableで登録される。ロード後、ドアオブジェクトは破棄されて再作成されたが、OnEnableがイベントデータベースのロード完了前に実行された。リスナーがnullイベント参照にサブスクライブしようとした。
ステップ5:修正の検証。 初期化順序の修正後、Event FinderでOnKeyPickedUpをスキャンし、ドアの参照がグリーン(有効)であることを確認。セーブ/ロードサイクルを再度プレイスルー。Recent Eventsを確認——イベントが発火。Listenersを確認——ドアがサブスクライブされている。ドアが開く。バグ修正完了。
合計調査時間: 約90秒。Debug.Logなし。推測なし。「自分の環境では動く」なし。
エディットタイム + プレイタイム = 完全なカバレッジ
Event FinderとRuntime Monitorは開発の異なるフェーズをカバーするため、互いを完璧に補完する:
| ツール | フェーズ | 答えられる問い |
|---|---|---|
| Event Finder | エディットタイム | 「このイベントを誰が参照している?」「リネーム/削除しても安全?」「すべてのバインディングは有効?」 |
| Monitor Dashboard | プレイタイム | 「今、イベントシステムは健全?」 |
| Monitor Performance | プレイタイム | 「どのイベントが遅い、なぜ?」 |
| Monitor Recent | プレイタイム | 「今何が起きた、どの順序で?」 |
| Monitor Statistics | プレイタイム | 「長期的な使用パターンは?」 |
| Monitor Warnings | プレイタイム | 「何を心配すべき?」 |
| Monitor Listeners | プレイタイム | 「今、誰が何をリッスンしている?」 |
| Monitor Automation | プレイタイム | 「イベント同士はどう接続されている?」 |
| Monitor Details | プレイタイム | 「この1つのイベントについてすべて教えて。」 |
Event Finderはリファクタリングへの自信を与える。Runtime Monitorは実行中のゲームが正しく振る舞っているという自信を与える。合わせて、イベント駆動アーキテクチャのデバッグをこれほど苛立たせるオブザーバビリティのギャップを埋める。
イベント駆動アーキテクチャはパワフルだ。しかし可視性のないパワーは、見つけられないバグを作る洗練された方法に過ぎない。これらのツールが可視性を与える。開発中はDashboardを開いたままにしよう。リファクタリングセッションの前にEvent Finderを実行しよう。何かがおかしいと感じたら、どのタブを確認すべきか分かるだろう——そして答えは500行のDebug.Log出力に埋もれることなく、そこで待っている。
🚀 グローバル開発者サービス
🇨🇳 中国開発者コミュニティ
- 🛒 Unity 中国アセットストア
- 🎥 Bilibili動画チュートリアル
- 📘 技術ドキュメント
- 💬 QQグループ (1071507578)
🌐 グローバル開発者コミュニティ
📧 サポート
- 🌐 TinyGiants Studio
- ✉️ サポートメール
