へっぽこSEが◯時間でFlutter(iOS、android、web)で2Dゲームアプリを作ってみる(13)

どうも、すえきあおいです。

YouTubeのチュートリアル動画を使った2Dゲーム(テトリス)モバイルアプリ開発が順調に進んでいます。前回はブロックが重なったり壁を通り抜けたりするのを修正し、スコア計算の実装までやりました。

フルのソースコードはこちらにあるので、面倒な人はダウンロードどうぞ。

 

今日は、ゲームオーバーロジックの実装をしていきます。

今日の内容は、チュートリアル動画の12:47〜21:51までです。

 

それでは行ってみましょう。

ゲームオーバーの実装(12:47〜)

lib/game.dart

class GameState extends State<Game> {
  bool isGameOver = false; //ゲームオーバーフラグ
...
  void startGame() {
    isGameOver = false; //ゲームオーバーフラグを初期化
...
  void onPlay(Timer timer){
...
    setState(() {
...
      // y座標がマイナス(ゲームエリアのTOPを超えた)でゲームオーバー
      if (status == Collision.LANDED_BLOCK && block.y < 0){
        isGameOver = true;
        endGame();
      }
...
  });
}
...
  // ブロックを描画する
  Widget drawBlocks(){
...
    if (isGameOver) {
      subBlocks.add(getGameOvertRect());
    }
    return Stack(children: subBlocks,);
  }

  // ゲームオーバーブロックを描画する
  Widget getGameOvertRect(){
    return Positioned( //配置可能なコンテナ
      child: Container(
        // ゲームオーバーブロックは大きいサイズにする
        width: subBlockWidth * 8.0,
        height: subBlockWidth * 3.0,
        alignment: Alignment.center,
        decoration: BoxDecoration(
            color: Colors.red,
            borderRadius: BorderRadius.all(Radius.circular(10.0))
        ),
        child: Text('Game Over',
          style: TextStyle(
            fontSize: 30,
            fontWeight: FontWeight.bold,
            color: Colors.white,
          ),
        ),
      ),
      // ゲームオーバーブロックを配置するポジションを指定
      left: subBlockWidth * 1.0,
      top: subBlockWidth * 6.0,
    );
  }

それでは実行してみましょう。

OKですね。Game Overってちゃんと表示されて、タイマーも止まりました。

 

Providerを使って別のWidgetにアクセスする(15:23〜)

ようやくゲームエリアを終えました。あと一息!がんばりましょー!

次は、スコアバーインジケータと次ブロックインジケータに、Gameウィジェットで作成されたスコア、次のブロックを渡して表示させます。

ここで、Providerを使っていきます。

Providerとは?

Providerとは、独自のデータセットを持つWidgetで、BuildContextを介してアプリ内のあらゆる場所でデータを利用できるようにします。

Providerを利用することで、データを一箇所に格納できます。というのも、全てのWidgetが一元化されたデータストレージにアクセスできます。

CallbackやAPIを使わなくてOKなのです。早速パッケージをゲットしましょう。

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter

  provider: ^4.2.0

Flutterのパッケージでどのバージョンを使うか迷ったら、とりあえず現時点での最新にしちゃいましょ。今は4.2ですが、皆さんがみている頃は変わってるかも。

最新はこちら↓

provider | Flutter PackageA wrapper around InheritedWidget to make them easier to use and more reusable.
provider | Flutter Package pub.dev
provider | Flutter Package

pubspec.yamlに追記したら、flutter pub getコマンドを実行してダウンロードします。

終わったら、main.dartに一元化されたデータストレージを配置します。なお、Providerは必要以上に配置するとスコープが汚染されるので、便利だからと言って配置しまくらないよう気を付けましょう。

Providerは、共有データを必要とする全てのWidgetがアクセスできる高い位置に配置します。

Providerにはいろんな種類がありますが、ここで使うのはChangeNotifierProvider(モデルオブジェクトの変更をリッスンし、通知をリッスンしているWidgetを再構築する)です。

データモデルでデータを共有したいので、ChangeNotifierProviderが適しています。MyAppはスコアバーインジケータWidgetと次のブロックインジケータWidgetの両方の上にあるので、ProviderはrunApp関数に配置します。

では、実装していきましょう。

Providerをインポートしてモデルオブジェクトを作成・参照する(16:30〜)

lib/main.dart

import 'package:provider/provider.dart';
...
void main() {
  runApp(
    // モデルオブジェクトに変更があると、リッスンしているWidget(配下の子Widget)を再構築する
    ChangeNotifierProvider(
      create: (context) => Data(),//モデルオブジェクトを作成する
      child: MyApp(),
    ),
  );
}
...
class _TetrisState extends State {
...
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
...
      body: SafeArea(
        child: Column(
          children:  [
...
                            RaisedButton( //スタートボタン
                              child: Text(
                                //ボタンのテキストはDataモデルのisPlaying値に依存する必要がある
                                  // 廃止。グローバルキーを使って、GameStateにアクセスする
                                  // 廃止。isPlaying変数には、GameStateのcurrentStateでアクセスできる
                                  // 廃止。 _keyGame.currentState != null
                                  // 廃止。  && _keyGame.currentState.isPlaying
                                Provider.of<Data>(context, listen: false).isPlaying
                                  ? 'End': 'Start',
                                style: TextStyle(
                                  fontSize: 18,
                                  color: Colors.grey[200],
                                ),
                              ),
                              color: Colors.indigo[700],
                              onPressed: () {
                                // ProviderのnotifyListeners()関数で全てのWidgetが再構築されるので、
                                // setStateは不要。
                                // 廃止。Flutterにボタンを再描画させるため、setStateを使う
                                // 廃止。setState(() {
                                  //ボタン押下時の動作はDataモデルのisPlaying値に依存する必要がある
                                    // 廃止。 グローバルキーを使って、GameStateにアクセスする
                                    // 廃止。_keyGame.currentState != null
                                    // 廃止。  && _keyGame.currentState.isPlaying
                                  Provider.of<Data>(context, listen: false).isPlaying
                                    ? _keyGame.currentState.endGame()
                                      : _keyGame.currentState.startGame();
                                // 廃止。});
...
}

// Dataモデルはwithキーワード(ミックスイン)を使ってChangeNotifier機能を拡張する
// ミックスイン: 別のクラスの機能を景勝せずに追加できる
class Data with ChangeNotifier {
  int score = 0;
  bool isPlaying = false;

  // スコアに任意の値を設定する(例:startGameでスコアを0に設定する)
  void setScore(score){
    this.score = score;
    notifyListeners(); // 全てのリスナーに自身を再構築するよう通知する
  }

  // ゲーム全体でスコアを累積する
  void addScore(score){
    this.score += score;
    notifyListeners(); // 全てのリスナーに自身を再構築するよう通知する
  }

  // isPlaying変数の値を変更する
  void setIsPlaying(isPlaying){
    this.isPlaying = isPlaying;
    notifyListeners(); // 全てのリスナーに自身を再構築するよう通知する
  }
}

補足:チュートリアル動画の通りにやったら「context.owner.debugBuilding || listen == false || debugIsInInheritedProviderUpdate」エラーになる件

startボタンを押しても、ブロックが落ちてきません。なんだろ?とデバッグしてみると、こんなエラーが。

The context used was: Tetris(dependencies: [_InheritedProviderScope<Data>], state: _TetrisState#9fbc2) ‘package:provider/src/provider.dart’: Failed assertion: line 200 pos 7: ‘context.owner.debugBuilding || listen == false || debugIsInInheritedProviderUpdate’

調べてみると、

Provider.of<Data>(context)

の部分を、

Provider.of<Data>(context, listen: false)

と書き換えてあげればOKです。

Providerのバージョンが4.0以上になった影響で、書き方が多少変わったようです。

 

Game WidgetでもProviderを使ってDataモデルオブジェクトを参照するよう修正する

気を取り直して、次に、Game Widgetに移ります。

Dataモデルはmain.dartに定義してあるので、game.dartにインポートします。Providerもインポートします。isPlayingとscoreはDataモデルで管理するので、元々あったisPlaying変数とscore変数は廃止します。

lib/game.dart

...
import 'package:provider/provider.dart';
import 'main.dart';
...
class GameState extends State<Game> {
...
  // Dataモデルで管理するので廃止。
  // bool isPlaying = false; //ゲームがプレイ中かどうかを示すフラグ
  // Dataモデルで管理するので廃止。
  // int score; //ゲームのスコア
...
  void startGame() {
...
    // score = 0; //スコアを初期化
    // isPlaying = true; //プレイ中フラグをONにする
    Provider.of<Data>(context, listen: false).setScore(0);
    Provider.of<Data>(context, listen: false).setIsPlaying(true);
...
  }
  void endGame() {
    // isPlaying = false; //プレイ中フラグをOFFにする
    Provider.of<Data>(context, listen: false).setIsPlaying(false);
    timer.cancel();
  }
...
  // スコアリング
  void updateScore(){
...
    //もし一行揃っていたら、スコアに加える
    rows.forEach((rowNum, count) {
      // y座標に含まれるサブブロックの数が、行サイズと同じ場合(一行揃ってる場合)
      if (count == BLOCKS_X){
        // score += combo++; //コンボを1増やす
        // print('score: $score');
        Provider.of<Data>(context, listen: false).setScore(combo++);

        rowsToBeRemoved.add(rowNum); //y座標を消す行のリストに入れる
      }
    });
...
  }

ScoreBar WidgetでもProviderを使ってDataモデルオブジェクトを参照するよう修正する

最後に、ScoreBar Widgetでも同様の変更を行います。

lib/score_bar.dart

import 'package:provider/provider.dart';
import 'main.dart';
...
class _ScoreBarState extends State {
  @override
  Widget build(BuildContext context) {
...
          Padding(
            padding: EdgeInsets.all(10.0),
            child: Text(
              'Score: ${Provider.of<Data>(context, listen: false).score})',
              style:TextStyle(
                fontSize: 20,
                fontWeight: FontWeight.bold,
                color: Colors.white,
              ),
            ),
          ),
...

それでは、いったん動かしてみましょう。

はい、行を消したらScoreが0から1に書き換わりました!成功です!

長くなっちゃったので続きは次回。いよいよ最終回です。

次回は、次のブロックインジケータを修正しておしまいです!

それでは!

末岐 碧衣
  • 末岐 碧衣
  • フリーランス のシステムエンジニア。独立後、一度も営業せずに月収 96 万円を達成。1986年大阪生まれ。早稲田大学理工学部卒。システムエンジニア歴 12年。
    2009年、ITコンサルティング企業に入社。3年目でコミュ障が爆発し人間関係が崩壊。うつにより休職するも、復帰後はコミュ障の自覚を持ち、「チームプレイ」を徹底的に避け、会社組織内においても「一人でできる仕事」に専念。社内外から評価を得た。
    無理に「チームプレイ」するよりも「一人でできる仕事」に専念した方が自分も周囲も幸せにできることを確信し、2015年フリーランスとして独立。