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

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

YouTubeのチュートリアル動画を使った2Dゲーム(テトリス)モバイルアプリ開発が順調に進んでいます。

前回はブロックをユーザが操作できるよう、回転と水平方向の移動を実装しました。しかし、ブロックがゲームエリアの壁に当たっても通り抜けてしまっていました。

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

 

今日は、ブロックが壁を通り抜けないように修正していきます。

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

 

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

ブロックが壁に当たったことをチェックする(7:25〜)

lib/game.dart

void onPlay(Timer timer){
...
  setState(() {
    // ユーザー入力であるアクションを実行する
    if (action != null) {
      // 壁に当たっていない限り、ブロックを動かせる
      if(!checkOnEdge(action)){
        block.move(action);
      }
    }
...
  }
...
// ブロックが壁をはみ出したことをチェックする
bool checkOnEdge(BlockMovement action){
  // 左に動かしてx座標がマイナス、または、
  return (action == BlockMovement.LEFT && block.x <= 0) ||
      // 右に動かした時のブロックの右端が、ゲームエリアの幅を超えていない時、trueを返す
      (action == BlockMovement.RIGHT && block.x + block.width >= BLOCKS_X);
}

では、動かしてみます。

はい、良いですね。壁に触れてからそれ以上壁の方向に動かそうとしても動きません。

 

ブロック同士が重なることがあるので直す(8:40〜)

ちょっとこれみてください。

ブロックが、重なっちゃってるのわかります? 横に動かすと、重なっちゃうんですよ。縦方向の衝突は検知してるんですけどね。これを直していきます。

lib/game.dart

// timer引数は必須だが、別に使わなくてもいい
void onPlay(Timer timer){
...
  setState(() {
    // ユーザー入力であるアクションを実行する
    if (action != null) {
...
    }

    // もし古いブロックに当たったら、ブロックを逆に動かしてキャンセルする
    for (var oldSubBlock in oldSubBlocks) {
      for (var subBlock in block.subBlocks) {
        // 絶対座標にする
        var x = block.x + subBlock.x;
        var y = block.y + subBlock.y;
        // もし古いサブブロックと重なっていたら(x座標とy座標が等しかったら)
        if(x == oldSubBlock.x && y == oldSubBlock.y) {
          // 逆に動かして移動をキャンセルする
          switch (action){
            // ユーザーが左に動かしていたら右に動かす(=左移動キャンセル)
            case BlockMovement.LEFT:
              block.move(BlockMovement.RIGHT);
              break;
            // ユーザーが右に動かしていたら左に動かす(=右移動キャンセル)
            case BlockMovement.RIGHT:
              block.move(BlockMovement.LEFT);
              break;
            // ユーザーが回転させたら、反時計回りに回転する(=回転キャンセル)
            case BlockMovement.ROTATE_CLOCKWISE:
              block.move(BlockMovement.ROTATE_COUNTER_CLOCKWISE);
              break;
            default:
              break;
          }
        }
      }
    }

    //ブロックが床に衝突したかチェックする
    if (!checkAtBottom()) {
...
    }

    //ブロックが床に着いた、もしくは、古いサブブロックに着いたら、次のブロックを落とす
    if(status == Collision.LANDED || status == Collision.LANDED_BLOCK) {
...
    }

    // ブロックに対するユーザーからの入力を初期化する
    action = null;
  });
}

ちょっとピンときにくいのですが、ユーザーが動かせないように制御するのではなく、ぶつかったら逆に動かして動きをキャンセルさせる、という発想です。

では動かしてみましょう。

良いですね!ちゃんと止まりました。

 

スコアリングを実装する(9:39〜)

lib/game.dart

まずは、GameStateの最初のところでスコアを格納する変数を宣言します。

class GameState extends State<Game> {
...
  int score; //ゲームのスコア
...
  void startGame() {
    score = 0; //スコアを初期化
...
  // スコアリング
  void updateScore(){
    var combo = 1; // コンボ数(同時に消した行数)を変数宣言する
    Map<int, int> rows = Map(); //消すy座標と、消すサブブロックの数のマップ
    List rowsToBeRemoved = List(); //消す行のy座標のリスト

    // サブブロックがあったら、行ごとにy座標とサブブロックの数をマッピングする
    oldSubBlocks?.forEach((subBlock) {
      //サブブロックのy座標をマップにセット
      rows.update(subBlock.y,
              //そのy座標にあるサブブロックの数をカウントしてマップにセット(ない場合は1)
              (value) => ++ value, ifAbsent: () => 1);
    });
    //マップを検査して、もし一行揃っていたらスコアに加える
    rows.forEach((rowNum, count) {
      // y座標に含まれるサブブロックの数が、行サイズと同じ場合(一行揃ってる場合)
      if (count == BLOCKS_X){
        score += combo++; //コンボを1増やす
        rowsToBeRemoved.add(rowNum); //y座標を消す行のリストに入れる
      }
    });

    // 揃った行のサブブロックを消す
    if(rowsToBeRemoved.length > 0) {
      removeRows(rowsToBeRemoved);
    }
  }

  // サブブロックを消す
  void removeRows(List rowsToBeRemoved) {
    rowsToBeRemoved.sort(); //並べ替える
    //上に表示されているブロックから(y座標が小さい順に)消していく
    rowsToBeRemoved.forEach((rowNum){
      //y座標が一致するサブブロックを消す
      oldSubBlocks.removeWhere((subBlock) => subBlock.y == rowNum);
      //消した一行分、他のサブブロックを一行ずつ下に移動させて表示させる(y座標の値に1を足す)
      oldSubBlocks.forEach((subBlock) {
        if(subBlock.y < rowNum) {
          ++subBlock.y;
        }
      });
    });
  }

removeRows関数のところで注意したいのが、並べ替えた時のy座標と見え方の逆転です。

このチュートリアルでは、x座標、y座標ともに左上が0なので、数値で並べると値が大きい方が右下になります。

Flutter公式でsort関数はこんな感じの動作になります。

List nums = [13, 2, -11];
nums.sort();
print(nums);  // [-11, 2, 13]

つまり、今回の場合だと下(に表示されている)ブロックから順番に消えていくことになります。

 

では、スコアをが入ること、一行揃ったら消えることを確認してみましょう。

updateScore関数をonPlayの最後に呼び出すよう追記し、スコアの値をコンソールに出すようprintします。

lib/game.dart

  // timer引数は必須だが、別に使わなくてもいい
  void onPlay(Timer timer){
...
    // Flutterがブロックの位置と状態が変化したことを認識するため、setStateを呼び出す
    setState(() {
...
      // ブロックに対するユーザーからの入力を初期化する
      action = null;
      updateScore();
    });
  }

  // スコアリング
  void updateScore(){
...
    //もし一行揃っていたら、スコアに加える
    rows.forEach((rowNum, count) {
      // y座標に含まれるサブブロックの数が、行サイズと同じ場合(一行揃ってる場合)
      if (count == BLOCKS_X){
        score += combo++; //コンボを1増やす
        print('score: $score');
        rowsToBeRemoved.add(rowNum); //y座標を消す行のリストに入れる
      }
    });
...
  }

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

OKですね。ちょっと見にくいですが、右のコンソールにもscore: 1と表示されました。

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

それでは!

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