プレイヤーが進む仕組みを理解しよう
忙しい方はここだけ見て
この章で見る大事な流れは、ここです。
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];
これは、
次の位置にあるマスを取り出す
という意味です。
例えば、nextPosition が 4 なら、
_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() でマスごとの処理へ進みます。
次の章では、止まったマスごとのイベント分岐を見ていきます。
