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

物件購入の仕組みを理解しよう

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

忙しい方はここだけ見て

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

Future<void> _handlePropertyTile(BoardTile tile) async {
  final PropertyOwner? owner = _findOwner(tile.index);

  if (owner == null) {
    if (_currentPlayer.cash >= tile.price) {
      _pendingPropertyTile = tile;
      _phase = GamePhase.choosingProperty;
      setState(() {});
    } else {
      _moveToNextPlayer();
    }
    return;
  }
}

物件購入の流れは、こうです。

物件マスに止まる
↓
その物件に所有者がいるか確認
↓
未購入なら、買えるか確認
↓
所持金が足りれば「購入する / 見送る」を表示
↓
購入すると、現金が減る
↓
物件の所有者として登録される

実際に購入している場所は、ここです。

void _buyPendingProperty() {
  final BoardTile? tile = _pendingPropertyTile;

  final PlayerState player = _currentPlayer;

  _players[_currentPlayerIndex] = player.copyWith(
    cash: player.cash - tile.price,
    properties: updatedProperties,
  );

  _propertyOwners.add(
    PropertyOwner(
      tileIndex: tile.index,
      ownerPlayerId: player.id,
    ),
  );
}

まずは、こう覚えればOKです。

物件を買う
↓
現金が減る
↓
自分の所有物になる

この章でやること

この章では、物件マスに止まったときの処理を見ていきます。

人生ゲームでは、物件を買うことで資産を増やせます。

このアプリでも、物件を買うとプレイヤーの所有物になります。

他のプレイヤーがそのマスに止まると、通行料が発生します。

物件を買う
↓
他の人が止まる
↓
通行料をもらえる

この章では、まず「物件を買うところ」までを理解します。


今日のゴール

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

1. 物件マスに止まったときの流れが分かる
2. 所持金が足りると購入できる仕組みが分かる
3. 購入すると現金が減り、所有者として登録されると分かる

全部を完璧に覚えなくて大丈夫です。

まずは、「物件は価格と通行料を持っている」と分かればOKです。


Step 1:物件マスを確認しよう

物件マスは、_createTiles() の中にあります。

例えば、このマスです。

BoardTile(
  index: 2,
  age: 23,
  stage: '社会人スタート期',
  label: '小さなカフェ',
  description: '小さなカフェに投資できます。所有すると、他のプレイヤーが止まった時に通行料を得られます。',
  type: TileType.property,
  price: 300,
  rent: 80,
),

大事なのは、この3つです。

type: TileType.property,
price: 300,
rent: 80,

意味はこちらです。

項目意味
type: TileType.property物件マス
price: 300購入価格
rent: 80通行料

つまり、このマスは「300で買えて、他の人が止まると80もらえる物件」です。


Step 2:物件マスに止まるとどうなる?

マスごとの分岐では、物件マスのときにこの処理へ進みます。

case TileType.property:
  await _handlePropertyTile(tile);

つまり、

物件マスに止まる
↓
_handlePropertyTile() に進む

という流れです。

物件の処理は少し長いので、別の関数に分けています。


Step 3:_handlePropertyTile()を探す

lib/main.dart を開いて、次を検索します。

_handlePropertyTile

このような関数があります。

Future<void> _handlePropertyTile(BoardTile tile) async {
  final PropertyOwner? owner = _findOwner(tile.index);

  if (owner == null) {
    if (_currentPlayer.cash >= tile.price) {
      await _showLifeEventModal(
        title: tile.label,
        categoryLabel: 'ASSET CHANCE',
        subtitle: '${tile.age}歳|${tile.stage}',
        description:
            '${tile.description}\n\n購入価格は ${tile.price}、通行料は ${tile.rent} です。',
        icon: Icons.storefront,
        accentColor: AppColors.gold,
      );

      _pendingPropertyTile = tile;
      _phase = GamePhase.choosingProperty;
      _addLog('${tile.label} は未購入です。価格 ${tile.price} で購入できます。');
      setState(() {});
    } else {
      _moveToNextPlayer();
    }
    return;
  }
}

