3DエンジンZFlutterを公式サイコロアプリから学ぶ

24 min読了の目安(約22200字TECH技術記事

Flutter:ZFlutter

背景

  • その中で、Flutter samples: Diceが面白い
    • サイコロの回転が表現されている
    • これには、ZFlutterというFlutter用の3Dエンジンが利用されている

ZFlutterについて

サンプル「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 > 【五の目】
          • ZCircle
          • ZPositioned > ZGroup >
            • ZPositioned > ZGroup
              • ZPositioned > ZCircle
              • ZPositioned > ZCircle
            • ZPositioned > ZGroup
              • ZPositioned > ZCircle
              • ZPositioned > ZCircle
        • ZPositioned > ZGroup > 【六の目】
          • ZPositioned > ZGroup >
            • ZPositioned > ZCircle
            • ZPositioned > ZCircle
          • ZPositioned > ZGroup >
            • ZPositioned > ZGroup
              • ZPositioned > ZCircle
              • ZPositioned > ZCircle
            • ZPositioned > ZGroup
              • ZPositioned > ZCircle
              • ZPositioned > ZCircle
  • 定数(const値)

    • tau
      • Type:double
      • package:zflutter/src/core/core.dart
      • 1ラウンド(=2 * pi)を表す
        • 例:4分の1回転させたい場合は、tau / 8と利用

コード実行方法

  • 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をいじると目の見え方がズレる
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()),
            ],
          ),
        ),
      ],
    );
  }
}