カードゲームの対戦機能をクライアント主体で設計したらこうなった
はじめに
お久しぶりです。にわとりです。
過去に個人開発でカードゲームをリリースしたことがありまして、その時の設計、特にインゲーム部分の設計を公開したいなと以前から思ってました。
この度 claude code のおかげでコードベースから設計書の書き起こしが大変簡単にできるようになったので、その内容を共有します。
僕個人は他人の設計書を読むのは比較的楽しいと感じるほうなので、そのような方、とくにカードゲームのロジックの複雑さを肴に酒が飲めるような方に楽しんでもらえると幸いです。
ゲーム概要やルール説明は一切省略して始めますがご了承ください。では、以下からは claude と一緒に書いた内容です。
概要
本ドキュメントでは、デジタルカードゲーム(DCG)のバトルシステムにおけるクラス間の関係性を、ゲームフローに沿って解説します。
前提条件
- 複雑なカード効果が必要
- カードの召喚、移動、攻撃
- トリガー型能力と継続効果
- エリア効果とパラメータ補正
- ターン制の複雑なフェーズ管理
- AI対戦とネットワーク対戦への対応
- リプレイ・観戦機能
- 途中参加・再接続機能
1. データ管理モデルとアーキテクチャ方針
1.1 クライアントサイド同期型アーキテクチャ
本バトルシステムはクライアントサイド同期型のアーキテクチャを採用しています。これは、サーバー上で盤面データを管理する方式とは対照的なアプローチです。
2つのアーキテクチャモデル:
| 項目 | サーバー管理型 | クライアントサイド同期型(本システム) |
|---|---|---|
| 盤面データの場所 | サーバー上で一元管理 | 各プレイヤーのクライアント上で独立管理 |
| 主体 | サーバーが絶対的な真実 | すべてのクライアントが対等(主従関係なし) |
| 同期方式 | クライアント → サーバー → クライアント | クライアント ⇄ クライアント(Commandの送受信) |
| 計算負荷 | サーバーに集中 | クライアントに分散 |
| 遅延の影響 | サーバー応答待ちで遅延発生 | ローカル処理のため即座に反応 |
| チート対策 | サーバーで検証(強固) | 署名検証 + 再接続時の整合性チェック |
1.2 クライアントサイド同期型の仕組み
本システムでは、すべてのプレイヤーのクライアントで全く同じ盤面進行が行われることを前提としています。
動作原理:
- 決定論的な処理: 同じCommand履歴を同じ順序で実行すれば、必ず同じBattleDataになる
- Commandの共有: 各プレイヤーは自分の操作をCommandに変換してネットワーク経由で送信
- 同期実行: 全クライアントが受信したCommandを同じ順序で実行
- 整合性の保証: Command自体がイミュータブル(不変)で、署名検証により改ざんを検知
1.3 このアーキテクチャの利点と課題
利点:
- ✅ 低レイテンシ: 自分の操作は即座にローカルで反映(サーバー応答待ちなし)
- ✅ サーバーコスト削減: ゲームロジックの計算をクライアントに分散
- ✅ リプレイ機能: Command履歴を保存するだけで対戦の完全再現が可能
- ✅ 再接続対応: Command履歴を取得して再実行すれば、切断後も復帰可能
課題:
- ⚠️ チート対策: クライアントで処理するため、メモリ改ざん等のリスク
- 対策: 署名検証、再接続時の整合性チェック
- ⚠️ 同期ズレのリスク: 実装バグがあると盤面がズレる
- 対策: 決定論的な処理の徹底、単体テストでの検証
2. ゲーム進行とクラスの役割
ユーザー操作やカード効果に応じて盤面状況を変化させる、というのが基本的な流れになります。
各コンポーネントの説明:
| コンポーネント | 説明 |
|---|---|
| BattleData | 盤面データの保持(詳細は別表) |
| Operation | ユーザーが実行可能な操作を表現(実行前の検証情報を含む) |
| Command | 盤面を更新する不変オブジェクト(履歴として保存可能) |
| Trigger | 盤面変更イベント(Ability発動条件、UI通知に使用) |
補足説明:
- Operation: カードの移動、攻撃、ターン終了など12種類。UI表示用の選択肢データ(AvailableCells等)や事前バリデーション結果(WillSuccessIfSelect)を含む。Commandとは異なり、実行前の「可能性」を表現する
- Command: 29種類の不変オブジェクト。攻撃宣言、ダメージ計算、ターン進行など、すべての盤面変更を表現。JSON形式でシリアライズ可能で、ネットワーク同期・リプレイ・再接続に使用
- Trigger: 40種類のイベント。Abilityの発動条件判定とUIへの変更通知の2つの用途で使用。Actor(主語)、Target(対象)、Damage(ダメージ量)などのコンテキスト情報を含む。UniRxのIObservable<IBattleTrigger>として購読可能
BattleDataの構成要素:
| 構成要素 | 説明 |
|---|---|
| State | 現在のゲームフェーズを表す33種類の状態。ドロー、召喚、攻撃などターン進行の各段階を管理し、実行可能なOperationやTrigger発火条件を決定。例:MainStateでは召喚・移動・攻撃が可能、DrawStateではドロー/チャージの選択のみ可能。Commandによって遷移する |
| Cards | すべてのカード情報を保持するOrderedDictionary。デッキ/手札/フィールド/墓地の区別はLayerKindプロパティ(Hand/Strategy/Field/Book/Grave)で管理。各カードはHP、攻撃力、位置、状態異常、Abilityリストなどのパラメータを持つ。すべてのカードが単一コレクションに統合され、LayerKindで位置を区別する設計 |
| ┗ Ability | カードに付与された効果。Trigger(発動条件)+ Targeting(対象選択)+ Effect(効果内容)の3要素で構成。JSONで定義され、実行時に動的に読み込まれる |
| Cells | フィールドのマス情報を保持するDictionary。各マスは(x, y)座標を持ち、カードの配置位置を管理 |
| Host/Guest | 2人のプレイヤー情報。HP、MP、攻撃回数、ドロー回数など、プレイヤー固有のパラメータを保持 |
| TurnCount | 現在のターン数を示す整数値。ターン開始時にインクリメントされ、一部の効果発動条件に使用される |
| IsFinished | バトル終了フラグ。trueの場合、勝敗が確定しゲーム進行を停止 |
2.1 Operation - ユーザー操作のデータ表現
Operationは、ユーザーが実行可能な操作を表現するデータ構造です。Commandとは異なり、**実行前の「可能性」**を表現します。
Operationの特徴:
-
UI表示用データを含む
-
AvailableCells: 選択可能なマスのリスト -
WillSuccessIfSelect: 選択すれば成功するかの事前判定
-
-
事前バリデーション結果
- 実行できない操作は
IsSuccess = falseとなり、UIでグレーアウト表示される - ユーザーに「なぜ実行できないか」を視覚的にフィードバック
- 実行できない操作は
-
BattleDataの読み取り専用参照
- BattleDataとStateを参照して、実行可能な操作を判定
- BattleDataを直接変更することはできない
全12種類のOperation:
| Operation種別 | 説明 | 例 |
|---|---|---|
| MoveDeclare | カードの移動宣言 | フィールド上のカードを別のマスへ移動 |
| Attack | 攻撃 | 敵カードへの攻撃 |
| Direct | 直接攻撃 | プレイヤーへの直接攻撃 |
| Summon | 召喚 | 手札からフィールドへカードを召喚 |
| Feint | フェイント召喚 | 伏せカードとして召喚 |
| TurnFinish | ターン終了 | 現在のターンを終了 |
| DrawOrCharge | ドロー/チャージ選択 | カードを引くかMPを回復するか選択 |
| StrategyMove | ストラテジー移動 | 初期配置時のカード移動 |
| ReactSummon | リアクション召喚 | 移動に対する召喚リアクション |
| ReactFeint | リアクションフェイント | 移動に対するフェイントリアクション |
| ReactNone | リアクションなし | リアクションをスキップ |
| MoveRetry | 移動先再選択 | リアクション後の移動先変更 |
2.2 State - ゲームフェーズ管理
DCGにおけるターンの各フェーズを表現します。
全33種類のState:
| カテゴリ | State名 | 説明(ゲームフロー視点) | 遷移タイプ |
|---|---|---|---|
| バトル開始・終了 | InitialState | 初期状態(セットアップ待ち) | プレイヤー入力待ち |
| CointossState | コイントス(先攻/後攻決定) | プレイヤー入力待ち | |
| StrategyState | ストラテジータイム(初期配置) | プレイヤー入力待ち | |
| BattleStartState | バトル開始 | 自動遷移 | |
| BattleFinishState | バトル終了 | 終了 | |
| ターン制御 | TurnStartState | ターン開始 | 自動遷移 |
| DrawState | ドロー/チャージ選択 | プレイヤー入力待ち | |
| MainState | メインフェーズ(行動選択) | プレイヤー入力待ち | |
| TurnFinishState | ターン終了 | 自動遷移 | |
| PoisonDamageState | 毒ダメージ処理 | 自動遷移 | |
| 移動 | MoveDeclareState | 移動宣言 | プレイヤー入力待ち |
| MoveRetryState | 移動再試行(リアクション) | プレイヤー入力待ち | |
| MoveResultState | 移動完了 | 自動遷移 | |
| 攻撃 | AttackDeclareState | 攻撃宣言 | 自動遷移 |
| AttackDamageState | 攻撃ダメージ計算 | 自動遷移 | |
| AttackDestroyState | 攻撃破壊判定 | 自動遷移 | |
| 直接攻撃 | DirectDeclareState | 直接攻撃宣言 | 自動遷移 |
| DirectDamageState | 直接攻撃ダメージ | 自動遷移 | |
| 召喚・フェイント | SummonDeclareState | 召喚宣言 | 自動遷移 |
| FeintDeclareState | フェイント宣言 | 自動遷移 | |
| 効果発動 | EffectDeclareState | 効果発動宣言 | 自動遷移 |
| EffectInterruptState | 効果割り込み状態 | 能力発動チェック |
遷移タイプの説明:
- プレイヤー入力待ち: プレイヤーの操作(Command)を待機する状態
- 自動遷移: 処理完了後、自動的に次のStateへ遷移
- 能力発動チェック: 発動可能な能力があれば EffectDeclareState へ、なければ元の State に戻る
状態遷移図:
状態遷移の特徴:
-
自動遷移:
AttackDeclareState→AttackDamageState→AttackDestroyStateのような連鎖は自動 -
能力割り込み: 各タイミングで
EffectInterruptStateに遷移し、能力発動後に元のStateに戻る -
プレイヤー入力待ち:
MainState,DrawState,MoveDeclareState等で入力を待機
2.3 Command - 盤面変更の指示
プレイヤーの行動やシステムの自動処理を表現するイミュータブルなデータです。
全29種類のCommand:
| カテゴリ | Command名 | 説明(ユーザー視点) | 実行タイプ |
|---|---|---|---|
| バトル開始・終了 | InitialCommand | 準備完了を宣言する | プレイヤー入力 |
| CointossCommand | コイントスで先攻/後攻を決定する | プレイヤー入力 | |
| StrategyCommand | 初期配置フェーズを開始する | プレイヤー入力 | |
| BattleFinishCommand | バトルを終了する | 自動実行 | |
| BattleForceFinishCommand | バトルを強制終了する(切断等) | 自動実行 | |
| ターン制御 | TurnStartCommand | 新しいターンを開始する | 自動実行 |
| TurnFinishCommand | ターンを終了する | プレイヤー入力 | |
| DrawCommand | カードを1枚引く、またはMPを1回復する | プレイヤー入力 | |
| 召喚 | SummonDeclareCommand | 手札のカードを場に召喚する | プレイヤー入力 |
| SummonFinishCommand | 召喚処理を完了する | 自動実行 | |
| 移動 | MoveDeclareCommand | 場のカードを指定したマスに移動させる | プレイヤー入力 |
| MoveReactNoneCommand | 移動に対してリアクションしない | プレイヤー入力 | |
| MoveRetryCommand | 移動を別のマスに変更する(リアクション時) | プレイヤー入力 | |
| MoveFinishCommand | 移動処理を完了する | 自動実行 | |
| 攻撃 | AttackDeclareCommand | 場のカードで攻撃する | プレイヤー入力 |
| AttackDamageCommand | 攻撃ダメージを計算する | 自動実行 | |
| AttackDestroyCommand | 破壊されたカードを墓地に送る | 自動実行 | |
| 直接攻撃 | DirectDeclareCommand | 相手プレイヤーに直接攻撃する | プレイヤー入力 |
| DirectDamageCommand | 直接攻撃ダメージを与える | 自動実行 | |
| フェイント | FeintDeclareCommand | フェイントを使用して攻撃を無効化する | プレイヤー入力 |
| FeintFinishCommand | フェイント処理を完了する | 自動実行 | |
| 能力発動 | EffectDeclareCommand | カードの能力を発動する | 自動実行 |
| EffectFinishCommand | 能力発動処理を完了する | 自動実行 | |
| 毒ダメージ | PoisonDamageCommand | ターン終了時の毒ダメージを処理する | 自動実行 |
| PoisonFinishCommand | 毒ダメージ処理を完了する | 自動実行 |
実行タイプの説明:
- プレイヤー入力: プレイヤーが明示的に選択する行動
- 自動実行: ゲームルールに従って自動的に実行される処理(プレイヤーは選択できないが、内部的に状態を進める)
重要な特性:
- すべてのCommandは履歴として保存される
- リプレイ機能では、この履歴を再実行することで対戦を再現
- ネットワーク対戦では、Commandを送受信して盤面を同期
- 署名検証により改ざんを検知(チート対策)
OperationとCommandの分割理由:
- Operationは自分の操作のみを表現(BattleDataを読み取って実行可能かを判定)
- Commandは自分と相手の操作の両方を表現(ネットワーク経由で受信した相手の操作にも対応)
- この分割により、自分の操作検証と相手の操作受信を独立して処理できる
2.4 Trigger - イベント通知
ゲーム内で発生したイベントを表現し、カード能力の発動タイミングを判定します。
全40種類のTrigger:
| カテゴリ | Trigger名 | 発生タイミング(ユーザー視点) |
|---|---|---|
| 召喚 | SummonedTrigger | カードが場に召喚された時 |
| StrategySummonedTrigger | 初期配置フェーズでカードが場に出た時 | |
| 移動 | MoveDeclaredTrigger | カードが移動を宣言した時 |
| MovedTrigger | カードが移動を完了した時 | |
| MoveFinishedTrigger | 移動処理が完全に終了した時 | |
| TeleportedTrigger | カードがテレポート(瞬間移動)した時 | |
| 攻撃 | AttackDeclaredTrigger | カードが攻撃を宣言した時 |
| AttackDamagedTrigger | カードが攻撃ダメージを受けた時 | |
| AttackDestroyedTrigger | カードが攻撃によって破壊された時 | |
| AttackFinishedTrigger | 攻撃処理が完全に終了した時 | |
| AttackCountChangedTrigger | カードの攻撃回数が変化した時 | |
| 直接攻撃 | DirectDeclaredTrigger | プレイヤーへの直接攻撃を宣言した時 |
| DirectDamagedTrigger | プレイヤーが直接攻撃ダメージを受けた時 | |
| DirectFinishedTrigger | 直接攻撃処理が完全に終了した時 | |
| 能力効果 | EffectDeclaredTrigger | カードの能力が発動した時 |
| EffectDamagedCardTrigger | カードが能力によってダメージを受けた時 | |
| EffectDamagedPlayerHpTrigger | プレイヤーが能力によってダメージを受けた時 | |
| EffectRecoveredCardTrigger | カードが能力によって回復した時 | |
| EffectRecoveredPlayerHpTrigger | プレイヤーが能力によって回復した時 | |
| EffectDestroyedTrigger | カードが能力によって破壊された時 | |
| ダメージ・破壊 | CostDamagedTrigger | コスト支払いによってダメージを受けた時 |
| CardGravedTrigger | カードが墓地に送られた時 | |
| ドロー | DrawTrigger | カードを引いた時 |
| 状態変化 | CardAilmentChangedTrigger | カードの状態異常が変化した時 |
| AbilityAttachedTrigger | カードに能力が付与された時 | |
| AbilityDetachedTrigger | カードから能力が除去された時 | |
| AbilityOblivionChangedTrigger | カードの能力封印状態が変化した時 | |
| プレイヤー | PlayerMpChangedTrigger | プレイヤーのMPが変化した時 |
| PlayerHpInitializedTrigger | プレイヤーのHPが初期化された時 | |
| ターン | TurnStartedTrigger | ターンが開始された時 |
| TurnFinishTrigger | ターンが終了した時 | |
| StrategyInitializedTrigger | 初期配置フェーズが開始された時 | |
| システム | StateChangedTrigger | バトルの状態が変化した時 |
| 悪夢 | NightmareSealedTrigger | 悪夢能力が封印された時 |
| NightmareUnsealedTrigger | 悪夢能力が解放された時 |
使用例:
-
「場に出た時、相手プレイヤーに1ダメージ」
→SummonedTriggerを条件に、PlayerDamageEffectを発動 -
「攻撃された時、攻撃者に2ダメージを与える」
→AttackDamagedTriggerを条件に、CardDamageEffectを発動 -
「ターン開始時、HPを1回復」
→TurnStartedTriggerを条件に、PlayerRecoverHpEffectを発動
重要な特性:
- Triggerは能力発動の条件判定に使用される
- 1つのアクションで複数のTriggerが発行されることがある(例: 攻撃→ダメージ→破壊)
- Triggerに反応した能力が実行されると、新たなTriggerが発行される(連鎖)
連鎖の具体例:
例1: 召喚→ダメージ→破壊の連鎖
カードA「場に出た時、敵全体に1ダメージ」を召喚
1. SummonedTrigger発行
2. → カードAの能力発動 (EffectDeclaredTrigger)
3. → CardDamageEffect実行 → 敵カードB,C,Dにダメージ
4. → EffectDamagedCardTrigger × 3発行
5. → カードB「ダメージを受けた時、相手プレイヤーに2ダメージ」発動
6. → PlayerDamageEffect実行
7. → EffectDamagedPlayerHpTrigger発行
8. → カードBのHP <= 0で破壊
9. → CardGravedTrigger発行
10. → カードC「味方が破壊された時、HPを2回復」発動
2.5 Ability - カード能力の仕組み
カードが持つ特殊能力を定義するシステム。Abilityは Targeting, Effect, Calculator, Comparator という4つの構成要素から組み立てられ、これらの組み合わせにより多様な能力を表現します。
Abilityの4つのタイプ
| タイプ | 構成要素 | 説明 |
|---|---|---|
| TriggerAbility | Targeting + Effect | 特定の条件で発動し、効果を実行 |
| ParameterAbility | Calculator + Targeting | パラメータを補正し、有効期間を管理 |
| AreaAbility | Targeting + Effect | 範囲内のカードに効果を付与 |
| NightmareAbility | Targeting | 条件を満たすまで他の能力を封印 |
構成要素1: Targeting(対象選択)
役割:
- 能力の発動条件を判定
- 効果の対象を選択
- 能力の有効期間を管理
使用箇所:
- TriggerAbility の
Conditions(発動条件) - ParameterAbility の
Lifetime(有効期間) - AreaAbility の
EffectTarget,ReachInCondition(範囲判定) - Effect の
Target(効果対象)
設計思想:
クエリ構造(Sentence/Paragraph)で宣言的に記述
例: 「敵の前列にいるHP3以下のカード」を選択
MapCardTargeting (すべてのカードを起点)
→ FilterCardOwnerTargeting (敵のカードに絞り込み)
→ FilterLayerTargeting (前列に絞り込み)
→ FilterCardHpTargeting (HP3以下に絞り込み)
Targeting の構造:
Targetingは Head(起点) と Pipe(フィルタ) を組み合わせたクエリ構造で構成されます。
Head(起点)- 対象の起点を定義:
| Targeting名 | 説明 | 使用例 |
|---|---|---|
| MapCardTargeting | すべてのカードを起点 | 「敵のカード」「HP3以下のカード」など |
| MapPlayerTargeting | プレイヤーを起点 | 「相手プレイヤー」「自分」など |
| MapCellTargeting | フィールドのマスを起点 | 「前列のマス」「空いているマス」など |
| MapActorCardTargeting | 能力の発動者カードを起点 | 「このカード」「発動者」など |
| MapTargetCardTargeting | 攻撃対象カードを起点 | 「攻撃されたカード」など |
| MapFollowCellTargeting | 特定のマスに隣接するマスを起点 | 「周囲1マス」「隣接マス」など |
| MapAbilityTargeting | 能力自体を起点 | 能力の発動回数チェックなど |
| AnyTargeting | 常に真を返す(条件なし) | 「常に発動」など |
Pipe(フィルタ)- 対象を絞り込む:
| カテゴリ | Targeting名 | 説明 | 使用例 |
|---|---|---|---|
| カード属性 | FilterCardOwnerTargeting | カードの所有者で絞り込み | 「敵のカード」「味方のカード」 |
| FilterCardKindTargeting | カードの種類で絞り込み | 「モンスター」「サポート」「イベント」 | |
| FilterCardCostTargeting | カードのコストで絞り込み | 「コスト3以下」「コスト5」 | |
| FilterCardAttackTargeting | カードの攻撃力で絞り込み | 「攻撃力2以上」 | |
| FilterCardDamageTargeting | カードのダメージで絞り込み | 「ダメージを受けているカード」 | |
| FilterCardAilmentTargeting | カードの状態異常で絞り込み | 「猛毒状態」「状態異常なし」 | |
| 位置情報 | FilterLayerTargeting | カードの列で絞り込み | 「前列」「後列」 |
| FilterCardCellTargeting | カードのマスで絞り込み | 「特定のマスにいるカード」 | |
| FilterInvaderTargeting | 侵入者判定で絞り込み | 「敵陣にいるカード」 | |
| Trigger関連 | FilterTriggerTypeTargeting | Triggerの種類で絞り込み | 「召喚Trigger」「攻撃Trigger」 |
| FilterTriggerKindTargeting | Triggerの詳細種類で絞り込み | 特定のTriggerイベント | |
| FilterTriggerDamageTargeting | Triggerのダメージ量で絞り込み | 「ダメージ3以上のTrigger」 | |
| FilterTargetCardTargeting | Triggerの対象カードで絞り込み | 「攻撃されたカード」「対象カード以外」 | |
| 能力関連 | FilterAbilityConsumerTargeting | 能力の適用先で絞り込み | 「この能力を持つカード」 |
| FilterAbilityProducerTargeting | 能力の発生源で絞り込み | 「この能力を付与したカード」 | |
| FilterActivateCountTargeting | 能力の発動回数で絞り込み | 「1ターンに1回まで」 | |
| プレイヤー情報 | FilterTurnPlayerTargeting | ターンプレイヤーで絞り込み | 「自分のターン中」 |
| FilterPlayerDirectDamageInThisTurnTargeting | 今ターンの直接攻撃で絞り込み | 「直接攻撃済み」 | |
| 数・選択 | TakeTargeting | 対象の数を制限 | 「ランダムに2枚」「最大3枚」 |
| CountTargeting | 対象の数をカウント | 「カードの枚数」 |
Comparator(比較器):
| Comparator | 説明 | 使用例 |
|---|---|---|
| Equal | 等しい | 「HP = 3」 |
| GreaterThan | より大きい | 「攻撃力 > 2」 |
| LessThan | より小さい | 「コスト < 4」 |
| GreaterThanOrEqual | 以上 | 「HP ≥ 5」 |
| LessThanOrEqual | 以下 | 「ダメージ ≤ 1」 |
Targetingの組み合わせ例(カードゲームでよくある効果):
-
敵の前列全体 - 前列に配置された敵カード全てを選択
MapCard & FilterCardOwner(Enemy) & FilterLayer(Front) -
HP3以下の敵カード1枚 - 残りHPが3以下の敵から1枚を選択
MapCard & FilterCardOwner(Enemy) & FilterCardDamage & Take(1) -
毒状態でない敵ランダム2枚 - 毒を持たない敵からランダムに2枚選択
MapCard & FilterCardOwner(Enemy) & FilterCardAilment(NotPoison) & Take(2, Random) -
コスト3以下の味方全体 - コストが3以下の味方カード全てを選択
MapCard & FilterCardOwner(Player) & FilterCardCost(LessThanOrEqual, 3) -
攻撃力2以上の敵 - 攻撃力が2以上の敵カードを選択
MapCard & FilterCardOwner(Enemy) & FilterCardAttack(GreaterThanOrEqual, 2) -
周囲1マスの味方 - 自カードの周囲1マス内にいる味方カードを選択
MapFollowCell(D1) & MapCard & FilterCardOwner(Player) -
場に出たカード - カードが場に出た時のトリガーで、そのカードを参照
FilterTriggerType(ActorTrigger) & MapActorCard -
攻撃されたカード - 攻撃対象になったカードを参照
FilterTriggerType(CardTargetTrigger) & MapTargetCard -
ダメージ3以上を与えた時 - 与えたダメージが3以上の攻撃成功時に発動
FilterTriggerKind(AttackDamaged) & FilterTriggerDamage(GreaterThanOrEqual, 3) -
自分のターン開始時 - 自分のターンが開始した瞬間に発動
FilterTriggerKind(TurnStarted) & FilterTurnPlayer -
敵陣にいる味方カード - 敵陣に侵入している味方カードを選択
MapCard & FilterCardOwner(Player) & FilterInvader -
破壊されたカード以外 - トリガーで破壊されたカードを除外して選択
MapCard & FilterTargetCard(Except) -
1ターンに1回まで - 同一ターン内で既に発動していないことを条件に
FilterActivateCount(LessThan, 1)
構成要素2: Effect(効果実行)
役割:
盤面を実際に操作する処理を実行
主な Effect の種類(12種類):
| Effect | 説明 |
|---|---|
| PlayerDamageEffect | プレイヤーにダメージ |
| PlayerRecoverHpEffect | プレイヤーのHP回復 |
| CardDamageEffect | カードにダメージ |
| CardRecoverEffect | カードのHP回復 |
| CardMoveEffect | カードを移動 |
| CardSwapEffect | カードの位置を入れ替え |
| CardAilmentEffect | カードに状態異常を付与 |
| AttachAbilityEffect | カードに能力を付与 |
| PlayerHandEffect | カードをドロー |
| ConsumeFeintEffect | フェイントを消費 |
使用箇所:
- TriggerAbility の
Effectプロパティ
重要な特性:
- Effect実行 → Trigger発行 → 連鎖発動
- すべての Effect は
Targetingで対象を選択
Effectの使用例(カードゲームでよくある効果):
-
場に出た時、敵プレイヤーに1ダメージ - 召喚時に相手の体力を削る効果
Conditions = TriggerIs(Summoned) & ActorIsConsumer() Effect = PlayerDamageEffect { Amount = 1, Target = EnemyPlayer() } -
攻撃時、自分のHPを2回復 - 攻撃するたびに自己回復
Conditions = TriggerIs(AttackDamaged) & ActorIsConsumer() Effect = PlayerRecoverHpEffect { Amount = 2, Target = MapPlayer() } -
破壊された時、周囲の味方全体に2ダメージ - 自爆効果
Conditions = TriggerIs(CardGraved) & ActorIsConsumer() Effect = CardDamageEffect { Amount = 2, Target = MapFollowCell(D1) & MapCard & FilterCardOwner(Player) } -
ターン開始時、ランダムな敵1体を毒状態に - 持続デバフ付与
Conditions = TriggerIs(TurnStarted) & FilterTurnPlayer() Effect = CardAilmentEffect { Ailment = Poison, Target = MapCard & FilterCardOwner(Enemy) & Take(1, Random) } -
HP3以下の敵を倒した時、1枚ドロー - 撃破報酬
Conditions = TriggerIs(CardGraved) & FilterCardOwner(Enemy) & FilterCardDamage(LessThanOrEqual, 3) Effect = PlayerHandEffect { Amount = 1, Target = MapPlayer() } -
場に出た時、前方1マスに移動 - 召喚時の自動前進
Conditions = TriggerIs(Summoned) & ActorIsConsumer() Effect = CardMoveEffect { Direction = Forward, Distance = 1, Target = MapActorCard() } -
攻撃時、対象と位置を入れ替える - 攻撃後に敵と場所交換
Conditions = TriggerIs(AttackDamaged) & ActorIsConsumer() Effect = CardSwapEffect { Target = MapTargetCard() } -
ターン終了時、味方全体のHPを1回復 - 全体回復オーラ
Conditions = TriggerIs(TurnFinished) Effect = CardRecoverEffect { Amount = 1, Target = MapCard & FilterCardOwner(Player) } -
敵を倒した時、その敵の能力をコピー - 撃破時能力獲得
Conditions = TriggerIs(CardGraved) & FilterCardOwner(Enemy) Effect = AttachAbilityEffect { AbilitySource = MapTargetCard(), Target = MapActorCard() } -
ダメージ5以上受けた時、フェイント1消費して無効化 - ダメージカット
Conditions = TriggerIs(Damaged) & ActorIsConsumer() & FilterTriggerDamage(GreaterThanOrEqual, 5) Effect = ConsumeFeintEffect { Amount = 1, Target = MapActorCard() }
構成要素3: Calculator(演算器)
役割:
ParameterAbility でパラメータの補正値を計算
使用箇所:
- ParameterAbility の
Calculatorプロパティ専用
演算モード:
- Add: 加算(元の値 + 補正値)
- Subtract: 減算(元の値 - 補正値)
- Multiply: 乗算(元の値 × 補正値)
- Equalizer: 固定値(補正値で上書き)
- Max: 最大値(元の値と補正値の大きい方)
- Min: 最小値(元の値と補正値の小さい方)
実装例(AbilityTemplate: JSONデータ出力・調整用の補助ツールより):
// 「このカードの攻撃力+2」
new ParameterAbility
{
ExpectedParameter = Attack
, Calculator = NumericCalculator.Add(2)
, Lifetime = Forever() // Targeting使用
}
// 「移動コストを0に固定」
new ParameterAbility
{
ExpectedParameter = MoveCost
, Calculator = NumericCalculator.Equalizer(0)
, Lifetime = UntilMoveDeclared() // Lifetimeパラメータ (複数のTargeting句の組み合わせ)
}
能力タイプの組み合わせ例
TriggerAbility(条件発動能力)
= Targeting(条件判定) + Effect(効果実行)
「攻撃された時、攻撃者に2ダメージ」
- Targeting: AttackDamagedTrigger & ActorIsConsumer()
- Effect: CardDamageEffect(2, Target=攻撃者)
ParameterAbility(パラメータ補正能力)
= Calculator(補正計算) + Targeting(有効期間)
「エンドタイムまで、このカードの攻撃力+3」
- Calculator: NumericCalculator.Add(3)
- Targeting: UntilTurnFinished()
AreaAbility(範囲効果能力)
= Targeting(範囲判定) + Effect(付与する能力)
「周囲1マスの味方の攻撃力+1」
- Targeting: Reach(D1) & PlayerRubric()
- Effect: AttachAbility(ParameterAbility(Attack, +1))
NightmareAbility(悪夢能力)
= Targeting(解放条件)
「自分のHPが10以下で解放」
- Targeting: PlayerHp <= 10
- 封印中は他の能力が無効化される
Ability設計の重要な特性
-
宣言的な記述
- Targeting による条件の組み合わせ
- Effect による効果の明示的な定義
- AbilityTemplate(JSONデータ出力・調整用の補助ツール)による再利用可能なパターン
-
連鎖発動の仕組み
- Effect実行 → Trigger発行 → TriggerAbility発動 → Effect実行...
- 無限ループ防止のため、発動回数制限を実装
-
動的な能力付与
- AttachAbilityEffect で実行時に能力を追加
- AreaAbility でカードの移動に応じて自動付与/削除
-
JSONシリアライズ可能な設計
- すべての Ability, Effect, Targeting, Calculator, Comparator は JSON 形式でシリアライズ可能
- カードデータはサーバーから JSON 形式で配信され、実行時に読み込まれる
- アプリをアップデートせずに新しいカード能力を追加できる
- 専用の JsonConverter でポリモーフィックなデシリアライズに対応
JSONシリアライズの実装詳細:
| コンポーネント | JsonConverter | 用途 |
|---|---|---|
| IBattleAbility | BattleAbilityConverter | TriggerAbility, ParameterAbility, AreaAbility, NightmareAbility の型判別 |
| IBattleEffect | BattleEffectConverter | PlayerDamageEffect, CardDamageEffect など12種類のEffect型判別 |
| IBattleTargetingPhrase | BattleTargetingPhraseConverter | MapCardTargeting, FilterXxxTargeting など20種類以上の型判別 |
| ICalculator | CalculatorConverter | NumericCalculator, BoolCalculator など6種類の型判別 |
| IComparator | ComparatorConverter | Equal, GreaterThan など5種類の型判別 |
カードデータの読み込みフロー:
1. サーバーからJSON取得 (CardsRepository)
↓
2. JSON → List<DisplayCard> へデシリアライズ
↓
3. DisplayCard.AbilitySet (JSON内に能力定義が含まれる)
↓
4. バトル開始時: DisplayCard → BattleCard へ変換
↓
5. AbilitySet.Clone() で実行時インスタンス化
利点:
- 新カード追加時、アプリ本体の再ビルド不要
- サーバー側でカードバランス調整が可能
- A/Bテストや期間限定カードの実装が容易
- JSONとコードの整合性チェック機能を実装済み (Editor拡張)
3. 具体例で理解するクラス間の関係
3.1 例1: 状態異常の付与と撃破報酬の連鎖(TriggerAbility)
使用カード: アビルマ・エスターシャ (ID:4, 攻撃2/HP6/コスト3)
能力:
- 能力1: 「攻撃で相手のモンスターにダメージを与えた時、ランダムな相手のモンスター2枚を【猛毒状態】にする」
- 能力2: 「攻撃で相手の状態異常のモンスターを墓地に送った時、このカードのライフを3回復する」
実行フロー(ターン1: アビルマが敵カードAを攻撃):
| State | Operation | Command | 盤面状態 | Trigger | 補足事項 |
|---|---|---|---|---|---|
| MainState | AttackOperation | - | 敵A(HP2), B(HP3), C(HP4) | - | ユーザーが攻撃操作を実行 → 実行可能判定 |
| AttackDeclareState | - | AttackDeclareCommand | 同上 | AttackDeclaredTrigger | 攻撃宣言処理 → 自動的にAttackDamageCommand生成 |
| AttackDamageState | - | AttackDamageCommand | 敵A(HP0) | AttackDamagedTrigger | アビルマの攻撃2ダメージ → 敵AのHP: 2→0 |
| (能力発動) | - | - | 敵B(猛毒), C(猛毒) | - | 能力1条件判定: TriggerIs(AttackDamaged) & ActorIsConsumer() & TargetIsRubric() → 満たすCardAilmentEffect実行: MapCard & FilterCardOwner(Enemy) & Take(2, Random) でランダムに敵2枚選択 → 敵B・Cに猛毒状態付与 |
| (能力発動) | - | - | 同上 | CardGravedTrigger | 敵Aが破壊される(HP≤0) → CardGravedTrigger発行 |
| (能力発動) | - | - | アビルマ(HP9) | - | 能力2条件判定: TriggerIs(CardGraved) & ActorIsConsumer() & TargetCardIsAilmented() → 満たすPlayerRecoverHpEffect実行: アビルマのHP +3(6→9) |
技術的特徴:
- TriggerAbilityは2つのコンポーネントで構成:
-
Conditions: 発動条件(Targeting式の組み合わせ) -
Effect: 実行する効果
-
- 攻撃による連鎖: AttackDamagedTrigger → CardAilmentEffect → CardGravedTrigger → PlayerRecoverHpEffect
- すべての条件式・効果はJSONでシリアライズ可能
3.2 例2: AreaAbility(エリア効果による範囲バフ)
使用カード: ファルハラ (ID:22)
能力 (AreaAbility): 「範囲【D1】にある自分のモンスターのアタックが1増加する」
実行フロー(ファルハラ召喚と味方移動による範囲バフの付与・解除):
| State | Operation | Command | 盤面状態 | Trigger | 補足事項 |
|---|---|---|---|---|---|
| MainState | SummonOperation | SummonCommand | ファルハラ + 味方A(左) + 味方B(右) + 味方C(下) | SummonedTrigger | ファルハラ召喚reach_in_condition評価: MapActorCard & MapFollowCell(D1) & FilterCardKind(Rubric) & FilterCardOwner(Player) → D1範囲内に味方A・Bを検出味方A・BにParameterAbility(攻撃+1, IsAttached=true)を付与味方Cは範囲外のため効果なし |
| MainState | MoveOperation | MoveCommand | ファルハラ + 味方A(遠方) + 味方B(右) + 味方C(下) | MovedTrigger | 味方Aが範囲外に移動 ファルハラの reach_in_condition再評価 → 味方Aが範囲外味方Aに付与していたParameterAbility( IsAttached=true)を自動解除味方Bのみバフ継続 |
| MainState | - | - | 味方A + 味方B + 味方C | CardGravedTrigger | ファルハラ破壊(攻撃により破壊されたと仮定)IsAttached=trueかつProducer=ファルハラの全ParameterAbilityを検索 → 味方Bの攻撃+1バフを削除全ての範囲バフが解除される |
JSONデータ(AreaAbilityの抜粋):
{
"kind": 3, // AreaAbility
"reach": 131, // D1範囲
"zone": 0,
"reach_in_condition": {
"phrases": [
{"kind": 10, "type": 0}, // MapActorCard (ファルハラ起点)
{"kind": 15}, // MapFollowCell (周囲のセル)
{"cardKind": 1, "kind": 3}, // FilterCardKind(Rubric)
{"kind": 5, "opponent": false} // FilterCardOwner(Player)
]
},
"effect": {
"calc": {"amount": 1, "kind": 0, "mode": 0}, // NumericCalculator.Add(1)
"kind": 1, // ParameterAbility
"parameter": 0, // Attack
"lifetime": []
}
}
技術的特徴:
- AreaAbilityは範囲式(
reach_in_condition)で対象を検索し、各対象にParameterAbilityを付与 - 付与されたParameterAbilityは
IsAttached=trueでマークされ、付与元が破壊されると自動削除 - 対象カードが範囲外に移動した場合も、次の盤面更新時に自動解除される
4. Facilitator/Facilitatee - バトル進行の制御層
4.1 Facilitatorの役割
Facilitator(進行役) は、バトル全体のセットアップと進行を管理する最上位のコントローラーです。BattleModelがゲームロジックに専念するため、プレイモード固有の処理はFacilitatorに委譲されています。
主な責務:
- バトルのセットアップ - プレイヤー情報、カードデータ、メタデータの初期化
- プレイモードの制御 - オンライン/ローカル/リプレイ等、モードごとの差分を吸収
- Facilitateeの管理 - Host/Guestプレイヤーの生成と管理
- ネットワーク同期 - コマンドの送受信、盤面復元
- ゲーム状態の監視 - バトル開始/終了の判定、タイムアウト処理
Facilitatorの種類(7種類):
| Facilitator | プレイモード | 主な用途 |
|---|---|---|
| PhotonFacilitatorModel | オンライン対戦 | Photon + Beyondサーバーでの通常対戦(再接続機能を含む) |
| DebugFacilitatorModel | デバッグ | 開発用ローカル対戦(任意のブックで検証可能) |
| TutorialFacilitatorModel | チュートリアル | 初回プレイ時のチュートリアル |
| TrainingFacilitatorModel | トレーニング | ローカルAI対戦 |
| SandbagFacilitatorModel | サンドバッグ | デッキテスト用(相手は何もしない) |
| ReplayFacilitatorModel | リプレイ | 過去の対戦を再生 |
| LearningFacilitatorModel | 機械学習 | AIの学習用環境 |
4.2 Facilitateeの役割
Facilitatee(参加者) は、各プレイヤー(Host/Guest/Audience)の行動を表現するコンポーネントです。
主な責務:
-
操作の実行可能性判定 -
GetAvailableOperations()でユーザーが選択可能な操作をリストアップ -
操作の処理 -
HandleNewBattleOperation()でユーザー操作をCommandに変換 -
Commandの送信 -
SendBattleCommand()でネットワーク経由またはローカルで送信 - ローカルイベント発行 - ストラテジータイム等のUI専用イベントを発行
Facilitateeの種類:
| Facilitatee | 役割 | 説明 |
|---|---|---|
| DefaultPlayerModel | セッションプレイヤー | ユーザー自身の操作を処理 |
| DefaultOpponentModel | 対戦相手 | ネットワーク経由で相手のCommandを受信 |
| TutorialOpponentModel | チュートリアルAI | 決められた行動を実行するAI |
| SandbagOpponentModel | サンドバッグAI | 何も行動しないAI(デッキテスト用) |
| ComputerOpponentModel | AI対戦相手 | 盤面を評価して行動するAI |
| AudiencePlayerModel | 観戦者 | 操作不可、盤面の監視のみ |
4.3 Facilitator/Facilitateeの協調動作
バトルセットアップフロー:
ユーザー操作の処理フロー:
4.4 プレイモードごとの差分
PhotonFacilitatorModel(オンライン対戦 + 再接続):
- Photon経由でコマンド送受信
- Beyondサーバーにバトル履歴を保存
- 対戦相手の切断監視とリトライ処理
- カード使用回数の記録
- 途中参加・再接続時のバトル復元機能(詳細は後述)
DebugFacilitatorModel(デバッグ):
- ローカルで完結(サーバー通信なし)
- 任意のブックを直接指定可能
- ローカルプレイモード(履歴を保存しない)も選択可能
- 盤面復元でデバッグ効率化
TutorialFacilitatorModel(チュートリアル):
- 固定されたブック構成
- TutorialOpponentModelで相手の行動を制御
- サーバー通信なし、ローカル完結
4.4.1 PhotonFacilitatorModelの再接続機能
PhotonFacilitatorModel は、オンライン対戦中の通信切断や途中参加に対応するため、バトル状態の復元機能を内蔵しています。独立した PhotonReconnectFacilitatorModel ではなく、PhotonFacilitatorModel 自身がこの機能を持っています。
再接続のシーケンス:
実装の詳細(PhotonFacilitatorModel.cs:289-332):
-
履歴取得(1回目):
-
GameCommandHistoryRepository.Get(RoomId)でBeyondサーバーからコマンド履歴を取得 - 履歴が空なら新規バトルとして開始
-
-
高速リプレイ:
- 取得した全コマンドを
BattleModel.UpdateBattleState()で順次実行 - UIは非表示のまま、FSMだけが高速で遷移
- 取得した全コマンドを
-
履歴取得(2回目):
- 復元中に対戦相手がコマンドを送信している可能性があるため、再度履歴を取得
-
histories2.Count > histories.Countなら追加コマンドを実行
-
バトル開始通知:
-
BattleModel.NotifyReadyToBattle()でUIに復元完了を通知 - ユーザーは途中の盤面から操作可能になる
-
設計上の利点:
| 観点 | 利点 |
|---|---|
| Commandパターンとの相性 | すべてのゲーム進行がCommandで表現されているため、履歴を再実行するだけで盤面を復元可能 |
| JSON直列化 | CommandはすべてJSONシリアライズ可能なため、サーバー保存・通信が容易 |
| 観戦者への応用 | 同じ仕組みでAudience(観戦者)も途中参加可能 |
| デバッグ効率化 | コマンド履歴をローカルに保存すればリプレイとして再利用可能 |
| 署名検証 | Commandには署名が含まれており、改ざん検知が可能 |
エラーハンドリング:
- ルーム未参加時:
!PhotonNetwork.InRoomで例外をスロー - 履歴取得失敗時:
!(historiesGet is RoomHistoriesGetSuccess)で例外をスロー - 対戦相手のTTL切れ:
BattleForceFinishCommandでバトルを異常終了 - コマンド送信失敗時: リトライ処理(最大10回、5秒間隔)
4.4.2 ComputerOpponentModel(AI対戦)
概要:
- TrainingFacilitatorModelで使用されるAI対戦相手
- BattleEvaluatorによる盤面評価で行動を自動決定
- 別スレッドでの並列評価により、UI停止なしで思考処理を実行
評価システムの核心:
| 要素 | 説明 |
|---|---|
| シミュレーション |
BattleModel.Clone()で未来の盤面を仮想実行 |
| スコアリング |
Score = Σ(報酬 - コスト) で各行動を評価 |
| 並列処理 | 最大12並列で評価、勝利確定手(Score>10000)発見時に早期終了 |
| 報酬 | プレイヤーダメージ、カード破壊による得点 |
| コスト | MP消費、移動距離による減点 |
他のOpponentModelとの違い:
| Model | 評価方式 | 強さ | 特徴 |
|---|---|---|---|
| ComputerOpponentModel | 盤面評価(BattleEvaluator) | 強い | スコアリング、並列探索、最適化 |
| TutorialOpponentModel | 固定パターン | 非常に弱い | 前方1マス移動のみ、予測可能 |
| SandbagOpponentModel | 消極的行動 | 弱い | 移動制限、召喚条件厳しい、練習台 |
技術的ポイント:
- 非同期評価エンジン(UniTask + CancellationToken)
- 評価ルート管理(複数ターン先の計画を保持)
- 枝刈り最適化(セルごとの最大スコア記憶、フェイント回避90%/70%)
4.5 責務分担表
| 責務 | Facilitator | Facilitatee | BattleModel |
|---|---|---|---|
| バトルセットアップ | ◎ | - | - |
| プレイヤー管理 | ◎ | - | - |
| ネットワーク同期 | ◎ | - | - |
| 盤面復元 | ◎ | - | - |
| 操作の検証 | - | ◎ | - |
| Commandの生成 | - | ◎ | - |
| ローカルイベント発行 | - | ◎ | - |
| FSM状態遷移 | - | - | ◎ |
| ゲームロジック | - | - | ◎ |
| Trigger発行 | - | - | ◎ |
4.6 設計上の利点
1. プレイモードの切り替えが容易
- 新しいプレイモードの追加は、新しいFacilitatorを実装するだけ
- BattleModel本体のロジックは変更不要
2. ネットワーク/ローカルの差分を吸収
- Facilitateeレベルでは送信先(ネットワーク/ローカル)を意識しない
- CommandRepositoryの差し替えで切り替え
3. AI対戦の実装が容易
- Facilitateeを差し替えるだけでAI対戦に対応
- ComputerOpponentModel, TutorialOpponentModel, SandbagOpponentModel等
4. テスタビリティの向上
- DebugFacilitatorModelで任意の盤面を再現可能
- 特定の状況をローカルで検証できる
5. 重要な設計パターンとその理由、まとめ
5.1 本バトルシステムで採用した設計パターン
このバトルシステムは、複雑なゲームロジックを整理・管理するために、複数の古典的な設計パターンを組み合わせて実装されています。
| 課題 | 設計パターン | 解決方法 |
|---|---|---|
|
複雑なフェーズ遷移の管理 DCGのターン進行は複雑なフェーズ遷移を伴う(ターン開始→ドロー→ストラテジー→バトル→ターン終了...) 各フェーズで実行可能なアクションが異なり、フェーズ間の遷移条件が多様(自動遷移、ユーザー入力待ち、条件分岐) |
State Pattern (状態パターン) |
33種類のStateクラスで各フェーズを表現し、各Stateは次のStateを返す ResolveState() メソッドを実装。連続実行が必要な場合は GetContinuousCommand() で自動遷移。利点: フェーズごとのロジックが独立したクラスに分離され可読性・保守性が向上。新しいフェーズの追加が容易(新しいStateクラスを作るだけ)。FSM(有限状態機械)として明確に表現できる。 |
|
ネットワーク同期、リプレイ、再接続 ネットワーク対戦で盤面を同期する必要がある リプレイ機能を実装したい 途中参加・再接続時にバトル状態を復元したい デバッグ時に特定の盤面を再現したい |
Command Pattern (コマンドパターン) |
すべてのゲーム進行を29種類のCommandで表現。すべてのCommandはイミュータブル(不変)でJSONシリアライズ可能。署名検証によるチート対策。 利点: Command履歴を再実行するだけで盤面を完全復元可能。ネットワーク越しにCommandを送るだけで同期完了。サーバーに履歴を保存すれば再接続・リプレイに対応。デバッグ時に問題のあったCommand列を再現できる。 |
|
能力の発動タイミング制御 カードの能力は特定のタイミング(Trigger)で発動する 1つのTriggerに複数の能力が反応することがある UIも盤面変化に反応して更新する必要がある |
Observer Pattern (オブザーバーパターン) |
40種類のTriggerでイベントを表現。Abilityが特定のTriggerに反応して効果を発動。ReactiveX (UniRx)でストリーム化し、UIと疎結合。 利点: 能力の発動タイミングを柔軟に制御できる。新しい能力を追加してもTrigger側の変更不要。UIへの通知も同じ仕組みで実現(疎結合)。 |
|
多様な対象選択の実装 カード能力の対象選択は多様(「敵の前列」「HP3以下」「召喚されたカード」など) 複数の条件を組み合わせたい(「敵の」「前列の」「HP3以下の」カード) 宣言的に記述したい(コードではなくデータで定義したい) |
Strategy Pattern (ストラテジーパターン) |
Head(起点)8種類 + Pipe(フィルタ)18種類 + Comparator 5種類。Pipeを連鎖させることで複雑な条件を表現。すべてJSONで記述可能。 利点: 複雑な対象選択を柔軟に実装できる。新しいフィルタを追加するだけで表現力が向上。データ駆動なので、プログラムを変更せずに新しい能力を追加可能。 |
|
プレイモード固有処理の分離 オンライン対戦、ローカル対戦、AI対戦、リプレイ、チュートリアル...と多様なプレイモードがある BattleModel自体はゲームロジックに専念したい プレイモード固有の処理(ネットワーク同期、AI思考、履歴再生)を分離したい |
Strategy Pattern + Abstract Factory Pattern | 7種類のFacilitatorでプレイモードを表現。Facilitatee(参加者)で各プレイヤーの行動を抽象化。BattleModelはプレイモードを意識しない純粋なゲームロジック層。 利点: 新しいプレイモードの追加が容易。ネットワーク/ローカルの差分を吸収。AI対戦の実装が容易(Facilitateeを差し替えるだけ)。テスタビリティの向上(DebugFacilitatorModelで任意の盤面を再現可能)。 |
|
能力の多様性への対応 カード能力は非常に多様(200種類以上) 継承による実装は硬直的で、組み合わせ爆発を起こす データ駆動で能力を定義したい |
Composition over Inheritance (合成による拡張) |
Ability(能力)をEffect(効果)、Targeting(対象)、Calculator(計算)、Trigger(発動条件)のコンポーネントに分解。コンポーネントを組み合わせることで能力を表現。すべてJSONシリアライズ可能。 利点: 新しい能力を追加する際、既存コンポーネントの組み合わせで実装可能。アプリをアップデートせずに新しいカードを追加できる。能力の再利用性が高い。 |
5.2 DCGとしての特徴
このバトルシステムは、一般的なDCG(デジタルカードゲーム)の要素を網羅しています:
- ✓ カードの召喚、移動、攻撃
- ✓ トリガー型能力と継続効果
- ✓ エリア効果とパラメータ補正
- ✓ ターン制の複雑なフェーズ管理
- ✓ AI対戦とネットワーク対戦への対応
- ✓ リプレイ・観戦機能
- ✓ 途中参加・再接続機能
これらの機能を、古典的な設計パターンを組み合わせたクリーンなアーキテクチャで実装することで、高い拡張性・保守性・テスタビリティを実現しています。
5.4 最後に:なぜこの設計を選んだのか
このバトルシステムの設計思想は、**「変更に強く、拡張しやすいアーキテクチャ」**です。
DCGは運用型ゲームであり、リリース後も継続的に新しいカードや機能が追加されます。そのため、以下の点を重視しました:
-
新しいカードを追加する際、既存コードへの影響を最小限にする
→ Composition over Inheritance -
複雑なゲームロジックを整理し、可読性を保つ
→ State Pattern -
ネットワーク対戦、AI対戦、リプレイ等、多様なモードに対応する
→ Command Pattern、Strategy Pattern + Abstract Factory Pattern -
デバッグとテストを容易にする
→ Command履歴、Clone()、Mock差し替え
結果として、200種類以上のカード能力、7種類のプレイモード、33種類のフェーズを持つ複雑なバトルシステムを、保守可能な形で実装できました。
これらの設計判断は、ゲーム開発以外のドメイン(業務システム、Webサービス等)でも応用可能な普遍的なパターンです。本ドキュメントが、複雑なドメインロジックをクリーンに実装する際の参考になれば幸いです。
Discussion