メインコンテンツまでスキップ

デザイナーがコードなしでイベントを設定:デザイナーとプログラマーの連携問題

TinyGiants
GES Creator & Unity Games & Tools Developer

火曜の午後3時。デザイナーが横から声をかけてくる。「ねえ、プレイヤーが50以上のダメージを受けた時の画面シェイク、もう少し強くできない?あと、ヒットサウンドの前に0.5秒のディレイ入れたいんだけど。あ、毒エフェクトのティック間隔も2秒じゃなくて1.5秒にしたい。」

3つの変更。デザイナーの視点からすれば実質15秒の判断。でも実際に起きることはこうだ:Sceneビューを閉じる。IDEを開く。読み込みを待つ。ダメージハンドラーを検索する。メソッド内に埋もれた画面シェイクの強度値を見つける。変更する。次にオーディオのディレイを探す——これは別のクラスにある。変更する。次に毒のコルーチンを探す——これはさらに別のクラスで、ティックレートはWaitForSecondsの中にある。変更する。3つのファイルをすべて保存。Unityに戻る。再コンパイルを待つ。テスト。

8分後、デザイナーが言う。「やっぱりシェイクは前の方が良かったかも、あと毒は1.8秒で試せる?」

これがゲーム開発のイテレーション速度を殺すループだ。大きなアーキテクチャの決定じゃない——パラメータの微調整のたびにプログラマーがコードを触らなきゃいけないという、絶え間ない摩擦だ。遅いだけじゃない。チームのイテレーション速度を根本的に制限するコラボレーションのボトルネックだ。

さらに最悪なのは、このループの間、プログラマーはプログラミングをしていないということだ。やっているのはデータ入力だ。0.5f0.3fに変えて、コンパイラを待っている。誰の時間もうまく使えていない。

デザイナーとプログラマーの連携問題

ほとんどのUnityチームがどう働いているか、正直に向き合おう。イベントレスポンスに触る必要がある人は2種類いる:システムを構築する人(プログラマー)と、パラメータを調整する人(デザイナー)。これらは根本的に異なるアクティビティであり、根本的に異なるツールが必要だ。

プログラマーにはIDE、デバッガー、バージョン管理、C#のフルパワーが必要だ。デザイナーにはスライダー、ドロップダウン、チェックボックス、即座のフィードバックが必要だ。両方のグループを同じコード-コンパイル-テストのパイプラインに通すと、どちらにも最適化できない。

典型的な依存ループ

僕が一緒に働いたすべてのチームで繰り返されるパターンはこうだ:

  1. デザイナーがアイデアを持つ:「ヒットリアクションの前に0.2秒のディレイを入れたらどうかな?」
  2. デザイナーはその変更ができない——コードの中にあるから
  3. デザイナーがプログラマーに頼む
  4. プログラマーは他のことをしている最中——コンテキストスイッチのペナルティ
  5. プログラマーがファイルを開き、変更し、コンパイルを待つ
  6. デザイナーがテスト:「うーん、0.15にしてみて」
  7. ステップ4-6を繰り返す
  8. 合計経過時間:概念的には5秒の調整に20分

これをプロジェクト全体のすべてのイベントレスポンスのすべてのパラメータに掛け合わせよう。さらにプロダクション期間の毎日で掛け合わせる。累積コストは途方もなく、ほとんどのチームは他の方法を知らないので普通のこととして受け入れている。

デザイナーが本当にコントロールしたいもの

分解してみると、デザイナーがイベントレスポンスについて調整する必要があるものは、いくつかの明確なカテゴリに分かれる:

何が起きるか。 イベントが発火した時、どのメソッドが呼ばれるか?サウンドを再生、パーティクルエフェクトをスポーン、UI要素を更新、アニメーションをトリガー。

どの条件で。 このレスポンスは毎回発火すべきか、それともダメージが閾値を超えた時だけ?プレイヤーのHPが30%以下の時だけ?特定のフラグがtrueの時だけ?

どのタイミングで。 レスポンスは即座か、0.2秒遅延か?繰り返すか?どのくらいの頻度で?何回?

これらはどれも「プログラミング」の質問ではない。デザインの質問だ。デザイナーは一行のコードも書かずにこれらに答えられるべきだ。

従来のソリューション(とその限界)

Unityの開発者たちはデザイナーにより多くのコントロールを与えるために様々なアプローチを試してきた。それぞれに大きな限界がある。

