へっぽこ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)), ), ), ); } }
さて、いかがでしょうか。
次回は、これをタイマー使って動かしていきます!
それでは