5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

FlutterでRPGゲームを作る【Part 4/5 - アイテム取得とイベント】

Last updated at Posted at 2022-10-27

Banner.gif

はじめに

開発環境

OS:       macOS 12.6
Flutter:  3.3.5
Bonfire:  2.10.10
Tiled:    1.9.2
Xcode:    14.0.1
テスト環境: Chrome, iOS Simulator

今回のパートの大まかな流れ

  1. アセットの準備とアイテムクラスの作成
  2. アイテムオブジェクトクラスの作成
  3. アイテムオブジェクトをマップに配置
  4. StateControllerの作成と実装
  5. イベント用Decorationの作成と配置

1. アセットの準備とアイテムクラスの作成

今回作成するアイテムは、このような機能実装になっています。

  • マップ上に配置されている
  • 触れるだけで取得できる
  • 所持数が管理できる
  • 取得済みのアイテムは再生成しない

アイテムの画像にはタイルセット中のキノコのマスを、16x16pxで切り出したものを利用します。
↓ ゲーム内に配置しイメージこちら
Screen Shot 2022-11-07 at 0.17.12.png

1.1 アセットの準備

  • アイテムの画像を追加
    assets/images/objects/ディレクトリを作成し画像を配置します。
    今回はファイル名をbig_shroom.pngとしました。
  • 追加したディレクトリをpubspec.yamlに追記
pubspec.yaml
flutter:
  uses-material-design: true
  assets:
    - assets/images/
    - assets/images/objects/ # 追加
    - assets/images/maps/
    - assets/images/charactors/

1.2 アイテムクラスの作成

  • ファイルの作成
    lib/model/ディレクトリを作成し、game_item.dartを作成します。
  • GameItemクラスの作成
    この記事シリーズで作成するアイテムは1種類だけですが、拡張性を考慮して汎用的なGameItemクラスを作っておきました。
    プロパティには画像パスと、おまけで日本語名と値段を持たせました。
    名前は「〇〇を手に入れた!」などとメッセージを出したいときに使えます (本記事では実装しません)。
  • BigShroomクラスの作成
    アイテムクラスを継承したBigShroomクラスを作成します。
    先に追加したアセットの画像のパスと、お好みで名前と値段をつけてあげてください。
lib/model/game_item.dart
// アイテムクラス
abstract class GameItem {
  GameItem({required this.imagePath, required this.jpName, required this.price});
  String imagePath;
  String jpName; // 日本語名
  int price; // 値段
}

// アイテムクラスを継承したキノコ
class BigShroom extends GameItem {
  BigShroom()
      : super(
          imagePath: 'objects/big_shroom.png',
          jpName: 'デカキノコ',
          price: 180,
        );
}

2. アイテムオブジェクトクラスの作成

  • ファイルの作成
    lib/decorations/ディレクトリを作成し、big_shroom.dartを作成します。
    先に作成したアイテムのクラスと衝突しないように、作成するファイル名やクラス名に注意します。
  • キノコオブジェクトクラスを作成
    次はマップに配置するためのオブジェクトクラス (GameDecoration)を作成します。
  • 接触を取得のトリガーとするためSensorMixinも継承します。
lib/decorations/big_shroom.dart
import 'package:bonfire/bonfire.dart';
import 'package:simple_bonfire/player/player_bearded_dude.dart';
import 'package:simple_bonfire/model/game_item.dart';

class BigShroomDecoration extends GameDecoration with Sensor {
  BigShroomDecoration(
      {required this.initPosition, required this.map, required this.id, required this.player})
      : super.withSprite(
          sprite: Sprite.load(gameItem.imagePath),
          position: initPosition,
          size: Vector2(48, 48),
        ) {
    setupSensorArea(areaSensor: [
      CollisionArea.rectangle(size: size, align: Vector2.zero()),
    ]);
  }
  static final gameItem = BigShroom(); // アイテムクラス

  final Vector2 initPosition; // 初期
  final Type map; // 配置されているマップ
  final int id; // マップ中のアイテムのID番号
  final PlayerBeardedDude player; // 取得するプレイヤーキャラクター

