[flutter] Clean-up asset bundle constructors, add Flame integration examples.

This commit is contained in:
Mario Zechner 2022-11-22 09:05:43 +01:00
parent 6efb197776
commit 1c0a860139
13 changed files with 243 additions and 43 deletions

View File

@ -1,5 +1,10 @@
# 0.0.4
* Clean-up `fromAsset()` factory methods so the atlas comes before skeleton data file name.
* Rename `Vector2` to `Vec2`.
* Make the bundle configurable in `SpineWidget.asset()`.
# 0.0.3
* Lower macOS deployment target to 10.11
* Lower macOS deployment target to 10.11.
# 0.0.2
* Fix package name in build system `spine_flutter` > `esotericsoftware_spine_flutter`.

View File

@ -36,7 +36,7 @@ class AnimationStateEvents extends StatelessWidget {
children: [
const Text("See output in console!"),
Expanded(
child: SpineWidget.asset("assets/spineboy-pro.skel", "assets/spineboy.atlas", controller)
child: SpineWidget.asset("assets/spineboy.atlas", "assets/spineboy-pro.skel", controller)
)
]
)

View File

@ -4,6 +4,7 @@ import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/painting.dart' as painting;
import 'package:esotericsoftware_spine_flutter/spine_flutter.dart';
import 'package:flutter/services.dart';
import 'package:raw_image_provider/raw_image_provider.dart';
class DressUp extends StatefulWidget {
@ -25,7 +26,7 @@ class DressUpState extends State<DressUp> {
void initState() {
reportLeaks();
super.initState();
SkeletonDrawable.fromAsset("assets/mix-and-match-pro.skel", "assets/mix-and-match.atlas").then((drawable) async {
SkeletonDrawable.fromAsset("assets/mix-and-match.atlas", "assets/mix-and-match-pro.skel").then((drawable) async {
for (var skin in drawable.skeletonData.getSkins()) {
if (skin.getName() == "default") continue;

View File

@ -0,0 +1,145 @@
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:esotericsoftware_spine_flutter/spine_flutter.dart';
class SpineComponent extends PositionComponent {
final BoundsProvider _boundsProvider;
final SkeletonDrawable _drawable;
late final Bounds _bounds;
final bool _ownsDrawable;
SpineComponent(this._drawable, {
bool ownsDrawable = false,
BoundsProvider boundsProvider = const SetupPoseBounds(),
super.position,
super.scale,
double super.angle = 0.0,
Anchor super.anchor = Anchor.topLeft,
super.children,
super.priority,
}) :
_ownsDrawable = ownsDrawable,
_boundsProvider = boundsProvider {
_drawable.update(0);
_bounds = _boundsProvider.computeBounds(_drawable);
size = Vector2(_bounds.width, _bounds.height);
}
static Future<SpineComponent> fromAssets(String atlasFile, String skeletonFile, {
AssetBundle? bundle, BoundsProvider boundsProvider = const SetupPoseBounds(),
Vector2? position,
Vector2? scale,
double angle = 0.0,
Anchor anchor = Anchor.topLeft,
Iterable<Component>? children,
int? priority,
}) async {
final drawable = await SkeletonDrawable.fromAsset(atlasFile, skeletonFile, bundle: bundle);
return SpineComponent(
drawable,
ownsDrawable: true,
boundsProvider: boundsProvider,
position: position,
scale: scale,
angle: angle,
anchor: anchor,
children: children,
priority: priority);
}
@override
void onRemove() {
if (_ownsDrawable) _drawable.dispose();
}
@override
void update(double dt) {
_drawable.update(dt);
}
@override
void render(Canvas canvas) {
canvas.save();
canvas.translate(-_bounds.x, -_bounds.y);
_drawable.renderToCanvas(canvas);
canvas.restore();
}
get animationState => _drawable.animationState;
get animationStateData => _drawable.animationStateData;
get skeleton => _drawable.skeleton;
}
class SimpleFlameExample extends FlameGame {
late final SpineComponent spineboy;
@override
Future<void> onLoad() async {
// Load the Spineboy atlas and skeleton data from asset files
// and create a SpineComponent from them, scaled down and
// centered on the screen
spineboy = await SpineComponent.fromAssets(
"assets/spineboy.atlas", "assets/spineboy-pro.skel",
scale: Vector2(0.4, 0.4),
anchor: Anchor.center,
position: Vector2(size.x / 2, size.y / 2)
);
// Set the "walk" animation on track 0 in looping mode
spineboy.animationState.setAnimationByName(0, "walk", true);
await add(spineboy);
}
}
class PreloadAndShareSpineDataExample extends FlameGame {
late final SkeletonData cachedSkeletonData;
late final Atlas cachedAtlas;
late final List<SpineComponent> spineboys;
@override
Future<void> onLoad() async {
// Pre-load the atlas and skeleton data once.
cachedAtlas = await Atlas.fromAsset("assets/spineboy.atlas");
cachedSkeletonData = await SkeletonData.fromAsset(cachedAtlas, "assets/spineboy-pro.skel");
// Instantiate many spineboys from the pre-loaded data. Each SpineComponent
// gets their own SkeletonDrawable copy derived from the cached data. The
// SkeletonDrawable copies do not own the underlying skeleton data and atlas.
final rng = Random();
for (int i = 0; i < 100; i++) {
final drawable = SkeletonDrawable(cachedAtlas, cachedSkeletonData, false);
final scale = 0.1 + rng.nextDouble() * 0.2;
final position = Vector2(rng.nextDouble() * size.x, rng.nextDouble() * size.y);
final spineboy = SpineComponent(
drawable,
scale: Vector2(scale, scale),
position: position
);
spineboy.animationState.setAnimationByName(0, "walk", true);
await add(spineboy);
}
}
@override
void onRemove() {
// Dispose the pre-loaded atlas and skeleton data when the game/scene is removed
cachedAtlas.dispose();
cachedSkeletonData.dispose();
}
}
class SpineFlameGameWidget extends StatelessWidget {
final FlameGame game;
const SpineFlameGameWidget(this.game, {super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Flame Integration')),
body: GameWidget(game: game)
);
}
}

View File

@ -25,7 +25,7 @@ class IkFollowingState extends State<IkFollowing> {
if (worldPosition == null) return;
var bone = controller.skeleton.findBone("crosshair");
if (bone == null) return;
var position = bone.getParent()?.worldToLocal(worldPosition.dx, worldPosition.dy) ?? Vector2(0, 0);
var position = bone.getParent()?.worldToLocal(worldPosition.dx, worldPosition.dy) ?? Vec2(0, 0);
bone.setX(position.x);
bone.setY(position.y);
});
@ -44,7 +44,7 @@ class IkFollowingState extends State<IkFollowing> {
body: GestureDetector(
onPanDown: (drag) => _updateBonePosition(drag.localPosition),
onPanUpdate: (drag) => _updateBonePosition(drag.localPosition),
child: SpineWidget.asset("assets/spineboy-pro.skel", "assets/spineboy.atlas", controller),
child: SpineWidget.asset("assets/spineboy.atlas", "assets/spineboy-pro.skel", controller),
));
}
}

View File

@ -1,12 +1,13 @@
import 'package:flutter/material.dart';
import 'package:esotericsoftware_spine_flutter/spine_flutter.dart';
import 'flame_example.dart';
import 'simple_animation.dart';
import 'animation_state_events.dart';
import 'pause_play_animation.dart';
import 'skins.dart';
import 'dress_up.dart';
import 'ik_following.dart';
import 'package:esotericsoftware_spine_flutter/spine_flutter.dart';
class ExampleSelector extends StatelessWidget {
const ExampleSelector({super.key});
@ -91,6 +92,30 @@ class ExampleSelector extends StatelessWidget {
);
},
),
spacer,
ElevatedButton(
child: const Text('Flame: Simple Example'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute<void>(
builder: (context) => SpineFlameGameWidget(SimpleFlameExample()),
),
);
},
),
spacer,
ElevatedButton(
child: const Text('Flame: Pre-load and share Spine data'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute<void>(
builder: (context) => SpineFlameGameWidget(PreloadAndShareSpineDataExample()),
),
);
},
),
spacer
]
)

