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クラスで描画しています。
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
に処理を記述する
また、フレーム自体に関しては、
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
でタップからドラッグまで、ほとんどの処理が取得可。
- ゲーム内オブジェクトを
RenderBox
にするとhandleEvent
から入力種別・座標の取得可。
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 で使用可能です。
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の画像描画はなにができるか
個人的に一番壁になりそうだったのが描画周りだったので、
描画周りについて何ができるか、使ってみて使いやすさはどんな感じか、さらに調べました。
ゲーム作成では画像の使用頻度が多いので、今回は画像描画に絞りました。
画像に関しての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)
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のビルドコマンドを追ってみよう」です!