はじめに

こんにちは。エンジニアの遠藤です。
今回は、以前のブログの最後で、公式が出している Game ライブラリらしきものを見かけたので試してみる、と書いたので、実際に試してみた記事になります。

試してみるのは、こちらです。
https://flutter.dev/games

注) 本記事は、 2023年4月13日に時点の master ブランチの内容を元に執筆しています。
https://github.com/flutter/samples/tree/5dc04a16be0ca47e4d8dcf43f36c0d4dd73bb575/game_template

動かしてみよう

いきなりですが、こういう楽しそうなプログラムはドキュメントもそこそこに、まずはいきなり動かしてみるのが良いですよね!
せっかくテンプレートが用意されていて、実行コマンドが置いてあったら、うっかり触ってしまいませんか?
そして何より、結果的にそれが一番理解が進むかなと思っています。(個人の見解です)

見た目がすでにちょっとかっこよくてワクワクします。

----------2023-04-07-20.48.03

(うひゃー○×ゲームができるのかなー?)

それでは、「Get the template」していきましょう。
まずは、プロジェクトをローカルに clone してきます。

$ git clone git@github.com:flutter/samples.git

https の場合は、以下のコマンドです。

$ git clone https://github.com/flutter/samples.git

続いて、 clone したリポジトリに移動します。

$ cd sample

この sample の下には、いくつかプロジェクトがあります。
どれも気になりますが、今回は game_template に移動しましょう。

$ cd game_template

あとは、実行するのみです。
ちなみに、そのまま iOS 向けに ipa ビルドや Android 向けに apk ビルドができます。

iOS 向けビルド

$ flutter build ipa

Android 向けビルド

$ flutter build apk

また、Readme に説明もありますが、 macOS 上のデスクトップアプリとしてデバッグ実行させることもできます。
ちょっと state を変更して動作確認する時などはこちらが便利ですね。

$ flutter run -d macOS

今回はせっかくなので、 macOS 向けに起動してみます。

$ flutter run -d macOS
Launching lib/main.dart on macOS in debug mode...
Syncing files to device macOS...                                    84ms
(中略)
Flutter run key commands.
r Hot reload. 🔥🔥🔥
R Hot restart.
h List all available interactive commands.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).

💪 Running with sound null safety 💪

An Observatory debugger and profiler on macOS is available at: http://127.0.0.1:57508/TIT26Ec5quc=/
http://127.0.0.1:9100?uri=http://127.0.0.1:57508/TIT26Ec5quc=/

色々と警告などが出てきてドキドキしますが...

テンプレートのゲーム画面

ダダーン!

----------2023-04-07-20.57.54

唐突におしゃれな音楽が流れ、アプリが起動します。
おおぉ!良いですね。

早速「Play」で遊んでみましょう。

ほうほう、レベルを選べると。
----------2023-04-07-20.59.18

そして、、、これがゲーム画面です!
(あれ、○×ゲームじゃないな...)
----------2023-04-07-21.02.58

スライダーを動かすと...
----------2023-04-07-21.02.15

ゲームクリア!
紙吹雪が舞います。
----------2023-04-07-21.02.24

そして、スコア画面!
----------2023-04-07-21.02.50

さらに設定画面!
(ここから○×ゲームを選べたりするのかなー?)
----------2023-04-07-21.28.27

以上です。

Readme を読む

あれ、○×ゲームはないの?

はい、ありません。
なんならストアに公開されているアプリでした。
https://play.google.com/store/apps/details?id=dev.flutter.tictactoe

さて、では改めて、 game_template の Readme の先頭を見てみましょう。

A starter game in Flutter with all the bells and whistles of a mobile (iOS & Android) game including the following features:

・ sound
・ music
・ main menu screen
・ settings
・ ads (AdMob)
・ in-app purchases
・ games services (Game Center & Google Play Games Services)
・ crash reporting (Firebase Crashlytics)