View File

@ -33,7 +33,7 @@ class PlayPauseAnimationState extends State<PlayPauseAnimation> {
return Scaffold(
appBar: AppBar(title: const Text('Play/Pause')),
body: SpineWidget.asset("assets/spineboy-pro.skel", "assets/spineboy.atlas", controller),
body: SpineWidget.asset("assets/spineboy.atlas", "assets/spineboy-pro.skel", controller),
floatingActionButton: FloatingActionButton(
onPressed: _togglePlay,
child: Icon(isPlaying ? Icons.pause : Icons.play_arrow),

View File

@ -14,10 +14,9 @@ class SimpleAnimation extends StatelessWidget {
return Scaffold(
appBar: AppBar(title: const Text('Simple Animation')),
body: SpineWidget.asset("assets/spineboy-pro.skel", "assets/spineboy.atlas", controller),
// body: SpineWidget.file("/Users/badlogic/workspaces/spine-runtimes/examples/spineboy/export/spineboy-pro.skel", "/Users/badlogic/workspaces/spine-runtimes/examples/spineboy/export/spineboy.atlas", controller),
// body: const SpineWidget.http("https://marioslab.io/dump/spineboy/spineboy-pro.json", "https://marioslab.io/dump/spineboy/spineboy.atlas"),
// body: SpineWidget.asset("assets/skeleton.json", "assets/skeleton.atlas", controller, alignment: Alignment.topLeft, fit: BoxFit.cover),
body: SpineWidget.asset("assets/spineboy.atlas", "assets/spineboy-pro.skel", controller),
// body: SpineWidget.file( "/Users/badlogic/workspaces/spine-runtimes/examples/spineboy/export/spineboy.atlas", "/Users/badlogic/workspaces/spine-runtimes/examples/spineboy/export/spineboy-pro.skel", controller),
// body: const SpineWidget.http("https://marioslab.io/dump/spineboy/spineboy.atlas", "https://marioslab.io/dump/spineboy/spineboy-pro.json"),
);
}
}

View File

@ -17,7 +17,7 @@ class SkinsState extends State<Skins> {
@override
void initState() {
super.initState();
SkeletonDrawable.fromAsset("assets/mix-and-match-pro.skel", "assets/mix-and-match.atlas").then((drawable) {
SkeletonDrawable.fromAsset("assets/mix-and-match.atlas", "assets/mix-and-match-pro.skel").then((drawable) {
for (var skin in drawable.skeletonData.getSkins()) {
_selectedSkins[skin.getName()] = false;
}

View File

@ -42,7 +42,7 @@ packages:
path: ".."
relative: true
source: path
version: "0.0.2"
version: "0.0.3"
ffi:
dependency: transitive
description:
@ -50,6 +50,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
flame:
dependency: "direct main"
description:
name: flame
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.0"
flutter:
dependency: "direct main"
description: flutter
@ -111,6 +118,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.0"
ordered_set:
dependency: transitive
description:
name: ordered_set
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.0"
path:
dependency: transitive
description:
@ -174,4 +188,4 @@ packages:
version: "2.1.2"
sdks:
dart: ">=2.17.6 <3.0.0"
flutter: ">=2.11.0"
flutter: ">=3.3.0"

View File

@ -14,6 +14,7 @@ dependencies:
path: ../
cupertino_icons: ^1.0.2
raw_image_provider: ^0.2.0
flame: ^1.4.0
dev_dependencies:
flutter_lints: ^2.0.0

View File

@ -50,11 +50,11 @@ class Bounds {
Bounds(this.x, this.y, this.width, this.height);
}
class Vector2 {
class Vec2 {
double x;
double y;
Vector2(this.x, this.y);
Vec2(this.x, this.y);
}
class Atlas {
@ -99,8 +99,9 @@ class Atlas {
return Atlas._(atlas, atlasPages, atlasPagePaints);
}
static Future<Atlas> fromAsset(AssetBundle assetBundle, String atlasFileName) async {
return _load(atlasFileName, (file) async => (await assetBundle.load(file)).buffer.asUint8List());
static Future<Atlas> fromAsset(String atlasFileName, {AssetBundle? bundle}) async {
bundle ??= rootBundle;
return _load(atlasFileName, (file) async => (await bundle!.load(file)).buffer.asUint8List());
}
static Future<Atlas> fromFile(String atlasFileName) async {
@ -160,11 +161,12 @@ class SkeletonData {
return data;
}
static Future<SkeletonData> fromAsset(AssetBundle assetBundle, Atlas atlas, String skeletonFile) async {
static Future<SkeletonData> fromAsset(Atlas atlas, String skeletonFile, {AssetBundle? bundle}) async {
bundle ??= rootBundle;
if (skeletonFile.endsWith(".json")) {
return fromJson(atlas, await assetBundle.loadString(skeletonFile));
return fromJson(atlas, await bundle.loadString(skeletonFile));
} else {
return fromBinary(atlas, (await assetBundle.load(skeletonFile)).buffer.asUint8List());
return fromBinary(atlas, (await bundle.load(skeletonFile)).buffer.asUint8List());
}
}
@ -652,16 +654,16 @@ class Bone {
_bindings.spine_bone_set_to_setup_pose(_bone);
}
Vector2 worldToLocal(double worldX, double worldY) {
Vec2 worldToLocal(double worldX, double worldY) {
final local = _bindings.spine_bone_world_to_local(_bone, worldX, worldY);
final result = Vector2(_bindings.spine_vector_get_x(local), _bindings.spine_vector_get_y(local));
final result = Vec2(_bindings.spine_vector_get_x(local), _bindings.spine_vector_get_y(local));
_allocator.free(local);
return result;
}
Vector2 localToWorld(double localX, double localY) {
Vec2 localToWorld(double localX, double localY) {
final world = _bindings.spine_bone_local_to_world(_bone, localX, localY);
final result = Vector2(_bindings.spine_vector_get_x(world), _bindings.spine_vector_get_y(world));
final result = Vec2(_bindings.spine_vector_get_x(world), _bindings.spine_vector_get_y(world));
_allocator.free(world);
return result;
}
@ -1593,9 +1595,9 @@ class PathAttachment extends VertexAttachment<spine_path_attachment> {
class PointAttachment extends Attachment<spine_point_attachment> {
PointAttachment._(spine_point_attachment attachment) : super._(attachment);
Vector2 computeWorldPosition(Bone bone) {
Vec2 computeWorldPosition(Bone bone) {
final position = _bindings.spine_point_attachment_compute_world_position(_attachment, bone._bone);
final result = Vector2(_bindings.spine_vector_get_x(position), _bindings.spine_vector_get_y(position));
final result = Vec2(_bindings.spine_vector_get_x(position), _bindings.spine_vector_get_y(position));
_allocator.free(position);
return result;
}
@ -3256,21 +3258,23 @@ class SkeletonDrawable {
skeleton = Skeleton._(_bindings.spine_skeleton_drawable_get_skeleton(_drawable));
animationStateData = AnimationStateData._(_bindings.spine_skeleton_drawable_get_animation_state_data(_drawable));
animationState = AnimationState._(_bindings.spine_skeleton_drawable_get_animation_state(_drawable), _bindings.spine_skeleton_drawable_get_animation_state_events(_drawable));
skeleton.updateWorldTransform();
}
static Future<SkeletonDrawable> fromAsset(String skeletonFile, String atlasFile) async {
var atlas = await Atlas.fromAsset(rootBundle, atlasFile);
var skeletonData = await SkeletonData.fromAsset(rootBundle, atlas, skeletonFile);
static Future<SkeletonDrawable> fromAsset(String atlasFile, String skeletonFile, {AssetBundle? bundle}) async {
bundle ??= rootBundle;
var atlas = await Atlas.fromAsset(atlasFile, bundle: bundle);
var skeletonData = await SkeletonData.fromAsset(atlas, skeletonFile, bundle: bundle);
return SkeletonDrawable(atlas, skeletonData, true);
}
static Future<SkeletonDrawable> fromFile(String skeletonFile, String atlasFile) async {
static Future<SkeletonDrawable> fromFile(String atlasFile, String skeletonFile) async {
var atlas = await Atlas.fromFile(atlasFile);
var skeletonData = await SkeletonData.fromFile(atlas, skeletonFile);
return SkeletonDrawable(atlas, skeletonData, true);
}
static Future<SkeletonDrawable> fromHttp(String skeletonFile, String atlasFile) async {
static Future<SkeletonDrawable> fromHttp(String atlasFile, String skeletonFile) async {
var atlas = await Atlas.fromUrl(atlasFile);
var skeletonData = await SkeletonData.fromHttp(atlas, skeletonFile);
return SkeletonDrawable(atlas, skeletonData, true);

View File

@ -2,6 +2,7 @@ import 'dart:math';
import 'package:flutter/rendering.dart' as rendering;
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'spine_flutter.dart';
@ -135,6 +136,7 @@ class ComputedBounds extends BoundsProvider {
class SpineWidget extends StatefulWidget {
final AssetType _assetType;
AssetBundle? _bundle;
final String? _skeletonFile;
final String? _atlasFile;
final SkeletonDrawable? _drawable;
@ -144,32 +146,37 @@ class SpineWidget extends StatefulWidget {
final BoundsProvider _boundsProvider;
final bool _sizedByBounds;
const SpineWidget.asset(this._skeletonFile, this._atlasFile, this._controller, {BoxFit? fit, Alignment? alignment, BoundsProvider? boundsProvider, bool? sizedByBounds, super.key})
SpineWidget.asset(this._skeletonFile, this._atlasFile, this._controller, {AssetBundle? bundle, BoxFit? fit, Alignment? alignment, BoundsProvider? boundsProvider, bool? sizedByBounds, super.key})
: _assetType = AssetType.Asset,
_fit = fit ?? BoxFit.contain,
_alignment = alignment ?? Alignment.center,
_boundsProvider = boundsProvider ?? const SetupPoseBounds(),
_sizedByBounds = sizedByBounds ?? false,
_drawable = null;
_drawable = null {
_bundle = bundle ?? rootBundle;
}
const SpineWidget.file(this._skeletonFile, this._atlasFile, this._controller, {BoxFit? fit, Alignment? alignment, BoundsProvider? boundsProvider, bool? sizedByBounds, super.key})
SpineWidget.file(this._skeletonFile, this._atlasFile, this._controller, {BoxFit? fit, Alignment? alignment, BoundsProvider? boundsProvider, bool? sizedByBounds, super.key})
: _assetType = AssetType.File,
_bundle = null,
_fit = fit ?? BoxFit.contain,
_alignment = alignment ?? Alignment.center,
_boundsProvider = boundsProvider ?? const SetupPoseBounds(),
_sizedByBounds = sizedByBounds ?? false,
_drawable = null;
const SpineWidget.http(this._skeletonFile, this._atlasFile, this._controller, {BoxFit? fit, Alignment? alignment, BoundsProvider? boundsProvider, bool? sizedByBounds, super.key})
SpineWidget.http(this._skeletonFile, this._atlasFile, this._controller, {BoxFit? fit, Alignment? alignment, BoundsProvider? boundsProvider, bool? sizedByBounds, super.key})
: _assetType = AssetType.Http,
_bundle = null,
_fit = fit ?? BoxFit.contain,
_alignment = alignment ?? Alignment.center,
_boundsProvider = boundsProvider ?? const SetupPoseBounds(),
_sizedByBounds = sizedByBounds ?? false,
_drawable = null;
const SpineWidget.drawable(this._drawable, this._controller, {BoxFit? fit, Alignment? alignment, BoundsProvider? boundsProvider, bool? sizedByBounds, super.key})
SpineWidget.drawable(this._drawable, this._controller, {BoxFit? fit, Alignment? alignment, BoundsProvider? boundsProvider, bool? sizedByBounds, super.key})
: _assetType = AssetType.Drawable,
_bundle = null,
_fit = fit ?? BoxFit.contain,
_alignment = alignment ?? Alignment.center,
_boundsProvider = boundsProvider ?? const SetupPoseBounds(),
@ -189,26 +196,25 @@ class _SpineWidgetState extends State<SpineWidget> {
if (widget._assetType == AssetType.Drawable) {
loadDrawable(widget._drawable!);
} else {
loadFromAsset(widget._skeletonFile!, widget._atlasFile!, widget._assetType);
loadFromAsset(widget._bundle, widget._skeletonFile!, widget._atlasFile!, widget._assetType);
}
}
void loadDrawable(SkeletonDrawable drawable) {
widget._controller._initialize(drawable);
drawable.update(0);
setState(() {});
}
void loadFromAsset(String skeletonFile, String atlasFile, AssetType assetType) async {
void loadFromAsset(AssetBundle? bundle, String atlasFile, String skeletonFile, AssetType assetType) async {
switch (assetType) {
case AssetType.Asset:
loadDrawable(await SkeletonDrawable.fromAsset(skeletonFile, atlasFile));
loadDrawable(await SkeletonDrawable.fromAsset(atlasFile, skeletonFile, bundle: bundle));
break;
case AssetType.File:
loadDrawable(await SkeletonDrawable.fromFile(skeletonFile, atlasFile));
loadDrawable(await SkeletonDrawable.fromFile(atlasFile, skeletonFile));
break;
case AssetType.Http:
loadDrawable(await SkeletonDrawable.fromHttp(skeletonFile, atlasFile));
loadDrawable(await SkeletonDrawable.fromHttp(atlasFile, skeletonFile));
break;
case AssetType.Drawable:
throw Exception("Drawable can not be loaded via loadFromAsset().");