MonoBehaviourに[SerializeField]フィールドを公開する。 シンプルな値には機能するが、すぐに散らかる。調整可能なパラメータごとにシリアライズされたフィールドが必要。Inspectorはラベルのないfloatの壁になる。グループ化もなく、条件もなく、タイミング制御もない。そしてプログラマーはデザイナーが調整したいかもしれないすべてのパラメータを予測しなければならない——一つ見逃せばコード-コンパイルのループに戻る。

// 「全部公開」アプローチ
public class DamageResponse : MonoBehaviour
{
[SerializeField] private float screenShakeIntensity = 0.5f;
[SerializeField] private float screenShakeDuration = 0.3f;
[SerializeField] private float soundDelay = 0.1f;
[SerializeField] private float damageThreshold = 50f;
[SerializeField] private bool enableScreenShake = true;
[SerializeField] private bool enableSound = true;
[SerializeField] private float poisonTickRate = 2.0f;
[SerializeField] private int poisonTickCount = 5;
// ... これは永遠に増え続ける
// そしてすべてが実装コードと絡み合っている
}

カスタムEditorスクリプト。 各システムに美しいカスタムInspectorを作ることができる。しかしそれはシステムごとに多大なエンジニアリング投資だ。そして基盤システムが変わるたびに、カスタムエディターも変更が必要。ほとんどのチームにはゲーム内のすべてのイベントレスポンスにこれを用意する余裕がない。

UnityEvent。 Unityの組み込みUnityEventシステムは、真のソリューションに最も近い。ターゲットオブジェクトをドラッグし、ドロップダウンからメソッドを選び、完了。デザイナーはコードなしでレスポンスを配線できる。しかしUnityEventには現実的な制限がある:

  • 条件システムがない——「値 > 50の時だけ発火」ができない
  • スケジューリングがない——ディレイ、リピート、タイミング制御がない
  • 文字列ベースのメソッドバインディング——リファクタリングに弱い
  • ジェネリック型のサポートが限定的——型付きイベントパラメータをきれいに扱えない
  • ステータスの可視性がない——一目でどのイベントにレスポンスが設定されているかわからない

UnityEventでは道のりの40%くらいしかカバーできない。残りの60%——条件、スケジューリング、型安全性、ステータスの可視性——が難しい部分だ。

本当の問い

デザイナーに、プロジェクト内のすべてのイベントにカスタムエディターを作ることなく、条件、タイミング、繰り返しを含むイベントレスポンスの完全なコントロールを与えられるか?

それがGES Behavior Windowが答える問いだ。

Behavior Window:完全なレスポンス制御、コードゼロ

Behavior Windowは、デザイナーでも、サウンドエンジニアでも、ゲームプレイプログラマーでも——誰でもビジュアルコントロールで完全なイベントレスポンスを設定できる単一のエディターインターフェースだ。IDEなし。コンパイルなし。待ち時間なし。

Behavior Window Full

4つのセクションが論理的な順序で流れる:「これは何のイベント?」>「レスポンスすべき?」>「何をする?」>「いつ、どのくらいの頻度で?」

レシーバーはこのウィンドウ内で直接設定する。GameObjectに追加する別の「リスナー」コンポーネントはない。イベントを選択し、Behavior Windowを開き、すべてを一箇所で設定する。

Event Info:「現在地」マーカー

Behavior Info

上部セクションは読み取り専用——設定中のイベントの識別情報を表示する:名前、タイプ(パラメータなし、型付き単一パラメータ、またはsender)、GUID、カテゴリ、データベース。

12個のイベントを連続で設定していて、どれを見ているかわからなくなるまでは些細に思えるかもしれない。Infoセクションは確認用だ。そしてGUIDはデバッグに本当に便利——ランタイムでコンソールログにイベントIDが表示された時、ここで即座にマッチできる。

Action Condition:「Then」の前の「If」

Behavior Condition

ここがBehavior WindowがUnityEventを超えるポイントだ。Action Conditionセクションは、イベントが発行された時にレスポンスが実際に発火すべきかを決定するビジュアルゲートだ。

Inspectorで条件ツリーを構築する:

  • 値の比較 —— 受信パラメータが閾値より大きいか、小さいか、等しいか?
  • ブール状態 —— フラグがtrueかfalseか?
  • 参照チェック —— 特定のオブジェクトがnullかnullでないか?
  • 複合条件 —— 上記のAND/OR組み合わせ

