FlutterでRPGゲームを作る【Part 2/5 - プレイヤー】

![Banner.gif](https://img.itch.zone/aW1nLzcyODIxNjYuZ2lm/189x150%23/pu%2FnNJ.gif)

## はじめに

- **Flutter**でマルチプラットフォーム対応 (Web, モバイル, PC) の**RPGゲーム**を作成します
- [ソースコードはこちら](https://github.com/kazmawedton/simple_bonfire) (アセットなど一部差し替えています)
- この記事は5つのPartに分かれていて、今回はそのPart 2です。
    - [Part 1：**マップ**と**UI**の実装](https://qiita.com/KAZMA_WED/items/ed43e1c7bbaec397c486)
    - Part 2：**プレイヤー**と当たり判定の実装
    - [Part 3：**マップ移動**の実装](https://qiita.com/KAZMA_WED/items/0d37df4a6681a2f97017)
    - [Part 4：**アイテム**と**取得イベント**の実装](https://qiita.com/KAZMA_WED/items/1272279fb3ea2c251fd9)
    - [Part 5：**NPC**と**会話**の実装](https://qiita.com/KAZMA_WED/items/c1d33ff4dcd293ade1af)
- [**Bonfire**](https://bonfire-engine.github.io/#/)という2Dゲームに特化したフレームワーク(Flutterパッケージ)を利用しています
- マップ制作には[**Tiled**](https://thorbjorn.itch.io/tiled)という無料のGUIツールを利用しています
**Tiled**は必須ではありませんが、使い方も簡単で制作作業がとても捗ります
- アセットは[**itch.io**](https://itch.io/)でダウンロードできるものを利用しています
- トップの画像は[利用しているアセット](https://franuka.itch.io/fantasy-rpg-halloween-pack)のサンプル画像です

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

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

1. プレイヤー用アニメーションスプライトの作成
1. プレイヤークラスの作成
1. マップ用タイルに当たり判定を設定 (Tiled利用)
1. 奥行きを表現する

## 1. プレイヤー用アニメーションスプライトの作成

### 1.1 アセットの準備
- [こちら](https://franuka.itch.io/rpg-townsfolk-pack)から画像をダウンロード
![sNCkWs.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2700602/90d0a9b0-715c-5d17-77b4-a314ceea2bdf.png)
`Download Now`から`No thanks, just take me to the downloads`を選択で無料ダウンロードできます
- ダウンロードした画像を`simple_bonfire/assets/images/charactors`に配置
(`simple_bonfire`はFlutterプロジェクト名です)
- `pubspec.yaml`に追加したアセットのディレクトリを記載
```pubspec.yaml
flutter:
  uses-material-design: true
  assets:
    - assets/images/
    - assets/images/maps/
    - assets/images/charactors/ # ←追加
```

### 1.2 追加したアセットからSpriteSheetを作成

- ゲームで使用できるSpriteSheet生成するためのクラスを実装
    - `lib`ディレクトリ下に`player/player_sprite.dart`を作成します
    - 今回は複数のキャラクターが1枚になったアセットを利用します
    - 画像内のそれぞれのコマは、左から右の順でコマ送りになっています

```simple_bonfire/lib/player/player_sprite.dart
import 'package:bonfire/bonfire.dart';

class PlayerSpriteSheet {
  static late SpriteSheet all;

  // ゲーム起動時に実行するメソッド
  static Future<void> load() async {
    // 追加したアセットのパスからSpriteSheetを作成
    all = await _create('charactors/franukas_charactors_16x24.png');
  }

  // 画像からSpriteSheetを生成する処理本体
  static Future<SpriteSheet> _create(String path) async {
    // ファイルパスから画像を取得して
    final image = await Flame.images.load(path);
    // 1枚ずつ分割する (横40x縦8)
    return SpriteSheet.fromColumnsAndRows(image: image, columns: 40, rows: 8);
  }
}

```

- 起動後にSpriteSheetを作成するよう`main()`に追加

```simple_bonfire/lib/main.dart
void main() async {
  // ...省略...

  // アセットからSpriteSheetを生成
  await PlayerSpriteSheet.load(); // ←追加
  runApp(const MyApp());
}
```

## 2. プレイヤークラスの作成

- 作成したSpriteSheetを実際に表示して動かすプレイヤークラスを作成します

### 2.1 プレイヤークラスの作成

- `lib/player`ディレクトリ下に`player_beared_dude.dart`を作成します
- プレイヤーは`Bonfire`の`SimplePlayer`クラスを継承します
    - `ObjectCollision Mixin`を継承し当たり判定を持たせます
- ここではコンストラクタ内で以下の4つを設定します
    - `idle`状態と`run`状態のアニメーション
    - ゲーム内での表示サイズ
    - `run`状態時の移動スピード
    - 当たり判定
- `idle`と`run`にはそれぞれ上下左右があるため、合計8種類分のアニメを設定します

```simple_bonfire/lib/player/player_beared_dude.dart
import 'package:bonfire/bonfire.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

class PlayerBeardedDude extends SimplePlayer with ObjectCollision {
  PlayerBeardedDude(
    position, {
    required this.spriteSheet,
    Direction initDirection = Direction.down,
  }) : super(
          // SpriteSheetからアニメーションを設定
          animation: SimpleDirectionAnimation(
            idleDown: spriteSheet.createAnimation(row: 0, stepTime: 0.4, from: 1, to: 3).asFuture(),
            idleLeft: spriteSheet.createAnimation(row: 1, stepTime: 0.4, from: 1, to: 3).asFuture(),
            idleRight:
                spriteSheet.createAnimation(row: 2, stepTime: 0.4, from: 1, to: 3).asFuture(),
            idleUp: spriteSheet.createAnimation(row: 3, stepTime: 0.4, from: 1, to: 3).asFuture(),
            runDown: spriteSheet.createAnimation(row: 0, stepTime: 0.1, from: 4, to: 8).asFuture(),
            runLeft: spriteSheet.createAnimation(row: 1, stepTime: 0.1, from: 4, to: 8).asFuture(),
            runRight: spriteSheet.createAnimation(row: 2, stepTime: 0.1, from: 4, to: 8).asFuture(),
            runUp: spriteSheet.createAnimation(row: 3, stepTime: 0.1, from: 4, to: 8).asFuture(),
          ),
          // 画面上の表示サイズ
          size: Vector2(16, 24) * 3,
          // 移動速度
          speed: 80 * 3,
          position: position,
          initDirection: initDirection,
        ) {
    // 当たり判定の設定
    setupCollision(
      CollisionConfig(
        collisions: [
          CollisionArea.rectangle(
            // 首から足下までの矩形を指定
            size: Vector2(size.x * 12 / 16, size.y * 7 / 24),
            align: Vector2(size.x * 2 / 16, size.y * 16 / 24),
          ),
        ],
      ),
    );
  }
  final SpriteSheet spriteSheet;
}
```

- `createAnimation()`メソッドについて
    アセットからコマを切り出して、コマ送りアニメーションを生成します
    引数の数値の設定方法にクセがあるので要注意です
    - `row`：スプライトシートの上から何行目かを指定
![Screen Shot 2022-10-24 at 0.18.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2700602/b1fe3637-7e19-2269-6c54-18cec13c34d9.png)
    - `from`：アニメの開始コマが左から何番目かを指定
    - `to`：アニメの終了コマが左から何番目かを**1始まりの番号 (要注意！)** で指定
 ![Screen Shot 2022-10-24 at 0.18.50 2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2700602/fcc05862-49c8-b568-bce9-a02c2b17520d.png)
    - `stepTime`：1コマあたりの表示時間、小さいほどコマ送りが速くなります

### 2.2 マップに配置する

- [Part 1](https://qiita.com/KAZMA_WED/items/ed43e1c7bbaec397c486)で作成したマップにプレイヤーを追加
    - 初期位置と初期方向`initDirection`を適当に決める
    - `spriteSheet`は先に作成した`PlayerSpriteSheet`から`all`を取得
- カメラの追従設定を好みで追加
    - `moveOnlyMapArea`：`true`でタイルの無い部分へのカメラ移動を制限
    - `sizeMovementWindow`： プレイヤーを収めるカメラ内の範囲
      (広くするほどカメラの追従が甘くなる)
    - `smoothCameraEnabled`：`true`でカメラの動きが滑らかになる
    - `smoothCameraSpeed`：`smoothCameraEnabled: true`時の、カメラの軽快さ
      (小さくするとカメラが緩やかに動き始め、緩やかに止まる)

```simple_bonfire/lib/maps/halloween_map_01.dart
// ...省略...

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

  @override
  Widget build(BuildContext context) {
    // 画面
    return BonfireWidget(
      showCollisionArea: true, // 当たり判定の可視化
      // マップ用jsonファイル読み込み
      map: WorldMapByTiled(
        'maps/halloween_map_01.json',
        forceTileSize: Vector2(tileSize, tileSize),
      ),

      // ---------- ここから追加 ----------
      // プレイヤーキャラクター
      player: PlayerBeardedDude(
        Vector2(tileSize * 7.5, tileSize * 4),
        spriteSheet: PlayerSpriteSheet.all,
        initDirection: Direction.down,
      ),
      // カメラ設定
      cameraConfig: CameraConfig(
        moveOnlyMapArea: true,
        sizeMovementWindow: Vector2.zero(),
        smoothCameraEnabled: true,
        smoothCameraSpeed: 10,
      ),
      // ---------- ここまで追加 ----------

      // ...以下略...
    );
  }
}

```

- `BonfireWidget`内に`showCollisionArea: true,`を追加して当たり判定を設定
`showCollisionArea`でより当たり判定を可視化することができます
この時点でジョイスティックやWASDキーでキャラクターが操作できる様になっているので、アニメーションや移動の速度、カメラの追従が想定通りに設定できているかチェックしてください
![Screen Shot 2022-10-24 at 0.40.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2700602/9391365c-1485-1588-cdaa-1935facc167c.png)


## 3. マップ用タイルに当たり判定を設定

- プレイヤー以外のオブジェクトに当たり判定を設定します
- [Part 1](https://qiita.com/KAZMA_WED/items/ed43e1c7bbaec397c486)で作成したタイルセットのjsonファイルをTiledを利用して編集します

### 3.1 柵にシンプルな当たり判定を追加

- `Open file or Project`を選択、[Part 1](https://qiita.com/KAZMA_WED/items/ed43e1c7bbaec397c486)で作成した`simple_bonfire.tiled-project`を開く
左のカラムにTiledプロジェクトのディレクトリと、[Part 1](https://qiita.com/KAZMA_WED/items/ed43e1c7bbaec397c486)で作成したjsonファイルが表示されます

![Screen Shot 2022-10-23 at 23.03.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2700602/e6a0e055-38d0-a1c8-0e08-0c8a547e9b7a.png)

- 左のカラムから`tileset.json`を選択して、[Part 1](https://qiita.com/KAZMA_WED/items/ed43e1c7bbaec397c486)で追加したアセットがタイル状に表示されるのを確認
ここでは上部の柵に当たり判定をつけるため、タイル一覧最上段の`ID 17` (左から18番目) のタイルを選択します

![Screen Shot 2022-10-23 at 23.06.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2700602/dc1d43bb-d75f-7d05-7340-40841cfc39a9.png)

- メニューから`タイルセット` > `タイルの当たり判定エディター`を選択
- 選択したタイルが拡大表示されたら`四角形を追加`を選び、画像の様に一つ四角形を追加する
右側の画面で視覚を追加した後、真ん中のプロパティから値を変更することもできます

![Screen Shot 2022-10-23 at 23.07.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2700602/2676da71-826a-98f4-8b32-a8a612a43119.png)

- 保存をすると`franuka_halloween_tileset.json`に`tiles`内の配列に`id: 17`の要素が追加されている

```franuka_halloween_tileset.json
{
    "columns": 32,
    "image": "franuka_halloween_tileset.png",
    "imageheight": 512,
    "imagewidth": 512,
    "margin": 0,
    "name": "franuka_halloween_tileset",
    "spacing": 0,
    "tilecount": 1024,
    "tiledversion": "1.9.2",
    "tileheight": 16,
    "tiles": [
        {
            "id": 17,
            "objectgroup": {
                "draworder": "index",
                "id": 2,
                "name": "",
                "objects": [
                    {
                        "class": "",
                        "height": 12,
                        "id": 4,
                        "name": "",
                        "rotation": 0,
                        "visible": true,
                        "width": 16,
                        "x": 0,
                        "y": 1
                    }
                ],
                "opacity": 1,
                "type": "objectgroup",
                "visible": true,
                "x": 0,
                "y": 0
            }
        }
    ],
    "tilewidth": 16,
    "type": "tileset",
    "version": "1.9"
}
```

- ゲーム画面内で想定通りの当たり判定が設定されているか確認
キャラクターを操作すると、柵より向こうに移動できなくなっているはずです
![Screen Shot 2022-10-23 at 23.24.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2700602/1a207819-7e23-fcc8-b83f-ed850f4c5f61.png)

### 3.2 複雑な形状の当たり判定の追加

- 同様に他のオブジェクトにも当たり判定を設定
四角形以外の図形や複数の図形を追加することもできますが、画像の例では、柵の角に二つの四角形を追加しています
![Screen Shot 2022-10-23 at 23.30.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2700602/f93e05d5-4fcb-1a53-c05b-f74871061310.png)

- ゲーム画面で想定通りの当たり判定が追加されたのを確認する
![Screen Shot 2022-10-23 at 23.32.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2700602/7df78cc5-ade2-daa1-deff-252d3c89e239.png)

## 4. 奥行きを表現する

- 木などの高さのあるオブジェクトは、当たり判定だけではうまく表現できません
- オブジェクトの後ろにキャラクターが回り込める様にして、高さと奥行きを表現します
- 画像は回り込みを実装する前なので、キャラクターが木の上に描画されてしまっています
![Screen Shot 2022-10-23 at 23.33.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2700602/e8b91196-a4c3-7534-409c-9c4302786d4a.png)
- 木の根元に当たり判定をつける
柵と同じ要領で、木の根元のタイルに当たり判定をつけます。
今回は左右のタイルに分かれているので、両方に適当な形を追加します。
![Screen Shot 2022-10-23 at 23.35.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2700602/d756bc05-2da1-3a8a-eb63-52bcf6323390.png)
- 木の上部に`above`属性を付与
木の上部は高さがありキャラクターと衝突しない想定のため、当たり判定はつけません
その代わりに、プロパティ内の`Class`に`above`を入力して、キャラクターより手前に描画される様にします
![Screen Shot 2022-10-23 at 23.38.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2700602/90868d35-2b51-b243-51aa-4d139bb2dc35.png)
- 保存後`franuka_halloween_tileset.json`内の`tiles`に`class: above`のタイルが登録されているのを確認する
```franuka_halloween_tileset.json
{
    "columns": 32,
    "image": "franuka_halloween_tileset.png",

    "長いので": "中略",

    "tiles": [
        {"長いので": "省略"}

        {
            "class": "above"
            "id": 205,
        }

        {"長いので": "省略"}
    ],
    "tilewidth": 16,
    "type": "tileset",
    "version": "1.9"
}
```

- ゲーム画面で当たり判定と、回り込みが再現できているか確認
画像の様に、木のまわりでの衝突と描画が上手く再現できたら完成です。
Bonfireにおけるタイルのタイプは他にも`dynamicAbove`があります。
詳しくは[**こちらをご覧ください**](https://bonfire-engine.github.io/#/tiled_support)。
![Screen Shot 2022-10-23 at 23.37.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2700602/cf25bb4d-c69c-b40a-5cfd-8d66b767a133.png)
![Screen Shot 2022-10-23 at 23.38.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2700602/4f42a5a7-bd8c-97cf-15f4-97ae9700ca65.png)

## おわりに

マップとキャラクターの作成ができました。
Bonfire (と裏で動いているFlameエンジン) のおかげで、とてもシンプルな手順でここまでゲーム開発ができます。
[次回のパート](https://qiita.com/KAZMA_WED/items/0d37df4a6681a2f97017)以降ではマップの移動、アイテムやNPCなどのRPGゲームらしい要素を実装します。
いいね、質問やコメントお待ちしております！

[**Part 3はこちら**](https://qiita.com/KAZMA_WED/items/0d37df4a6681a2f97017)