虎の穴ラボ技術ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

Flutter ✕ Flameで作るジャンプゲーム

こんにちは、虎の穴ラボFantia開発エンジニアの吉岡です。

今回はFlutterとそのゲームエンジンであるFlameを使って簡単なジャンプゲームを作っていきます。

Flameとは

Flutterのプラグインとして提供されている2Dゲーム向けのゲームエンジンです。
ユーザ入力、物理演算のサポート、サウンド・アニメーションの再生など一通りの機能が備わっており FlutterやDartを使ったことある人なら簡単にゲームを作ることが出来ます。

またFlutterのプラグインであるためWebやスマホ、デスクトップのマルチプラットフォーム対応も簡単に行えるのが特徴です。

今回作るゲーム

今回は比較的シンプルなゲームとして「メイドちゃんのジャンプゲーム」を作って行きます。
イメージとしてはGoogle Chromeがオフラインの時に表示される恐竜のゲームです。

今回の実行環境はchromeをメインにしています。iOSやAndroidで実行する際は表示位置等調整が必要です。

環境

今回使うFlutterのバージョンになります。

$ flutter --version
Flutter 3.24.5 • channel stable • https://github.com/flutter/flutter.git
Framework • revision dec2ee5c1f (12 hours ago) • 2024-11-13 11:13:06 -0800
Engine • revision a18df97ca5
Tools • Dart 3.5.4 • DevTools 2.37.3

実装

プロジェクト作成・ライブラリ追加

まずはflutterのプロジェクトを作っていきます。

flutter create jump_game

jump_game のプロジェクトが出来たら pubspec.yaml を編集して flameのライブラリを追加します。

~~

dependencies:
  flutter:
    sdk: flutter
  flame: ^1.19.0 # 今回追加

追加をしたら保存をして

flutter pub get

を実行してインストール完了です。

ゲーム画面の作成

まずはメイドちゃんと障害物が表示されるゲームの画面を作成していきます。
Flameは様々な要素をComponentとして扱っていきます。 ゲームで使用するComponentの追加、更新などを行うベースとなるComponentとして lib/games/jump_game.dart というファイルを用意してその中に、 FlameGame クラスを継承した JumpGame というクラスを作成し画面端のあたり判定を追加します。

今後はこの JumpGame クラスへキャラクターや障害物、ユーザの入力処理などを追加していきます。

class JumpGame extends FlameGame{
  // 初回の読み込み時に呼ばれるメソッド
  // 他のComponentの初期化などを行う
  @override
  Future<void> onLoad() async {
    super.onLoad();
    add(ScreenHitbox()); // 画面端のあたり判定を追加
  }
}

Componentの追加は add メソッドに引数として渡すことでゲーム内へ追加することが出来ます。 ここで作ったJumpGameをFlutterで表示させるため Widgetを作成します。 lib/pages/jump_game_page.dartJumpGamePage クラスを作ります。

class JumpGamePage extends StatelessWidget {
  JumpGamePage({super.key});

  // 今回作成したゲームを持つ
  final game = JumpGame();

  @override
  Widget build(BuildContext context) {
    // FlutterへFlameのGameWidgetを追加
    return GameWidget(game: game);
  }
}

ついでにmain.dartも修正します。

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // web以外の場合は横向きの画面にする
  if (!kIsWeb) {
    await Flame.device.setLandscape();
    await Flame.device.fullScreen();
  }
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override

  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Jump Games",
      home: JumpGamePage(),
    );
  }
}

キャラクターのComponentの作成

ゲーム内でプレイヤーが操作するキャラクターのComponentを作成していきます。 今回はメイドちゃんがキャラクターのため lib/components/maid_component.dart のファイルを作成し、 SpriteComponent を継承した MaidComponent クラスを作ります。

class MaidComponent extends SpriteComponent{
  MaidComponent({
    super.position,
    super.size,
    super.sprite
  }):super(
    anchor: Anchor.center,
    paint: BasicPalette.gary.paint()
  );
}

