Flutterアプリケーション開発概論

プレイヤーが進む仕組みを理解しよう

11人生ゲームを作る。サイコロ処理・イベント分岐・プレイヤー管理・データ設計・アニメーション
FlutteriOSAndroidMacOSWindows基礎から学ぶ開発アプリ開発

忙しい方はここだけ見て

この章で見る大事な流れは、ここです。

final int dice = _random.nextInt(6) + 1;
final int nextPosition = min(player.position + dice, _tiles.length - 1);
final BoardTile landedTile = _tiles[nextPosition];

_players[_currentPlayerIndex] = player.copyWith(position: nextPosition);

意味はこうです。

サイコロの数字を出す
↓
今いる場所 + サイコロの数
↓
次に止まるマスを決める
↓
プレイヤーの位置を更新する

プレイヤーの位置は、position で管理しています。

position: 0,

0 はスタート地点です。

例えば、今 position: 2 でサイコロが 4 なら、

2 + 4 = 6

6番目のマスに進みます。


この章でやること

この章では、サイコロを振ったあと、プレイヤーがどうやって進むのかを見ていきます。

人生ゲームでは、サイコロの数だけコマが進みます。

このアプリでも、同じ考え方です。

現在地
+
サイコロの数
=
次の位置

この考え方が分かれば、プレイヤー移動の基本は理解できます。


今日のゴール

この章のゴールは、次の3つです。

1. position が現在地だと分かる
2. サイコロの数だけ進む計算が分かる
3. 進んだあとに止まったマスを取得する流れが分かる

難しい言葉は少しずつで大丈夫です。

まずは、「位置は数字で管理している」と分かればOKです。


Step 1:プレイヤーの現在地を見る

プレイヤーの現在地は、position で管理しています。

最初のプレイヤー設定を見ると、こうなっています。

PlayerState(
  id: 0,
  name: 'Player 1',
  color: AppColors.red,
  position: 0,
  cash: 1500,
  properties: <int>{},
  isFinished: false,
),

大事なのはここです。

position: 0,

これは、0番目のマスにいるという意味です。


Step 2:0番目のマスとは?

マスは、_createTiles() の中で作られています。

最初のマスはこれです。

BoardTile(
  index: 0,
  age: 20,
  stage: 'スタート',
  label: '人生のはじまり',
  description: 'ここから人生ゲームが始まります。',
  type: TileType.start,
),

つまり、

position: 0
↓
index: 0 のマス
↓
人生のはじまり

という関係です。


Step 3:サイコロの数だけ進む

サイコロを振ると、1〜6の数字が出ます。

final int dice = _random.nextInt(6) + 1;

そして、次の位置を計算します。

final int nextPosition = min(player.position + dice, _tiles.length - 1);

基本の考え方はこれです。

次の位置 = 今の位置 + サイコロの数

例えば、

今の位置:0
サイコロ:3
次の位置:3

になります。


Step 4:実際の例で見てみよう

例えば、プレイヤーがスタート地点にいるとします。

position: 0

サイコロで 4 が出たら、

0 + 4 = 4

なので、4番目のマスに進みます。

4番目のマスは、コードではこのようなマスです。

BoardTile(
  index: 4,
  age: 25,
  stage: '初任給・収入期',
  label: '初任給',
  description: '仕事の成果として給料を受け取ります。',
  type: TileType.payday,
),

つまり、サイコロで4が出ると、初任給マスに進む可能性があります。


Step 5:ゴールを超えないようにする

次のコードには、min() が使われています。

final int nextPosition = min(player.position + dice, _tiles.length - 1);

これは、ゴールを超えないようにするためです。

例えば、最後のマスが19番だとします。

今の位置が17で、サイコロが6の場合、

17 + 6 = 23

になります。

でも、23番のマスはありません。

そこで、min() を使います。

23 と 19 の小さい方を使う
↓
19になる

つまり、ゴールを超えそうなときは、ゴールに止まります。


Step 6:止まったマスを取り出す

