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.windowonBeginFrameに処理を記述する

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

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.windowonPointerDataPacketで入力データパケットを取得可。


描画処理

描画処理に関してはImageクラスにAPIが少なく、Canvasクラスを使用しようしないときついようです。

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

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

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 transforms, List rects, List colors, BlendMode blendMode, Rect cullRect, Paint paint)

上記APIについて、ひよこのドット画像で検証します。

0.png


drawImage: 一枚絵画像の描画

drawImageはシンプルに座標の位置に画像全体を表示ができます。

Unityとは異なり初期設定でエイリアスがないので

ドット絵のゲームの制作に良さそうです。

座標系は2Dらしく左上が(0.0, 0.0)です。

座標系はすべてdoubleで指定します。

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

    canvas.drawImage(bg, Offset(0.0, 0.0), Paint());

canvas.drawImage(image, Offset(20.0, 20.0), Paint());


drawImageRect: 画像の一部を描画

drawImageRectは画像の一部を切り抜き表示します。

パラパラアニメの表示を行うことができます。

LTWHはLeft-Top-Width-Heightの頭文字です。

    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());


drawImageNine: 9分割画像を描画

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

    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());


drawAtlas: アトラス描画

drawAtlasはなぜかドキュメントに説明がありません。

シューティングゲームの弾などまとめて描画する場合に使用するようです。

第二引数と第三引数の配列数は同じでなければなりません。

Rotation, Scaleの指定が可能です。

translateX, translateYで別途座標ズレを指摘します3

また、Transform, BlendModeが出現しますがPaint, Canvasで後述します。

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()
);


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

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

    Paint paint = Paint()

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


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

drawImage系には回転はありますが反転がありません。

このためCanvasに行列計算を行い反転します。

反転する箇所のCanvasをsave - restore でくくります。

行列計算のためtranslate -> rotate の処理順も重要であることに注意します。

またアンカーポイントがないので反転位置にも注意します。

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

    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);


まとめ/感想


  • Flutterは低レイヤープログラミング可能。ドキュメントもあり。


    • ゲームに向いていそうだが、描画関連が辛い。

    • フレーム処理、キー入力において各レイヤーで様々なアプローチが可能。



  • Flutterでゲーム制作はできそうか


    • アニメーションは充実しているが描画関連がつらそう。

    • 特に反転サポート・AnchorPointがないのがつらい。

    • 描画部分の拡張と衝突判定が自作か別ライブラリで必要そう。

    • ゲームオブジェクトをWidgetで持ってCanvasで描画するのが良さげ




Flutter #2 Advent Calendar

この記事はFlutter #2 Advent Calendar 2018の記事になりました。

あした、あさっては登録募集中で12/5は@D_R_1009さんの「Flutterのビルドコマンドを追ってみよう」です!