ギリギリオンライン — タップゲームをオンライン対戦化する MVP
| 開発記録 | ギリギリジャンパー
タグ: #Flutter #ゲーム #MVP
「激ムズ!! ギリギリジャンパー」をオンライン対戦に拡張するべく、MVP としてオフライン CPU 対戦版を作りました。MVP の切り方とオンライン化への道筋を整理します。
オンライン対戦化という目標
単体プレイで完結していた「激ムズ!! ギリギリジャンパー」を、リアルタイムオンライン対戦化したい。けれど、最初からマッチング・サーバー・通信を作るのはリスクが高すぎる。
そこで MVP として「オフライン CPU 対戦版」をまず作り、ゲームバランス・操作感・UI を固めることにしました。
MVP の範囲
- 1 人 vs CPU の対戦モード
- スコアの比較、勝敗判定
- 対戦中の UI(自分の画面と相手の画面を同時表示)
- CPU の AI(複数の難易度)
通信レイヤーは一切作らず、すべてローカル完結。「対戦の体験」だけを先に固めるのが狙いです。
既存ゲームコードの分離
元のゲームは「シングルプレイ」を前提に設計されていたので、ゲームロジックを純粋関数として切り出す作業から始めました。
- 入力(タップ)
- ゲーム状態(プレイヤー位置、障害物、スコア)
- 出力(次の状態)
この 3 つを明確に分けることで、CPU AI も「ゲーム状態を見て次のタップタイミングを返す関数」として実装できます。
ゲームロジックを純粋関数として切り出すと、状態は GameState、入力は PlayerInput のように型で固定でき、テスト・リプレイ・通信化のすべての足場になります。
/// ステートレスなゲーム遷移関数。
GameState advance(GameState state, PlayerInput input, double dt) {
if (state.isGameOver) return state;
final nextPlayer = _movePlayer(state.player, input, dt);
final nextObstacles = state.obstacles
.map((o) => o.shifted(dt))
.where((o) => !o.isOutOfScreen)
.toList(growable: false);
final hit = nextObstacles.any((o) => o.overlaps(nextPlayer.hitbox));
return state.copyWith(
player: nextPlayer,
obstacles: nextObstacles,
score: hit ? state.score : state.score + 1,
isGameOver: hit,
);
}
CPU AI の作り方
CPU は「障害物のバーが近づいてきたタイミングでジャンプする」アルゴリズムを実装しました。
- 弱い AI: 反応が遅い、ジャンプタイミングが甘い
- 中 AI: 標準的なタイミング
- 強い AI: ギリギリでジャンプする(コンボボーナス狙い)
プレイヤーが弱 AI に勝てる難易度から始め、徐々に強くなる調整をしています。
画面構成
対戦画面は上下分割で、上が自分、下が CPU。両者の同時進行が見えるレイアウトです。
オンライン化への道筋
MVP の体験が固まったら、次は通信レイヤーの追加です。
- ローカル状態の更新を「イベント」に分解
- イベントをサーバー経由でブロードキャスト
- クライアント間で状態を同期
ゲーム状態は基本的に「タップイベント」のタイムスタンプで決まるので、通信量は少なく済むはずです。リアルタイム対戦のハマりポイントは予測補正と遅延処理なので、そこは MVP の次フェーズで対応します。
まとめ
- オンライン化は「対戦体験」を先に固めてから通信を足す
- ゲームロジックは純粋関数化するとテストもしやすい
- CPU は AI 難易度の段階を最初から用意する
MVP を切ることで、「先にやらなくていいこと」を明確にできるのが大きな利点です。