Flutterの低レイヤー周り - Flutterでゲーム制作はできそうか調査してみた

# Flutter低レイヤープログラミング

Flutter公式のExamplesフォルダにはひときわ異質なフォルダがあります。

Examples of Flutter's layered architecture
https://github.com/flutter/flutter/tree/master/examples/layers

上記フォルダの`raw/hello_world.dart`ソースコードでは通常のWidgetを組み合わせた作り方ではなくいわゆる低レイヤー処理を使って描画を行っています。
`dart:ui.windows` のフレーム毎に呼ばれる処理を直接呼び出し、
描画もWidgetを使用せずCanvasクラスで描画しています。

https://github.com/flutter/flutter/blob/master/examples/layers/raw/hello_world.dart

``` hello_world.dart
import 'dart:ui' as ui;

void beginFrame(Duration timeStamp) {
  : //処理とか描画とか
}

void main() {

  // The engine calls onBeginFrame whenever it wants us to produce a frame.
  ui.window.onBeginFrame = beginFrame;

  // Here we kick off the whole process by asking the engine to schedule a new
  // frame. The engine will eventually call onBeginFrame when it is time for us
  // to actually produce the frame.
  ui.window.scheduleFrame();
}

```

最近、スマホ界隈でも低レイヤー処理が必要となる場合はめったにないように思います。
ちなみに携帯電話のアプリ本体に10KB制限があった2001年頃、docomoのレポートでは
言語や時代は違いますが低レイヤー処理(低レベルAPI)について以下のように述べられています。

高機能iモード携帯機特集 携帯機組み込みJavaの実用化 (p18)
https://www.nttdocomo.co.jp/binary/pdf/corporate/technology/rd/technical_journal/bn/vol9_1/vol9_1_016jp.pdf

> コンポーネントを用いた場合、アプリケーション作成者の自由度は低くなるが、少ないコードで高機能なアプリケーションを作成することが可能となる.
> 低レベルAPIを用いると、(中略)特にゲームのようなアプリケーションを作成する場合に有効である. したがって、アプリケーション作成者の自由度は高いが, その代わりにアプリケーション作成者がアプリケーションの挙動すべてを管理しなければならない.

というわけで通常のWidgetのみでなく低レイヤーも使って、Flutterでゲーム制作できそうか調べました。

# Flutterでゲーム制作はどこまでいけそうか

Flutterの仕様として2D専用のため2Dゲームに限られます。
以下について調べました。

- フレーム毎の処理
- 入力受付処理
- 描画処理
- 衝突判定


## フレーム毎の処理

フレーム毎の処理では以下で実現できます。

- 短いアニメーションのみであれば標準の`AnimationController`を使用する
- フレーム毎に処理が必要なWidgetのみ`Ticker`を使用する
- 低レイヤー: `ui.window`の `onBeginFrame`に処理を記述する

また、フレーム自体に関しては、

https://flutter.io/docs/testing/ui-performance

> Flutter aims to provide 60 frames per second (fps) performance, or 120 fps performance on devices capable of 120Hz updates.

パフォーマンスのページでは60fpsを目指していると書いてあり、実際にサンプルアプリは60fps付近で動作していますが、
fpsの変更機能の提供は見つかりませんでした。

アニメーションライブラリは標準でTweenあり、Easingありで充実しています。

## 入力受付処理

Flutter自体がモバイル向けなのでタップ入力メインです。以下の方法で可能です。

- Widgetであれば `GestureDetector`でタップからドラッグまで、ほとんどの処理が取得可。

https://docs.flutter.io/flutter/widgets/GestureDetector-class.html

- ゲーム内オブジェクトを`RenderBox`にすると`handleEvent`から入力種別・座標の取得可。

https://github.com/flutter/flutter/blob/master/examples/layers/rendering/touch_input.dart

``` touch_input.dart
class RenderDots extends RenderBox {
:
  @override
  void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
   :
  }
:

```

https://docs.flutter.io/flutter/gestures/PointerEvent-class.html
https://docs.flutter.io/flutter/rendering/BoxHitTestEntry-class.html


