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

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

YouTubeのチュートリアル動画を使った2Dゲーム(テトリス)モバイルアプリ開発が順調に進んでいます。前回はブロック、ブロックを構成するサブブロックのクラスを作りました。

今日は上記のクラスを使って具体的なブロック(T字形、L字型など)を作っています。

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

 

今日の内容は、こちらの動画の9:55〜15:28までの内容になっています。

 

早速、ブロックを作っていきましょう。

Iブロックを作る

Blockクラスの下に、以下を追記していきます。まずはIブロックを書いてみます。

lib.block.dart

class IBlock extends Block{
  IBlock(int orientationIndex)
    : super ([
      [SubBlock(0,0),SubBlock(0,1),SubBlock(0,2),SubBlock(0,3)],
      [SubBlock(0,0),SubBlock(1,0),SubBlock(2,0),SubBlock(3,0)],
      [SubBlock(0,0),SubBlock(0,1),SubBlock(0,2),SubBlock(0,3)],
      [SubBlock(0,0),SubBlock(1,0),SubBlock(2,0),SubBlock(3,0)],
  ], Colors.red[400], orientationIndex);
}

それぞれのブロックはBlockクラスを継承ているので、コンストラクタには、orientationsリスト、色、現在の方向をセットします。

ポイントは、各方向分の4つのリストを作るときのサブブロックの座標は、絶対位置ではなく相対位置であることです。4マスかける4マスの中で、どのマスにそれぞれのサブブロックがあるかを定義します。

同様に、他のJ、L、O、T、S、Zブロックを作ります。

lib.block.dart

class JBlock extends Block{
  JBlock(int orientationIndex)
      : super ([
    [SubBlock(1,0),SubBlock(1,1),SubBlock(1,2),SubBlock(0,2)],
    [SubBlock(0,0),SubBlock(0,1),SubBlock(1,1),SubBlock(2,1)],
    [SubBlock(0,0),SubBlock(1,0),SubBlock(0,1),SubBlock(0,2)],
    [SubBlock(0,0),SubBlock(1,0),SubBlock(2,0),SubBlock(2,1)],
  ], Colors.yellow[300], orientationIndex);
}

class LBlock extends Block{
  LBlock(int orientationIndex)
      : super ([
    [SubBlock(0,0),SubBlock(0,1),SubBlock(0,2),SubBlock(1,2)],
    [SubBlock(0,0),SubBlock(1,0),SubBlock(2,0),SubBlock(0,1)],
    [SubBlock(0,0),SubBlock(1,0),SubBlock(1,1),SubBlock(1,2)],
    [SubBlock(2,0),SubBlock(0,1),SubBlock(1,1),SubBlock(2,1)],
  ], Colors.green[300], orientationIndex);
}

class OBlock extends Block{
  OBlock(int orientationIndex)
      : super ([
    [SubBlock(0,0),SubBlock(1,0),SubBlock(0,1),SubBlock(1,1)],
    [SubBlock(0,0),SubBlock(1,0),SubBlock(0,1),SubBlock(1,1)],
    [SubBlock(0,0),SubBlock(1,0),SubBlock(0,1),SubBlock(1,1)],
    [SubBlock(0,0),SubBlock(1,0),SubBlock(0,1),SubBlock(1,1)],
  ], Colors.blue[300], orientationIndex);
}

class TBlock extends Block{
  TBlock(int orientationIndex)
      : super ([
    [SubBlock(0,0),SubBlock(1,0),SubBlock(2,0),SubBlock(1,1)],
    [SubBlock(1,0),SubBlock(0,1),SubBlock(1,1),SubBlock(1,2)],
    [SubBlock(1,0),SubBlock(0,1),SubBlock(1,1),SubBlock(2,1)],
    [SubBlock(0,0),SubBlock(0,1),SubBlock(1,1),SubBlock(0,2)],
  ], Colors.blue, orientationIndex);
}

class SBlock extends Block{
  SBlock(int orientationIndex)
      : super ([
    [SubBlock(1,0),SubBlock(2,0),SubBlock(0,1),SubBlock(1,1)],
    [SubBlock(0,0),SubBlock(0,1),SubBlock(1,1),SubBlock(1,2)],
    [SubBlock(1,0),SubBlock(2,0),SubBlock(0,1),SubBlock(1,1)],
    [SubBlock(0,0),SubBlock(0,1),SubBlock(1,1),SubBlock(1,2)],
  ], Colors.orange[300], orientationIndex);
}

