ただ🐈が歩き回るだけのゲーム作った

## はじめに
### 概要
Flutterで遊んでみよう期間が続いています。
今回は「RPG風のマップ」上に存在する🐈を、ただグリグリ動かせるコードを書きました。

https://github.com/kawamou/rpg

![cat.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287774/e75c8cc5-b6e7-9fcb-1204-a42a21c14356.gif)

### 記事の内容

前回のブロック崩しに引き続き[Flame](https://pub.dev/packages/flame)を使用しました。

https://qiita.com/kawamou/items/dc940961186c1c2f1571

Flameの概念や簡単な使い方は上記の前回記事で触れているため割愛します。

- アニメーションするキャラクターの作成
- マップの生成
- キャラクターを追尾するカメラ
- ジョイスティックによるキャラクター操作
- 障害物との当たり判定

等々のRPGっぽい2Dゲームを作る必要最小限の実装について以降では説明します。

ちなみに本気で（Flutter使って）RPGを作りたい場合はFlameをもとにした[Bonfire](https://bonfire-engine.github.io/#/)があるので、そちらを使用する方が開発コスト的に良いかと思います。

### 環境
```
Flutter 2.8.0
Flame 1.0.0
```

## 実装編
### アニメーションするキャラクターの作成

一般的なRPGのように立ち絵がアニメーションするキャラクターを作るには、スプライトシートを利用するのが一般的です。

>「⁠スプライトシート」というのは，アニメーションのひとコマひとコマをひとつにまとめた画像ファイルだ。ファイルをひとつにすると，読込みが1回で済むため効率がいい。画像からそれぞれのコマのグラフィックイメージを切り出して，アニメーションとして再生する。パフォーマンスを重く見るゲームコンテンツでも使われる仕組みだ。

[第12回　スプライトシートでアニメーションをつくる](https://gihyo.jp/design/serial/01/createjs/0012)

ひとつのファイル内に複数コマをまとめた画像のことをスプライトシートと呼びます。
今回の🐈の場合は下記のような`cat.png`を準備しました（画像は[ぴぽや倉庫](https://pipoya.net/sozai/)さんにお借りしました）。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287774/4914dffc-61b3-ba8e-c052-e9bf6668e7b6.png)

1行目が🐈が下に進んでいるときのアニメーション、2行目が左、3行目が右、4行目が上です。

`SpliteSheet`に各コマのサイズ（今回の場合各コマは32×32ピクセル）と画像ファイルを渡して読み込みます。
後は`createAnimation()`で「どの行を読み込むか」「各行の長さ」等々を指定してあげるだけでOKです。

```cat.dart
Future<void> _loadAnimations() async {
    final spriteSheet = SpriteSheet(
      image: await gameRef.images.load('cat.png'),
      srcSize: Vector2(32.0, 32.0),
    );
    _runDownAnimation =
        spriteSheet.createAnimation(row: 0, stepTime: _animationSpeed, to: 3);
    _runLeftAnimation =
        spriteSheet.createAnimation(row: 1, stepTime: _animationSpeed, to: 3);
    _runRightAnimation =
        spriteSheet.createAnimation(row: 2, stepTime: _animationSpeed, to: 3);
    _runUpAnimation =
        spriteSheet.createAnimation(row: 3, stepTime: _animationSpeed, to: 3);
  }
```

読み込んだアニメーションは、`SpriteAnimationComponent`等が持つ`animation`プロパティに代入するだけでキャラクターがアニメーションを始めます。
上を向いたときには上向きのアニメーションが、下を向いたときには下向きのアニメーションが...といったように、キャラクターの方向と対応するアニメーションが読み込まれるようにしています。

```cat.dart
SpriteAnimation _updateAnimation(direction d) {
    if (d == direction.up) {
      return _runUpAnimation;
    } else if (d == direction.right) {
      return _runRightAnimation;
    } else if (d == direction.down) {
      return _runDownAnimation;
    } else {
      return _runLeftAnimation;
    }
  }

@override
  void update(double dt) {
    ...
      animation = _updateAnimation(currentDirection);
    ...
  }
```

## マップの生成

背景であるマップ画像は、一枚絵の画像ファイル（png等）を読み込む方法もありますが、大きなマップになると画像の重さがキツくなってきます。
今回はタイルセット（マップチップ）を使ってマップを作成することにしました。
タイルセットとは、マップのもととなる要素（木、芝生、花...）が敷き詰められた画像のことです（こちらも[ぴぽや倉庫](https://pipoya.net/sozai/)さんのものを利用しています）。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287774/cf8f00f9-2048-74cf-4adb-bece6ff7f839.png)

タイルセットを**Tiled（Tiled Map Editor）等のマップエディタ**に読み込めば任意のマップを作成することができます。

https://nut-softwaredevelopper.hatenablog.com/entry/2015/05/06/004127

[Bonfireの公式も推奨](https://bonfire-engine.github.io/#/map)しているので、今回はTiledを利用しました。
Tiledの詳細な使い方は下記の記事等が詳しいです。

https://qiita.com/muzudho1/items/ac3602a29f8536031fe4

簡単な操作感としては、タイルセットの画像から任意の要素をドラッグ&ドロップしながらマップを作っていく感覚です。
レイヤの構造の定義等もできます。最終的には下記のようなマップを作成しました。
衝突するための障害物と芝生だけを敷き詰めた汚いマップです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287774/6e064959-ed62-a546-1690-368fce2b1e1d.png)

Tiledで作成したマップは`.tmx`と`.tsx`拡張子のファイルで出力されます。
ざっくり言えば`.tmx`にはマップの情報が、`.tsx`にはタイルセットの情報が載っています。

https://s-fifteen.hatenadiary.org/entry/20130830/p1

Flutterで使う上では、まず上記の2ファイルと、タイルセットの画像ファイルをFlutterの`assets`ディレクトリに格納し`pubspec.yaml`で指定します。

```pubspec.yaml
assets:
    ...
    - assets/tilesets.png
    - assets/tilesets.tsx
    - assets/grassland.tmx
```

Flameの`TiledComponent`に`.tmx`ファイルを読み込むとマップが表示されます（`.tmx`ファイル内で`.tsx`やタイルセットの画像ファイルを参照しています）。

```game.dart
@override
  Future<void>? onLoad() async {
    ...
    final tiledMap = await TiledComponent.load(
        'grassland.tmx', Vector2(32, 32));
    add(WorldMap(tiledMap, Vector2(0, 0), Vector2(canvasSize.x, canvasSize.y)));
    ...
  }
```

## キャラクターを追尾するカメラ

RPGを行う上ではキャラクターが画面の中央に表示されているのが好ましいです。
Flameでは追尾したいコンポーネントを`camera.followComponent()`に入れるだけで実現できます。

```game.dart
@override
  Future<void>? onLoad() async {
    ...
    camera.followComponent(cat);
    ...
  }
```

## ジョイスティックによるキャラクター操作

キャラクターをグリグリ動かすジョイスティックは`JoystickComponent`で実現できます。

```joystick.dart
class JoystickController extends JoystickComponent {
  JoystickController(Paint knobPaint, Paint backgroundPaint)
      : super(
            background: CircleComponent(radius: 100, paint: backgroundPaint),
            knob: CircleComponent(radius: 30, paint: knobPaint),
            margin: const EdgeInsets.only(left: 40, bottom: 40));
}
```

ジョイスティックがどれくらい動いているかの情報を別のコンポーネントに渡したい場合は、コンストラクタで渡してあげます。
今回の場合、🐈をグリグリ動かしたいので🐈のコンストラクタにジョイスティックを渡します。

```game.dart
@override
  Future<void>? onLoad() async {
    ...
    final joystick = JoystickController(knobPaint, backgroundPaint);
    await images.load('cat.png');
    final cat = Cat(joystick);
    ...
  }
```

参照渡しになっているので、ジョイスティックの情報が都度渡る形になります。

https://www.cresc.co.jp/tech/java/Google_Dart2/language/variables/variables.html


## 障害物との当たり判定

当たり判定はかなりマッチョな実装になってしまったのでコード自体は割愛します。
ざっくり言うと、マップは32×32ピクセルのタイルから生成されているので、その内障害物が存在するタイルの座標に32×32ピクセルの`Hitbox`を配置する実装です[^1]。

本来なら、Tiledではマップに衝突判定等の情報も付与できます。

https://qiita.com/muzudho1/items/1cb926c980b8327f093d

もしこの情報が読み込めるのならもっとエレガントに書けそうですが、Flameの`TiledComponent`では読み込めないっぽいので諦めました（調査不足の可能性アリ）。

## 最後に

**昔から2DのRPGをどう作るのか興味があったためRPGのプロトタイプ的コードを書いてみた、というのが本記事のモチベーション**でした。
必要最小限の理解はできたかなと思います。
それにしても初心者でも簡単に触れるFlutter（Flame）スゴイ。

## 余談

当たり判定の実装は超適当なので、壁に衝突したまま他の壁に衝突するとセカイの外に出て戻って来れなくなります。

![cat2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287774/2712df8a-01c4-50e1-5cad-35b6a788678e.gif)

## 参考

こちらの記事を大いに参考にさせていただきました。

https://www.raywenderlich.com/27407121-building-games-in-flutter-with-flame-getting-started

[^1]: 実際にはもっと綺麗に書ける。障害物が存在するタイルを1、それ以外を0にしたJSONを読み込む、等。めんどいのでやってないが