一旦これでゲームにメイドちゃんを表示させるための MaidComponent が出来ました。 ジャンプやあたり判定などは後ほど実装していきます。

キャラクターを表示させる

先ほど作った MaidComponentJumpGame へ追加してゲーム内に表示せていきます。 まず表示させる画像を用意します。今回は虎の穴ラボのWebサイトを使います。 画像を用意ししたら assets/images/maid01.png として保存します。

次にFlutter内で画像を使うために pubspec.yaml にassetsの追加を行います。

~~
flutter:
  ~~
  assets:
    - assets/images/

これで画像を使える様になりました。次にゲーム内に表示させるための処理を作っていきます。 lib/games/jump_game.dartJumpGame を編集します。

class JumpGame extends FlameGame{
  late MaidComponent _maid; // MaidComponentの用意
  @override
  Future<void> onLoad() async {
    super.onLoad();
    add(ScreenHitbox());
    final maidSprite = await Sprite.load('maid01.png'); // メイドちゃんの画像の読み込み
    _maid = MaidComponent(
      position: Vector2(100, size.y * 0.8), // 初期の表示位置を指定
      size: Vector2.all(size.x * 0.1), // サイズを指定
      sprite: maidSprite, // メイドちゃんの画像を読み込んだspriteを指定
    )

    add(_maid); // ゲーム内へMaidComponentを追加する
  }
}

これで実行するとメイドちゃんが表示されるようになりました! 実際に実行して確認してみます。

flutter run -d chrome

ジャンプさせる

MaidComponent をジャンプできるように実装していきます。

class MaidComponent extends SpriteComponent{
  bool isJump = false; // ジャンプ動作のフラグ
  bool isJumpUp = false; // ジャンプ方向のフラグ
  final double maxJump = 450.0; // ジャンプの最高到達点
  final double jumpSpeed = 10.0; // ジャンプするスピード
  final double basePos; // ジャンプ開始・終了の高さ
  MaidComponent({
    super.position,
    super.size,
    super.sprite,
    required this.basePos
  }):super(
    anchor: Anchor.center,
    paint:: BasicPalette.gary.paint()
  );

  // フレームごとに呼ばれるメソッド
  @override
  void update(double dt){
    super.update(dt);
    // ジャンプの動作を実装
    // 最高到達点へ行ったら下に落ちる
    if (isJump) {
      if (isJumpUp) {
        position.y -= jumpSpeed;
        if (position.y <= maxJump) {
          isJumpUp = false;
        }
      } else {
        position.y += jumpSpeed;
        if (position.y >= basePos) {
          positionReset();
          isJump = false;
        }
      }
    }
  }

  // ジャンプした際に呼ばれる
  void jump(){
    isJump = false;
    isJumpUp = false;
  }

  // キャラクター位置をリセット
  void positionReset(){
    position.y = basePos;
  }
}

これでMaidComponentはジャンプする準備が出来ました。 次はJumpGame側からキーイベント、タップを検知して実際にジャンプするようにしていきます。
物理演算等を使えば簡単にジャンプの動作も実装出来ますが、他に必要なライブラリを追加したり説明が多くなるので今回は省きます。
気になる方はflame_forge2d等を確認してください。

// タップのイベントを取るための TapCallbacks
// キーボードのイベントを取るための KeyboardEvents
// をそれぞれmixin
class JumpGame extends FlameGame with TapCallbacks, KeyboardEvents {
  late MaidComponent _maid; // MaidComponentの用意
  @override
  Future<void> onLoad() async {
    ~ 略 ~
  }

  // キーイベントを取るコールバック
  @override
  KeyEventResult onKeyEvent(
      KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
    // スペースキーを押されたらキャラクターがジャンプ
    if (keysPressed.contains(LogicalKeyboardKey.space)) {
      _maid.jump();
    }
    return KeyEventResult.ignored;
  }

  // タップされたイベントを取るコールバック
  @override
  void onTapDown(TapDownEvent event) {
    // クリックでもキャラクターがジャンプ
    super.onTapDown(event);
    _maid.jump();
  }
}

