モンスターブレイカー — ブロック崩しと RPG を融合させた Flame ゲーム
| 開発記録 | モンスターブレイカー
タグ: #Flame #ゲーム #Flutter
ブロック崩しは単調、RPG は操作が複雑。だったら混ぜてみよう、で始めたのがモンスターブレイカーです。Flame でコアロジックを純 Dart に切り出し、Wave 制バトルとパーティ・装備・呪文を載せました。初期 HP 5 だとすぐ死ぬ、みたいな調整の記録です。
コンセプト
ブロック崩しは飽きない。でも繰り返すだけだと単調です。RPG は深い。でも操作が複雑になりがちです。
この 2 つを掛け合わせました。「ボールを打ち返すアクションで RPG のバトルが進行する」ゲーム。それがモンスターブレイカーです。
Flame でのコア構成
Flame 公式のサンプルをベースに、独自要素を載せました。
- BreakoutSimulation(純 Dart のコアロジック)
- StageLayout(グリッド → ワールド座標マッピング)
- 円 vs AABB の swept collision
- 4 種のブロック型(BlockComponent 階層)
- 党(Party)駆動のゲームプレイ、降下・クリア・ゲームオーバー判定
ポイントは、ロジック層を純 Dart で切り出したことです。Flame に依存しない形にしました。こうするとテストが書けます。
ボールとブロックの当たり判定は swept(連続)方式で計算しています。フレームの dt が大きいときに「すり抜け」が起きないようにするためです。概念だけ抜き出すと次のとおりです。
class BreakoutSimulation {
CollisionResult? sweepBallVsBlocks({
required Ball ball,
required Iterable<Block> blocks,
required double dt,
}) {
final motion = ball.velocity * dt;
CollisionResult? earliest;
for (final block in blocks) {
final hit = _sweepCircleAabb(
center: ball.position,
radius: ball.radius,
motion: motion,
box: block.aabb,
);
if (hit == null) continue;
if (earliest == null || hit.t < earliest.t) {
earliest = CollisionResult(block: block, hit: hit);
}
}
return earliest;
}
}
Wave 制バトル
通常のブロック崩しは「ブロックを全部消したらクリア」です。本作は Wave 制にしました。
- 弱モンスター(雑魚)が複数体出現する Wave
- 中ボスが出現する Wave
- ボス Wave
段階的に難易度が上がります。各 Wave をクリアすると、生存メンバーが HP の 50% を回復します。RPG らしい段階感が出ました。
マルチボール
ボール 1 個では火力が足りませんでした。そこでマルチボールに対応しました。ボールが分裂して、複数の敵を同時に攻撃できます。
ダメージポップアップとヒット位置
ヒットしたときのダメージ数字を、ヒット位置にポップアップ表示するようにしました。最初はパドルの上に出していたのですが、これがぜんぜん駄目でした(どこを攻撃したのか自分でも分からない)。ヒット位置に出すのが正解です。
さらに、ダメージ数字は「黒文字+白アウトライン」にしました。どんな背景色でも見やすくなります。
HP バランス調整
プレイヤーの初期 HP は当初 5 でした。すぐ死にます。20 にしました。
バランス調整はテストプレイの繰り返しです。コミット履歴には balance: Bump playerInitialHp 5 -> 20 のような調整が並んでいます。
SaveData と永続化
パーティ編成・装備・呪文・所持アイテムは SharedPreferences に永続化しました。あいだに SaveRepository を挟んでいます。Flame コンポーネントから直接 SharedPreferences を触らせないためです。
まとめ
- ロジック層を Flame から切り離すとテストが書ける
- Wave 制は単調さを打破する強力なギミック
- バランス調整はテストプレイの量が全て
ゲーム開発はコードより「触ってみての気持ちよさ」が全てです。数字をいじって自分で遊んで、また数字をいじる。この往復にいちばん時間を使いました。