diff --git a/spine-flutter/CHANGELOG.md b/spine-flutter/CHANGELOG.md index d10036dd9..f9e792c37 100644 --- a/spine-flutter/CHANGELOG.md +++ b/spine-flutter/CHANGELOG.md @@ -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`. diff --git a/spine-flutter/example/lib/animation_state_events.dart b/spine-flutter/example/lib/animation_state_events.dart index cfc56bdb8..9e0e435be 100644 --- a/spine-flutter/example/lib/animation_state_events.dart +++ b/spine-flutter/example/lib/animation_state_events.dart @@ -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) ) ] ) diff --git a/spine-flutter/example/lib/dress_up.dart b/spine-flutter/example/lib/dress_up.dart index c44235ea7..e225d084d 100644 --- a/spine-flutter/example/lib/dress_up.dart +++ b/spine-flutter/example/lib/dress_up.dart @@ -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 { 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; diff --git a/spine-flutter/example/lib/flame_example.dart b/spine-flutter/example/lib/flame_example.dart new file mode 100644 index 000000000..7a2aafca6 --- /dev/null +++ b/spine-flutter/example/lib/flame_example.dart @@ -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 fromAssets(String atlasFile, String skeletonFile, { + AssetBundle? bundle, BoundsProvider boundsProvider = const SetupPoseBounds(), + Vector2? position, + Vector2? scale, + double angle = 0.0, + Anchor anchor = Anchor.topLeft, + Iterable? 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 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 spineboys; + + @override + Future 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) + ); + } +} \ No newline at end of file diff --git a/spine-flutter/example/lib/ik_following.dart b/spine-flutter/example/lib/ik_following.dart index 1f948a9bb..52e5d7f7d 100644 --- a/spine-flutter/example/lib/ik_following.dart +++ b/spine-flutter/example/lib/ik_following.dart @@ -25,7 +25,7 @@ class IkFollowingState extends State { 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 { 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), )); } } diff --git a/spine-flutter/example/lib/main.dart b/spine-flutter/example/lib/main.dart index f4e085be9..a10ed227d 100644 --- a/spine-flutter/example/lib/main.dart +++ b/spine-flutter/example/lib/main.dart @@ -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( + builder: (context) => SpineFlameGameWidget(SimpleFlameExample()), + ), + ); + }, + ), + spacer, + ElevatedButton( + child: const Text('Flame: Pre-load and share Spine data'), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SpineFlameGameWidget(PreloadAndShareSpineDataExample()), + ), + ); + }, + ), spacer ] ) diff --git a/spine-flutter/example/lib/pause_play_animation.dart b/spine-flutter/example/lib/pause_play_animation.dart index 8c5df0770..9173a51a7 100644 --- a/spine-flutter/example/lib/pause_play_animation.dart +++ b/spine-flutter/example/lib/pause_play_animation.dart @@ -33,7 +33,7 @@ class PlayPauseAnimationState extends State { 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), diff --git a/spine-flutter/example/lib/simple_animation.dart b/spine-flutter/example/lib/simple_animation.dart index d8c8e4f45..5e14bcb7c 100644 --- a/spine-flutter/example/lib/simple_animation.dart +++ b/spine-flutter/example/lib/simple_animation.dart @@ -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"), ); } } \ No newline at end of file diff --git a/spine-flutter/example/lib/skins.dart b/spine-flutter/example/lib/skins.dart index a9ae55522..d1f5ae576 100644 --- a/spine-flutter/example/lib/skins.dart +++ b/spine-flutter/example/lib/skins.dart @@ -17,7 +17,7 @@ class SkinsState extends State { @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; } diff --git a/spine-flutter/example/pubspec.lock b/spine-flutter/example/pubspec.lock index a24662f3b..55bd56e9d 100644 --- a/spine-flutter/example/pubspec.lock +++ b/spine-flutter/example/pubspec.lock @@ -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" diff --git a/spine-flutter/example/pubspec.yaml b/spine-flutter/example/pubspec.yaml index 8cc37f1c3..eb4be6a6b 100644 --- a/spine-flutter/example/pubspec.yaml +++ b/spine-flutter/example/pubspec.yaml @@ -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 diff --git a/spine-flutter/lib/spine_flutter.dart b/spine-flutter/lib/spine_flutter.dart index 3293b5d46..c09942949 100644 --- a/spine-flutter/lib/spine_flutter.dart +++ b/spine-flutter/lib/spine_flutter.dart @@ -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 fromAsset(AssetBundle assetBundle, String atlasFileName) async { - return _load(atlasFileName, (file) async => (await assetBundle.load(file)).buffer.asUint8List()); + static Future fromAsset(String atlasFileName, {AssetBundle? bundle}) async { + bundle ??= rootBundle; + return _load(atlasFileName, (file) async => (await bundle!.load(file)).buffer.asUint8List()); } static Future fromFile(String atlasFileName) async { @@ -160,11 +161,12 @@ class SkeletonData { return data; } - static Future fromAsset(AssetBundle assetBundle, Atlas atlas, String skeletonFile) async { + static Future 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 { class PointAttachment extends 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 fromAsset(String skeletonFile, String atlasFile) async { - var atlas = await Atlas.fromAsset(rootBundle, atlasFile); - var skeletonData = await SkeletonData.fromAsset(rootBundle, atlas, skeletonFile); + static Future 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 fromFile(String skeletonFile, String atlasFile) async { + static Future 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 fromHttp(String skeletonFile, String atlasFile) async { + static Future fromHttp(String atlasFile, String skeletonFile) async { var atlas = await Atlas.fromUrl(atlasFile); var skeletonData = await SkeletonData.fromHttp(atlas, skeletonFile); return SkeletonDrawable(atlas, skeletonData, true); diff --git a/spine-flutter/lib/spine_widget.dart b/spine-flutter/lib/spine_widget.dart index c4b79e8ac..a15830f9b 100644 --- a/spine-flutter/lib/spine_widget.dart +++ b/spine-flutter/lib/spine_widget.dart @@ -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 { 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().");