実行してスペースキーの入力かスマホの場合は画面タップでメイドちゃんがジャンプするようになりました!

ジャンプ

障害物の実装

次にメイドちゃんへ向かって飛んでくる障害物、今回は玉を実装していきます。 lib/components/ball_component.dart ファイルを作成しその中で CircleComponent を継承した BallComponentを作って、 玉が画面の右端から左へ飛んでいき画面端へ当たると消えるように実装します。

class BallComponent extends CircleComponent
      with CollisionCallbacks { // 当たり判定を取るCollisionCallbacks をmixin
  BallComponent({super.position}): super(radius: 10);
  final double ballSpeed = 10.0;

  @override
  void update(double dt) {
    super.update(dt);
    // 右から左へ移動する
    position.x -= ballSpeed;
  }

  // 玉が何かにあたった時に呼ばれるコールバック
  @override
  void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
    super.onCollision(intersectionPoints, other);
    // 壁に当たった場合の処理
    if (other is ScreenHitbox) {
      removeFromParent(); // 自身をゲームから削除する
    }
  }
}

玉の準備は出来ました!これをゲームに追加していきます。 玉の出現をランダムにすればゲーム性が出て面白いですが今回はシンプルに5秒ごとに玉が飛んでくるようにします。 5秒おきに実行する処理はDartの標準クラスのTimerではなくFlameに用意されている TimerComponent を使用します。 TimerComponentを使用することでゲーム全体をpauseさせた際の挙動などFlameに合わせて設計されています。

// 当たり判定を取るためにCollisionCallbacksをmixin
class JumpGame extends FlameGame with TapCallbacks, KeyboardEvents, CollisionCallbacks {
  late MaidComponent _maid; // MaidComponentの用意
  @override
  Future<void> onLoad() async {
    ~ 略 ~
    // 5秒ごとに球を発射するようにTimerComponentを追加
    add(TimerComponent(
        period: 5, // 間隔を指定
        repeat: true, // 繰り返し実行のフラグ
        onTick: () {
          add(BallComponent(
              position:
                  Vector2(size.x - 30, size.y * 0.8))); // 5秒ごとに球をゲームに追加
        }));
  }

  @override
  KeyEventResult onKeyEvent(
      KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
        ~ 略 ~
  }

  @override
  void onTapDown(TapDownEvent event) {
    ~ 略 ~
  }
}

これで玉が飛んでくるようになりました! 実行して確認してみましょう。

flutter run -d chrome

障害物

メイドちゃんの当たり判定

メイドちゃんが玉に当たった場合の動作を実装していきます。 メイドちゃんがたまに当たった場合は玉の表示を消し画像を「スライムになったメイドちゃん」に切り替えます。

「スライムになったメイドちゃん」の画像をassets/images/slime.png に保存します。

MaidComponent に当たり判定を追加します。

class MaidComponent extends SpriteComponent
     with CollisionCallbacks{ // 当たり判定を取る CollisionCallbacks をmixin
  bool isJump = false;
  bool isJumpUp = false;
  final double maxJump = 450.0;
  final double jumpSpeed = 10.0;
  final double basePos;
  MaidComponent(~略~):super(
    anchor: Anchor.center,
    paint:: BasicPalette.gary.paint()
  );

  @override
  void update(double dt){
    ~ 略 ~
  }

  //当たり判定のコールバック
  @override
  void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
    super.onCollision(intersectionPoints, other);
    // 玉に当たったらゲームオーバに
    if (other is BallComponent) {
      gameOver();
    }
  }

  void jump(){
    ~ 略 ~
  }

  void positionReset(){
    ~ 略 ~
  }

  // ゲームオーバーの処理
  void gameOver() async{
    // スライムの画像を読み込み
    // spriteを置き換える
    final slimeSprite = await Sprite.load('slime.png');
    sprite = slimeSprite;
  }
}

メイドちゃん側の当たり判定は出来ました、合わせて玉の当たり判定も追加しておきます。