ざっくり翻訳すると、「このテンプレートには、サウンド、音楽、メインメニュー、設定、広告、アプリ内課金、ゲームサービス、クラッシュレポートの機能が一通り入っています。」ということです。

なるほど、私は勝手に Unity とか UE のようなゲームエンジンを勝手に想像していましたが、全く違うものだったようです。

ゲーム画面の実装を見てみても、特別なフレームワークは使われておらず、customPainter を利用している点などは、前回自分で実装したアプリと大きな差はなさそうでした。

とはいえ、確かに、後半の

  • 広告
  • アプリ内課金
  • ゲームサービス
  • クラッシュレポート

この辺りは自分で一つずつ準備して調べて、実装して、、、というのは意外と手間がかかるものですし、あったらちょっと便利ですね。

コードの中身を見てみよう

このままだと、内容のないブログになってしまいそうなので、少しテンプレートの中身を見てたいと思います。

main.dart

まずは main.dart の中身です。
構造としては以下のようになっています。

  AppLifecycleObserver(
    child: MultiProvider(
      child: Builder(builder: (context) {
        return MaterialApp.router(
          title: 'Flutter Demo',
          ),
        );
      }),
    ),
  );

一番外側に、 WidgetsBindingObserver を継承した AppLifecycleObserver を配置してアプリの状態(フォアグラウンドやバックグラウンド)を取得できるようにしています。

その中には、 MultiProvider を配置して、ゲームの進捗や、アプリ内購入の状態、ゲームの設定などをまとめて管理できるようにしています。

そして、最下層に GoRoute により router が定義されています。

分かりやすいですし、便利そうですね。

ads

テスト用のIDも設定されていて、広告を有効にしたければ、コメントアウトを外して、

  if (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) {
    /// Prepare the google_mobile_ads plugin so that the first ad loads
    /// faster. This can be done later or with a delay if startup
    /// experience suffers.
    adsController = AdsController(MobileAds.instance);
    adsController.initialize();
  }

main.dart に以下の import文を追加するだけで OK。

import 'dart:io';
import 'package:google_mobile_ads/google_mobile_ads.dart';

ゲームクリア画面にサンプルのバナーが表示されます。

表示している該当コードは以下です。
https://github.com/flutter/samples/tree/5dc04a16be0ca47e4d8dcf43f36c0d4dd73bb575/game_template/lib/src/win_game/win_game_screen.dart#L41-L43

この辺りも、ゲーム中に広告を出さずに、クリア画面に出そうね、という Google 側からのメッセージが伝わってきて良いなと思いました。

ご丁寧に、課金状態まで判定して、広告の非表示ができるサンプルも置いてあります。

    final adsRemoved =
        context.watch<InAppPurchaseController?>()?.adRemoval.active ?? false;

https://github.com/flutter/samples/tree/5dc04a16be0ca47e4d8dcf43f36c0d4dd73bb575/game_template/lib/src/win_game/win_game_screen.dart#L27-L28

app_lifecycle

main.dart の説明でも触れた通り、 AppLifecycleObserver が定義されています。
今回は後述する audio_controller の再生/停止などをコントロールしているようです。

https://github.com/flutter/samples/tree/5dc04a16be0ca47e4d8dcf43f36c0d4dd73bb575/game_template/lib/src/audio/audio_controller.dart#L154-L169

paused, detached イベントが発火した際に音源を停止して resumed で再生しています。

発火したイベントをどこでどうやってハンドリングするのが良いか、意外と悩ましいポイントなので、コントローラとの連携までサンプルで実装されているのはありがたいですね。

audio

内部的には、効果音とバックミュージックを分けて管理しています。
いずれも AudioPlayer を利用していてバックミュージック用のインスタンス一つと、効果音を再生する用の複数のインスタンスを生成して、同時にいくつかの音を再生できるように管理しています。
効果音の再生中にバックミュージックが消えたらゲームが台無しなので、参考になる実装です。

