はじめに
Unityでゲームを作っていて、こんな悩みはありませんか?
• PlayerManager.cs が1000行を超えて何が書いてあるか分からない
• ダメージ計算のロジックがあちこちの Update() に散らばっている
• テストコードを書こうとしたけど、MonoBehaviourが絡んでいて無理だった
今回は、そんな「Unityあるある」なスパゲッティコードを整理するための設計思想、ドメイン駆動設計(DDD)のエッセンスをUnity開発に取り入れる方法を考察します。
なぜUnityでDDDなのか?
DDDはそもそも、コード綺麗にするものじゃなくて、エンジニアやそれに指示する人との認識をあわせて、仕様を落とし込んで、コードを見ることで、その仕様も把握できるというもの。
Unity開発では、油断するとすべてのロジックを MonoBehaviour(コンポーネント)に書きがちです。
しかし、ゲームの規模が大きくなると「ゲームのルール(計算や判定)」と「表示・演出(Unityの機能)」が密結合し、修正が困難になります。DDDを取り入れる最大のメリットは、この2つを明確に分離できることです。
• Unityの世界(View): 入力を受け取る、アニメーション再生、音を鳴らす
• Pure C#の世界(Domain): ダメージ計算、レベルアップ判定、所持金管理
これらを分けることで、バグが減り、仕様変更に強いコードになります。
Step 1: プリミティブ型をやめる (Value Object)
まずは一番導入しやすく、効果が高い「値オブジェクト (Value Object)」から始めましょう。
例えば、プレイヤーのHPを int で管理しているとします。
//よくある書き方
public class Player : MonoBehaviour {
[SerializeField] private int _hp;
[SerializeField] private int _maxHp;
public void Damage(int value) {
_hp -= value;
// マイナスにならないように...
if (_hp < 0) _hp = 0;
}
}これだと、「HPがマイナスにならない」というルールを、HPを減らす全ての場所で書く必要があります。
そこで、HPという概念自体をクラスにします。
// Value Objectの例
// MonoBehaviourは継承しない!Pure C#クラス
public class HitPoint {
public int Value { get; }
public int Max { get; }
public HitPoint(int value, int max) {
if (value < 0) value = 0;
if (value > max) value = max;
Value = value;
Max = max;
}
// ロジックをこのクラスに閉じ込める
public HitPoint Damage(int damageAmount) {
return new HitPoint(Value - damageAmount, Max);
}
public bool IsDead => Value == 0;
}こうすると、使う側(Playerコンポーネント)は計算ロジックを気にする必要がなくなります。
public class Player : MonoBehaviour {
private HitPoint _hp;
public void OnHit(int damage) {
// 計算はHitPointクラスにお任せ
_hp = _hp.Damage(damage);
if (_hp.IsDead) {
// Unityのアニメーション再生などはここでやる
_animator.SetTrigger("Die");
}
}
}Step 2: 3層に分けて考える
本格的にDDDをやるなら、フォルダ(namespace)を大きく3つに分けるのがおすすめです。
1. Domain層 (ゲームのルール)
• 依存先: なし
• 中身: Value Object, Entity, Repositoryインターフェース
• ルール: UnityEngine を使わない(Debug.LogくらいはOK)。Pure C#クラスのみ。
2. Application層 (進行役)
• 依存先: Domain層
• 中身: UseCase(「攻撃する」「アイテムを使う」などの操作単位)
• ルール: Domain層のオブジェクトを使ってゲームを進める。
3. Presentation / Infrastructure層 (Unityの実装)
• 依存先: Application層, Domain層
• 中身: MonoBehaviour, View, Repositoryの実装クラス
• ルール: ここで初めて SerializeField や UnityEvent が登場する。ユーザーの入力をApplication層に渡したり、結果を画面に反映する。
Unity x DDD の壁と対策
とはいえ、UnityはInspectorで値を設定したい事情(シリアライズ)があるため、完全なDDDは難しい場合があります。
対策: ViewとModelの同期
Pure C#クラス(Domain)はInspectorに表示されません。
そこで、「初期設定用のMonoBehaviour」 と 「実行時のDomainオブジェクト」 を分けるパターンが有効です。
• PlayerView (MonoBehaviour): Inspectorで初期HPなどを設定。Start()でDomainオブジェクトを生成。
• Player (Domain Entity): 実際のゲームロジックを持つ。
まとめ
UnityでDDDを始めるなら、まずは「重要なロジックをMonoBehaviourから剥がして、普通のクラスにする」ところから始めるのがよいかも。
1. int や string をそのまま使わず、Value Object にする。
2. 「計算」と「演出」を分ける意識を持つ。
これだけでも、コードの可読性と保守性は劇的に向上します。
おわりに
関連にDIコンテナ VContainerなどがあります。ある意味Playmakerを使うことでDDD的な動きになるんだよな。VOは別として。
特に「ステート(状態)とイベント(遷移)」など状態がある点において、Playmakerはドメインの振る舞いを可視化していると言える。
Playmakerが「DDDっぽい」理由
1. 「状態」の言語化 (ユビキタス言語)
• FSM(有限ステートマシン)を作る時、自然と「待機(Idle)」「攻撃(Attack)」「死亡(Dead)」といった ドメインの言葉 で状態を定義しますよね。これがそのまま仕様書代わりに近い。
2. ロジックの分離 (関心の分離)
• 各Stateの中にあるAction(「移動する」「音を鳴らす」)は、細かい実装コードを隠蔽した 「意図」 の集まりです。プログラマ以外(プランナーなど)でも「ここで何が起きるべきか(ルール)」を構築できるのは、実装の詳細から切り離されているから。
3. イベント駆動
• 「ダメージを受けた(ON_DAMAGE)」「HPが0になった(HP_ZERO)」というイベントで遷移させる作りは、ドメインイベントに近い考え方になるがDDDになりきれない部分。
一方で、Playmaker特有の罠もあり、これがさっきいってたVOは別って言った内容で、
• グローバル変数の乱用: Global Variables に何でも突っ込むと、DDDで一番避けたい「どこからでも変更できる密結合なデータ」になりがちです。
• ロジックとViewの混在: Actionの中に「HP計算」と「エフェクト再生」を同じリストに並べてしまうと、結局「計算」と「演出」が混ざってしまいます(ここは設計者の腕の見せ所)。
「FSMで業務フロー(ドメインロジック)を描き、Actionで具体的な処理を行う」 と割り切って使えば、Playmakerは立派な設計ツールになるかと思います!
現場はPdMがダメすぎなのに指示は絶対だったり、現場独自の言葉があると、???ってのができあがってること、あるよね。ソモソモPdM2年で入れ替わったりして過去のが意味なくなったりすることあるよね。エンジニアリーダーもいなくなって、読み返してもナニコレ的なのが無くならない。