class BallComponent extends CircleComponent
      with CollisionCallbacks { // 当たり判定を取るCollisionCallbacks をmixin
  BallComponent({super.position}): super(radius: 10);
  final double ballSpeed = 10.0;

  @override
  void update(double dt) {
    ~ 略 ~
  }

  // 玉が何かにあたった時に呼ばれるコールバック
  @override
  void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
    super.onCollision(intersectionPoints, other);
    // 壁に当たった場合の処理
    if (other is ScreenHitbox) {
      removeFromParent(); // 自身をゲームから削除する
    }

    //メイドちゃんに当たった場合
    if(other is MaidComponent){
      removeFromParent(); // 自身をゲームから削除する
    }
  }
}

これで実行すると、メイドちゃんが玉に当たった際にスライムになるように出来ました!

スライムメイドちゃん

スタート画面・ゲームオーバー画面の追加

現在のままではゲームを起動したら5秒後に玉が飛んできて、メイドちゃんが玉に当たってもひたすた玉が飛んできます。 もう少しゲーム性を持たせるために、スタート画面とメイドちゃんが玉に当たったあとのゲームオーバー画面を作っていきます。 この2つの画面はFlutterのWidgetを使用して作成し、Flameのオーバーレイ機能を使ってゲーム上に表示させます。

まずはlib/pages/start_page.dart にスタート画面のStartPageクラスを作ります。

class StartPage extends StatelessWidget {
  final JumpGame jumpGame;
  const StartPage({super.key, required this.jumpGame});

  @override
  Widget build(BuildContext context) {
    // スタートボタンが押されたらJumpGameのgameStartメソッド(後述)を呼び出す
    return Center(
      child: ElevatedButton(
          onPressed: () => jumpGame.gameStart(), child: const Text("Start")),
    );
  }
}

次にlib/pages/game_over_page.dartにゲームオーバー画面 GameOverPageの作成をします

class GameOverPage extends StatelessWidget {
  final JumpGame jumpGame;
  const GameOverPage({super.key, required this.jumpGame});

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Row(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            Material(child: Text("結果:${jumpGame.jumpCount}")), // 避けれた玉の数を表示
            // ボタンを押すとJumpGameのgameRestart(後述)を呼び出す
            ElevatedButton(
                onPressed: () => jumpGame.gameRestart(),
                child: const Text("Re Start")),
          ]),
    );
  }
}

この2つのWidgetでスタート画面とゲームオーバー画面の準備が出来ました。 次にオーバーレイの設定をします。 lib/pages/jump_game_page.dartJumpGamePage を修正します。

class JumpGamePage extends StatelessWidget {
  JumpGamePage({super.key});

  final game = JumpGame();

  @override
  Widget build(BuildContext context) {
    // FlutterへFlameのGameWidgetを追加
    return GameWidget(
      // FlutterのUI(Widget)を追加する
      overlayBuilderMap: {
        // スタート画面のWidget
        "start": (context, JumpGame jumpGame) => StartPage(
              jumpGame: jumpGame,
            ),
        // ゲームオーバー画面のWidget
        "gameOver": (context, JumpGame jumpGame) =>
            GameOverPage(jumpGame: jumpGame)
      },
      // ゲーム自体はJumpGameを設定
      game: game,
      initialActiveOverlays: const ['start'], // 起動時はスタート画面を設定
    );
  }
}

この修正でゲームが起動するとまずはスタート画面が表示されます。

スタート画面
ゲームオーバー画面

ただこのままだとスタート画面やゲームオーバー画面の裏でゲームの処理、今回では玉の発射やキャラクターのジャンプが動き続きます。
そこでスタートボタンが押されるまでとゲームオーバー後はFlameGamepausedtrueにしてゲーム全体を止めます。
gameStartメソッド, gameRestartメソッドの実装と合わせてpausedの追加もやっていきましょう。

class JumpGame extends FlameGame with TapCallbacks, KeyboardEvents, CollisionCallbacks {
  late MaidComponent _maid;
  int jumpCount = 0; //ゲームオーバー画面で表示するジャンプの成功数を保持
  @override
  Future<void> onLoad() async {
    ~ 略 ~
  }

