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

サイコロ処理を理解しよう

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

忙しい方はここだけ見て

この章で見る場所は、ここです。

Future<void> _rollDiceAndMove() async {
  if (_phase != GamePhase.waitingRoll) {
    return;
  }

  final PlayerState player = _currentPlayer;

  _startDiceShuffle();

  setState(() {
    _phase = GamePhase.rolling;
  });

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

  _stopDiceShuffle();

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

サイコロの数字を作っているのは、この1行です。

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

意味はこれです。

0〜5の数字をランダムで作る
↓
+1する
↓
1〜6のサイコロになる

つまり、

_random.nextInt(6) + 1

が、サイコロ処理の中心です。


この章でやること

この章では、サイコロを振る処理を見ていきます。

人生ゲームでは、サイコロを振るとプレイヤーが進みます。

このアプリでも、流れは同じです。

サイコロを振る
↓
1〜6の数字が出る
↓
その数だけ進む
↓
止まったマスのイベントが起きる

まずは、この流れが分かればOKです。


今日のゴール

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

1. サイコロの数字がどこで作られているか分かる
2. 1〜6の数字をランダムで出す仕組みが分かる
3. サイコロ後にプレイヤーが進む流れが分かる

難しいところは、あとからで大丈夫です。

まずは「ランダムで数字を出している」と分かればOKです。


Step 1:_rollDiceAndMove()を探す

lib/main.dart を開きます。

検索で、次の文字を探してください。

_rollDiceAndMove

この関数が、サイコロを振ってプレイヤーを進める処理です。

Future<void> _rollDiceAndMove() async {
  if (_phase != GamePhase.waitingRoll) {
    return;
  }

  final PlayerState player = _currentPlayer;

  if (player.isFinished) {
    _moveToNextPlayer();
    return;
  }

  _startDiceShuffle();

  setState(() {
    _phase = GamePhase.rolling;
  });

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

  _stopDiceShuffle();

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

長く見えますが、やっていることはシンプルです。

今サイコロを振ってよいか確認
↓
サイコロ演出を開始
↓
少し待つ
↓
1〜6の数字を作る
↓
プレイヤーの次の位置を決める

Step 2:サイコロを押せる状態か確認する

最初に、この処理があります。

if (_phase != GamePhase.waitingRoll) {
  return;
}

これは、

今が「サイコロ待ち」の状態でなければ、何もしない

という意味です。

例えば、イベント画面が出ている最中に、何度もサイコロを押せたら困ります。

そのため、今のゲーム状態を確認しています。


Step 3:GamePhaseを思い出そう

ゲームの状態は、GamePhase で管理しています。

enum GamePhase {
  waitingRoll,
  rolling,
  showingModal,
  choosingProperty,
  gameOver,
}

意味はこちらです。

状態意味
waitingRollサイコロを振ってよい
rollingサイコロ中
showingModalイベント画面を表示中
choosingProperty物件購入を選択中
gameOverゲーム終了

サイコロを振れるのは、waitingRoll のときだけです。


Step 4:現在のプレイヤーを取り出す

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

final PlayerState player = _currentPlayer;

これは、今のターンのプレイヤーを取り出しています。

例えば、今が Player 1 のターンなら、

player = Player 1

になります。

今が Player 2 のターンなら、

player = Player 2

になります。


Step 5:すでにゴールしているか確認する

次に、この処理があります。

if (player.isFinished) {
  _moveToNextPlayer();
  return;
}

これは、

もし今のプレイヤーがゴール済みなら
次のプレイヤーに交代する

という意味です。

ゴールした人が、またサイコロを振らないようにしています。


Step 6:サイコロ演出を始める

次に、サイコロの数字が動く演出を始めます。

_startDiceShuffle();

この中では、短い間隔でサイコロの見た目の数字を変えています。

void _startDiceShuffle() {
  _diceShuffleTimer?.cancel();
  _diceShuffleTimer = Timer.periodic(
    const Duration(milliseconds: 80),
    (_) {
      if (!mounted) {
        return;
      }

      setState(() {
        _visibleDice = _random.nextInt(6) + 1;
      });
    },
  );
}

ここでは、80ミリ秒ごとに数字を変えています。

1
4
2
6
3
...

のように、サイコロが回っているように見せています。


Step 7:ゲーム状態をrollingにする

次に、ゲーム状態を変更します。

setState(() {
  _phase = GamePhase.rolling;
});

これは、

今はサイコロ中です

という状態に変えています。

setState() は、画面を更新するための命令です。

状態が変わったら、画面も変わります。


Step 8:少し待つ

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

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

これは、750ミリ秒だけ待つ処理です。

750ミリ秒 = 0.75秒

サイコロを押してすぐ結果が出るより、少し待った方がゲームっぽく見えます。

この待ち時間が、サイコロ演出の時間です。


Step 9:サイコロ演出を止める

待ち時間が終わったら、演出を止めます。

_stopDiceShuffle();

中身はこうです。

void _stopDiceShuffle() {
  _diceShuffleTimer?.cancel();
  _diceShuffleTimer = null;
}

これは、数字をシャッフルするタイマーを止めています。

止めないと、ずっと数字が変わり続けてしまいます。


Step 10:サイコロの数字を決める

ここが一番大事です。

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

この1行で、1〜6の数字を作っています。

分解すると、こうです。

_random.nextInt(6)

これは、0〜5の数字をランダムで作ります。

0, 1, 2, 3, 4, 5

でも、サイコロは1〜6です。

そこで、+ 1 します。

_random.nextInt(6) + 1

すると、こうなります。

0 + 1 = 1
1 + 1 = 2
2 + 1 = 3
3 + 1 = 4
4 + 1 = 5
5 + 1 = 6

これで、サイコロになります。


Step 11:プレイヤーの次の位置を決める

サイコロの数字が決まったら、次の位置を計算します。

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

意味はこうです。

今いる位置 + サイコロの数

例えば、今の位置が2で、サイコロが4なら、

2 + 4 = 6

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


Step 12:min()でゴールを超えないようにする

この部分も大事です。

min(player.position + dice, _tiles.length - 1)

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

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

現在地が17で、サイコロが6の場合、

17 + 6 = 23

になってしまいます。

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

そこで、min() を使って、

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

という処理をしています。

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


Step 13:実際にプレイヤーの位置を変える

次のコードで、プレイヤーの位置を更新しています。

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

これは、

今のプレイヤーの位置を
nextPositionに変更する

という意味です。

copyWith() は、プレイヤー情報の一部だけを変えるための関数です。

ここでは、position だけ変えています。


Step 14:画面に表示するサイコロも更新する

次のコードもあります。

_lastDice = dice;
_visibleDice = dice;

意味はこちらです。

変数役割
_lastDice最後に出たサイコロの目
_visibleDice画面に表示するサイコロの目

サイコロ演出中は、_visibleDice がどんどん変わります。

最後に、確定した dice を入れます。


Step 15:ログを追加する

サイコロを振った結果は、ログにも追加されます。

_addLog(
  '${player.name} が $dice を出しました。${landedTile.age}歳「${landedTile.label}」に進みました。',
);

例えば、こう表示されます。

Player 1 が 4 を出しました。25歳「初任給」に進みました。

ログがあると、何が起きたか分かりやすくなります。


Step 16:止まったマスの処理へ進む

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

await _handleTileEffect(landedTile);

これは、

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

という意味です。

例えば、

給料マスならお金が増える
税金マスならお金が減る
イベントマスならランダムイベント
物件マスなら購入確認
ゴールなら終了

という流れになります。


サイコロ処理の全体像

ここまでをまとめると、サイコロ処理はこうです。

サイコロボタンを押す
↓
今サイコロを振れる状態か確認
↓
サイコロ演出を開始
↓
0.75秒待つ
↓
1〜6の数字を決める
↓
次の位置を計算する
↓
プレイヤーの位置を更新する
↓
止まったマスの処理をする

これが、人生ゲームの基本の動きです。


触ってみよう

今回は、サイコロ演出の時間を少し変えてみます。

変更前です。

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

少し短くしたい場合です。

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

少し長くしたい場合です。

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

まずは、1000 にしてみましょう。

サイコロを押す
↓
少し長く回る
↓
結果が出る

ゲームっぽさが変わるのを確認できます。


もう1つ触ってみよう

サイコロの数字が切り替わる速さも変えられます。

変更前です。

const Duration(milliseconds: 80)

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

const Duration(milliseconds: 150)

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

const Duration(milliseconds: 50)

最初は、150 にして試してみましょう。


よくあるエラーと直し方

1. + 1 を消してしまった

悪い例です。

final int dice = _random.nextInt(6);

これだと、0が出ることがあります。

0, 1, 2, 3, 4, 5

サイコロとして使うなら、正しくはこちらです。

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

2. nextInt(6) の数字を変えてしまった

例えば、こうすると1〜10のサイコロになります。

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

これは悪くありません。

ただし、ゲームの進み方がかなり速くなります。

初心者のうちは、まず 6 のままでOKです。


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

悪い例です。

final int nextPosition = player.position + dice;

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

安全な書き方はこちらです。

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

ゴールを超えないために必要です。


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

setState() を消すと、データは変わっていても画面が更新されないことがあります。

setState(() {});

これは、

画面を更新してください

という合図です。

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


5. awaitを消してしまった

悪い例です。

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

これだと、待たずに次へ進んでしまいます。

正しくはこちらです。

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

await は、

ここで少し待ってから次に進む

という意味です。


この章で覚えること

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

1. サイコロは _random.nextInt(6) + 1 で作る
2. player.position + dice で次の位置を決める
3. min() でゴールを超えないようにする

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


やる気を維持するコツ

サイコロ処理が分かると、ゲーム開発の楽しさが一気に見えてきます。

なぜなら、ランダムが入ると、毎回違う展開になるからです。

同じボタンを押しても
毎回違う数字が出る
↓
止まるマスが変わる
↓
イベントも変わる
↓
ゲームになる

ランダムは、ゲームを面白くする強力な仕組みです。

まずは、サイコロの速さや待ち時間を少し変えて遊んでみましょう。


チェックリスト

□ _rollDiceAndMove() を見つけた
□ サイコロを作る1行を見つけた
□ _random.nextInt(6) + 1 の意味が分かった
□ 0〜5に+1して1〜6にしていると分かった
□ nextPosition の意味が分かった
□ min() がゴール超え防止だと分かった
□ _visibleDice が画面表示用だと分かった
□ サイコロ演出の待ち時間を変えてみた
□ 保存して動作確認した

まとめ

この章では、サイコロ処理を確認しました。

サイコロの数字は、_random.nextInt(6) + 1 で作っています。

プレイヤーの次の位置は、player.position + dice で決まります。

ただし、ゴールを超えないように min() を使っています。

次の章では、プレイヤーが進む仕組みを見ながら、現在地の更新や画面スクロールの流れを理解していきます。

教材トップへ戻る