🦋
3DエンジンZFlutterを公式サイコロアプリから学ぶ
Flutter:ZFlutter
背景
- 公式のFlutter samplesが素晴らしい
- その中で、Flutter samples: Diceが面白い
- サイコロの回転が表現されている
- これには、ZFlutterというFlutter用の3Dエンジンが利用されている
ZFlutterについて
- ZFlutterというFlutter用の3Dエンジン
サンプル「Dice」
サイコロの作り方概要
-
サイコロ本体の表現方法
- 4つの丸みのある長方形を組み合わせてサイコロ本体を作成
- 表面に円を組み合わせてサイコロの目を作成
-
サイコロの目の表現方法
- 円を利用し、一の目を作成
- 一の目を利用し、二の目を作成
- 一の目を利用し、三の目を作成
- 二の目を利用し、四の目を作成
- 一の目と四の目を利用し、五の目を作成
- 二の目と四の目を利用し、六の目を作成
ZFlutter事前知識
-
用語(※ポイントとなるクラス、プロパティのみ記載)
-
ZIllustration
-
3Dエンジンのトップレベルwidget
-
プロパティ
-
List<Widget> children
- 形を作るshape widget等
-
double zoom
-
画面全体に比例させた拡大縮小を行う用の変数
(画面サイズに対応できるようにするのに役立ちます)
-
ZPositionedが行う拡大縮小のベクトル変換には影響しない
-
-
-
-
ZGroup
- 一つのウィジェット内で複数の形を作るwidgetをグループ化
- ZPositioned変換は引き続き継承
- ZGroupにZGroupを入れることも可能
- プロパティ
-
List<Widget> children
- 形を作るshape widget等
-
SortMode sortMode
- 内部でwidgetのレンダリング順序を制御
-
SortMode.update
はchildrenの変換に応じて、新しい順序を作成
-
-
ZCircle
- 円の形を作るshape widget
-
ZRect
- 長方形の形を作るshape widget
-
ZPositioned
- 移動、回転、拡大縮小を行う変換用widget
- 変換順番は以下
- scale(拡大縮小)
- rotate(回転)
- translate(移動)
-
ZVector
- 移動、回転、拡大縮小などのプロパティ設定に便利なクラス
- 利用方法は以下
-
ZVector(a, b, c)
:x=a、y=b、z=cが設定される -
ZVector.only(x: a)
:yとzは0に設定される -
ZVector.all(a)
:x、y、zはaが設定される
-
-
-
階層構造の例
- 例①
- ZIllustration > ZCircle
- 例②
- ZIllustration > ZPositioned > ZGroup > ZPositioned > ZCircle
- 例③(今回のサンプルで作成するサイコロ)※ZCircleの数が目の数
- ZIllustration > ZPositioned > ZGroup > ZPositioned > ZGroup >
- ZGroup > 【サイコロ本体】
- ZPositioned > ZRect
- ZPositioned > ZRect
- ZPositioned > ZRect
- ZPositioned > ZRect
- ZPositioned > ZCircle 【一の目】
- ZPositioned > ZGroup > 【二の目】
- ZPositioned > ZCircle
- ZPositioned > ZCircle
- ZPositioned > ZGroup > 【三の目】
- ZCircle
- ZPositioned > ZCircle
- ZPositioned > ZCircle
- ZPositioned > ZGroup > 【四の目】
- ZPositioned > ZGroup >
- ZPositioned > ZCircle
- ZPositioned > ZCircle
- ZPositioned > ZGroup >
- ZPositioned > ZCircle
- ZPositioned > ZCircle
- ZPositioned > ZGroup >
- ZPositioned > ZGroup > 【五の目】
- ZCircle
- ZPositioned > ZGroup >
- ZPositioned > ZGroup
- ZPositioned > ZCircle
- ZPositioned > ZCircle
- ZPositioned > ZGroup
- ZPositioned > ZCircle
- ZPositioned > ZCircle
- ZPositioned > ZGroup
- ZPositioned > ZGroup > 【六の目】
- ZPositioned > ZGroup >
- ZPositioned > ZCircle
- ZPositioned > ZCircle
- ZPositioned > ZGroup >
- ZPositioned > ZGroup
- ZPositioned > ZCircle
- ZPositioned > ZCircle
- ZPositioned > ZGroup
- ZPositioned > ZCircle
- ZPositioned > ZCircle
- ZPositioned > ZGroup
- ZPositioned > ZGroup >
- ZGroup > 【サイコロ本体】
- ZIllustration > ZPositioned > ZGroup > ZPositioned > ZGroup >
- 例①
-
定数(const値)
- tau
- Type:double
- package:
zflutter/src/core/core.dart
- 1ラウンド(=2 * pi)を表す
- 例:4分の1回転させたい場合は、tau / 8と利用
- tau
コード実行方法
- github
- 実行方法(想定環境:Android Studio等の統合環境)
-
pubspec.yaml
にzflutterを追加 - 最下部記載のコードを実行(※1ファイルで実行可能とするため微修正:クラス名変更、使わていないコード削除)
-
コードを上から順に確認
ZFlutterを中心に抜粋し、解説
import文
import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';
import 'package:zflutter/zflutter.dart';
main
void main() => runApp(Dices());
Dices
クラス
class Dices extends StatefulWidget {
_DicesState createState() => _DicesState();
}
_DicesState
クラス
class _DicesState extends State<Dices> with SingleTickerProviderStateMixin {
変数定義
- 変数説明
- animationController:AnimationControllerでサイコロを振るアニメーション管理
- simulation:サイコロを落とす具合調整
- num:サイコロの目(初期値1)
- num2:サイコロの目(初期値2)
- zRotation:Z軸方向の回転にランダム性を持たせる用
AnimationController animationController;
SpringSimulation simulation;
int num = 1;
int num2 = 1;
double zRotation = 0;
initState関数
void initState() {
super.initState();
simulation = SpringSimulation(
SpringDescription(
mass: 1,
stiffness: 20,
damping: 2,
),
1, // starting point
0, // ending point
1, // velocity
);
animationController =
AnimationController(vsync: this, duration: Duration(milliseconds: 2000))
..addListener(() {
setState(() {});
});
}
random関数
- Z軸方向の回転具合と、サイコロの目をランダムに決める
void random() {
zRotation = Random().nextDouble() * tau;
num = Random().nextInt(5) + 1;
num2 = 6 - Random().nextInt(5);
}
build関数
Widget build(BuildContext context) {
CurvedAnimation
final curvedValue = CurvedAnimation(
curve: Curves.ease,
parent: animationController,
);
final firstHalf = CurvedAnimation(
curve: Interval(0, 1),
parent: animationController,
);
final secondHalf = CurvedAnimation(
curve: Interval(0, 0.3),
parent: animationController,
);
zoom
- zoom値の定義
- 例えば末尾の0.5を0.3とすると、画面の小さいスマホでもすべて収まるようになる
-
Dice
クラスの引数(ZIllustration
のプロパティ)として渡すことで、サイコロ表面の近さを表現
final zoom = (simulation.x(animationController.value)).abs() / 2 + 0.5;
GestureDetector
-
onTap
- アニメーションが動いている状態の場合、初期状態にリセット
- アニメーションが止まっている状態の場合、アニメーションスタート+
random
関数でサイコロの目を決める
-
サイコロの3DトップレベルクラスとしてZIllustrationをContainer内に記載
- サイコロ①(画面右側)
- 色の引数を渡し、緑色とする
- サイコロ②(画面左側)
- 色の引数を渡さず、デフォルトの赤色とする
- サイコロ①(画面右側)
return GestureDetector(
onTap: () {
if (animationController.isAnimating)
animationController.reset();
else {
animationController.forward(from: 0);
random();
}
},
child: Container(
color: Colors.transparent,
child: ZIllustration(
zoom: 1.5,
children: [
ZPositioned(
translate: ZVector.only(x: 100 * zoom),
child: ZGroup(
children: [
ZPositioned(
scale: ZVector.all(zoom),
rotate:
getRotation(num2).multiplyScalar(curvedValue.value) -
ZVector.all((tau / 2) * (firstHalf.value)) -
ZVector.all((tau / 2) * (secondHalf.value)),
child: ZPositioned(
rotate: ZVector.only(
z: -zRotation * 1.9 * (animationController.value)),
child: Dice(
zoom: zoom,
color: Colors.green,
)),
),
],
),
),
ZPositioned(
translate: ZVector.only(x: -100 * zoom),
child: ZGroup(
children: [
ZPositioned(
scale: ZVector.all(zoom),
rotate: getRotation(num).multiplyScalar(curvedValue.value) -
ZVector.all((tau / 2) * (firstHalf.value)) -
ZVector.all((tau / 2) * (secondHalf.value)),
child: ZPositioned(
rotate: ZVector.only(
z: -zRotation * 2.1 * (animationController.value)),
child: Dice(zoom: zoom)),
),
],
),
),
],
),
),
);
- build関数:【終端】
}
dispose関数
void dispose() {
animationController.dispose();
super.dispose();
}
-
_DicesState
クラス:【終端】
}
getRotation関数
- 最終的に出る目の値を決める
- 目の数字に従って、その目を出すためのxまたはy軸方向に回転させるZVector値を返す
ZVector getRotation(int num) {
switch (num) {
case 1:
return ZVector.zero;
case 2:
return ZVector.only(x: tau / 4);
case 3:
return ZVector.only(y: tau / 4);
case 4:
return ZVector.only(y: 3 * tau / 4);
case 5:
return ZVector.only(x: 3 * tau / 4);
case 6:
return ZVector.only(y: tau / 2);
}
throw ('num $num is not in the dice');
}
Face
クラス
- サイコロ本体を作るための部品shape widget(ZRect)
class Face extends StatelessWidget {
final double zoom;
final Color color;
const Face({Key key, this.zoom = 1, this.color}) : super(key: key);
Widget build(BuildContext context) {
return ZRect(
stroke: 50 * zoom,
width: 50,
height: 50,
color: color,
);
}
}
Dot
クラス
- サイコロの一の目をつくるためのshape widget(ZCircle)
- diameter:幅15pxの円
- stroke:線の太さ・丸み
- color:色
class Dot extends StatelessWidget {
Widget build(BuildContext context) {
return ZCircle(
diameter: 15,
stroke: 0,
fill: true,
color: Colors.white,
);
}
}
GroupTwo
クラス
- サイコロの二の目をつくるためのwidget
- ZPositionedでtranslateのみを利用することで、2つのDotを移動させ二の目を作成
- ここのtranslateをいじると目の見え方がズレる
- ZPositionedでtranslateのみを利用することで、2つのDotを移動させ二の目を作成
class GroupTwo extends StatelessWidget {
Widget build(BuildContext context) {
return ZGroup(
sortMode: SortMode.update,
children: [
ZPositioned(translate: ZVector.only(y: -20), child: Dot()),
ZPositioned(translate: ZVector.only(y: 20), child: Dot()),
],
);
}
}
GroupFour
クラス
- サイコロの四の目をつくるためのwidget
class GroupFour extends StatelessWidget {
Widget build(BuildContext context) {
return ZGroup(
sortMode: SortMode.update,
children: [
ZPositioned(translate: ZVector.only(x: 20, y: 0), child: GroupTwo()),
ZPositioned(translate: ZVector.only(x: -20, y: 0), child: GroupTwo()),
],
);
}
}
Dice
クラス
class Dice extends StatelessWidget {
final Color color;
final double zoom;
const Dice({Key key, this.zoom = 1, this.color = const Color(0xffF23726)})
: super(key: key);
build関数
Widget build(BuildContext context) {
ZGroup
- コード上から順に6,1,2,5の面側に4つの(丸みを帯びた)長方形を作ることでサイコロの本体を作る
- サイコロの形を作ったあとに表面に目を張り付ける
return ZGroup(
children: [
ZGroup(
sortMode: SortMode.update,
children: [
ZPositioned(
translate: ZVector.only(z: -25),
child: Face(zoom: zoom, color: color)),
ZPositioned(
translate: ZVector.only(z: 25),
child: Face(zoom: zoom, color: color)),
ZPositioned(
translate: ZVector.only(y: 25),
rotate: ZVector.only(x: tau / 4),
child: Face(
zoom: zoom,
color: color,
)),
ZPositioned(
translate: ZVector.only(y: -25),
rotate: ZVector.only(x: tau / 4),
child: Face(zoom: zoom, color: color)),
],
),
//one
ZPositioned(translate: ZVector.only(z: 50), child: Dot()),
//two
ZPositioned(
rotate: ZVector.only(x: tau / 4),
translate: ZVector.only(y: 50),
child: ZGroup(
sortMode: SortMode.update,
children: [
ZPositioned(translate: ZVector.only(y: -20), child: Dot()),
ZPositioned(translate: ZVector.only(y: 20), child: Dot()),
],
),
),
//three
ZPositioned(
rotate: ZVector.only(y: tau / 4),
translate: ZVector.only(x: 50),
child: ZGroup(
sortMode: SortMode.update,
children: [
Dot(),
ZPositioned(translate: ZVector.only(x: 20, y: -20), child: Dot()),
ZPositioned(translate: ZVector.only(x: -20, y: 20), child: Dot()),
],
),
),
//four
ZPositioned(
rotate: ZVector.only(y: tau / 4),
translate: ZVector.only(x: -50),
child: ZGroup(
sortMode: SortMode.update,
children: [
ZPositioned(
translate: ZVector.only(x: 20, y: 0), child: GroupTwo()),
ZPositioned(
translate: ZVector.only(x: -20, y: 0), child: GroupTwo()),
],
),
),
//five
ZPositioned(
rotate: ZVector.only(x: tau / 4),
translate: ZVector.only(y: -50),
child: ZGroup(
sortMode: SortMode.update,
children: [
Dot(),
ZPositioned(child: GroupFour()),
],
),
),
//six
ZPositioned(
translate: ZVector.only(z: -50),
child: ZGroup(
sortMode: SortMode.update,
children: [
ZPositioned(rotate: ZVector.only(z: tau / 4), child: GroupTwo()),
ZPositioned(child: GroupFour()),
],
),
),
],
);
}
}
コード全行
import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';
import 'package:zflutter/zflutter.dart';
void main() => runApp(Dices());
class Dices extends StatefulWidget {
_DicesState createState() => _DicesState();
}
class _DicesState extends State<Dices> with SingleTickerProviderStateMixin {
AnimationController animationController;
SpringSimulation simulation;
int num = 1;
int num2 = 1;
double zRotation = 0;
void initState() {
super.initState();
simulation = SpringSimulation(
SpringDescription(
mass: 1,
stiffness: 20,
damping: 2,
),
1, // starting point
0, // ending point
1, // velocity
);
animationController =
AnimationController(vsync: this, duration: Duration(milliseconds: 2000))
..addListener(() {
setState(() {});
});
}
void random() {
zRotation = Random().nextDouble() * tau;
num = Random().nextInt(5) + 1;
num2 = 6 - Random().nextInt(5);
}
Widget build(BuildContext context) {
final curvedValue = CurvedAnimation(
curve: Curves.ease,
parent: animationController,
);
final firstHalf = CurvedAnimation(
curve: Interval(0, 1),
parent: animationController,
);
final secondHalf = CurvedAnimation(
curve: Interval(0, 0.3),
parent: animationController,
);
final zoom = (simulation.x(animationController.value)).abs() / 2 + 0.5;
return GestureDetector(
onTap: () {
if (animationController.isAnimating)
animationController.reset();
else {
animationController.forward(from: 0);
random();
}
},
child: Container(
color: Colors.transparent,
child: ZIllustration(
zoom: 1.5,
children: [
ZPositioned(
translate: ZVector.only(x: 100 * zoom),
child: ZGroup(
children: [
ZPositioned(
scale: ZVector.all(zoom),
rotate:
getRotation(num2).multiplyScalar(curvedValue.value) -
ZVector.all((tau / 2) * (firstHalf.value)) -
ZVector.all((tau / 2) * (secondHalf.value)),
child: ZPositioned(
rotate: ZVector.only(
z: -zRotation * 1.9 * (animationController.value)),
child: Dice(
zoom: zoom,
color: Colors.green,
)),
),
],
),
),
ZPositioned(
translate: ZVector.only(x: -100 * zoom),
child: ZGroup(
children: [
ZPositioned(
scale: ZVector.all(zoom),
rotate: getRotation(num).multiplyScalar(curvedValue.value) -
ZVector.all((tau / 2) * (firstHalf.value)) -
ZVector.all((tau / 2) * (secondHalf.value)),
child: ZPositioned(
rotate: ZVector.only(
z: -zRotation * 2.1 * (animationController.value)),
child: Dice(zoom: zoom)),
),
],
),
),
],
),
),
);
}
void dispose() {
animationController.dispose();
super.dispose();
}
}
ZVector getRotation(int num) {
switch (num) {
case 1:
return ZVector.zero;
case 2:
return ZVector.only(x: tau / 4);
case 3:
return ZVector.only(y: tau / 4);
case 4:
return ZVector.only(y: 3 * tau / 4);
case 5:
return ZVector.only(x: 3 * tau / 4);
case 6:
return ZVector.only(y: tau / 2);
}
throw ('num $num is not in the dice');
}
class Face extends StatelessWidget {
final double zoom;
final Color color;
const Face({Key key, this.zoom = 1, this.color}) : super(key: key);
Widget build(BuildContext context) {
return ZRect(
stroke: 50 * zoom,
width: 50,
height: 50,
color: color,
);
}
}
class Dot extends StatelessWidget {
Widget build(BuildContext context) {
return ZCircle(
diameter: 15,
stroke: 0,
fill: true,
color: Colors.white,
);
}
}
class GroupTwo extends StatelessWidget {
Widget build(BuildContext context) {
return ZGroup(
sortMode: SortMode.update,
children: [
ZPositioned(translate: ZVector.only(y: -20), child: Dot()),
ZPositioned(translate: ZVector.only(y: 20), child: Dot()),
],
);
}
}
class GroupFour extends StatelessWidget {
Widget build(BuildContext context) {
return ZGroup(
sortMode: SortMode.update,
children: [
ZPositioned(translate: ZVector.only(x: 20, y: 0), child: GroupTwo()),
ZPositioned(translate: ZVector.only(x: -20, y: 0), child: GroupTwo()),
],
);
}
}
class Dice extends StatelessWidget {
final Color color;
final double zoom;
const Dice({Key key, this.zoom = 1, this.color = const Color(0xffF23726)})
: super(key: key);
Widget build(BuildContext context) {
return ZGroup(
children: [
ZGroup(
sortMode: SortMode.update,
children: [
ZPositioned(
translate: ZVector.only(z: -25),
child: Face(zoom: zoom, color: color)),
ZPositioned(
translate: ZVector.only(z: 25),
child: Face(zoom: zoom, color: color)),
ZPositioned(
translate: ZVector.only(y: 25),
rotate: ZVector.only(x: tau / 4),
child: Face(
zoom: zoom,
color: color,
)),
ZPositioned(
translate: ZVector.only(y: -25),
rotate: ZVector.only(x: tau / 4),
child: Face(zoom: zoom, color: color)),
],
),
//one
ZPositioned(translate: ZVector.only(z: 50), child: Dot()),
//two
ZPositioned(
rotate: ZVector.only(x: tau / 4),
translate: ZVector.only(y: 50),
child: ZGroup(
sortMode: SortMode.update,
children: [
ZPositioned(translate: ZVector.only(y: -20), child: Dot()),
ZPositioned(translate: ZVector.only(y: 20), child: Dot()),
],
),
),
//three
ZPositioned(
rotate: ZVector.only(y: tau / 4),
translate: ZVector.only(x: 50),
child: ZGroup(
sortMode: SortMode.update,
children: [
Dot(),
ZPositioned(translate: ZVector.only(x: 20, y: -20), child: Dot()),
ZPositioned(translate: ZVector.only(x: -20, y: 20), child: Dot()),
],
),
),
//four
ZPositioned(
rotate: ZVector.only(y: tau / 4),
translate: ZVector.only(x: -50),
child: ZGroup(
sortMode: SortMode.update,
children: [
ZPositioned(
translate: ZVector.only(x: 20, y: 0), child: GroupTwo()),
ZPositioned(
translate: ZVector.only(x: -20, y: 0), child: GroupTwo()),
],
),
),
//five
ZPositioned(
rotate: ZVector.only(x: tau / 4),
translate: ZVector.only(y: -50),
child: ZGroup(
sortMode: SortMode.update,
children: [
Dot(),
ZPositioned(child: GroupFour()),
],
),
),
//six
ZPositioned(
translate: ZVector.only(z: -50),
child: ZGroup(
sortMode: SortMode.update,
children: [
ZPositioned(rotate: ZVector.only(z: tau / 4), child: GroupTwo()),
ZPositioned(child: GroupFour()),
],
),
),
],
);
}
}
Discussion