  @override
  void update(double dt){
    // メイドちゃんがゲームオーバーになったらgameOverメソッドを呼び出す
    if(_maid.isGameOver){
      gameOver()
    }
  }

  // onLoadが完了したあとに呼ばれるコールバック
  @override
  void onMount(){
    super.onMount();
    paused = true; // trueを代入することでゲーム全体を停止させる
  }

  // スタートボタンを押した際に呼ばれるメソッド
  void gameStart() async {
    // オーバーレイのスタート画面を削除する
    overlays.remove('start');
    // ポーズを解除
    paused = false;
    // メイドちゃんをスタート状態にする
    _maid.gameStart();
  }

  void gameOver(){
    // ゲームをポーズ状態にする
    paused = true;
    // ゲームオーバー画面をオーバーレイする
    overlays.add("gameOver");
  }

  // ゲームオーバー画面でリスタートボタンを押された時に呼ばれる
  void gameRestart(){
    // ゲームオーバー画面のオーバーレイを削除して
    // スタート画面をオーバレイする
    overlays.remove("gameOver");
    overlays.add("start");
  }

  @override
  KeyEventResult onKeyEvent(
      KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
        ~ 略 ~
  }

  @override
  void onTapDown(TapDownEvent event) {
    ~ 略 ~
  }
}

これでスタート画面とゲームオーバー画面の切り替えゲームの停止ができるようになりました。 次はMaidComponentgameStartメソッド実装とgameOverメソッドの修正をします。

class MaidComponent extends SpriteComponent
    with CollisionCallbacks, HasGameRef<JumpGame> {
  ~ 略 ~
  // ゲームスタート、リスタート時に呼ばれる
  // 画像、フラグのリセット
  void gameStart() async {
    final maidSprite = await Sprite.load('maid01.png');
    sprite = slimeSprite;
    isGameOver = false;
  }

  // ゲームオーバー
  // 画像の差し替え、フラグの変更
  void gameOver() async {
    positionReset();
    final slimeSprite = await Sprite.load('slime.png');
    sprite = slimeSprite;
    isGameOver = true;
  }
}

最後にBallComponent内でジャンプの成功数をカウントします。

// HasGameRefをmixinする
// JumpGameを指定することでJumpGame側のメソッドや変数にアクセスできるようになる
class BallComponent extends CircleComponent
    with CollisionCallbacks, HasGameRef<JumpGame> {

  ~ 略 ~
  @override
  void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
    super.onCollision(intersectionPoints, other);
    if (other is ScreenHitbox) {
      // 壁に当たったらカウントして球を消す
      gameRef.jumpCount++; //gameRefを使うことでJumpGameのjumpCountをインクリメントできる
      removeFromParent();
    }

    if (other is MaidComponent) {
      // プレイヤーキャラクターに当たったら球を消す
      removeFromParent();
    }
  }
}

これでスタート画面からゲームオーバーまでの一連の流れが出来ました!

また、ジャンプの高さの調整などは必要ですがiPhoneでも実行が可能です!

iPhone

最終的なlib以下のファイル構成は以下のようになっています。

lib
├── components
│   ├── ball_component.dart
│   └── maid_component.dart
├── games
│   └── jump_game.dart
├── main.dart
└── pages
    ├── game_over_page.dart
    ├── init_page.dart
    └── jump_game_page.dart

まとめ

Flameを使うことでFlutter上でも簡単に2Dの実装を行うことが出来ました。
有名なゲームエンジンのような派手なGUIでのゲーム制作では無いですが、普段から使ってるFlutterで簡単にマルチプラットフォーム対応のゲームを作る事が出来ました。
今回は簡単なジャンプゲームだけでしたが、アイデア次第で色々なゲームが作れて楽しそうです。

Fantia開発採用情報

虎の穴ラボでは現在、一緒にFantiaを開発していく仲間を積極募集中です!
多くのユーザーに使っていただけるtoCサービスの開発をやってみたい方は、ぜひ弊社の採用情報をご覧ください。
https://toranoana-lab.co.jp/job/302toranoana-lab.co.jp