ターン制戦闘をフィールドに統合する vs 専用画面に分ける:設計比較と判断基準
ターン制RPGを開発する際、エンジニアが最初に直面する大きな設計判断の一つが「戦闘をどこで行うか」です。具体的には、不思議のダンジョンのように**フィールド上でそのまま戦う(フィールド統合型)のか、ドラゴンクエストのように専用の戦闘画面に遷移する(専用画面型)**のか、という選択です。
この選択は単なるビジュアルの違いに留まらず、状態管理(State Management)、当たり判定、AIの実装、そしてスケーラビリティに決定的な影響を与えます。本稿では、TypeScriptを用いた架空のゲームの実装例を交えながら、両アプローチの設計思想と判断基準を深く掘り下げます。
1. 2つのアプローチの比較
まずは、それぞれの特性をトレードオフの観点から整理します。
| 項目 | フィールド統合型 (Seamless) | 専用画面型 (Isolated) |
|---|---|---|
| UXの印象 | テンポが良い、空間の繋がりを感じる | 演出が豪華、戦略に集中できる |
| 状態管理 | 非常に複雑(フィールド+戦闘の混合) | 比較的単純(画面ごとにStateを全入れ替え) |
| 位置情報の意味 | 極めて重要(射程、視線、逃走経路) | 抽象的(前衛・後衛、ターゲット選択) |
| AIの実装コスト | 高い(地形考慮、パス検索が必要) | 低い(コマンド選択アルゴリズムに集中) |
| 拡張性 | 難しい(新しいギミックが戦闘に影響する) | 容易(戦闘専用の特殊ルールを作りやすい) |
2. フィールド統合型の実装:空間と時間の同期
フィールド統合型(ローグライク方式)では、「移動」と「攻撃」が同じタイムライン上で扱われます。
なぜその設計にするか
プレイヤーが「一歩動く」ことと「剣を振る」ことが同等のコスト(1ターン)を持つため、戦略が空間的になります。壁を背にする、通路に誘い込むといった地形利用が自然にゲームプレイに組み込まれるのが最大のメリットです。
アクション設計の例
TypeScriptでのアクション定義は以下のようになります。
type FieldAction =
| { type: 'FIELD_PLAYER_MOVE'; direction: Vector2 }
| { type: 'FIELD_PLAYER_ATTACK'; targetId: string }
| { type: 'FIELD_MONSTER_TURN_START' }
| { type: 'FIELD_DAMAGE_ENTITY'; entityId: string; amount: number }
| { type: 'FIELD_KILL_MONSTER'; monsterId: string };
interface FieldState {
player: Player;
monsters: Record<string, Monster>;
tiles: TileMap;
turnOwner: 'PLAYER' | 'MONSTER';
animations: AnimationQueue;
}
実装のポイント
この形式では、Reducer が非常に巨大になりがちです。なぜなら、「移動した結果、トラップを踏み、そのダメージでHPが0になり、死亡処理が走る」という一連の連鎖(Side Effects)を、同一のグリッド座標系で計算しなければならないからです。
const fieldReducer = (state: FieldState, action: FieldAction): FieldState => {
switch (action.type) {
case 'FIELD_PLAYER_ATTACK':
const monster = state.monsters[action.targetId];
if (!monster) return state;
// 距離計算が必須
const dist = calculateDistance(state.player.pos, monster.pos);
if (dist > state.player.range) return state;
return {
...state,
// 戦闘結果を直接フィールドの状態に反映
monsters: {
...state.monsters,
[action.targetId]: { ...monster, hp: monster.hp - state.player.atk }
},
animations: [...state.animations, { type: 'SLASH', pos: monster.pos }]
};
// ...
}
};
3. 専用画面型の実装:コンテキストの分離
専用画面型(エンカウント方式)では、戦闘が開始された瞬間にフィールドのコンテキストがシリアライズされ、独立した「戦闘エンジン」に制御が移ります。
なぜその設計にするか
最大の理由は**「複雑度のカプセル化」**です。戦闘中、背後の木々や迷路のような地形を考慮する必要がなくなります。これにより、派手なエフェクト、複雑なバフ/デバフ、召喚魔法といった「戦闘専用のロジック」を、フィールドのシステムを壊すことなく自由に追加できます。
ステート遷移の設計
ゲーム全体のステートを以下のように分離します。
type GameMode =
| { type: 'EXPLORATION'; fieldData: FieldState }
| { type: 'BATTLE'; battleData: BattleState };
interface BattleState {
allies: Combatant[];
enemies: Combatant[];
turnIndex: number;
selectedCommand?: Command;
phase: 'INPUT' | 'EXECUTION' | 'RESULT';
}
実装のポイント
戦闘開始時に「どのモンスターと、どの地形で」遭遇したかという最小限の情報だけを渡します。
function transitionToBattle(field: FieldState, monsterId: string): BattleState {
const enemyGroup = spawnEnemyGroup(field.monsters[monsterId].type);
return {
allies: [transformToCombatant(field.player)],
enemies: enemyGroup,
turnIndex: 0,
phase: 'INPUT'
};
}
この設計の美しさは、BattleReducer がフィールドの座標(Vector2)を一切知らなくて良い点にあります。ターゲット選択はインデックス(enemies[0])で行われ、AIは純粋な「期待値計算」に専念できます。
4. ハイブリッド(フィールド上のUI重ね)の罠
「フィールドが見えたまま、メニューだけが戦闘用になる」というハイブリッド型を検討する人も多いですが、これは**「中途半端な実装負荷」**を招きやすい危険な道です。
- 入力の競合: 「十字キーで移動したい」のか「メニューを選択したい」のかのフラグ管理が複雑化します。
- 視覚的同期の不一致: フィールド上のキャラがアニメーションしている間に、裏でStateが更新され、UIのHPバーと実際のデータがズレる等の問題が発生しやすくなります。
もしハイブリッドにするなら、**「操作モードを完全にロックする」か、あるいは「UIをフィールドの一部(World Space UI)として描画する」**覚悟が必要です。
5. 判断基準:どちらを選ぶべきか
設計を選択する際のチェックリストです。
「フィールド統合型」を選ぶべきケース
- リソース管理が主題: 「一歩歩くごとに腹が減る」ような、探索そのものが戦闘であるゲーム。
- ポジショニングが核: 挟み撃ち、ノックバックによる壁衝突など、位置関係に戦術の8割がある場合。
- シームレスな体験: ロードや画面転換による没入感の中断を極端に嫌う場合。
「専用画面型」を選ぶべきケース
- ビルドの多様性: 数百種類のスキル、複雑な属性相性、装備の組み合わせを重視する場合。
- 演出の重視: キャラクターのカットインや、ダイナミックなカメラワークを多用したい場合。
- 開発チームの分業: 「フィールド担当」と「戦闘ロジック担当」でコードベースを綺麗に分けたい場合。
6. まとめ
フィールド統合型は**「空間の整合性」を保つためにエンジニアリングの努力を注ぎ、専用画面型は「ロジックの深さ」**を追求するためにコンテキストを分離します。
TypeScriptで実装する場合、前者は Reducer 内での座標計算と Side Effect の管理が、後者は FieldState から BattleState へのシリアライズ/デシリアライズの堅牢性が、プロジェクトの成否を分けるポイントになります。
あなたが作ろうとしているゲームの「面白さのコア」はどこにあるでしょうか? 座標の上にありますか、それともコマンドの選択肢の中にありますか? その答えが、自ずと取るべき設計を示してくれるはずです。