ここで、物件を買えるかどうかを判断しています。


Step 4:所有者がいるか確認する

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

final PropertyOwner? owner = _findOwner(tile.index);

これは、

この物件を誰かが持っているか確認する

という意味です。

まだ誰も買っていない場合は、owner == null になります。

owner == null
↓
所有者がいない
↓
まだ買える

すでに誰かが買っている場合は、所有者情報が入っています。


Step 5:_findOwner()とは?

所有者を探しているのは、この関数です。

PropertyOwner? _findOwner(int tileIndex) {
  for (final PropertyOwner owner in _propertyOwners) {
    if (owner.tileIndex == tileIndex) {
      return owner;
    }
  }
  return null;
}

やっていることはシンプルです。

所有者リストを見る
↓
同じマス番号の物件を探す
↓
見つかったら所有者を返す
↓
なければ null を返す

null は、「何もない」という意味です。


Step 6:未購入なら買えるか確認する

次に、この条件を見ます。

if (owner == null) {
  if (_currentPlayer.cash >= tile.price) {
    // 購入できる
  } else {
    // お金が足りない
  }
}

意味はこうです。

所有者がいない
↓
今のプレイヤーの所持金を確認
↓
所持金が価格以上なら購入できる

例えば、所持金が1500で、物件価格が300なら買えます。

1500 >= 300
↓
買える

所持金が100で、物件価格が300なら買えません。

100 >= 300
↓
買えない

Step 7:購入するか選ぶ状態にする

買える場合は、この処理が動きます。

_pendingPropertyTile = tile;
_phase = GamePhase.choosingProperty;

意味はこちらです。

コード意味
_pendingPropertyTile = tile購入候補の物件を保存する
_phase = GamePhase.choosingProperty物件購入を選ぶ状態にする

つまり、

この物件を買うかどうか、今から選びます

という状態にしています。


Step 8:購入ボタンと見送りボタンが出る

_phaseGamePhase.choosingProperty になると、下のボタンが切り替わります。

購入する
見送る

この表示を作っているのは、こちらです。

Widget _buildPropertyActions() {
  final BoardTile? tile = _pendingPropertyTile;

  if (tile == null) {
    return _buildDiceActions(canRoll: false, isRolling: false);
  }

  return Column(
    children: <Widget>[
      Text(
        '${tile.label} を購入しますか? 価格 ${tile.price} / 通行料 ${tile.rent}',
      ),
      Row(
        children: <Widget>[
          FilledButton.icon(
            onPressed: _buyPendingProperty,
            label: const Text('購入する'),
          ),
          OutlinedButton.icon(
            onPressed: _skipPendingProperty,
            label: const Text('見送る'),
          ),
        ],
      ),
    ],
  );
}

難しく見えますが、やっていることはこうです。

購入候補の物件がある
↓
購入する / 見送る ボタンを表示

Step 9:購入する処理を見る

購入ボタンを押すと、この関数が動きます。

void _buyPendingProperty() {
  final BoardTile? tile = _pendingPropertyTile;

  if (tile == null) {
    return;
  }

  final PlayerState player = _currentPlayer;

  if (player.cash < tile.price) {
    _addLog('${player.name} は資金不足で購入できません。');
    _pendingPropertyTile = null;
    _phase = GamePhase.waitingRoll;
    _moveToNextPlayer();
    return;
  }

  final Set<int> updatedProperties = <int>{
    ...player.properties,
    tile.index,
  };

  _players[_currentPlayerIndex] = player.copyWith(
    cash: player.cash - tile.price,
    properties: updatedProperties,
  );

  _propertyOwners.add(
    PropertyOwner(
      tileIndex: tile.index,
      ownerPlayerId: player.id,
    ),
  );

  _addLog('${player.name} は ${tile.label} を ${tile.price} で購入しました。');

  _pendingPropertyTile = null;
  _phase = GamePhase.waitingRoll;
  _moveToNextPlayer();
}