  @override
  void onContact(GameComponent component) {
    // プレイヤーが接触したら
    if (component is player) {
      // 画面から消える
      removeFromParent();
    }
  }
}

  • まだ状態管理 (プレイヤーが取得したアイテムの管理) を実装していないので、ひとまずonContactで画面から消えるまでを実装をします。
  • 引数について
    • initPosition:マップ上の表示位置
    • map, id:どのマップの、どのアイテムか識別用
      後に実装する状態管理で使います
    • player:プレイヤーキャラクター
      接触の判定と、後に実装する状態管理でも使います
  • super.withSprite()について
    表示の設定をします。
  • setupSensorArea()について
    当たり判定の設定をします
    今回は16x16pxで、表示よりも大きめに設定しています。

3. アイテムオブジェクトをマップに配置

  • アイテムの種類について
    今回のアイテムは一度きりしか取得できないようにします。他にも、マップ移動のたびに繰り返し生成され取得できるものも考えられると思います。

    • 一度しか取得できないアイテム:ゼルダの伝説のダンジョンのカギなど
    • 繰り返し生成されるアイテム:ゼルダの伝説の壺など
  • 繰り返し生成されるものや状態管理が不要なものであれば、Part 2で実装したマップ移動用のセンサーのように、BonfireWidget内のobjectBuilderで生成するのが簡単です。
    サンプルこちら

3.1 アイテム生成メソッドを作成

Part 2で作成したマップにメソッドを追加、実装します。

  • BonfireGame.add()メソッド実装
    表示中のゲームの状態管理しているBonfireGameadd()メソッドでDecorationをオブジェクトとしてマップに追加します。
    作成したBigShroomDecorationに初期位置と、状態管理用にマップWidgetとID番号、状態管理と接触の判定用にプレイヤーを渡します。
lib/maps/halloween_map_02.dart
import 'package:simple_bonfire/decorations/big_shroom.dart'; // 追加

// ...省略...

class _HalloweenMap02State extends State<HalloweenMap02> {
  // ...省略...

  // 追加
  void _addGameItems(BonfireGame game) {
    // キノコ1つ目
    game.add(
      BigShroomDecoration(
        initPosition: Vector2(tileSize * 9, tileSize * 3),
        map: widget.runtimeType,
        id: 0,
        player: game.player! as PlayerBeardedDude,
      ),
    );
    // キノコ2つ目
    game.add(
      BigShroomDecoration(
        initPosition: Vector2(tileSize * 13, tileSize * 4),
        map: widget.runtimeType,
        id: 1,
        player: game.player! as PlayerBeardedDude,
      ),
    );
  }
}

3.2 アイテム生成メソッドを実装

BonfireWidgetonReadyを追加し、作成したメソッドを実行します。

lib/maps/halloween_map_02.dart
import 'package:simple_bonfire/decorations/big_shroom.dart';

// ...省略...

class _HalloweenMap02State extends State<HalloweenMap02> {
  // ...省略...

  @override
  Widget build(BuildContext context) {
    return BonfireWidget(
      // showCollisionArea: true,
      onReady: (game) {
        _addGameItems(game); // 追加
      },

      // ...省略...
    );
  }
}

  • ゲーム画面でキノコが表示され、接触で消えることを確認
    Videotogif(1).gif

4. StateControllerの作成と実装

マップにアイテムを配置して消すだけでは、マップ移動をするたびにアイテムが再生成されてしまいますし、アイテムの所持や使用をすることもできません。
まずは取得したアイテムを管理し、アイテムが再生成されないようにします。

4.1 StateControllerの作成と実装

  • ファイルの作成
    lib/player/ディレクトリにplayer_bearded_dude_controller.dartを作成します。
  • StateControllerクラスの作成
    ここではBearedDudeControllerという名前にしました。
    クラス内にはアイテムの状態を格納するためにプロパティを設けます。
    • itemCounts:各アイテムの所有数
    • itemObtained:マップごとの取得済みアイテム
lib/player/player_bearded_dude_controller.dart
import 'package:bonfire/bonfire.dart';
import 'package:simple_bonfire/model/game_item.dart';
import 'package:simple_bonfire/player/player_bearded_dude.dart';

class BearedDudeController extends StateController<PlayerBeardedDude> {
  Map<GameItem, int> itemCounts = {}; // 持っているアイテムの数
  Map<Type, Set<int>> itemObtained = {}; // マップごとの取得済みアイテム

