FlutterのCasual Game ToolKitを使ってみる! その4

ブログ
FlutterのCasual Game ToolKitを使ってみる! その3
FlutterのCasual Game ToolKitを使ってみましょう。

引き続き前回のソースコードを編集していきます。

Flutter Casual Game Toolkit:

samples/game_template at main · flutter/samples
AcollectionofFlutterexamplesanddemos.Contributetoflutter/samplesdevelopmentbycreatinganaccountonGitHub.

go_routerパッケージ

go_routerパッケージを使用することで、画面遷移を簡単に実装することができます。Casual Game Toolkit内のmain.dartでgo_routerパッケージを使用した変数を宣言しており、ここを使ってみましょう。

main.dart

(抜粋)
static final _router = GoRouter(
    routes: [
      GoRoute(
          path: '/',
          builder: (context, state) =>
              const MainMenuScreen(key: Key('main menu')),
          routes: [
            GoRoute(
                path: 'play',
                pageBuilder: (context, state) => buildMyTransition<void>(
                      key: ValueKey('play'),
                      child: const LevelSelectionScreen(
                        key: Key('level selection'),
                      ),
                      color: context.watch<Palette>().backgroundLevelSelection,
                    ),
                routes: [
                  GoRoute(
                    path: 'session/:level',
                    pageBuilder: (context, state) {
                      final levelNumber =
                          int.parse(state.pathParameters['level']!);
                      final level = gameLevels
                          .singleWhere((e) => e.number == levelNumber);
                      return buildMyTransition<void>(
                        key: ValueKey('level'),
                        child: PlaySessionScreen(
                          level,
                          key: const Key('play session'),
                        ),
                        color: context.watch<Palette>().backgroundPlaySession,
                      );
                    },
                  ),
                  GoRoute(
                    path: 'won',
                    redirect: (context, state) {
                      if (state.extra == null) {
                        // Trying to navigate to a win screen without any data.
                        // Possibly by using the browser's back button.
                        return '/';
                      }

                      // Otherwise, do not redirect.
                      return null;
                    },
                    pageBuilder: (context, state) {
                      final map = state.extra! as Map<String, dynamic>;
                      final score = map['score'] as Score;

                      return buildMyTransition<void>(
                        key: ValueKey('won'),
                        child: WinGameScreen(
                          score: score,
                          key: const Key('win game'),
                        ),
                        color: context.watch<Palette>().backgroundPlaySession,
                      );
                    },
                  )
                ]),
            GoRoute(
              path: 'settings',
              builder: (context, state) =>
                  const SettingsScreen(key: Key('settings')),
            ),
          ]),
    ],
  );

routerという変数にGoRouterウィジェットを宣言しています。main.dart内でこれを使用しているのが以下の箇所です。

@override
  Widget build(BuildContext context) {
    return AppLifecycleObserver(

      ~~~ 省略~~~

        child: Builder(builder: (context) {
          final palette = context.watch<Palette>();

          return MaterialApp.router( <-ここ!
            title: 'Flutter Demo',
            theme: ThemeData.from(
              colorScheme: ColorScheme.fromSeed(
                seedColor: palette.darkPen,
                background: palette.backgroundMain,
              ),
              textTheme: TextTheme(
                bodyMedium: TextStyle(
                  color: palette.ink,
                ),
              ),
              useMaterial3: true,
            ),
            routeInformationProvider: _router.routeInformationProvider,<-ここ!
            routeInformationParser: _router.routeInformationParser,<-ここ!
            routerDelegate: _router.routerDelegate,<-ここ!
            scaffoldMessengerKey: scaffoldMessengerKey,<-ここ!
          );
        }),
      ),
    );
  }

このように宣言したrouterという変数をMaterialApp.router内におまじないを書くことで使うことができるようです。

というわけでこの例に倣って、まずはrouterという変数を宣言してみましょう。

その下準備として遷移前の画面を1つ準備します。ここは遷移させるボタンだけあればいいので適当でOK。

main_page.dart


class MainPage extends StatelessWidget {
  const MainPage({super.key, required this.title});

  final String title;

  @override
  Widget build(BuildContext context) {
    var counter = context.watch<CounterState>();
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'Start App!',
            ),
            FloatingActionButton(
              onPressed: () {
                   // あとで実装
              },
              child: const Icon(Icons.play_arrow_outlined),
            ),
          ],
        ),
      ),
    );
  }
}

FloatingActionButtonはあとで遷移させる処理を追加しましょう。

