本記事は CA Tech Lounge Advent Calendar 2025 7日目の記事となります。
はじめに
Unityにおける設計の参考となる指針として、とりすーぷ氏(@toRisouP)の設計レベルの定義が存在しますが、具体的にどのようなコードがどの設計レベルに該当するのでしょうか?
本記事シリーズでは、簡単な将棋ゲームを具体例にリファクタリングを繰り返し、より保守性や安全性、パフォーマンスの高いコードを目指します。
本記事では筆者が設計レベルを独自解釈して進めていきます。
そのため参考記事の設計レベルと必ずしも一致しない場合があります。
1つの設計の手法としてお楽しみください。
ロードマップ
Part1 : 体裁を整える
Part2 : データをプログラムで保持する
Part3 : オブジェクトに分割する
Part4 : 依存の方向性を整える ←この記事
以下作業中(変わる可能性あり)
Part5 : ロジックと演出を分離する
Part6 : インターフェースを導入する
Part7 : ModelをUnityから切り離す
Part8 : 安全性を考える
Part9 : パフォーマンスを考える
Part10 : 拡張性があるか検証する(オンライン対応する)
Part4 : 依存の方向性を整える
本章では、循環依存や、修正や拡張のしやすい依存の方向性について考え、依存の方向性を整えていきます。
1. 循環依存をなくす
循環依存とは
2つのオブジェクトがお互いに
- メソッドを呼び出す
- メンバーを直接参照する
といったことを行い、処理が密接に関連している状態です。
この状態は一般に問題の起きやすい設計といわれています。
類義語 : 相互依存, 循環参照, 密結合 など
本記事の将棋アプリだと、以下の部分が循環依存になっています
GameManagerではプレイヤーのターン開始時にPlayerのターン開始処理を呼び出している
// 次のプレイヤーのターンを開始
PlayerSide nextPlayerSide = (endedPlayerSide == PlayerSide.BOTTOM) ? PlayerSide.TOP : PlayerSide.BOTTOM;
if (nextPlayerSide == PlayerSide.BOTTOM)
{
_playerBottom.StartPlayerTurn();
}
else
{
_playerTop.StartPlayerTurn();
}
Playerでは王をとってゲームが終了した際にGameManagerの終了処理を呼び出している
// 王 or 玉なら試合終了
if(occupyingPiece.PieceType == PieceType.OU || occupyingPiece.PieceType == PieceType.GYOKU)
{
// ゲーム終了処理を実行
_currentPhase = PlayerTurnPhaseType.GAME_ENDED;
_gameManager.OnGameEnded(piece.PlayerSide);
yield break; // コルーチンを終了
}
循環依存の問題点
循環依存ではどんなことが問題となりやすいのでしょうか?
1.依存するということは修正回数が多くなる
依存しているということはそのオブジェクトのメソッドを呼び出したり、メンバーを参照して処理を行うということになります。
これは、依存されている側のオブジェクトの変更の影響をそのまま受け、依存する側の処理も修正する必要があります。
ここで重要なのは、
「依存されている側は、依存している側の変更による修正が発生しない」
ということです。
2.「どちらに責務を持たせるべきか」の判断が難しくなる
循環依存をしている状態では、欲しい情報に簡単にアクセスでき、どちらにも処理を書くことができるため、一見便利に感じます。
しかし、どちらに書いても実現できることが裏目となり、オブジェクトの責務が分かりにくくなりやすいです。
3.両方ないと動かない状態となり、独立で動かしたり、取り換えることが難しくなる
道具が正しく動作するかどうかをテストしたり、使用者を別のオブジェクトに変更しようとする際、
一方向の参照であれば、依存する側を取り換えるだけでテストや道具の使用が行えます。
ですが循環参照の場合、道具のテストのためだけに、使用者もあわせてテストする必要が発生したり、別オブジェクトでも使用できるように道具側にも変更を加える必要が生じます。
循環依存の対策
これらの問題点から循環依存を防ごうと考えたとき、どのようなことをすればよいのでしょうか?
防ぐための基本方針は単純で、 「依存を片方向に寄せる」 ことです。
しかし、Playerのターンが終了したことをGameManagerはどのように認識すればよいのかわからず、仕方なく循環依存を作っている人もいるかもしれません。
次の項では、依存方向を逆転する方法について述べていきます。
2. 依存方向を逆転する方法 : イベントの活用
プログラミング言語には、関数を変数に登録し、変数経由で処理を呼び出すための機能が備わっています。
例 : Cの関数ポインタ, C#のデリゲート
詳細は書籍やWebを参照してください。
Unityで使用されている身近な例としてButtonがあります。
_button.onClick.AddListener(() =>
{
// ボタンが押された際の処理をここに書く
});
デリゲートを用いることで、
依存される側のタイミングで、依存する側が実行したい処理を実行
することができます。
また、処理の登録は依存する側や第三者のオブジェクトから行うことができるため、依存される側からの循環依存をなくすことができます。
class GameManaer
{
private Player _player;
Start()
{
// ターン終了のタイミングでターン終了処理を実行するように登録する
_player.OnTurnEnd += TurnEnd;
}
TurnEnd()
{
// ターン終了時の処理
}
}
class Player
{
// ターン終了時に実行するイベント
public event Action OnTurnEnd;
EnterTurnEnd()
{
// ターン終了のタイミングで登録された処理を実行
OnTurnEnd?.Invoke();
}
}
イベント利用の副効果
イベントの利用による副効果として、オブジェクトごとの責務がさらに明確になります。
被使用者は
「こういうことが起きた」という事実だけを通知すればよく、
その通知が どのように解釈・利用されるか を意識する必要がなくなります。
例えば
これまで Player は、「ターンの終了」を意識してGameManager の処理を直接呼び出す必要がありました。
しかしデリゲートを用いることで、Player は「駒の移動が完了した」というイベントを発行するだけで済み、そのイベントを GameManager 側が「ターン終了」として解釈します。
また、その通知を新たな別オブジェクトが受け取って処理を行うことも可能です
(画像例ではUIが該当)
デリゲートの弱点
このように大きな利点のあるデリゲートですが、
利用が難しい場面もいくつかあります。
1. ライフタイム管理が難しい
デリゲートは次のような形で登録と解除が行えますが、ラムダ式や匿名関数を用いた場合には管理が難しくなります。
someEvent += SomeMethod; // 登録
someEvent -= SomeMethod; // 解除
// ラムダ式を用いた処理の登録
someEvent += () =>
{
Debug.Log("イベント発行");
};
// 同一インスタンスではないため、解除できない
someEvent -= () =>
{
Debug.Log("イベント発行");
};
ラムダ式の解除については、 おみずさん が詳しく解説されていましたので紹介します。
2. イベントの受け取り条件の細かな設定ができない
デリゲート自体は登録された処理を順に実行する機能しか持たないため、
- 1度だけ実行したい
- 状態が○○のときだけ実行したい
といった条件を設けて処理を実行する場合、
呼ばれる側で条件を管理する必要があります。
bool isAlreadyCalled = false;
void OnEvent()
{
// すでに呼ばれていたら実行しない
if (!isAlreadyCalled) return;
isAlreadyCalled = true;
// 処理
}
次の項では、これらの弱点を克服し、より柔軟に使用できるR3(Rx)を紹介します。
R3(Rx)について
Rx(Reactive Extension) は、C#のオブザーバーパターンライブラリであり、
LINQと同じ操作で購読条件の設定ができ、
購読解除についても自由に管理できます。
R3 はCysharpの開発したRxの現代的な実装であり、
UniRxと同様にUnity向けのイベント(Collider、Click など)も豊富に用意されています。
使用例
- 1回だけ受け取りたい
_objA.OnEvent
.Take(1)
.Subscribe(_ =>
{
Debug.Log("1度だけ処理");
});
- ライフタイムを紐づけたい
_objA.OnEvent
.Subscribe(_ =>
{
Debug.Log("処理");
})
.AddTo(this); // このGameObjectの破棄に合わせて購読を解除
IDisposable b = _objB.OnEvent
.Subscribe(_ =>
{
Debug.Log("処理");
});
b?.Dispose(); // 任意のタイミングでこの購読だけ解除
- UnityのイベントをRxで受け取りたい
private void OnTriggerEnter(Collider other)
{
Debug.Log("標準バージョン");
}
this.OnTriggerEnterAsObservable()
// .Where(c => c.CompareTag("aaa")) // タグを事前に絞ることもできる
.Subscribe(_ =>
{
Debug.Log("R3バージョン");
});
次項からは、ここまでで紹介した依存方向の修正方法を用いて、
どのように依存方向を決定していくかについて述べていきます。
3. 依存方向を決めるための考え方
循環依存を治す際、どちらを依存する側に回せば
開発が有利になりやすいのでしょうか?
また、元々1方向だった依存についても
その方向性が本当に有利になっているのか考えたいところです。
そこで、依存の方向性について考えるための指針を3つ紹介します。
指針① : 変更の多さで考える
変更回数が多いということはその依存先に修正を必要としやすいということです。
そのため、変更の多いオブジェクトは依存する側に回すことで、変更の少ないオブジェクトへの影響を減らすことができます。
この指針で将棋ゲームの依存方向を評価してみます。
UI
UIは、様々なパラメータを表示したり、演出を変更する関係上、変更が多いオブジェクトになります
ゲーム進行管理者(GameManager)
ゲーム進行管理は、「待った」機能の追加等、ゲームの流れを変える際に変更が発生します。
ゲームの仕様変更は演出の変更ほどではないですが、割と発生するものです。
駒
駒は将棋において種類やできることが決まっているオブジェクトであり、自作ルールを駒に実装するようなことのない限り変更が発生しません。
そのため、このゲームにおいては変更の少ないオブジェクトといえます。
以上を踏まえて
以上の考察を踏まえると、
- UI : 変更多
- ゲーム進行管理者 : 変更中
- 駒 : 変更少
であるため、以下のような階層の依存関係にするとよさそうです。
指針② : 使用者・被使用者で考える
- 使用者 : 命令を出す側
- 被使用者 : 具体的な動きをする側
といったように役割分担を意識することで、依存関係について整理することができます。
今回もこの指針で将棋ゲームの依存方向を評価してみます。
将棋盤
盤上の駒を 「管理」 する
将棋盤 → 駒
Player
将棋盤や駒を 「使用」 する
Player → 将棋盤, 駒
ゲーム進行管理者
Playerのターンを 「管理」 する
GameManager → Player
UI
Playerや駒の状態を 「使用」 する
UI → Player, 駒
PlayerがHPを表示するためにUIを使用する といったように考えることもできますが、ここは他の指針(①変更量, ③知る必要)を考えて、UIが使用する側としています。
駒
自分の動きだけ を考える
→ 依存なし
以上を踏まえて
以上の考察を踏まえると、次のような依存フローが考えられます。
指針③ : 「知る必要があるか」で考える
これは 「知らなくても成り立つ情報には依存しない」 という考え方で、
依存関係を断ち切り、オブジェクトが独立で動くようにします。
例として、アクションゲームでPlayerにかかわりのある要素として、HPや武器などがあります。
ですが、「HPが減るとUIのHPバーが徐々に減少する」といったことは知らずともPlayerは動くことができます。
この指針も将棋ゲームにあてはめてみましょう。
Player
- 駒や将棋盤の状態を知る必要はある
- 駒やマスがどのような方法で選択されたかは知らなくてよい
- 矢印キーで選択するのか、マウスクリックで選択するのか等
- ゲームの進行方法を知らなくてよい
- 自分のターンに盤面だけ見て行動すればよいはず
→ GamaManagerやInputは知らなくてよい、駒や将棋盤は知る必要がある
入力管理者
- 入力で選択される対象については知る必要がある
- 駒, 将棋盤のマス
- 入力情報をPlayerに流すことは知らなくてもよいはず
- 今後、入力情報を欲しがるオブジェクトは増えるかもしれない
→ Player(入力情報の届け先)については知らなくてよい、駒や将棋盤のマスは知る必要がある
以上を踏まえて
以上の考察を踏まえると、InputとPlayerは互いに知る必要がなさそうです。
実態としては、Playerは選択情報が必要なので、InputとPlayerのイベントを登録するBindの役割を用意します。
Bindを追加し、PlayerとInputを独立させると以下のような依存関係になります。
独立で動かすことができるこの考え方は強力ですが、
どこまで分離するかをよく考えないと、
逆にどこから呼ばれているのかを探ることが難しくなる ため、吟味が必要です。
(使用者関係が明確で、指針②が適用できる場合はそちらを適用する)
指針に従って将棋ゲームをリファクタリングする
前項で評価した指針に戻づいて、依存方向を整理します。
ついでに、
今まで
駒が「移動」の際に「成りの確認」と「UIの選択の待機」
を行っていた部分を修正し、
「成りの確認」と「UIの選択の待機」をPlayerで行い、駒の「移動」と「成る」をどちらもPlayer経由で呼ぶ
ように変更します。
主な変更点
- UIがGameManagerに依存するように依存の方向を修正(指針①)
- GameManagerとPlayerの循環依存を解消し、GameManagerがPlayerに依存するように修正(指針①)
- GameManager側でPlayerの状態の切り替わりを監視し、駒の移動の終了に合わせて次のターンに回す処理を登録
- 成り確認を表示する部分はIngameUIManagerとPlayerの間にPromotionDecisionを挟むことでお互いの関心を分離(指針②)
- Playerは成るかどうかの結果だけ問い合わせ、詳しいことはPromotionDecisionに任せる
- PromotionはUIを表示してその結果を返す具体的な操作を行う
- Playerをターンから切り離し、王をとったイベントの発行、ターン終了や相手のターン待ち状態はまとめてIDLE状態に変更(指針③)
- InputManagerとPlayerを外から紐づけるInputBinderを追加し、PlayerとInputManagerの依存関係を分離(指針③)
今回の変更にかかわる部分だけを取り出すと下記の依存関係になりました。
Player→PromotionDecision→IngameUIManager
の部分が逆流しているように見えますが、
こちらはインターフェース回(Part5予定)で修正します
実際の変更内容
おまけ : namespace分割
このタイミングでオブジェクトごとにnamespaceを区切り、フォルダ分けを行いました。
実際の変更内容
Part4まとめ
Part4では、依存の方向性について、何が問題となるのか、問題になりにくい依存の方向性とはどんなものなのかについて述べ、依存方向を修正しました。
- 循環依存は避け、依存方向を意識する
- 判断基準は
- ① 変更の多さ
- ② 使用者・被使用者の関係
- ③ 知る必要があるか
- イベントやR3を使って、依存を逆転し責務を整理する
Part5では変更の多い演出(View)と変更の少ないビジネスロジック(Model)を分離するMVPパターンについて解説します。
Part5も近々投稿します。