ここでデザイナーとプログラマーのコラボレーションが本当にうまくいく。プログラマーはFloat32GameEventOnDamageReceivedを作成し、それを発行するコードを書く:

[GameEventDropdown, SerializeField] private Float32GameEvent onDamageReceived;

// ダメージ計算のどこかで:
onDamageReceived.Raise(calculatedDamage);

プログラマーの仕事は終わりだ。デザイナーがBehavior Windowを開き、条件を設定する:「ダメージ値が50より大きい場合のみレスポンスする。」デザイナーはその閾値を30、80、1000に変更して、Playモードで即座にそれぞれテストできる。コード変更なし。再コンパイルなし。プログラマーの空き待ちなし。

条件はオプションだ。何も設定しなければ、イベントが発行されるたびにレスポンスが発火する。多くのユースケースでは、まさにそれが正しい——すべてのレスポンスにゲートが必要なわけではない。

条件ツリーシステムは、従来カスタムコードが必要だった複雑なシナリオも処理する。「ダメージが30より大きく、かつプレイヤーが戦闘モード」は条件ツリーの2つのノードになる。if文を書く必要も、boolを公開する必要もない。

Event Action:実際に何が起きるか

Behavior Action

Event Actionセクションは、イベントが発火し条件がパスした時に何が起きるかを定義する。UnityのInspectorでButtonのonClickを使ったことがあれば、基本パターンは知っているだろう:ターゲットオブジェクトをドラッグし、ドロップダウンからメソッドを選ぶ。Behavior Windowは同じパターンを使い、GESの型システムをサポートするように拡張されている。

パラメータなしイベントでは、標準のアクションバインディングが得られる。ターゲットをドラッグし、パラメータなしのメソッドを選ぶ:

// これらのメソッドはBehavior Windowからバインドできる:
public void PlayExplosionEffect() { /* ... */ }
public void ShakeCamera() { /* ... */ }
public void IncrementKillCounter() { /* ... */ }

型付きイベントでは、受信したイベントデータが自動的にバインドされたメソッドに渡される。Behavior Windowはイベントのパラメータ型を理解し、互換性のあるメソッドのみを表示する:

// Float32GameEvent(OnDamageReceived)の場合:
public void ApplyDamage(float amount)
{
currentHealth -= amount;
UpdateHealthBar();
}

// StringGameEvent(OnDialogueTriggered)の場合:
public void ShowDialogue(string text)
{
dialogueBox.SetText(text);
dialogueBox.Show();
}

senderイベントでは、データとソースGameObjectの両方が得られる:

// sender Float32GameEvent(OnDamageDealt)の場合:
public void HandleDamage(float amount, GameObject source)
{
currentHealth -= amount;
FaceToward(source.transform);
SpawnHitParticles(source.transform.position);
}

Behavior Action Add

アクションバインディングはDynamicStaticのパラメータモードをサポートする。Dynamicモードはイベントのランタイム値をメソッドに渡す——実際に発生したダメージ量だ。Staticモードはデザイナーがイベントデータを無視してInspectorで固定値を設定できる。両方のモードが便利:Dynamicは「実際のダメージを適用」に、Staticは「ダメージ量に関係なく常に大きな爆発サウンドを再生」に。

1つのビヘイビアに複数のアクションをバインドできる。イベントが発火し条件がパスすると、バインドされたすべてのアクションが順番に実行される。僕が常に使うパターンはこれだ:1つのイベントを3つの異なるオブジェクトのメソッドにバインドする。イベントは1回発火するが、オーディオマネージャーがサウンドを再生し、VFXマネージャーがパーティクルをスポーンし、UIマネージャーが通知を表示する。3つのシステムが独立して応答し、互いに完全に疎結合だ。

Schedule:コルーチンなしのタイミング

Behavior Schedule

Scheduleセクションは、Behavior Windowを「便利」から「これがコード不要って信じられない」に引き上げる場所だ。すべてビジュアルフィールドからの完全なタイミングとライフサイクル制御。

Action Delay —— イベントが発火してからアクションが実行されるまでの秒数。即座は0。0.5秒は0.5。3秒は3.0。

これだけでも価値がある。爆発イベントを考えてみよう:

Event: OnExplosion
Behavior 1: ShakeCamera() -- Delay: 0.0s
Behavior 2: PlayExplosionSFX() -- Delay: 0.05s
Behavior 3: ShowDamageNumber() -- Delay: 0.3s
Behavior 4: FadeSmoke() -- Delay: 1.5s

コルーチンなし。Invoke呼び出しなし。タイマー管理コードなし。デザイナーが4つのディレイ値を設定するだけで、完璧にシーケンスされた爆発レスポンスが得られる。距離が遠いシミュレーションのためにサウンドディレイを0.05から0.1に変更?フィールド1つ。即座にテスト。

Repeat Interval —— 繰り返し実行の間隔。1.0にすると、アクションは毎秒繰り返される。

Repeat Count —— アクションの繰り返し回数:

  • 0 —— 一度だけ実行、繰り返しなし(デフォルト)
  • N —— 最初の実行後にN回追加実行
  • -1 —— キャンセルされるかオブジェクトが破棄されるまで無限に繰り返し

これらを組み合わせると、一行のコードもなしでループするビヘイビアが得られる:

Event: OnPoisoned
Action: ApplyPoisonTick(5.0f)
Delay: 0.0s
Repeat Interval: 2.0s
Repeat Count: 5
Result: 即座に5ダメージ、その後2秒ごとに5回
Total: 6ティック x 5ダメージ = 10秒間で合計30毒ダメージ

毒を1.5秒ごとに3ダメージ、8ティックに変更したい?3つの数値を変更。即座にテスト。デザイナーはプログラマーが知らないうちにDoT(ダメージオーバータイム)システムを調整してしまった。

Persistent Event —— DontDestroyOnLoadを使用するオブジェクトでビヘイビアがシーンロードを生き延びる。オーディオマネージャー、アナリティクストラッカー、実績システムなど、どのシーンがアクティブかに関係なくイベントに応答する必要があるグローバルシステムに不可欠。

カラーコードステータス:アーキテクチャを一目で把握

GESエコシステムで僕のお気に入りの一つが、ツールチェーン全体で見えるカラーコードビヘイビアステータスだ:

  • —— このイベントにはBehavior Windowで設定されたビヘイビアがある。レスポンスが設定済みで準備完了。
  • —— このイベントにはコードを通じてランタイムに登録されたリスナーがある。ビヘイビアは存在するが、プログラマティックに配線された。
  • オレンジ —— このイベントには設定されたビヘイビアがない。未使用か、レスポンスの設定を忘れたか。

Event Editorでオレンジの海が見えるなら、誰もリスニングしていないイベントがあるということだ。クリーンアップすべきデッドコードか、設定すべきレスポンスが欠けているか。いずれにしても、プレイヤーがバグを報告して初めて気づくのではなく、一目でわかる。

ワークフローの変革

冒頭のシナリオに戻ろう。デザイナーが3つの変更を求めている:大きなヒットの画面シェイクを強く、ヒットサウンドに0.5秒のディレイ、毒のティックレートを変更。

旧ワークフロー: デザイナーがプログラマーに頼む。プログラマーがコンテキストスイッチ。3つのファイル、3つの変更、1回のコンパイル、1回のテスト、1回の「やっぱり別の値にして」、もう1回のコンパイル。20分。

新ワークフロー: デザイナーがBehavior Windowを開く。画面シェイクの条件閾値を変更。サウンドディレイフィールドを変更。毒のリピート間隔を変更。Playモードでテスト。調整。再テスト。完了。3分。プログラマーは自分のタスクから離れなかった。

プログラマーはアーキテクチャを定義し、publicメソッドを公開するシステムを構築する。そしてRaise()呼び出しを書く:

[GameEventDropdown, SerializeField] private Float32GameEvent onDamageReceived;

public void TakeDamage(float amount)
{
// プログラマーの責任:データ付きでイベントを発行
onDamageReceived.Raise(amount);

// このイベントにレスポンスするすべてのものは
// デザイナーがBehavior Windowで設定する。
// プログラマーはそれらのレスポンスが何かを知る必要も気にする必要もない。
}

これがクリーンな分離だ。プログラマーは「どのイベントが存在し、いつ発火するか」を所有する。デザイナーは「レスポンスで何が起き、どのタイミングか」を所有する。どちらも相手をブロックしない。

実践例:完全なダメージレスポンスシステム