- 低レイヤー: `ui.window`の `onPointerDataPacket`で入力データパケットを取得可。


## 描画処理

描画処理に関してはImageクラスにAPIが少なく、Canvasクラスを使用しようしないときついようです。
https://docs.flutter.io/flutter/dart-ui/Image-class.html

Canvas自体は以下のようにWidgetからでもWidget -> RenderBox -> Canvas で使用可能です。

```.dart
class GameObjectWidget extends SingleChildRenderObjectWidget {
  @override
  RenderObject createRenderObject(BuildContext context){
    return new GameObject();
  }
}

class GameObject extends RenderBox {

  @override
  bool get sizedByParent => true;

  @override
  void performResize() {
    size = constraints.biggest;
  }

  @override
  bool hitTestSelf(Offset position) => true;

  @override
  void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
  }

  @override
  void paint(PaintingContext context, Offset offset) {
  }
}
```

また、低レイヤーでは`ui.window.render(scene)`を呼び出すことで描画可能です。

描画に関するの詳細は次章に記載します。


## 衝突判定

標準ではありません。
自作か外部ライブラリを使用する必要があります。

googleからDart版のBox2Dを提供しています。
https://github.com/google/box2d.dart

# Flutterの画像描画はなにができるか

個人的に一番壁になりそうだったのが描画周りだったので、
描画周りについて何ができるか、使ってみて使いやすさはどんな感じか、さらに調べました。
ゲーム作成では画像の使用頻度が多いので、今回は画像描画に絞りました。

https://docs.flutter.io/flutter/dart-ui/Canvas-class.html

画像に関してのAPIは以下4つがあります。

- drawImage(Image image, Offset p, Paint paint)
- drawImageRect(Image image, Rect src, Rect dst, Paint paint)
- drawImageNine(Image image, Rect center, Rect dst, Paint paint)
- drawAtlas(Image atlas, List<RSTransform> transforms, List<Rect> rects, List<Color> colors, BlendMode blendMode, Rect cullRect, Paint paint)

