业余空闲时制作了一个简单的2D开心乐小游戏,本文主要讲解一些重点核心关键点实现,相关关键实现技术讲解完,相信应该你们也可以完全动手实现一个类似的小游戏玩玩的!
前提:你必须会在flutter组件使用,以了解过flutter的相关动画组件,这样更容易理解。
先发体验地址及仓库
github地址:https://github.com/tec8297729/hh_game
APP扫码下载体验,密码:111111
基础概要
实现这样一款简单的小游戏,主要核心是实现一个旋转双面的卡牌、一个可以控制的表情小脸蛋,如果对某块技术点感兴趣的话留言提出来,我会在空闲时间逐个技术分析总结,相对会较慢一点。
可控制的表情实现
Flare动画是什么?
它就类似动画设计网站,可以为App、游戏和网页制作矢量动画模型。
当你制作好了动画模型,是可以在flutter中控制某个动画运行指定帧数,以达到酷炫动画效果,并且能够完全hold控制住动画流程。
Flare制作网站地址:https://rive.app
设计师可直接在rive网站制作 相应设计稿及动画,使得工程技术人员更好的协同工作,下面看一段使用Rive和Flutter结合的交互效果视频,在登录界面中通过加入flare矢量动画,让界面变的栩栩如生。
PS: 请准备好墙外环境,才能看到如下视频:
flutter基础页面搭建
首先就是搞一个flutter的基础页面环境出来,这里就不单个去写了,我就用现成的flutter_flexible脚手一键生成页面了(主要是便利快捷生成),毕竟主点是讲解动画技术。
1、输入命令安装脚手架cli,并且创建一个新的项目。
1 2 3 |
npm i -g flib-cli // 全局安装cli脚手架 flib create // 建一个新的flutter项目,根据提示输出项目名称,默认直接回车就行。 |
PS:注意要有node环境哦,可以网上下载一个node环境安装包,如果怕麻烦就自己手动创建一个flutter空页面也是一样的。
2、进入创建好的项目文件夹内,安装插件及启动项目
1 2 |
flutter pub get flutter run // 记的开好模拟器,或用真机 |
3、接着进入lib\pages\AppHomePage\Home\Home.dart 页面组件中,去除演示demo代码,准备好基础页面环境。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import 'package:flutter/material.dart'; class Home extends StatefulWidget { Home({Key key, this.params}) : super(key: key); final params; @override _HomeState createState() => _HomeState(); } class _HomeState extends State<Home> { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Text('jonhuu.com'), ), ); } } |
可控制的动态表情实现
Flare动画导入flutter页面
1、flr人脸动画素材下载地址》》https://rive.app/a/tec8297729/files/flare/face
进入地址后右上角导出flr文件动画后面使用。关于怎样制作Flare动画可以查看相关资料,蛮多文章讲解的蛮细的。
PS:这里我们使用网上制作好的素材,降低一些技术门槛,对于一些小伙伴门在学flr制作可能又是很长一篇文章了。但有兴趣的小伙可以查阅一下资料自己在搞一个动画。
2、把下载好的flr文件拷贝到APP根项目中的asset目录中去,接着配置pubspec.yaml文件指定静态资源文件,配置如下:
1 2 3 4 5 6 |
# ...显示关键的部份 flutter: uses-material-design: true # 静态资源 assets: - asset/face.flr |
3、flr动画组件默认是不支持直接使用的,需要安装一下第三方插件库flare_flutter,在pubspec.yaml文件如下。
1 2 3 |
# ...省略 dependencies: flare_flutter: ^2.0.3 |
3、在home页面组件中引用flr动画表情
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 |
import 'package:flare_flutter/flare_actor.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter/material.dart'; class Home extends StatefulWidget { Home({Key key, this.params}) : super(key: key); final params; @override _HomeState createState() => _HomeState(); } class _HomeState extends State<Home> { @override Widget build(BuildContext context) { return Scaffold( body: Column( children: <Widget>[ faceFLr(), ], ), ); } // 表情 Widget faceFLr() { return Center( child: SizedBox( width: 500.w, height: 380.h, child: FlareActor( 'asset/face.flr', artboard: 'Artboard', // 画板名称 ), ), ); } } |
此时flr动画初始形态已经出来,如下:
另外在补一下flr动画网站上,二处位置的含义。
添加表情动画控制器
目前动画还是不会动的,这时候我们要拿到动画控制器的类,然后才能控制它,开始改造
1、插件有提供FlareController控制抽象类,需要自己实现里面的方法,在Home组件文件中创建components/FlareRateController.dart 文件
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 |
// FlareRateController.dart import 'package:flare_flutter/flare.dart'; import 'package:flare_dart/math/mat2d.dart'; import 'package:flare_flutter/flare_controller.dart'; import 'dart:math' as math; class FlareRateController extends FlareController { ActorAnimation _actorAnimation; double _slidePercent = 0.0; double _currentSlide = 0.0; double _snoothTine = 5.0; // 更新flr动画当前值 void updataPercent(double val) { _slidePercent = val; } double get getPercent => _slidePercent; // 初始化 @override void initialize(FlutterActorArtboard artboard) { // 判断flr画板的名称 if (artboard.name.compareTo('Artboard') == 0) { _actorAnimation = artboard.getAnimation('slide'); // 获取动画名称 } } // 动画运行时持续触发函数 @override bool advance(FlutterActorArtboard artboard, double elapsed) { // 判断flr画板的名称 if (artboard.name.compareTo('Artboard') == 0) { _currentSlide += (_slidePercent - _currentSlide) * math.min(1, elapsed * _snoothTine); // 指定动画当前时间,画板,最小值 _actorAnimation.apply( _currentSlide * _actorAnimation.duration, artboard, 1); } return true; } @override void setViewTransform(Mat2D viewTransform) {} } |
2、在home页面组件中引入控制器,并且制作一个滑块,用来测试动画,代码如下:
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 |
import 'package:flare_flutter/flare_actor.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter/material.dart'; import 'components/FlareRateController.dart'; class Home extends StatefulWidget { Home({Key key, this.params}) : super(key: key); final params; @override _HomeState createState() => _HomeState(); } class _HomeState extends State<Home> { FlareRateController _flareRateController; // flr动画控制器 double sliderValue = 0; // 当前滑动的值 @override void initState() { super.initState(); _flareRateController = FlareRateController(); } @override Widget build(BuildContext context) { return Scaffold( body: Column( children: <Widget>[ faceFLr(), sliderWidget(), ], ), ); } // 表情 Widget faceFLr() { return Center( child: SizedBox( width: 500.w, height: 380.h, child: FlareActor( 'asset/face.flr', artboard: 'Artboard', // 画板名称 controller: _flareRateController, ), ), ); } // 测试滑块 Widget sliderWidget() { return Slider( value: sliderValue, // 当前拖动的值 min: 0, max: 1, onChanged: (v) { setState(() { sliderValue = v; }); }, ); } } |
此时只是先把测试组件滑块,和控制器加入进来,还不能控制!下一步就开始控制flr动画了。
3、关联控制器让动画表情动走来,改造sliderWidget小组件方法,加入更新动画方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// ...省略 // 测试滑块 Widget sliderWidget() { return Slider( value: sliderValue, // 当前拖动的值 min: 0, max: 1, onChanged: (v) { setState(() { sliderValue = v; // 把动画进度值传入,更新flr动画表情 _flareRateController.updataPercent(sliderValue); }); }, ); } |
PS: 这里需要说明一下 _flareRateController类上面是有一个更新动画的方法updataPercent,之前上面我们实现了一个控制flr动画的FlareController抽象类。
以免到了这一步就晕了,为什么会有这个方法,这个并不是控制器自带的哦!而自己实现方法暴露出来供更新动画用的。
此时就能自由控制动画表情了,效果如下:
改造成小脸蛋
我们现在要对动画进行好好的包装一下,让他更加生动有灵魂,有感情。
1、还是在home页面组件中,添加一个圆形组件对之前flr组件包裹起来
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 |
class _HomeState extends State<Home> { // ...省略 @override Widget build(BuildContext context) { return Scaffold( body: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ _buildFlareActor(), sliderWidget(), ], ), ); } // 把flr包裹成圆脸蛋 Widget _buildFlareActor() { return AnimatedContainer( duration: Duration(seconds: 1), child: CircleAvatar( child: faceFLr(), radius: 150.w, ), ); } // ...省略 Widget faceFLr() {} } |
此时也有个小像样的脸蛋了,但我们还要在接着融入一丢丢感情色彩进来。
2、对表情加入动态颜色情感表达,对_buildFlareActor组件改造,添加颜色区间值。
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 |
class _HomeState extends State<Home> { // ...省略 // 添加颜色区间值,flr动画依次0-1之间显示的颜色 Animatable<Color> backgroundTween = TweenSequence<Color>([ TweenSequenceItem( tween: ColorTween( begin: Color(0xFFFF0000), end: Color(0xFF70C100), ), weight: 1.0, ), TweenSequenceItem( tween: ColorTween( begin: Color(0xFF70C100), end: Color(0xFFFFFFFF), ), weight: 1.0, ), TweenSequenceItem( tween: ColorTween( begin: Color(0xFFFFFFFF), end: Color(0xFFFFFFFF), ), weight: 1.0, ), TweenSequenceItem( tween: ColorTween( begin: Color(0xFFFFFFFF), end: Color(0xFFF8ECBD), ), weight: 1.0, ), TweenSequenceItem( tween: ColorTween( begin: Color(0xFFF8ECBD), end: Color(0xFF20BEFD), ), weight: 1.0, ), ]); // ...省略不重要部份 Widget _buildFlareActor() { return AnimatedContainer( duration: Duration(seconds: 1), child: CircleAvatar( // 添加颜色,并且getPercent给的值是Flr当前动画的具体值0-1之间 backgroundColor: backgroundTween.evaluate( AlwaysStoppedAnimation(_flareRateController.getPercent), ), child: faceFLr(), radius: 150.w, ), ); } } |
此时表情有了颜色的装饰,但总感觉还是少了点什么???
似乎是有那么点味道,那咱们在加点生气情感表达一下咯,开干。
3、对表情添加生气的时特殊效果,这里想到用振动效果来阐述生气,改造home组件,添加AnimationController控制器等
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 |
class _HomeState extends State<Home> with SingleTickerProviderStateMixin { FlareRateController _flareRateController; // flr动画控制器 double sliderValue = 0; // 当前滑动的值 AnimationController _controller; // 表情整体动画控制器 @override void initState() { super.initState(); _flareRateController = FlareRateController(); _controller = AnimationController( vsync: this, duration: Duration(milliseconds: 750), )..addListener(() => setState(() {})); } @override void dispose() { _controller.dispose(); super.dispose(); } // 计算矩阵位置 vector.Vector3 _shake() { double offest = math.sin(_controller.value * math.pi * 60); return vector.Vector3(offest * 2, offest * 2, 0.0); } // 更新表情状态效果 void updateDragPos(double details) { setState(() { _flareRateController.updataPercent(details); // 更新flr动画 // 指定范围振动 if (details >= 0 && details < 0.3) { _controller.forward(from: 0); } else if (details >= 0.3 && details < 0.7) { _controller.stop(); } }); } // ...build还是一样。省略 } |
添加完animate控制器一些函数后,下面要结合在表情组件中一起使用了,需要在改造_buildFlareActor组件、faceFLr组件。
4、_buildFlareActor组件、faceFLr组件改造,具体添加了Transform让期有运动起来,并且滑动控制器也改改逻辑,代码如下:
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 |
// 把flr包裹成圆脸蛋 Widget _buildFlareActor() { return AnimatedContainer( duration: Duration(seconds: 1), child: Transform( transform: Matrix4.translation(_shake()), // 转换矩阵 child: CircleAvatar( // 添加颜色,并且getPercent给的值是Flr当前动画的具体值0-1之间 backgroundColor: backgroundTween.evaluate( AlwaysStoppedAnimation(_flareRateController.getPercent), ), child: faceFLr(), radius: 150.w, ), ), ); } // 测试滑块 Widget sliderWidget() { return Slider( value: sliderValue, // 当前拖动的值 min: 0, max: 1, onChanged: (v) { setState(() { sliderValue = v; this.updateDragPos(v); }); }, ); } |
此时表情有点feel了,那么我们不用滑动组件,搞几个按钮直接显示你想要的表情状态。
5、加入几个按钮直接跳入某个表情中去。在home中添加几个btn组件,代码如下:
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 |
import 'dart:math' as math; // ... class _HomeState extends State<Home> with SingleTickerProviderStateMixin { // ... @override Widget build(BuildContext context) { return Scaffold( body: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ _buildFlareActor(), sliderWidget(), Container( margin: EdgeInsets.only(top: 30), child: Text('jonhuu.com'), ), btnWidget('生气', 0), // 生气 btnWidget('一般', 0.3), // 一般 btnWidget('普通', 0.5), // 普通 btnWidget('开心', 0.7), // 开心 btnWidget('高兴', 1), // 高兴 ], ), ); } // ... // 按钮 Widget btnWidget(String text, double sliderData) { return RaisedButton( child: Text(text), color: Colors.primaries[(sliderData * 30).toInt() % Colors.primaries.length], onPressed: () { this.updateDragPos(sliderData); }, ); } } |
此时看效果就相对比较生动,且有点灵魂了。
此时小脸蛋的核心技术完结,home组件的完整代码如下:
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 |
import 'package:flare_flutter/flare_actor.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter/material.dart'; import 'components/FlareRateController.dart'; import 'dart:math' as math; import 'package:vector_math/vector_math_64.dart' as vector; class Home extends StatefulWidget { Home({Key key, this.params}) : super(key: key); final params; @override _HomeState createState() => _HomeState(); } class _HomeState extends State<Home> with SingleTickerProviderStateMixin { FlareRateController _flareRateController; // flr动画控制器 double sliderValue = 0; // 当前滑动的值 AnimationController _controller; // 表情整体动画控制器 // 添加颜色区间值,flr动画依次0-1之间显示的颜色 Animatable<Color> backgroundTween = TweenSequence<Color>([ TweenSequenceItem( tween: ColorTween( begin: Color(0xFFFF0000), end: Color(0xFF70C100), ), weight: 1.0, ), TweenSequenceItem( tween: ColorTween( begin: Color(0xFF70C100), end: Color(0xFFFFFFFF), ), weight: 1.0, ), TweenSequenceItem( tween: ColorTween( begin: Color(0xFFFFFFFF), end: Color(0xFFFFFFFF), ), weight: 1.0, ), TweenSequenceItem( tween: ColorTween( begin: Color(0xFFFFFFFF), end: Color(0xFFF8ECBD), ), weight: 1.0, ), TweenSequenceItem( tween: ColorTween( begin: Color(0xFFF8ECBD), end: Color(0xFF20BEFD), ), weight: 1.0, ), ]); @override void initState() { super.initState(); _flareRateController = FlareRateController(); _controller = AnimationController( vsync: this, duration: Duration(milliseconds: 750), )..addListener(() => setState(() {})); } @override void dispose() { _controller.dispose(); super.dispose(); } // 计算矩阵位置 vector.Vector3 _shake() { double offest = math.sin(_controller.value * math.pi * 60); return vector.Vector3(offest * 2, offest * 2, 0.0); } // 更新表情状态效果 void updateDragPos(double details) { setState(() { _flareRateController.updataPercent(details); // 更新flr动画 // 指定范围振动 if (details >= 0 && details < 0.3) { _controller.forward(from: 0); } else if (details >= 0.3 && details < 0.7) { _controller.stop(); } }); } @override Widget build(BuildContext context) { return Scaffold( body: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ _buildFlareActor(), sliderWidget(), Container( margin: EdgeInsets.only(top: 30), child: Text('jonhuu.com'), ), btnWidget('生气', 0), // 生气 btnWidget('一般', 0.3), // 一般 btnWidget('普通', 0.5), // 普通 btnWidget('开心', 0.7), // 开心 btnWidget('高兴', 1), // 高兴 ], ), ); } // 把flr包裹成圆脸蛋 Widget _buildFlareActor() { return AnimatedContainer( duration: Duration(seconds: 1), child: Transform( transform: Matrix4.translation(_shake()), // 转换矩阵 child: CircleAvatar( // 添加颜色,并且getPercent给的值是Flr当前动画的具体值0-1之间 backgroundColor: backgroundTween.evaluate( AlwaysStoppedAnimation(_flareRateController.getPercent), ), child: faceFLr(), radius: 150.w, ), ), ); } // 表情 Widget faceFLr() { return Center( child: SizedBox( width: 500.w, height: 380.h, child: FlareActor( 'asset/face.flr', artboard: 'Artboard', // 画板名称 controller: _flareRateController, ), ), ); } // 测试滑块 Widget sliderWidget() { return Slider( value: sliderValue, // 当前拖动的值 min: 0, max: 1, onChanged: (v) { setState(() { sliderValue = v; this.updateDragPos(v); }); }, ); } // 按钮 Widget btnWidget(String text, double sliderData) { return RaisedButton( child: Text(text), color: Colors.primaries[(sliderData * 30).toInt() % Colors.primaries.length], onPressed: () { this.updateDragPos(sliderData); }, ); } } |
另外源码放在hh_game的仓库flrdemo分支上了,https://github.com/tec8297729/hh_game/tree/flrdemo