  @override
  void update(dt, component) {}
}

プロパティだけでなくプレイヤーに関するメソッドなどのロジカルな要素も、プレイヤークラスとは切り離してStateContoroller内に記述するとよいでしょう。

  • プレイヤーにStateControllerを継承
    MixinとしてUseStateControllerを追加します。
lib/player/player_bearded_dude.dart
// 追加
import 'package:simple_bonfire/player/player_bearded_dude_controller.dart';
// ...省略...

// UseStateControllerを追加
class PlayerBeardedDude extends SimplePlayer
    with ObjectCollision, UseStateController<BearedDudeController> {
  // ...省略...
}
  • StateControllerInject
    作成したBearedDudeControllerをゲーム内のどこからでも単一のインスタンスとして扱える (繰り返し再生成されずに状態を保持する) 様に、main()BonfireInjector().put()を実行します。
lib/main.dart
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  if (!kIsWeb) {
    await Flame.device.setLandscape();
    await Flame.device.fullScreen();
  }

  await PlayerSpriteSheet.load();

  // StateControllerをInject
  BonfireInjector().put((i) => BearedDudeController());

  runApp(const MyApp());
}
  • アイテムの取得処理を実装する
    UseStateControllerを継承したプレイヤークラスは、下記の様にcontrollerでプロパティの参照と更新ができる様になります。
  • itemCountsitemObtainedにアイテムを追加するだけですが、それぞれmap型で管理しているため、初回取得時のnullだけガードしておきます。
lib/decorations/big_shroom.dart
import 'package:bonfire/bonfire.dart';
import 'package:simple_bonfire/player/player_bearded_dude.dart';
import 'package:simple_bonfire/model/game_item.dart';

class BigShroomDecoration extends GameDecoration with Sensor {

  //...省略...

  @override
  void onContact(GameComponent component) {
    // プレイヤーに接触
    if (component == player) {
      // アイテム所持数の更新
      if (player.controller.itemCounts[gameItem] == null) { // map要素のnullガード
        player.controller.itemCounts[gameItem] = 0;
      }
      player.controller.itemCounts[gameItem] = player.controller.itemCounts[gameItem]! + 1;

      // マップのアイテム取得状態の更新
      if (player.controller.itemObtained[map] == null) { // map要素のnullガード
        player.controller.itemObtained[map] = {};
      }
      player.controller.itemObtained[map]!.add(id);

      // 画面から消える
      removeFromParent();
    }
  }
}

4.2 アイテムが再生成されない実装

先の2. アイテムオブジェクトの作成でも触れた様に、今回はアイテムを生成するかどうかの分岐を、アイテム側で行います。
アイテムがマップに追加された直後に実行されるonMount()内でアイテムの取得状態を確認し、取得済みであればマップから削除します。

lib/decorations/big_shroom.dart
import 'package:bonfire/bonfire.dart';
import 'package:simple_bonfire/player/player_bearded_dude.dart';
import 'package:simple_bonfire/model/game_item.dart';

class BigShroomDecoration extends GameDecoration with Sensor {
  // ...省略...

  @override
  void onMount() {
    if (player.controller.itemObtained[map] != null &&
        player.controller.itemObtained[map]!.contains(id)) {
      removeFromParent();
    }
    super.onMount();
  }

  // ...省略...
}

ゲーム画面で、一度取得したキノコがマップ移動を挟んでも再生成されないのを確認する。
Screen Shot 2022-10-27 at 0.46.35.png

5. イベント用Decorationの作成と配置

  • イベントと一言で言っても色々なパターンが考えられますが、今回は「スイッチで扉が開く」的なイベントを実装します。状態に応じて画像と当たり判定を更新します。
    今回はアセットやマップの準備の手間を惜しんで「キノコを拾うとカカシの画像が変化し、当たり判定がなくなる」という例を紹介します。

5.1 アセットの準備

キノコと同様のタイルセットから、今回は縦長の16x32pxで切り出したものを利用します。

↓ ゲーム内に配置したイメージ、変化前がこちらで
Screen Shot 2022-11-07 at 0.14.29.png
↓ ゲーム内に配置したイメージ、変化後はこちら
Screen Shot 2022-11-07 at 0.14.36.png