class ZBlock extends Block{
  ZBlock(int orientationIndex)
      : super ([
    [SubBlock(0,0),SubBlock(1,0),SubBlock(1,1),SubBlock(2,1)],
    [SubBlock(1,0),SubBlock(0,1),SubBlock(1,1),SubBlock(0,2)],
    [SubBlock(0,0),SubBlock(1,0),SubBlock(1,1),SubBlock(2,1)],
    [SubBlock(1,0),SubBlock(0,1),SubBlock(1,1),SubBlock(0,2)],
  ], Colors.cyan[300], orientationIndex);
}

サブブロックの一辺の長さの算出方法

ゲームエリアは、幅:高さ=1:2にしてあります。

lib/game.dart

const BLOCKS_X = 10;
const BLOCKS_Y = 20;
...
aspectRatio: BLOCKS_X / BLOCKS_Y, //高さに対する幅の比率

なので、1マス(=サブブロック)の一辺の長さは、ゲームエリアの幅を10で割った値として算出できます。

 

ゲームエリアの幅はどうやって取得するかというと、、、チュートリアル動画ではこのように言っています。

ゲームエリアのcontextにアクセスする必要があります。

そのために最も簡単な方法が、Global Keyを使うことです。

 

ん〜〜〜〜

ゲームエリアのcontext(直訳すると文脈、背景、状況)って何? これまでFlutter触ってた時も正直よくわからないままスルーしてきましたが、ここらでちょっとだけ頑張って調べてみました。で、難しいことはかんがえずに秒で解説しました。30秒で読める記事になってるので、よかったらご参照ください。

Global Keyを使う

Global Keyとは、任意の画面 (ページ) や Widget ツリーの全く別の階層から特定の Widget にアクセスするために利用する鍵です。このチュートリアルでは他のページから参照する必要はないので、プライベート(変数名の頭に_をつける)としています。

よくわからん、という人は後述するソースを読んでください。コメントつけてあります。

ゲームクラスでブロックを作る

ひとまずブロックを作るところまで、game.dartに書いてみました。

lib/game.dart

import 'dart:math';
import 'package:flutter/material.dart';
import 'block.dart';

const BLOCKS_X = 10;//ゲームエリアの幅
const BLOCKS_Y = 20;//ゲームエリアの高さ
const GAME_AREA_BORDER_WIDTH = 2.0; //ゲームエリアの枠線の幅

class Game extends StatefulWidget {
  @override
  State createState() => _GameState();
}

class _GameState extends State {
  double subBlockWidth;

  GlobalKey _keyGameArea = GlobalKey(); //秘密にしたいのでプライベート(_から始める)

  Block block;

  Block getNewBlock() {
    int blockType = Random().nextInt(7);
    int orientationIndex = Random().nextInt(4);

    switch (blockType) {
      case 0:
        return IBlock(orientationIndex);
      case 1:
        return JBlock(orientationIndex);
      case 2:
        return LBlock(orientationIndex);
      case 3:
        return OBlock(orientationIndex);
      case 4:
        return TBlock(orientationIndex);
      case 5:
        return SBlock(orientationIndex);
      case 6:
        return ZBlock(orientationIndex);
      default:
        return null;
    }
  }

  void startGame() {
    //GlobalKeyを使い、ゲームエリアの現在のcontextにアクセスする
    //findRenderObjectで、レンダリングされたゲームエリアのオブジェクトを取得できる
    RenderBox renderBoxGame = _keyGameArea.currentContext.findRenderObject();

    //利用するゲームエリアは、ゲームエリアの枠線の幅を含まない
    subBlockWidth = (renderBoxGame.size.width - GAME_AREA_BORDER_WIDTH * 2) / BLOCKS_X;

    block = getNewBlock();
  }

  @override
  Widget build(BuildContext context) {
    return AspectRatio(
      aspectRatio: BLOCKS_X / BLOCKS_Y, //高さに対する幅の比率
      child: Container(
        key: _keyGameArea, //ゲームエリアの鍵
        decoration: BoxDecoration(
          color: Colors.indigo[800],
          border: Border.all(
            width: 2.0,
            color: Colors.indigoAccent
          ),
          borderRadius: BorderRadius.all(Radius.circular(10.0)),
        ),
      ),
    );
  }
}

 

さて、いかがでしょうか。

次回は、これをタイマー使って動かしていきます!

それでは

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