次に、このコードがあります。

final BoardTile landedTile = _tiles[nextPosition];

これは、

次の位置にあるマスを取り出す

という意味です。

例えば、nextPosition4 なら、

_tiles[4]

を取り出します。

_tiles[4] は、4番目のマスです。


Step 7:landedTileとは?

landedTile は、プレイヤーが止まったマスです。

final BoardTile landedTile = _tiles[nextPosition];

英語の landed は、「着地した」という意味です。

つまり、

landedTile = 止まったマス

と考えればOKです。

このあと、止まったマスの種類によって、給料・税金・イベントなどが起きます。


Step 8:プレイヤーの位置を更新する

プレイヤーの位置を実際に変えているのは、このコードです。

_players[_currentPlayerIndex] = player.copyWith(position: nextPosition);

これは、

今のプレイヤーのpositionを
nextPositionに変える

という意味です。

例えば、

今のposition:0
nextPosition:4

なら、プレイヤーの位置が4になります。


Step 9:copyWithを使う理由

copyWith() は、データの一部だけを変えるための便利な関数です。

今回の場合、プレイヤーの情報はたくさんあります。

id
name
color
position
cash
properties
isFinished

でも、移動するときに変えたいのは position だけです。

そこで、こう書きます。

player.copyWith(position: nextPosition)

これは、

プレイヤー情報はそのまま
positionだけ新しい値にする

という意味です。


Step 10:画面を更新する

位置を変えたあと、画面を更新します。

setState(() {});

setState() は、

データが変わったので、画面を更新してください

という命令です。

これがあることで、プレイヤーのコマが新しい位置に表示されます。

初心者のうちは、

データを変えたら setState()

と覚えておくと分かりやすいです。


Step 11:自動でスクロールする

プレイヤーが進んだあと、画面もそのマスに近づくようにスクロールします。

_scrollToTile(nextPosition);

これは、次のマスが見える位置まで画面を動かす処理です。

中身はこうです。

void _scrollToTile(int tileIndex) {
  WidgetsBinding.instance.addPostFrameCallback((_) {
    if (!_timelineScrollController.hasClients) {
      return;
    }

    final double maxScroll =
        _timelineScrollController.position.maxScrollExtent;

    final double targetOffset = min(
      maxScroll,
      max(0, tileIndex * 100.0 - 100.0),
    );

    _timelineScrollController.animateTo(
      targetOffset,
      duration: const Duration(milliseconds: 400),
      curve: Curves.easeInOutCubic,
    );
  });
}

全部を理解しなくても大丈夫です。

ここでは、

止まったマスが見えるように
画面を自動でスクロールしている

と分かればOKです。


Step 12:スクロール位置の考え方

この部分を見てみます。

final double targetOffset = min(
  maxScroll,
  max(0, tileIndex * 100.0 - 100.0),
);

ざっくり言うと、

マス番号 × 100くらいの位置にスクロールする

という考え方です。

例えば、

tileIndex: 5
↓
5 × 100 = 500

なので、500pxくらい下にスクロールします。

細かく完璧に理解しなくても大丈夫です。


Step 13:少し待ってからマスの処理をする

スクロールしたあと、少し待ちます。

await Future<void>.delayed(const Duration(milliseconds: 240));

これは、移動したあとすぐにイベントを出すのではなく、少し間を作るためです。

プレイヤーが進む
↓
少し待つ
↓
イベント画面が出る

この一瞬の間があると、ゲームの流れが自然になります。


Step 14:止まったマスの効果を実行する

最後に、止まったマスの効果を実行します。

await _handleTileEffect(landedTile);

これは、

止まったマスで何が起きるかを決める処理

です。

例えば、

給料マス → 現金が増える
税金マス → 現金が減る
イベントマス → ランダムイベント
物件マス → 購入するか選ぶ
ゴールマス → 終了

という分岐につながります。


Step 15:移動処理の全体像

ここまでの流れをまとめると、こうです。

