見出し画像

Flutter + Flameで某スイカなゲームを作ってみた

はじめに

2023年も色々なことがありました。
阪神のリーグ優勝から、藤井棋士の八冠制覇など、色々と印象深い出来事があったのではないかと思います。

中でも、最近で印象深いのは、スイカゲームの流行ではないでしょうか。
シンプルながらも奥深いゲーム性と、時折起きるハプニングとで、YouTube等でもよくプレイ動画を見かけるのではないかと思います。

この記事では、Flutter + Flameを使って、そんなスイカゲームを作成してみます。

なお、作ったゲームはFirebase Hostingへデプロイしているため、次リンクより遊んでいただけます。


Flameとは

FlameはFlutter上で動くゲームエンジンです。

追加パッケージを使用することで物理演算を行うこともでき、Flutterプラットフォーム上で動く様々なゲームを作ることができます。
スイカゲームを作成する上では、フルーツが落下する部分で物理演算を使用することからも、今回はこのFlameを使用して、ゲームを作成していきます。

設計

まずはゲーム(システム)における登場人物を洗い出し、設計をしていきます。
スイカゲームでの要件を箇条書きすると、次のようになるかと思います。

- フルーツが落下する
- 同じサイズのフルーツが衝突すると、ひと回り大きなフルーツになる
- ひと回り大きなフルーツができると、スコアが加算される
...

要件を箇条書きしたら、要件に登場する名詞を洗い出していきます。

- フルーツ
- スコア
- 壁
- フルーツが落下する位置を予測する白線

これらがシステム上で登場するオブジェクトとなります。

また、システム上では上記オブジェクトを表現していくことになりますが、アーキテクチャ的には次のようにシンプルなアーキテクチャを採用することにします。

  • UI層…. ゲームでのUIを表示する層

  • State層… ゲーム内での状態を管理する層。また、ドメインロジックを担当する層

  • Repository層… データ永続化に関して担当する層

  • Presenter層… 画面表示等に関して担当する層

実装

ゲームでの中心的要素である、同じサイズでのフルーツが衝突したら、ひと回り大きなフルーツとなる、部分を実装すると次でのようになります。
(onLoad()はゲーム初期化時に呼ばれるメソッドで、onUpdate()はゲームでのメインループで定期的に呼ばれるメソッドです)

class MainGame extends Forge2DGame {
  @override
  Future<void> onLoad() async {
    // フルーツ同士の衝突を検出するリスナーを設定する
    world.physicsWorld.setContactListener(
      FruitsContactListener(),
    );
  }

  @override
  void update(double dt) {
    super.update(dt);
    final collidedFruits = GetIt.I.get<GameRepository>().getCollidedFruits();
    if (collidedFruits.isEmpty) {
      return;
    }
    for (final collideFruit in collidedFruits) {
      final fruit1 = collideFruit.fruit1.userData! as PhysicsFruit;
      final fruit2 = collideFruit.fruit2.userData! as PhysicsFruit;
      final newFruit = _getNextSizeFruit(
        fruit1: fruit1,
        fruit2: fruit2,
      );
      world
        ..remove(fruit1)
        ..remove(fruit2)
        ..add(newFruit);
    }
  }
}

class FruitsContactListener extends ContactListener {
  FruitsContactListener();

  @override
  void beginContact(Contact contact) {
	// 2つのfixture(物理特性が定義されたオブジェクト)が衝突すると、このメソッドが呼ばれる
    final bodyA = contact.fixtureA.body;
    final bodyB = contact.fixtureB.body;
    final userDataA = bodyA.userData;
    final userDataB = bodyB.userData;

    if (userDataA is PhysicsFruit && userDataB is PhysicsFruit) {
      if (userDataA.isStatic || userDataB.isStatic) {
        return;
      }
      // 同じサイズのフルーツが衝突した
      if (userDataA.fruit.radius == userDataB.fruit.radius) {
        GetIt.I.get<GameRepository>().addCollidedFruits(
         CollidedFruits(bodyA, bodyB),
       );
      }
    }
  }
}

上記コードではまず、onLoad()で同じサイズでのフルーツが衝突したことを検出するリスナーを設定しています。
そして、フルーツ同士が衝突すると、GameRepositoryを通じて、衝突したフルーツに関する情報を保存します。

保存された「衝突したフルーツ」は、update()内で取り出され、画面上から削除された後に、ひと回り大きなサイズでのフルーツを画面に追加しています。

作成したコード

作成したコードは次リポジトリへ置いているため、ソースコード全体はそちらよりご確認いただけます。

まとめ

この記事では、Flutter+Flameを使ってスイカゲームを作ってみました。
Flameを使用するのは初めてでしたが、非常に手軽にゲームが作れて良いですね。
本格的なゲームを作る用途ではUnity等エコシステムが充実したゲームエンジンに軍配が上がりそうですが、カジュアルなゲームをマルチプラットフォームで展開する用途だと、Flameも選択肢に入ってくるのではないかと思います。

個人としては、今年は業務もFlutter漬けな一年でしたが、まだまだFlameのように知らない世界もあるので、また来年も引き続きFlutterを楽しんでいければと思います。


この記事が気に入ったらサポートをしてみませんか?