また、ゲームでは、効果音だけ消したい、といったユーザの設定もありそうですし、この辺りを分けているのは気が利いているなと思いました。

crashlytics

runZonedGuarded で発生したエラーをキャッチして、 crashlytics に送信するコードが書かれています。

コードを有効にする方法は Readme に丁寧に書いていあります。
Readme の手順を試してみたところ、 google-service.json などが自動的にプロジェクトに追加されました。
参考までに実行したコードをまとめると、以下通りです。

$ curl -sL https://firebase.tools | bash
$ npm install -g firebase-tools
$ flutterfire configure
$ dart pub global activate flutterfire_cli
$ export PATH="$PATH":"$HOME/.pub-cache/bin"
$ firebase login
$ flutterfire configure

game_internal

これはゲーム内部で使う state を管理するクラスが定義されています。
サンプルでは、 level_state のみ存在していて、この中で、クリアしたかどうかなどの判定が行われています。

game_services

ここでは、 Game Center&Play Games Services に関するコードが書かれています。
games_services プラグインを使用しており、 games_services.dart では

  • プラグインの初期化(ログイン)
  • 実績の達成
  • スコアの送信
  • リーダーボードのオーバーレイ表示

などが実装されています。
また、それぞれ、 game_service が有効になっている場合にのみ呼び出されるように実装もされています。

見れば見るほど親切ですね。

ただ、プラグインと同じ名前のファイルを用意しているのはちょっとイケてないのでは?と思いました。

in_app_purchase

ここはアプリ内課金が実装されています。
in_app_purchase プラグインを使用しています。
広告の除去アイテムを購入しているかどうかを制御できるようになっています。
また、 in_app_purchase が有効になっている場合に、設定画面から購入できるように実装が済んでいます。
https://github.com/flutter/samples/tree/5dc04a16be0ca47e4d8dcf43f36c0d4dd73bb575/game_template/lib/src/settings/settings_screen.dart#L61
ads のところでも触れましたが、購入した場合に バナーが出ない制御も実装されています。

これもプラグインと同名なのはどうなのよ?という気持ちがあります。。

level_selection

ここはゲームのレベル設定画面を表示しているだけですね。
レベルの表示は column で一覧しているだけで、各レベルは、以下のような GameLevel クラスで定義されています。
achievementId がちょっと特殊ですが、それ以外は特筆するような実装はなさそうです。

class GameLevel {
  final int number;

  final int difficulty;

  /// The achievement to unlock when the level is finished, if any.
  final String? achievementIdIOS;

  final String? achievementIdAndroid;

  bool get awardsAchievement => achievementIdAndroid != null;

  const GameLevel({
    required this.number,
    required this.difficulty,
    this.achievementIdIOS,
    this.achievementIdAndroid,
  }) : assert(
            (achievementIdAndroid != null && achievementIdIOS != null) ||
                (achievementIdAndroid == null && achievementIdIOS == null),
            'Either both iOS and Android achievement ID must be provided, '
            'or none');
}

アプリ起動時に表示されるメインメニュー画面の実装です。
ここも特筆するものはなく、ざっくり以下のような構成の画面を表示しているだけでした。

  • ResponsiveScreen
    • Column
      • [FilledButton]

play_session

肝心の(?)ゲーム画面です。
ここはテキストとSliderが置いてあるだけですね。

基本的にはこの画面で自分の好きなゲームを実装していくイメージになります。

player_progress

ここではユーザのゲームの進行状況を保存してく実装がされています。
persistence/ 以下では local_storage と memory のそれぞれで実装のサンプルが用意されています。
初期状態では、 local_storage が有効になっており、 shared_preferences プラグインを使ってデータが保存されています。

settings

設定画面に関する実装がされています。
設定画面も、 player_progress と同様に persistence/ 以下に保存の処理が用意されています。
個人的にはデータの保存に関しては database/ みたいなディレクトリを用意してその中に全部まとめたい気持ちもありますが、各画面ごとに実装していく方針もあるのですね。
これはこれで、管理しやすい気もするので、参考になります。