上記APIについて、ひよこのドット画像で検証します。
![0.png](https://qiita-image-store.s3.amazonaws.com/0/19692/3cd7e6de-3ebb-f5c6-b51a-6be4dca42e87.png)


## drawImage: 一枚絵画像の描画

drawImageはシンプルに座標の位置に画像全体を表示ができます。
Unityとは異なり初期設定でエイリアスがないので
ドット絵のゲームの制作に良さそうです。
座標系は2Dらしく左上が(0.0, 0.0)です。
座標系はすべてdoubleで指定します。

Paintのカスタマイズについては後述します。

``` .dart
    canvas.drawImage(bg, Offset(0.0, 0.0), Paint());
    canvas.drawImage(image, Offset(20.0, 20.0), Paint());
```

<img src="https://qiita-image-store.s3.amazonaws.com/0/19692/c9441eab-f59d-341a-385c-89ab5c660e28.png" width=180 />


## drawImageRect: 画像の一部を描画

drawImageRectは画像の一部を切り抜き表示します。
パラパラアニメの表示を行うことができます。
LTWHはLeft-Top-Width-Heightの頭文字です。

``` .dart
    final src = Rect.fromLTWH(24.0 * 0, 0.0, 24.0, 24.0);
    final dst = Rect.fromLTWH(30.0, 189.0 - 24.0, 24.0, 24.0);

    canvas.drawImageRect(image, src, dst, Paint());
```

<img src="https://qiita-image-store.s3.amazonaws.com/0/19692/b4d3a18c-b082-b45d-fa51-befc9601965c.png" width=180 />

## drawImageNine: 9分割画像を描画

drawImageNineはウィンドウ画像のような9分割画像を表示します。


``` .dart
    final src = Rect.fromLTWH(16.0, 16.0, 32.0, 32.0);
    final dst = Rect.fromLTWH(30.0, 30.0, 120.0, 64.0);
    canvas.drawImageNine(windowImage, src, dst, Paint());
```

<img src="https://qiita-image-store.s3.amazonaws.com/0/19692/543ffffa-bdf0-fb50-81d2-12a04c85fec6.png" width=180 />

## drawAtlas: アトラス描画

drawAtlasはなぜかドキュメントに説明がありません。
シューティングゲームの弾などまとめて描画する場合に使用するようです。
第二引数と第三引数の配列数は同じでなければなりません。
Rotation, Scaleの指定が可能です。
translateX, translateYで別途座標ズレを指摘します3
また、Transform, BlendModeが出現しますが`Paint`, `Canvas`で後述します。

``` .dart
final src = Rect.fromLTWH(24.0 * 0, 0.0, 24.0, 24.0);
canvas.drawAtlas(
        image,
        [
          RSTransform.fromComponents(
              rotation: 0.0,
              scale: 1.0,
              anchorX: 0.0,
              anchorY: 0.0,
              translateX: 30.0,
              translateY: 30.0),
          RSTransform.fromComponents(
              rotation: 0.0,
              scale: 1.0,
              anchorX: 0.0,
              anchorY: 0.0,
              translateX: 30.0,
              translateY: 60.0)
        ],
        [
          src,
          src,
        ],
        [], //No need for colors
        BlendMode.src,
        null, //No need for cullRect
        Paint()
    );
```

<img src="https://qiita-image-store.s3.amazonaws.com/0/19692/f590e38f-86e8-9eb7-d57a-edf0c98c1a4f.png" width=180 />


## Paintの機能: 透過, ブレンドモード

drawImage最後尾の引数Paintでは透過度やブレンドモードを設定できます。

``` .dart
    Paint paint = Paint()
      ..color = Color.fromARGB(128, 0, 0, 0)
      ..blendMode = BlendMode.hardLight;
    canvas.drawImage(image, Offset(20.0, 20.0), paint);

```

<img src="https://qiita-image-store.s3.amazonaws.com/0/19692/d6b6427f-9c3a-e01a-51df-83ad79acc03b.png" width=180 />

## Canvasの機能: 反転, 変形, クリッピング

drawImage系には回転はありますが反転がありません。
このためCanvasに行列計算を行い反転します。
反転する箇所のCanvasをsave - restore でくくります。
行列計算のためtranslate -> rotate の処理順も重要であることに注意します。
またアンカーポイントがないので反転位置にも注意します。

また、Canvasには指定の領域にのみ描画するクリッピング機能があります。

``` .dart
    canvas.save();

    Matrix4 cc = Matrix4.identity()
      ..translate(60.0, 60.0)
      ..translate(24.0, 0.0)
      ..rotateY(180.0 * 3.14 / 180);

    canvas.transform(cc.storage);
    final dst = ui.Rect.fromLTWH(10.0, 0.0, 24.0, 24.0);
    canvas.drawImageRect(image, src, dst, Paint());
    
    canvas.restore();

    canvas.clipRect(dst);
```

<img src="https://qiita-image-store.s3.amazonaws.com/0/19692/a3db1196-f66b-c938-8985-c49e3c94aaa4.png" width=180 />

# まとめ／感想

- Flutterは低レイヤープログラミング可能。ドキュメントもあり。
  - ゲームに向いていそうだが、描画関連が辛い。
  - フレーム処理、キー入力において各レイヤーで様々なアプローチが可能。
- Flutterでゲーム制作はできそうか
  - アニメーションは充実しているが描画関連がつらそう。
  - 特に反転サポート・AnchorPointがないのがつらい。
  - 描画部分の拡張と衝突判定が自作か別ライブラリで必要そう。
  - ゲームオブジェクトをWidgetで持ってCanvasで描画するのが良さげ

# Flutter #2 Advent Calendar 
この記事は[Flutter #2 Advent Calendar 2018](https://qiita.com/advent-calendar/2018/flutter-2)の記事になりました。
あした、あさっては登録募集中で12/5は@D_R_1009さんの「Flutterのビルドコマンドを追ってみよう」です！