
こんにちは。
スマホアプリをメインに開発している子育てハードモードのロッキーカナイです。
Flutterでちょっと物理演算を使いたいといった状況が発生しましたので、実装した内容を記事にしてみました。
背景
物理演算って使う状況としては2Dゲームが思い当たると思うのですが、その場合はflameを使う必要があります。
flameはがっつりゲーム作れるものなのでそこまではしたくはないという状況の場合には最適かと思います。比較的簡単に作る事ができましたので紹介します。
プラグインについて
主に使うプラグインはforge2dとspritewidgetです。
forge2d : 物理演算エンジンのbox2DをDartへ移植したもので、flameというゲームエンジンのチームがメンテナンスしている物理演算ライブラリです。
spritewidget : Flutterを使って複雑で高性能なアニメーションや2Dゲームを構築するためのツールキットです。
※Flutterのプラグインでbox2dが存在し、動作する事も確認しましたが(box2d0.4.0/FutterSDK2.0.3現在)、非推奨ですのでforge2dを使用します。
やること
いくつかの泡を画面中央付近まで移動させる。泡は交わらずお互い反発させたいので物理演算を使用するという感じ。
出来上がったものがこの画面頭あたりのgif画像になります。実際にはもっとスムーズに動くので、mp4も用意しました。
ちなみに、SpotifyAPIを使ってプレイリストのトラックを取ってきてアルバム画像を泡にしてます。
コード
コードは以下の通りです。
FutterSDK2.0.3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
... dependencies: flutter: sdk: flutter spritewidget: forge2d: ^0.8.1 # SpotifyAPIから画像取得、サイズ変更等で必要なので、物理演算のみであれば不要 flutter_shapes: extended_math: ^0.0.29+1 spotify: ^0.5.1 http: any image: ^3.1.0 ... |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 |
// dart import 'dart:async'; import 'dart:math'; import 'dart:ui' as ui; // flutter import 'package:flutter/material.dart'; // 3rd party plugin import 'package:http/http.dart' as http; import 'package:image/image.dart' as image; import 'package:extended_math/extended_math.dart'; import 'package:forge2d/forge2d.dart'; import 'package:spritewidget/spritewidget.dart'; import 'package:spotify/spotify.dart'; void main() => runApp(MaterialApp(home: const _App())); class _App extends StatelessWidget { const _App(); @override Widget build(BuildContext context) { final size = MediaQuery.of(context).size; return Scaffold( body: SafeArea(child: SpriteWidget(Scene(size))), ); } } class Scene extends NodeWithSize { Scene( Size size, ) : super(size) { /// 初期化 _init(); } /// 2Dのワールドを生成(引数は重力) World _world = World(Vector2.zero()); /// 物体ノード(バブル)の配列 List<_BubbleNode> _nodes = []; /// 物理演算の制御に必要なものとか Vector2 get _center => Vector2(size.width / 2, size.height / 2); final _centerAreaHeight = 50.0; Rect get _centerArea => Rect.fromLTWH( _center.x - _centerAreaHeight, _center.y - _centerAreaHeight, _centerAreaHeight, _centerAreaHeight, ); bool _isContainsCenter = false; bool _isMoveActive = true; bool _isMount = false; /// 初期設定 void _init() async { // SpotifyApiを使ってTOPトラック情報を取得する final spotify = SpotifyApi( SpotifyApiCredentials( "*********", "*********", ), ); final tracks = await spotify.artists.getTopTracks( '5yCWuaBlu42BKsnW89brND', "JP", ); // バブルを生成 _createBubbles(tracks); // マウントフラグ(updateでの物理実行制御) _isMount = true; } @override void update(double dt) { super.update(dt); _world.stepDt(dt); // 物理演算制御 if (_isMount && _isMoveActive) { _nodes.asMap().forEach((int i, _BubbleNode node) { if (!_isContainsCenter) { if (_centerArea .contains(Offset(node.body.position.x, node.body.position.y))) { _isContainsCenter = true; return; } _applyImpulse(node); } else { if (_isMoveActive && node.isUpdated && node.body.position .distanceTo(Vector2(node.before.dx, node.before.dy)) < 0.1) { _isMoveActive = false; _world.clearForces(); } } }); } } /// 物体に衝撃を与える void _applyImpulse(_BubbleNode node) { final a = _center.x + node.body.position.x * -1.0; final b = _center.y + node.body.position.y * -1.0; node.body.applyLinearImpulse(Vector2(a, b) * node.body.mass); } /// バブルをトラック数分生成する void _createBubbles(Iterable<Track> tracks) async { tracks.forEach( (track) async { final radius = 30.0 + (20.0 * _getRandPercent()); final image = await _loadImageURL(track.album.images.first.url, radius.toInt() * 2, radius.toInt() * 2); _nodes.add( _createBubble( Offset(size.width * _getRandPercent(), size.height + 50.0), radius: radius, image: image, ), ); }, ); } /// バブル生成(物理関連のデータをセット) _BubbleNode _createBubble( Offset position, { double radius = 30.0, double friction = 0, // 摩擦係数 double restitution = 0, // 反発係数 double linearDamping = 10.0, // 移動速度の減衰率 ui.Image image, }) { // 物体固有データ final FixtureDef fixtureDef = FixtureDef(CircleShape()..radius = radius); fixtureDef.friction = friction; fixtureDef.restitution = restitution; fixtureDef.density = 0; // 密度 // 物体データ final BodyDef bodyDef = BodyDef(); bodyDef.position = Vector2(position.dx, position.dy); bodyDef.type = BodyType.dynamic; bodyDef.linearDamping = linearDamping; // ワールドへ追加 final Body body = _world.createBody(bodyDef); body.createFixture(fixtureDef); // アルバムの画像の物体を生成 final _BubbleNode node = _BubbleNode( body, image, radius: radius, )..position = position; addChild(node); return node; } /// ネットワークURLからui.Image生成 Future<ui.Image> _loadImageURL(String imageUrl, int height, int width) async { final http.Response response = await http.get(imageUrl); final image.Image baseSizeImage = image.decodeImage(response.bodyBytes.buffer.asUint8List()); final image.Image resizeImage = image.copyResize(baseSizeImage, height: height, width: width); final ui.Codec codec = await ui.instantiateImageCodec(image.encodePng(resizeImage)); final ui.FrameInfo frameInfo = await codec.getNextFrame(); return frameInfo.image; } /// ランダム値生成 double _getRandPercent({int max = 100}) { return Random().nextInt(max + 1) * 0.01; } } /// バブルノード class _BubbleNode extends Node { _BubbleNode( this.body, this.image, { this.radius = 30, }); final Body body; final double radius; final ui.Image image; Offset before; bool get isUpdated => before != null; @override void update(double dt) { super.update(dt); before = position; position = Offset(body.position.x, body.position.y); } @override void paint(Canvas canvas) { final Paint paintBorder = Paint()..color = Colors.white; canvas.drawCircle(Offset.zero, radius, paintBorder); final Path path = Path() ..addOval(Rect.fromLTWH(-radius, -radius, radius * 2, radius * 2)); canvas.clipPath(path); canvas.drawImage(image, Offset(-radius, -radius), paintBorder); } } |
物理の動き制御の部分は結構適当なので、物理演算をFlutterで使う場合の大枠として参考にしていただければと思います。
さいごに
今回登場した私のおすすめプレイリスト。