サイコロを振る
↓
diceが決まる
↓
現在地 + dice で次の位置を計算
↓
ゴールを超えないように調整
↓
止まったマスを取得
↓
プレイヤーのpositionを更新
↓
画面を更新
↓
そのマスまでスクロール
↓
止まったマスの効果を実行

この流れが、プレイヤー移動の基本です。


触ってみよう

今回は、スクロール速度を少し変えてみます。

探す場所はこちらです。

_timelineScrollController.animateTo(
  targetOffset,
  duration: const Duration(milliseconds: 400),
  curve: Curves.easeInOutCubic,
);

変更前です。

duration: const Duration(milliseconds: 400),

ゆっくりにするなら、こうです。

duration: const Duration(milliseconds: 800),

速くするなら、こうです。

duration: const Duration(milliseconds: 200),

まずは 800 にして、ゆっくりスクロールするか確認してみましょう。


もう1つ触ってみよう

止まったあと、イベント画面が出るまでの待ち時間も変えられます。

変更前です。

await Future<void>.delayed(const Duration(milliseconds: 240));

少し長くするなら、こうです。

await Future<void>.delayed(const Duration(milliseconds: 600));

すると、

止まる
↓
少し間がある
↓
イベント画面が出る

という感じになります。

ゲームっぽい間を作りたいときに使えます。


よくあるエラーと直し方

1. position を文字にしてしまった

悪い例です。

position: '0',

正しくはこちらです。

position: 0,

position は数字なので、' で囲みません。


2. min() を消してしまった

悪い例です。

final int nextPosition = player.position + dice;

これだと、ゴールを超えて存在しないマスに進む可能性があります。

正しくはこちらです。

final int nextPosition = min(player.position + dice, _tiles.length - 1);

ゴールを超えないために大事です。


3. _tiles[nextPosition] の意味が分からない

final BoardTile landedTile = _tiles[nextPosition];

これは、配列から指定した番号のマスを取り出しています。

例えば、

_tiles[4]

なら、4番目のマスを取り出します。


4. setState() を消してしまった

setState() を消すと、画面が更新されないことがあります。

setState(() {});

データを変えたあとに、画面を更新するために必要です。

初心者のうちは、消さないようにしましょう。


5. スクロールがうまく動かない

スクロール処理は、画面ができてから動かす必要があります。

そのため、次のように書かれています。

WidgetsBinding.instance.addPostFrameCallback((_) {
  // スクロール処理
});

難しく見えますが、今はこう覚えればOKです。

画面ができたあとでスクロールするための書き方

この章で覚えること

この章で覚えることは、3つだけです。

1. position はプレイヤーの現在地
2. nextPosition は次に止まる場所
3. landedTile は止まったマス

まずはこれだけで大丈夫です。


やる気を維持するコツ

プレイヤーが進む仕組みが分かると、ゲームの中心が見えてきます。

難しいように見えても、基本はとてもシンプルです。

今いる場所
+
サイコロの数
=
次の場所

この考え方は、すごろく、RPG、ボードゲーム、マップ移動など、いろいろなゲームで使えます。

ひとつ理解できると、他のゲームにも応用できます。


チェックリスト

□ position が現在地だと分かった
□ dice がサイコロの数字だと分かった
□ nextPosition が次の位置だと分かった
□ player.position + dice の意味が分かった
□ min() がゴール超え防止だと分かった
□ landedTile が止まったマスだと分かった
□ copyWith(position: nextPosition) の意味が少し分かった
□ setState() が画面更新だと分かった
□ _scrollToTile() が自動スクロールだと分かった
□ スクロール速度を変えて確認した

まとめ

この章では、プレイヤーが進む仕組みを確認しました。

プレイヤーの現在地は position で管理しています。

サイコロの数字を足して nextPosition を作り、copyWith(position: nextPosition) で位置を更新します。

そのあと、止まったマスを landedTile として取り出し、_handleTileEffect() でマスごとの処理へ進みます。

次の章では、止まったマスごとのイベント分岐を見ていきます。

教材トップへ戻る