Flutter Spine 动画插件使用指南
环境 sdk: '>=3.0.0 <3.1.0'
Spine版本: spine_flutter: ^4.2.12
4.0版本以下的Spine动画软件导出的动画文件只能使用4.0版本以下的Spine插件版本, 4.0以上的Spine导出动画格式跟之前的格式有一定差异,只能使用4.0版本以上的spine插件
本篇文章所涉及的Spine动画版本说明使用 都是指定 4.0版本以上,因为官方插件已经升级适配4.0版本,所以没有必要再去使用4.0之前的版本,4.0版本前的动画文件在使用4.0之后的插件的时候会出现解析异常 且加载使用方式有差异,所以如果有这个需求的尽量使用4.0以上的版本,避免以后切换Spine 软件版本之后导出的文件不兼容的问题。
spine 介绍
通过spine软件制作出来的2d骨骼动画。
spine 动画组成:
一个Spine动画(也可以称为一组Spine动画,因为动画里可以添加多个动作(action),也就是相同的spine文件可以通过控制器切换不同的动作 ) ,至少需要3个文件或3个以上资源文件才能完成播放(非json格式文件,其中包含一个.skel 、.atlas 、以及一个或多个.png文件)
比如动画名称为animate,那么至少需要以下三个文件 :animate.atlas animate.skel animate.png
如果动画较大的情况下,可能会有多张png文件
使用时同一动画文件需要放在同一层目录下,且animate.atlas 文件中关联的图片需要一致,比如上述的 animate.png
Flutter 中Spine插件使用步骤:
1、添加插件 pubspec.yaml文件中添加
spine_flutter: ^4.2.12
添加spine动画目录
在项目中新建assets文件夹,assets文件夹中新建spine文件夹,将spine动画资源导入 到spine文件夹中
flutter:
uses-material-design: true
assets:
- assets/spine/
2、初始化Spine插件
void main() {
WidgetsFlutterBinding.ensureInitialized();
await initSpineFlutter(enableMemoryDebugging: false);
}
3、Spine动画在普通Flutter页面使用
import 'package:spine_flutter/spine_flutter.dart';
late SpineWidgetController _controller;
///初始化Spine动画控制器
initState(){
_controller = SpineWidgetController(
onInitialized: (controller) {
///可以通过controller调整缩放
controller.skeleton.setScaleX(0.5);
controller.skeleton.setScaleY(0.5);
///设置初始动作以及重复状态,动画文件中定义的动作比如:action1 ,action2
controller.animationState.setAnimationByName(0, "action1", true);//设置action1重复
}
);
}
@override
Widget build(BuildContext context) {
reportLeaks();
return Scaffold(
body: SpineWidget.fromAsset(
"assets/spine/animate.atlas",
"assets/spine/animate.skel",
controller,
boundsProvider: SetupPoseBounds(),
//boundsProvider 有三种模式,
//1、SetupPoseBounds(),默认模式,不需要配置参数,根据动画文件配置内容进行显示
//2、RawBounds(),我称之为偏移模式,当动画文件没有居中或更实际显示有出入时,可以设置
// x、y、width、height 来调整
//3、SkinAndAnimationBounds(animation: ""),样式计算模式
),
floatingActionButton: FloatingActionButton(
onPressed: _togglePlay,
child: Icon(controller.isPlaying ? Icons.pause : Icons.play_arrow),
),
);
}
加载App assets资源的Spine资源动画, _controller跟上面的一样
SpineWidget.formAsset(
"assets/spine/animate.atlas",
"assets/spine/animate.skel",
_controller
)
加载App文件夹资源的Spine资源动画 , _controller跟上面的一样
SpineWidget.fromFile(
"/data/user/0/com.xxx.xxx/app_flutter/animate.atlas",
"/data/user/0/com.xxx.xxx/app_flutter/animate.skel",
_controller,
);
4、Spine在Flutter Flame 游戏框架中的使用
第一步:创建 spine 需要的 drawable
Future<SkeletonDrawable> createDrawAble(){
late final SkeletonData cachedSkeletonData;
late final Atlas cachedAtlas;
//本地文件
//cachedAtlas = await Atlas.fromFile("atlasPath");
//cachedSkeletonData = await SkeletonData.fromFile(cachedAtlas, "skelPath");
//assets资源
//cachedAtlas = await Atlas.fromAsset("assets/spine/animate.atlas");
//cachedSkeletonData = await SkeletonData.fromAsset(cachedAtlas, "assets/spine/animate.skel");
return SkeletonDrawable(cachedAtlas, cachedSkeletonData, false);
}
不想封装的也可以使用插件提供的方法:
late SkeletonDrawable _drawable;
@override
void initState() {
reportLeaks();
super.initState();
SkeletonDrawable.fromAsset(
"assets/spine/animate.atlas",
"assets/spine/animate.skel"
).then((drawable) async {
_drawable = drawable;
});
}
第二步:将Spine动画封装成游戏组件
class SpineComponent extends PositionComponent with HasGameRef{
final BoundsProvider _boundsProvider;
final SkeletonDrawable _drawable;
late final Bounds _bounds;
final bool _ownsDrawable;
late double speed;//速度
late Vector2 pos;//位置
SpineComponent(
this._drawable, , this.speed , this.pos ,{
bool ownsDrawable = true,
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 {
return SpineComponent(await SkeletonDrawable.fromAsset(atlasFile, skeletonFile, bundle: bundle),
ownsDrawable: true,
boundsProvider: boundsProvider,
position: position,
scale: scale,
angle: angle,
anchor: anchor,
children: children,
priority: priority);
}
void dispose() {
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 GameTest extends FlameGame with HasGameRef {
late final SpineComponent spineComponent ;
@override
Future<void> onLoad() async {
spineComponent = SpineComponent(await createDrawAble(),
}
}
Flutter页面中使用Flame游戏场景
class GameViewPage extends StatefulWidget {
CatchGameViewPage({Key? key}
class _GameViewPageState extends State<GameViewPage> {
late GameTest gameTest;
@override
void initState() {
gameTest = GameTest();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body:Stack(
children:[
Positioned.fill(
child : GameWidget(
game:gameTest,
)
)
]
)
}
}
游戏没有路由,所以路由跳转都是依赖Flutter原生路由,主要体现在游戏菜单、引导等浮层的切换使用 Flame 游戏浮层使用
GameWidget 有一个overlayBuilderMap参数
overlayBuilderMap: {
Consts.gameExplainId : (_,game) {
return GameExplainOverlay(game: testGame);
},
Consts.gameFinishedId : (_,game){
return GameFinishedOverlay( game: testGame );
},
Consts.gameProgressId :(_ , game){
return GameProgressOverlay(game: testGame,);
},
Consts.gameMenuId :(_ , game){
return GameMenuOverlay(game: testGame);
},
},
GameExplainOverlay 、GameFinishedOverlay、GameProgressOverlay、GameMenuOverlay 分别为游戏介绍、游戏结束、游戏进度、游戏菜单浮层,都是由Flutter普通页面实现
class GameExplainOverlay extends StatefulWidget {
Game game;//传入游戏对象
GameExplainOverlay({Key? key , required this.game }) : super(key: key);
}
class _GameExplainOverlayState extends State<GameExplainOverlay> {
@override
void initState() {
super.initState();
///播放音频或动画结束之后,通过widget.game.overlays.remove(Consts.gameExplainId) 即可移除
///通过widget.game.overlays.add(Consts.gameProgressId)添加游戏进度
}
@override
Widget build(BuildContext context) {
return Scaffold(
body:Container(
child:Text(
"游戏说明"
)
)
);
}
}