長いですが、中心は3つです。

現金を減らす
持ち物に物件を追加する
所有者リストに登録する

Step 10:現金を減らす

購入すると、物件価格の分だけ現金が減ります。

cash: player.cash - tile.price,

例えば、所持金が1500で、物件価格が300なら、

1500 - 300 = 1200

になります。

つまり、購入後の現金は1200です。


Step 11:持っている物件を増やす

次に、このコードを見ます。

final Set<int> updatedProperties = <int>{
  ...player.properties,
  tile.index,
};

これは、プレイヤーの持っている物件一覧に、今回買った物件を追加しています。

tile.index は、物件マスの番号です。

例えば、小さなカフェの index が2なら、

properties に 2 を追加する

という意味です。


Step 12:…player.propertiesとは?

少し難しく見えるのが、この部分です。

...player.properties

これは、今持っている物件をそのまま引き継ぐための書き方です。

イメージはこうです。

今まで持っていた物件
+
今回買った物件
=
新しい物件一覧

例えば、すでに2番の物件を持っていて、新しく5番の物件を買うなら、

{2} + {5} = {2, 5}

になります。


Step 13:プレイヤー情報を更新する

次に、プレイヤー情報を更新します。

_players[_currentPlayerIndex] = player.copyWith(
  cash: player.cash - tile.price,
  properties: updatedProperties,
);

これは、

現金を減らす
持っている物件を増やす

という更新です。

copyWith() を使うことで、名前や色や現在地はそのままにして、cashproperties だけ変えています。


Step 14:所有者リストに登録する

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

_propertyOwners.add(
  PropertyOwner(
    tileIndex: tile.index,
    ownerPlayerId: player.id,
  ),
);

これは、

この物件は、このプレイヤーが持っています

という情報を登録しています。

例えば、

tileIndex: 2
ownerPlayerId: 0

なら、

2番の物件は、Player 1が持っている

という意味です。


Step 15:なぜpropertiesと_propertyOwnersの両方があるの?

少しややこしいですが、役割が違います。

データ役割
player.propertiesプレイヤーが持っている物件一覧
_propertyOwnersどの物件を誰が持っているかの一覧

イメージはこうです。

player.properties
↓
Player 1は、2番と5番を持っている

_propertyOwners
↓
2番はPlayer 1
5番はPlayer 1
8番はPlayer 2

どちらも、物件管理のために使っています。


Step 16:購入が終わったら次のプレイヤーへ

購入処理の最後には、このような処理があります。

_pendingPropertyTile = null;
_phase = GamePhase.waitingRoll;
_moveToNextPlayer();

意味はこちらです。

コード意味
_pendingPropertyTile = null購入候補を空にする
_phase = GamePhase.waitingRollサイコロ待ち状態に戻す
_moveToNextPlayer()次のプレイヤーへ交代する

購入が終わったら、次の人のターンに進みます。


Step 17:見送る処理もある

購入しない場合は、この関数が動きます。

void _skipPendingProperty() {
  final BoardTile? tile = _pendingPropertyTile;

  if (tile != null) {
    _addLog('${_currentPlayer.name} は ${tile.label} の購入を見送りました。');
  }

  _pendingPropertyTile = null;
  _phase = GamePhase.waitingRoll;
  _moveToNextPlayer();
}

これは、

物件を買わない
↓
購入候補を空にする
↓
サイコロ待ちに戻す
↓
次のプレイヤーへ

という流れです。


Step 18:お金が足りない場合

所持金が足りない場合は、購入できません。

if (_currentPlayer.cash >= tile.price) {
  // 買える
} else {
  await _showLifeEventModal(
    title: tile.label,
    categoryLabel: 'ASSET CHANCE',
    description:
        '${tile.description}\n\n購入価格は ${tile.price} ですが、現在の所持金では購入できません。',
    icon: Icons.storefront,
    accentColor: AppColors.gold,
  );

  _addLog('${_currentPlayer.name} は ${tile.label} を購入する資金が足りません。');
  _moveToNextPlayer();
}

