- Published on
- | 12 mins read
Chap 3 - Add a Rive Animated Button to a Flutter Flame Game
- Authors
- Name
- Miller Go Dev
- @millergodev
Overview
Table of Contents
Problem
In the previous article, we added a pause button. But it's just a static image. How boring it is! Whenever a user taps on a button, she must have a "feeling" that it is like a "real" button, not a static image. In other words, it should be animated.
Ok, so how can we make an animated button?
Some solutions
As far as I know, there are at least 2 ways to add an animated button into a Flame-based Flutter game:
- Use the HudButtonComponent. It lets us set callbacks to handle tap events, but it requires us to provide 2 separate images for 2 states of a button: idle and pressed.
- Use the
RiveComponent
from theflame_rive
package along with theTappable
mixin. We must get or create a Rive file using rive.app and manage to animate it on tap event. Sounds pretty harder but it opens the door to adding more awesome animations to our game.
So, I chose to use Rive with the hope that we can easily make this game such an animated beast once we get used to it.
Add flame_rive dependency
Run this command to add flame_rive
package:
flutter pub add flame_rive
Our pubspec.yaml
should become this:
json_annotation: ^4.7.0
flame: ^1.3.0
flame_svg: ^1.5.0
+ flame_rive: ^1.5.1
Add the Rive file
To show an animation using Rive, we need a Rive file that has *.riv
extension. To create that file, we need to use the rive.app
editor. Open and register a FREE account to use it at editor.rive.app.
You can learn how to create animations with Rive on the Rive official documentation site. I prepared a buttons.riv
file for you. It is supposed to contain all the Rive animated buttons for our game. However, for now, it is only having the animated pause button.
This is how the buttons.riv
file looks like in the Rive editor:
There are 2 main concepts you need to know about a Rive file. We will need them in the code.
artboard
: Each Rive file can have multiple artboards. Each artboard contains a group of shapes that will show up as a single UI component on the screen.In this case, the artboard
pause-one
(its name is the SVG file name I downloaded from IconPark) contains the pause button component. We can name the artboard whatever we want.animation
: Each artboard can have multiple animations. Each animation is a series of states of the original object on the artboard. Of course, we have to design those states on our own. I just cut that design part from this article because it's out of scope.In this case, the animation
Press
(we can name it whatever we want) will be like this if I press the play button:
Now, we export this into the buttons.riv
file and save it in the assets/images
directory.
Play the Rive animation when tapping on the button
We need to load the Rive file into the app to use a Rive animation. Here are the code changes:
import 'package:flame/game.dart';
-import 'package:flame_svg/flame_svg.dart';
+import 'package:flame_rive/flame_rive.dart';
import 'package:flutter/widgets.dart';
+import 'package:rive/rive.dart';
class MainGame extends FlameGame {
@override
Color backgroundColor() => const Color(0xFFFFFFFF);
@override
Future<void>? onLoad() async {
- final pauseIcon = await Svg.load('images/ic_pause.svg');
- final pauseIconComponent = SvgComponent(
- svg: pauseIcon,
+ final riveFile = RiveFile.asset('assets/images/buttons.riv');
+ final artboard = await loadArtboard(
+ riveFile,
+ artboardName: 'pause-one',
);
+ final pauseIconComponent = RiveComponent(
+ artboard: artboard,
position: Vector2.all(0),
size: Vector2.all(100),
);
}
}
In the main_game.dart
file, we replace the old code that loads the SVG image. First, it loads the RiveFile by calling RiveFile.asset('assets/images/buttons.riv')
. Then, we must specify which artboard
we want to show on the screen. Do you remember the pause-one
artboard I mentioned above? We load it by calling loadArtboard()
with the pause-one
value of the artboardName
argument.
After that, we need to create a RiveComponent
instance from that artboard
instead of the SvgComponent
. This component still needs to hold the size and position, so that the game knows how to draw it on the screen.
Do you notice that we imported the rive/rive.dart
package? We need it to be able to use the RiveFile
class. So, remember to add the package to our app by running this command:
flutter pub add rive
And looking for the change in pubspec.yaml
:
json_annotation: ^4.7.0
flame: ^1.3.0
flame_svg: ^1.5.0
flame_rive: ^1.5.1
+ rive: ^0.9.1
Here is the full current code in main_game.dart
:
import 'package:flame/game.dart';
import 'package:flame_rive/flame_rive.dart';
import 'package:flutter/widgets.dart';
import 'package:rive/rive.dart';
class MainGame extends FlameGame {
Color backgroundColor() => const Color(0xFFFFFFFF);
Future<void>? onLoad() async {
final riveFile = RiveFile.asset('assets/images/btn_pause.riv');
final artboard = await loadArtboard(
riveFile,
artboardName: 'pause-one',
);
final pauseIconComponent = RiveComponent(
artboard: artboard,
position: Vector2.all(0),
size: Vector2.all(100),
);
await add(pauseIconComponent);
}
}
Let's run the app!
Trigger the animation
But the button hasn't animated when we tap it. To achieve that, we need to do 2 things:
- Detect tap events to run our code.
- The code must run the animation.
Our main_game.dart
will become this:
+import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame_rive/flame_rive.dart';
import 'package:flutter/widgets.dart';
import 'package:rive/rive.dart';
-class MainGame extends FlameGame {
+class PauseBtnComponent extends RiveComponent with Tappable {
+ PauseBtnComponent({required super.artboard})
+ : super(
+ position: Vector2.all(0),
+ size: Vector2.all(100),
+ );
+}
+
+class MainGame extends FlameGame with HasTappables {
@override
Color backgroundColor() => const Color(0xFFFFFFFF);
...
riveFile,
artboardName: 'pause-one',
);
- final pauseIconComponent = RiveComponent(
- artboard: artboard,
- position: Vector2.all(0),
- size: Vector2.all(100),
- );
+ final pauseIconComponent = PauseBtnComponent(artboard: artboard);
await add(pauseIconComponent);
}
}
The RiveComponent
doesn't have the ability to detect tap events. To extend its ability, we must create a separate class to extend the RiveComponent
class and combine with the Tappable mixin
. This mixin
has the ability to handle tap events. So, the PauseBtnComponent
class also inherits that ability. We are gonna handle the tap event in a callback function later.
In Flame engine, if a game contains at least 1 Tappable
component, the game class must inherit the HasTappables mixin
. Otherwise, it will throw this exception message: Tappable components can only be added to a FlameGame with HasTappables
. So, we must add with HasTappables
to our FlameGame
class to make it work.
Now, to run the animation on a tap down event, we need to change our code again:
import 'package:flame/game.dart';
+import 'package:flame/input.dart';
import 'package:flame_rive/flame_rive.dart';
import 'package:flutter/widgets.dart';
import 'package:rive/rive.dart';
class PauseBtnComponent extends RiveComponent with Tappable {
position: Vector2.all(0),
size: Vector2.all(100),
);
+
+ late final RiveAnimationController _controller;
+
+ @override
+ Future<void>? onLoad() {
+ _controller = OneShotAnimation('Press', autoplay: false);
+ artboard.addController(_controller);
+ return super.onLoad();
+ }
+
+ @override
+ bool onTapDown(TapDownInfo info) {
+ _controller.isActive = true;
+ return true;
+ }
}
class MainGame extends FlameGame with HasTappables {
To be able to run the animation, we grab a RiveAnimationController
. In this case, we want the animation to run from start to end and then stop until the next tap event. So, it should be an OneShotAnimation
. We pass the Press
argument, which is the name of our animation in the artboard pause-one
in our Rive file if you still remember. The autoplay
named argument is false
because we don't want it to play right after the button shows up. We will trigger the animation on our will later.
Then, remember to attach this new controller to the artboard
by calling addController()
function. Otherwise, the artboard will not run the animation.
Now, in a Tappable
object, the onTapDown()
callback will be called on a tap down event. So, we override it and change the isActive
of the _controller
to true
here. That means the animation specified in the controller will run.
That's all, this is the full code of the main_game.dart
file:
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame/input.dart';
import 'package:flame_rive/flame_rive.dart';
import 'package:flutter/widgets.dart';
import 'package:rive/rive.dart';
class PauseBtnComponent extends RiveComponent with Tappable {
PauseBtnComponent({required super.artboard})
: super(
position: Vector2.all(0),
size: Vector2.all(50),
);
late final RiveAnimationController _controller;
Future<void>? onLoad() {
_controller = OneShotAnimation('Press', autoplay: false);
artboard.addController(_controller);
return super.onLoad();
}
bool onTapDown(TapDownInfo info) {
_controller.isActive = true;
return true;
}
}
class MainGame extends FlameGame {
Color backgroundColor() => const Color(0xFFFFFFFF);
Future<void>? onLoad() async {
final riveFile = RiveFile.asset('assets/images/buttons.riv');
final artboard = await loadArtboard(
riveFile,
artboardName: 'pause-one',
);
final pauseIconComponent = PauseBtnComponent(artboard: artboard);
await add(pauseIconComponent);
}
}
Let's run the app!
It's good.
Check out the commit for this article in this pull request.