FlutterでRPGゲームを作る【Part 1/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 1です
    - Part 1：**マップ**と**UI**の実装
    - [Part 2：**プレイヤー**と当たり判定の実装](https://qiita.com/KAZMA_WED/items/b9e5413050bba938a528)
    - [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)のサンプル画像です

## 作成するゲームについて
![Screen Shot 2022-10-24 at 16.09.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2700602/09c16897-cec2-10fa-b7e2-cdf159e039a9.png)

- 見下ろし型のRPGゲームです
- タッチパネルとキーボードの入力に対応しています
- いくつかのイベントがあります
    - NPCとの会話する
    - アイテムを拾う・使う
    - スイッチを押して扉を開く(的なイベント)
- 戦闘はありません (Bonfireは戦闘アクションの実装機能も充実しています)

## Bonfireについて

- Flutterで**2D**の**RPGゲーム**作るためのフレームワークです
- **画面**や**ロジック**の実装から、**状態管理**までBonfireだけで完結させることができます
- [**Flame**](https://pub.dev/packages/flame)というFlutter用のゲームエンジンに依存しています
- [**Bonfire** 公式HP](https://bonfire-engine.github.io/#/) ← 解説やコードサンプル、[Demoゲーム](https://bonfire-engine.github.io/examples/bonfire/#/)があります
- [Flutterドキュメントページ](https://pub.dev/packages/bonfire)
- [Github](https://github.com/RafaelBarbosatec/bonfire)

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

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

1. Flutterプロジェクトとパッケージ、アセットの準備
1. マップ用タイルセットの作成 (Tiled利用)
1. マップの作成 (Tiled利用)
1. WidgetとUIを実装して動作確認

## 1. Flutterプロジェクトとパッケージ、アセットの準備

### 1.1 プロジェクト作成

```zsh
$ flutter create --platforms=web,ios simple_bonfire
```
- ターミナルから、プロジェクトを作成するディレクトリで`flutter create`
- プラットフォームにiOSとWebを指定
- プロジェクト名`simple_bonfire`はお好みで変更可能

### 1.2 `bonfire`パッケージの追加
- プロジェクトディレクトリで`flutter pub add`
```zsh
$ flutter pub add bonfire
```
- `pubspec.yaml`ファイルに`bonfire`が追加されているのを確認
```pubspec.yaml
dependencies:
  flutter:
    sdk: flutter  
  cupertino_icons: ^1.0.2
  bonfire: ^2.10.10
```

### 1.3 アセットの準備

- [こちら](https://franuka.itch.io/fantasy-rpg-halloween-pack)などからタイルセット画像を取得
[無料で利用できるのアセットもたくさんあります](https://0x72.itch.io/dungeontileset-ii)
- ダウンロードした画像を`simple_bonfire/assets/images/maps`に配置
(`simple_bonfire`はFlutterプロジェクト名)
- `pubspec.yaml`に追加したアセットのディレクトリを記載
```pubspec.yaml
flutter:
  uses-material-design: true
  assets:
    - assets/images/
    - assets/images/maps/
```

## 2. マップ用タイルセットの作成

- 元のpngファイルをタイルセットとして利用するための設定jsonファイルを作成します
- このjsonファイルを作成するためのツールがTiledです

### 2.1 Tiledプロジェクトの作成

- [こちら](https://thorbjorn.itch.io/tiled)よりTiledをダウンロードしてください
- Tiledを起動して、`New Project...`を選択し`simple_bonfire/assets/images/maps`下にプロジェクトを作成
`simple_bonfire.tiled-project`と`simple_bonfire.tiled-session`の2つのファイルが生成されます
![Screen Shot 2022-10-22 at 23.51.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2700602/15c8c1f8-e4bc-d07c-6c1b-9626f142189d.png)

### 2.2 アセットpngファイルの登録と分割

- `新しいタイルセット`を選択し、ポップアップのフィールドにそれぞれ値を入力
![Screen Shot 2022-10-22 at 23.53.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2700602/213a5622-1a47-d969-e737-86ab669d437b.png)
    - 名前：タイルセットにわかりやすい名前をつける
    - 種類：均等にタイルが… (デフォルト)
    - 画像：先にダウンロードしたアセットpngファイルを選択
    - タイルの幅、高さ：アセットに合わせて`16px`に変更
- `ファイル名をつけて保存`を選択し`.tsj`で保存した後、`.json`に変更する
(`json`に変更するのはいろいろと便利のため)
```assets/maps/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,
    "tilewidth": 16,
    "type": "tileset",
    "version": "1.9"
}
```
- Tiled上で、画像が正しくタイル上に分割されているのを確認する
![Screen Shot 2022-10-23 at 0.07.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2700602/723ef5af-98fa-cfb7-2a9f-7fb856b7faca.png)

## 3. マップの作成

- まず、作成したタイルセットを使ってマップ生成用jsonファイルを生成します
- Tiledで`ファイル` > `新規` > `新しいマップ`の順に選択
- ポップアップのフィールドにそれぞれ値を入力
![Screen Shot 2022-10-23 at 0.19.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2700602/ba012f5f-5327-c7eb-c692-8475eea0575d.png)
    - 種類：四角型タイル (デフォルト)
    - 出力形式：CSV (デフォルト)
    - 描画順序：左から右、上から下 (デフォルト)
    - マップの大きさ：任意の値を入力
    - タイルの大きさ：アセットに合わせて幅、高さともに`16px`
- マップ編集画面が表示されたら、まず地面のレイヤーを作成
    - 右側のタイルを選択して、真ん中のカラムのセルに配置していきます
    (Tiledの詳しい操作は割愛しますが、画像の状態で完成です)
![Screen Shot 2022-10-23 at 0.27.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2700602/e65d793f-7a8c-dd1a-82da-fc7ad01e539d.png)
- 壁やオブジェクト用のレイヤーを作成
    - 右上のレイヤー枠で右クリック > `タイルレイヤー`を選択 (`オブジェクトレイヤー`ではありません)
    - タイルレイヤー2が生成されたら、タイルレイヤー1の上に移動
    - 柵やオブジェクトを自由に配置
    - タイルレイヤー1を非表示 (目のアイコンをクリック) して画像の様な表示になったら完成
    (確認後に非表示を解除してください)
![Screen Shot 2022-10-23 at 0.37.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2700602/174c892f-4242-26fd-78dc-70b7f9594e70.png)
- `ファイル` > `保存`を選択し`.tmj`形式で保存後、拡張子は`.json`に変更
```assets/maps/halloween_map_01.json
{
   "compressionlevel": -1,
   "height": 16,
   "infinite": false,
   "layers": [
      {
         "data":[514, 514, 514, 514, 514, 514, 514, 514, 514, 514, 514, 514, 514, 514, 514, 514, 514, 514, 514, 514,
            "...とても長いので中略..."
            514, 514, 514, 514, 514, 514, 514, 514, 514, 514, 514, 514, 514, 514, 514, 514, 514, 514, 514, 514],
         "height":16,
         "id":1,
         "name":"\u30bf\u30a4\u30eb\u30ec\u30a4\u30e4\u30fc1",
         "opacity":1,
         "type":"tilelayer",
         "visible":true,
         "width":20,
         "x":0,
         "y":0
      }, 
      {
         "data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 13, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 18, 15, 0,
            0, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 0,
            "...とても長いので中略...",
            0, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 0,
            0, 77, 18, 18, 18, 18, 18, 19, 0, 0, 0, 0, 17, 18, 18, 18, 18, 18, 79, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
         "height": 16,
         "id": 2,
         "name": "\u30bf\u30a4\u30eb\u30ec\u30a4\u30e4\u30fc2",
         "opacity": 1,
         "type": "tilelayer",
         "visible": true,
         "width": 20,
         "x": 0,
         "y": 0
      }
   ],
   "nextlayerid": 3,
   "nextobjectid": 1,
   "orientation": "orthogonal",
   "renderorder": "right-down",
   "tiledversion": "1.9.2",
   "tileheight": 16,
   "tilesets": [
      {
         "firstgid": 1,
         "source": "franuka_halloween_tileset.json"
      }
   ],
   "tilewidth": 16,
   "type": "map",
   "version": "1.9",
   "width": 20
}
```

## 4 WidgetとUIを実装して動作確認

- 作成したマップ生成用jsonファイルを使い、FlutterのWidgetを作成します

### 4.1 マップWidgetの作成
- Statefull Widgetの`build`で`BonfireWidget`を`return`します
- `BonfireWidget`内で以下の3つを設定しています
    - マップ用jsonファイルの読み込み
    - 画面上にジョイスティックやボタンと、キーボードの入力の有効化
    - 遷移直後の読み込み画面
- 細かな説明はコメントに留めます

```lib/maps/halloween_map_01.dart
import 'package:flutter/material.dart';
import 'package:bonfire/bonfire.dart';
import 'package:flutter/services.dart';

class HalloweenMap01 extends StatefulWidget {
  const HalloweenMap01({Key? key}) : super(key: key);
  @override
  State<HalloweenMap01> createState() => _HalloweenMap01State();
}

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

  @override
  Widget build(BuildContext context) {
    // ゲーム画面Widget
    return BonfireWidget(
      // マップ用jsonファイル読み込み
      map: WorldMapByTiled(
        'maps/halloween_map_01.json',
        forceTileSize: Vector2(tileSize, tileSize),
      ),
      // 入力インターフェースの設定
      joystick: Joystick(
        // 画面上のジョイスティック追加
        directional: JoystickDirectional(
          color: Colors.white,
        ),
        actions: [
          // 画面上のアクションボタン追加
          JoystickAction(
            color: Colors.white,
            actionId: 1,
            margin: const EdgeInsets.all(65),
          ),
        ],
        // キーボード用入力の設定
        keyboardConfig: KeyboardConfig(
          keyboardDirectionalType: KeyboardDirectionalType.wasdAndArrows, // キーボードの矢印とWASDを有効化
          acceptedKeys: [LogicalKeyboardKey.space], // キーボードのスペースバーを有効化
        ),
      ),
      // ロード中の画面の設定
      progress: Container(
        width: double.maxFinite,
        height: double.maxFinite,
        color: Colors.black,
      ),
    );
  }
}

```

### 4.2 マップWidgetの実装

- いよいよ作成したマップを表示してゲームを動かします
- `MaterialApp`の`home`に作成したマップWidgetを入れます

```lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:bonfire/bonfire.dart';
import 'package:simple_bonfire/maps/halloween_map_01.dart';

void main() async {
  // iOSでは横画面にする
  WidgetsFlutterBinding.ensureInitialized();
  if (!kIsWeb) {
    await Flame.device.setLandscape();
    await Flame.device.fullScreen();
  }
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Simple Bonfire',
      debugShowCheckedModeBanner: false,
      home: HalloweenMap01(),
    );
  }
}
```

### 4.3 起動して表示と動作の確認

- 今回はWebとiOS版で動作確認をしています
- キャラクターは実装していませんが、矢印キーやWASD、ジョイスティックでカメラが動くのが確認できます
![Screen Shot 2022-10-23 at 11.48.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2700602/859e2d64-bc4a-4bd5-d4ae-857500ce01fb.png)
![Simulator Screen Shot - iPhone 14 Pro Max - 2022-10-23 at 11.49.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2700602/31f16cf8-56b5-a2a7-82bf-027efd2c2140.png)

## おわりに

アセットやマップの準備には外部のツールも使用し一手間かかりましたが、Flutterの実装部分はとてもシンプルだったと思います。
次回はプレイヤーを実装し、当たり判定などゲームとして必要な要素をさらに追加していきます。
いいね、質問やコメントお待ちしております！

[Part 2：**プレイヤー**と当たり判定の実装へと続く](https://qiita.com/KAZMA_WED/items/b9e5413050bba938a528)