style

画面内で使う色や、画面遷移などがまとまって実装されています。
ゲームクリア時の紙吹雪もここで実装されていました。
util とか common みたいなところにまとめがちな機能ですが、見た目に関する共通機能は style/ にまとめるのも良いかもしれないですね。
ただ、見た目に関わるってどこからどこまでよ?みたいなところで頭を使いたくないので、これからも util を使い続ける気もします...

さて、 style の中では、 Confetti という紙吹雪を散らすクラスが実装されています。
こちらの実装でも、 CustomPainter を使ってアニメーションさせているのですが、私が以前のブログで実装した scheduleFrameCallback は使わずに、 AnimationController を使用することで描画を実現していました。

    ConfettiPainter(
      colors: widget.colors,
      animation: _controller,
    )

    _controller = AnimationController(
      duration: const Duration(seconds: 1),
      vsync: this,
    );
    _controller.repeat();

なるほどなるほど、勉強になります。

紙吹雪は、左右にコサインカーブを描いて落ちていきます。

    position.x += cos(time * oscillationSpeed) * xSpeed * dt;

https://github.com/flutter/samples/tree/5dc04a16be0ca47e4d8dcf43f36c0d4dd73bb575/game_template/lib/src/style/confetti.dart#L213

紙がヒラヒラ舞うように、裏と表の表現をしているのですが、表の color に対して、 alphaBlend をしています。

  late final Color backColor = Color.alphaBlend(backSideBlend, frontColor);

https://github.com/flutter/samples/tree/5dc04a16be0ca47e4d8dcf43f36c0d4dd73bb575/game_template/lib/src/style/confetti.dart#L180

また、Y軸方向の回転を表現するために、元々持っている 4点の vector 情報のうち y 座標のみに、コサインカーブの値を掛けることで、紙の回転が表現できています。

    final path = Path()
      ..addPolygon(
        List.generate(
            4,
            (index) => Offset(
                  position.x + corners[index].x * size,
                  position.y + corners[index].y * size * cosA,
                )),
        true,
      );

https://github.com/flutter/samples/tree/5dc04a16be0ca47e4d8dcf43f36c0d4dd73bb575/game_template/lib/src/style/confetti.dart#L196-L205

x座標に cosA を掛けると、 X軸方向の回転も表現できます。
紙が右から左に流されている時なんかには使っても良いかもと思いました。

高校数学(文系なので数ⅡBですが)まではまぁまぁ勉強していたので、計算内容は理解できるのですが、矩形をY軸方向に回転させようと思った時に、この計算方法を思いつく自信はありません。
すごいなぁ。。

screen-20230412-235626_3_AdobeExpress

↑ gif にすると見た目がひどいですが、実機で見ると確かにそれっぽく見えます。

win_game

ゲームクリア画面の実装です。
ここも特筆すべきことはなく、メッセージやスコアが Text で表示されているだけでした。

終わりに

というわけで、「Flutter Casual Games Toolkit」のテンプレートを実行して、コードを解説した記事になりました。

個人的に勝手に期待値を上げてしまい、勝手にガッカリした内容になってしまいました。

とはいえ、細かく実装を見ていくと、ちょっとした発見があったり、意外と凝った実装があったり、調べるのに手間取りそうな実装が済んでいたりと、良いところがたくさんありました。

さて。
前回は 「flutter game」 で調べて、トップに公式サイトがヒットしたため、飛びついてしまいましたが、私が欲しかったものは、ゲームエンジンでした。
気をあらためて「flutter ゲームエンジン」で調べてみると、しっかりいくつか見つかりました。
なかでもFrame Engineが最も一般的なようでしたので、次回こそ、ゲームエンジンを使ったゲームを実装して、子供を喜ばせてみたいと思います!

(でも、あの「Get the template」画面のスクリーンショットは騙されるよなぁ...ぶつぶつ