すべてをまとめよう。プレイヤーがダメージを受けた時に以下のレスポンスが欲しい:

  1. 即座に画面を赤くフラッシュ
  2. 小さなディレイの後にヒットサウンドを再生
  3. フローティングダメージナンバーを表示
  4. カメラシェイク、ただし大きなヒット(50ダメージ以上)のみ
  5. 6秒間で3回ティックする出血エフェクト

Behavior 1:画面フラッシュ

  • 条件:なし(常に発火)
  • アクション:ScreenEffects.FlashRed()
  • ディレイ:0.0s、リピート:0

Behavior 2:ヒットサウンド

  • 条件:なし
  • アクション:AudioManager.PlayHurtSound()
  • ディレイ:0.03s、リピート:0

Behavior 3:ダメージナンバー

  • 条件:なし
  • アクション:DamageUI.ShowNumber(float) —— ダメージ値をDynamicに受け取る
  • ディレイ:0.1s、リピート:0

Behavior 4:カメラシェイク

  • 条件:value > 50.0
  • アクション:CameraController.HeavyShake()
  • ディレイ:0.0s、リピート:0

Behavior 5:出血エフェクト

  • 条件:なし
  • アクション:PlayerHealth.ApplyBleedTick(float)
  • ディレイ:1.0s、リピート間隔:2.0s、リピート回数:3

すべてBehavior Windowで設定。デザイナーにできること:

  • カメラシェイクの閾値を50から30にフィールド1つの編集で変更
  • 出血のタイミングを2秒間隔から1.5に調整
  • 画面フラッシュをビヘイビアの削除で完全に無効化
  • 新しいレスポンス(コントローラー振動)を別のビヘイビアの追加で追加
  • ディレイを並べ替えてヒットの「手触り」を変える

これらの変更はどれもコードに触れる必要がない。再コンパイルも不要。ScreenEffectsAudioManagerCameraControllerPlayerHealthを作ったプログラマーはpublicメソッドを公開するだけでよかった。Behavior Windowがすべての配線、条件、スケジューリングを処理する。

Behavior Windowを使うべき時とコードを使うべき時

Behavior Windowはコードベースのイベント処理の置き換えではない。補完だ。実践でうまくいく分担はこうだ:

Behavior Windowを使うべき時:

  • レスポンスが単純(メソッド呼び出し、値の設定)
  • デザイナーがパラメータをイテレーションする必要がある
  • タイミングを素早く実験したい
  • レスポンスに複雑な分岐ロジックが不要

コードリスナーを使うべき時:

  • レスポンスに複雑なステートマシンロジックが含まれる
  • レスポンス前にイベントデータの処理が必要
  • レスポンスに非同期操作や複雑なコルーチンチェーンが含まれる
  • タイトループでパフォーマンスが重要

ほとんどのプロジェクトでは、60-70%のレスポンスがBehavior Windowで、30-40%がコードで設定される。デザイナーが多いチームほどBehavior Window側に偏る。重要なのは、デザイナー主導のレスポンスがプログラマーの空き状況にブロックされないことだ。

より大きな絵

Behavior Windowの本質は時間の節約ではない(もちろんそうなるが)。チームで誰が何をできるかを変えることだ。

従来のモデルでは、イベントレスポンスはプログラマーの領域だ。すべての微調整、すべての実験、すべての「これを試してみたら」がコード-コンパイルパイプラインを通る。これがデザイナーの創造性がプログラマーの空き状況にゲートされるボトルネックを生む。

Behavior Windowモデルでは、プログラマーはシステムを構築しイベントを発行する。デザイナーはレスポンスを設定し手触りをイテレーションする。ハンドオフはクリーンで、イテレーションは速く、どちらの役割も相手をブロックしない。これはツールの改善ではない——ワークフローの変革だ。

チームがイテレーション速度に苦しんでいるなら——すべての小さなゲームプレイ変更にコードコミットと再コンパイルサイクルが必要なら——Behavior Windowが最もインパクトのある変更かもしれない。1週間本気で試してほしい。デザイナーに自由に使わせてほしい。イテレーション速度に何が起きるか見届けてほしい。

次の投稿では、[GameEventDropdown]アトリビュートについて見ていく——コードに一行追加するだけで、Inspector内に検索可能で型安全なカテゴリ分けされたイベントピッカーが得られる。


🚀 グローバル開発者サービス

🇨🇳 中国開発者コミュニティ

🌐 グローバル開発者コミュニティ

📧 サポート