ギリギリオンライン — タップゲームをオンライン対戦化する 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 の体験が固まったら、次は通信レイヤーの追加です。

  1. ローカル状態の更新を「イベント」に分解
  2. イベントをサーバー経由でブロードキャスト
  3. クライアント間で状態を同期

ゲーム状態は基本的に「タップイベント」のタイムスタンプで決まるので、通信量は少なく済むはずです。リアルタイム対戦のハマりポイントは予測補正と遅延処理なので、そこは MVP の次フェーズで対応します。

まとめ

  • オンライン化は「対戦体験」を先に固めてから通信を足す
  • ゲームロジックは純粋関数化するとテストもしやすい
  • CPU は AI 難易度の段階を最初から用意する

MVP を切ることで、「先にやらなくていいこと」を明確にできるのが大きな利点です。