流れはこうです。

所持金が足りない
↓
買えないメッセージを表示
↓
次のプレイヤーへ

このようにして、所持金より高い物件は買えないようにしています。


触ってみよう

今回は、物件価格を変えてみましょう。

_createTiles() の中から、小さなカフェを探します。

変更前です。

BoardTile(
  index: 2,
  age: 23,
  stage: '社会人スタート期',
  label: '小さなカフェ',
  description: '小さなカフェに投資できます。所有すると、他のプレイヤーが止まった時に通行料を得られます。',
  type: TileType.property,
  price: 300,
  rent: 80,
),

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

price: 500,
rent: 120,

変更後です。

BoardTile(
  index: 2,
  age: 23,
  stage: '社会人スタート期',
  label: '小さなカフェ',
  description: '小さなカフェに投資できます。所有すると、他のプレイヤーが止まった時に通行料を得られます。',
  type: TileType.property,
  price: 500,
  rent: 120,
),

保存して、物件マスに止まったときに価格が変わっていれば成功です。


もう1つ触ってみよう

物件名を変えてみます。

変更前です。

label: '小さなカフェ',

変更後です。

label: '小さな本屋',

説明文も合わせて変えます。

description: '小さな本屋に投資できます。所有すると、他のプレイヤーが止まった時に通行料を得られます。',

これだけでも、ゲームの世界観が変わります。


よくあるエラーと直し方

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

悪い例です。

price: '500',

正しくはこちらです。

price: 500,

数字は ' で囲みません。


2. rentを文字にしてしまった

悪い例です。

rent: '120',

正しくはこちらです。

rent: 120,

pricerent は数字です。


3. TileType.propertyを消してしまった

物件マスにしたい場合は、これが必要です。

type: TileType.property,

これを別の種類にすると、物件購入処理に進まなくなります。


4. _pendingPropertyTileを消してしまった

_pendingPropertyTile = tile;

これは、購入候補の物件を保存する大事な処理です。

消すと、購入ボタンを押したときに、どの物件を買うのか分からなくなります。

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


5. _moveToNextPlayer()を消してしまった

購入後や見送り後には、次のプレイヤーへ進みます。

_moveToNextPlayer();

これを消すと、ターンが進まなくなることがあります。


この章で覚えること

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

1. 物件マスは TileType.property
2. price は購入価格、rent は通行料
3. 購入すると cash が減り、所有者として登録される

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


やる気を維持するコツ

物件購入が分かると、人生ゲームが一気にゲームらしくなります。

ただ進むだけではなく、

買うか
見送るか
お金を残すか
資産を増やすか

という選択が生まれるからです。

ゲームが面白くなるポイントは、「選べること」です。

まずは物件名や価格を変えて、自分なりのバランスを試してみましょう。


チェックリスト

□ 物件マスが TileType.property だと分かった
□ price が購入価格だと分かった
□ rent が通行料だと分かった
□ _handlePropertyTile() を見つけた
□ _findOwner() が所有者確認だと分かった
□ owner == null が未購入という意味だと分かった
□ 所持金が足りると購入ボタンが出ると分かった
□ _buyPendingProperty() が購入処理だと分かった
□ 購入すると cash が減ると分かった
□ _propertyOwners に所有者が登録されると分かった
□ 物件価格を変更して動作確認した

まとめ

この章では、物件購入の仕組みを確認しました。

物件マスは TileType.property で作ります。

price が購入価格、rent が通行料です。

プレイヤーが未購入の物件マスに止まり、所持金が足りていれば、購入するか見送るかを選べます。

購入すると、プレイヤーの現金が減り、物件が所有者リストに登録されます。

次の章では、通行料の仕組みを見ながら、他のプレイヤーの物件に止まったときのお金の移動を理解していきます。

教材トップへ戻る