では、以下のようなパスと画面遷移の構成でCasual Game Toolkitの例に倣ってrouterという変数を準備してみましょう。

  • “/”:MainPageを表示する
  • /homepage:既に今まで作成してきたカウンターアプリの本体。

/にあるボタンをクリックすると、/homepageの画面を表示させる。

main.dart(抜粋)

static final _router = GoRouter(routes: [
    GoRoute(
      path: '/',
      builder: (context, state) =>
          const MainPage(title: 'Flutter Demo Home Page'),
      routes: [
        GoRoute(
          path: 'homepage',
          pageBuilder: (context, state) => buildMyTransition<void>(
            key: ValueKey('homepage'),
            child: const MyHomePage(title: 'Flutter Demo Home Page'),
            color: Colors.deepPurple,
          ),
        ),
      ],
    ),
  ]);

とりあえず何も考えずにコピーアンドペーストしてみましたが、buildMyTransitionの箇所でエラーになっていると思います。

というわけで、このbuildMyTransitionを使用しているコンポーネントをCasual Game Toolkitから取り込んでみましょう。

my_transition.dart

CustomTransitionPage<T> buildMyTransition<T>({
  required Widget child,
  required Color color,
  String? name,
  Object? arguments,
  String? restorationId,
  LocalKey? key,
}) {
  return CustomTransitionPage<T>(
    child: child,
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      return _MyReveal(
        animation: animation,
        color: color,
        child: child,
      );
    },
    key: key,
    name: name,
    arguments: arguments,
    restorationId: restorationId,
    transitionDuration: const Duration(milliseconds: 700),
  );
}

class _MyReveal extends StatefulWidget {
  final Widget child;

  final Animation<double> animation;

  final Color color;

  const _MyReveal({
    required this.child,
    required this.animation,
    required this.color,
  });

  @override
  State<_MyReveal> createState() => _MyRevealState();
}

class _MyRevealState extends State<_MyReveal> {
  static final _log = Logger('_InkRevealState');

  bool _finished = false;

  final _tween = Tween(begin: const Offset(0, -1), end: Offset.zero);

  @override
  void initState() {
    super.initState();

    widget.animation.addStatusListener(_statusListener);
  }

  @override
  void didUpdateWidget(covariant _MyReveal oldWidget) {
    if (oldWidget.animation != widget.animation) {
      oldWidget.animation.removeStatusListener(_statusListener);
      widget.animation.addStatusListener(_statusListener);
    }
    super.didUpdateWidget(oldWidget);
  }

  @override
  void dispose() {
    widget.animation.removeStatusListener(_statusListener);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      fit: StackFit.expand,
      children: [
        SlideTransition(
          position: _tween.animate(
            CurvedAnimation(
              parent: widget.animation,
              curve: Curves.easeOutCubic,
              reverseCurve: Curves.easeOutCubic,
            ),
          ),
          child: Container(
            color: widget.color,
          ),
        ),
        AnimatedOpacity(
          opacity: _finished ? 1 : 0,
          duration: const Duration(milliseconds: 300),
          child: widget.child,
        ),
      ],
    );
  }

  void _statusListener(AnimationStatus status) {
    _log.fine(() => 'status: $status');
    switch (status) {
      case AnimationStatus.completed:
        setState(() {
          _finished = true;
        });
      case AnimationStatus.forward:
      case AnimationStatus.dismissed:
      case AnimationStatus.reverse:
        setState(() {
          _finished = false;
        });
    }
  }
}

内容自体は画面遷移時の演出を主に行っているようですね。

最後にMainPageに画面遷移の処理を入れることで完成です。

main_page.dart

~~~ 抜粋 ~~~
 FloatingActionButton(
              onPressed: () {
                   // あとで実装
                   GoRouter.of(context).go('/homepage'); <-追加
              },
              child: const Icon(Icons.play_arrow_outlined),
            ),

これを実施に動かしてみます。ボタンを押すと演出が入った上で画面遷移が行われることがわかると思います。

まとめ

これで画面遷移周りの環境は整った感じですね。

これで一通りの基本的な要素は全て実装したわけですが、まだまだ足りていない機能が盛りだくさんです。具体的なアプリケーションの部分を除いてもこんなにあるわけです。

  • Firebaseとの連携周り
  • App内課金周りの処理の実装
  • 音楽再生・効果音の再生機能
  • 各画面の作り込み

とはいえやはりゲームといえばBGMです。また、画面もまだまだ作り込みたいのが正直なところ。

というわけで、どちらもできるように画面を作り込みつつ設定画面を作り込んでみましょう!合わせてBGM機能を実装していく感じですね。

コメント