5.2 イベントオブジェクトクラスの実装

  • lib/decorations/scarecrow.dartを作成します。
  • カカシはただの置き物なので、Sensorは継承させず、代わりに当たり判定を持たせるためにObjectCollisionを継承します。
  • 2マス分の縦長の画像ですが、接地部分は画像の下半分の想定なので、position.yをマイナスにして上方向に飛び出す様にしています
  • setupCollision()で当たり判定を定義しています。
  • 変化する前後の画像それぞれ必要なので、画像パスを二つ定義します。
  • BonfireInjector().get()でプレイヤーのStateControllerを参照しています。
lib/decorations/scarecrow.dart
import 'package:bonfire/bonfire.dart';
import 'package:simple_bonfire/maps/halloween_map_02.dart';
import 'package:simple_bonfire/player/player_bearded_dude_controller.dart';

class ScarecrowDecoration extends GameDecoration with ObjectCollision {
  ScarecrowDecoration({required this.initPosition})
      : super.withSprite(
          sprite: Sprite.load(imagePath),
          position: initPosition - Vector2(0, -48),
          size: Vector2(48, 96),
        ) {
    setupCollision(
      CollisionConfig(
        collisions: [
          CollisionArea.rectangle(size: Vector2(12, 18), align: Vector2(18, 78)),
        ],
      ),
    );
  }
  final Vector2 initPosition; 

  static const imagePathOn = 'objects/scarecrow_pumpkin_on.png';
  static const imagePath = 'objects/scarecrow_pumpkin_off.png';

  // StateControllerを取得
  final controller = BonfireInjector().get<BearedDudeController>();
  // 連続処理のガード用
  bool lightOn = false;

  @override
  void update(dt) async {
    // HalloweenMap02のアイテム取得状態を参照して
    final itemObtained = controller.itemObtained[HalloweenMap02];
    // キノコ取得済みなら
    if (itemObtained != null && itemObtained.containsAll({0, 1})) {
      // 画像を変更
      sprite = await Sprite.load(imagePathOn);
      // 当たり判定を変更
      setupCollision(
        CollisionConfig(collisions: []),
      );
      lightOn = true;
    }
    super.update(dt);
  }
}

5.3 オブジェクトの生成メソッドの作成と実装

アイテム追加とほぼ同じ手順で、まずは生成メソッドを作成し、BonfireWidgetonReady内で実行します。
解説はコメントに留めます。

a.dart
import 'package:simple_bonfire/decorations/scarecrow.dart';

// ...省略...

class _HalloweenMap02State extends State<HalloweenMap02> {
  final tileSize = 48.0; // タイルのサイズ定義

  // カカシ追加メソッドを追加
  void _addScarecrow(BonfireGame game) {
    game.add(ScarecrowDecoration(
      initPosition: Vector2(tileSize * 15, tileSize * 7),
    ));
  }

  @override
  Widget build(BuildContext context) {
    // 画面
    return BonfireWidget(
      // ...省略...
      // デコレーションの配置
      onReady: (game) {
        _addGameItems(game);
        _addScarecrow(game); // 追加
      },
      
      // ...省略...
    );
  }
}

  • 最初は大人しかったカカシが…
    Screen Shot 2022-10-27 at 0.39.29.png
  • キノコを取るとピカっと光り、すり抜ける様になったら完成!
    Screen Shot 2022-10-27 at 0.39.50.png
    ちなみに、Part 3でマップをTiledで作成した段階では、カカシは木などと同じくマップ内のタイルとして配置していました。
    今回、カカシはオブジェクト化に際してマップからは削除しました。

おわりに

状態管理と分岐を実装し、かなりゲームらしく仕上がってきたと思います。

今回は、プレイヤークラスにマップ毎のアイテムの状態管理まで任せてしまいましたが、実際は所持品などの管理のみに留める方が汎用的だと思います。
あくまで最も簡単な実装例のひとつとしてご参考にしてください。

次回最終パートでは、動きのあるNPCと会話のイベントを実装します。
いいね、コメントや質問などいつでもお待ちしております!!

Part 5はこちら

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?