Advanced Animation with Flutter Flame Engine — Ep.2 Rain Effect

Amorn Apichattanakul
KBTG Life
Published in
7 min readApr 17

--

Photo by Mike Kotsch on Unsplash

In my last article, I discussed how I imported a Sprite Sheet animation into Flutter Flame and used it in our Flutter application to enhance our animations.

You can pull the code from here to play with it.

In this article, I will show you how to add Sprite Sheet and collision detection to create a rainy effect, as mentioned in my previous article.

Game Widget

Firstly, I added GameWidget and put the RainEffect class inside it so that the game and app parts would stay on the same page but divided separately. To display a Flutter widget on top of the Flutter Flame section, I used the overlayBuilderMap API provided by Flutter Flame, through which you can import the Flutter widget. Don't forget to add initialActiveOverlays, otherwise your Flutter widget won't appear in Flutter Flame. You have to use a string key and put it into this section. For example, I used userArea for the section where I added the TextField widget, and container1 for a simple button. You can add an action like onPressed or anything else inside the button, and Flutter Flame will recognize it.

Expanded(
child: GameWidget<RainEffect>(game: game, overlayBuilderMap: {
'userArea': (ctx, game) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 80),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
TextField(
decoration: InputDecoration(
filled: true,
fillColor: Colors.white,
hintText: 'Username')),
TextField(
decoration: InputDecoration(
filled: true,
fillColor: Colors.white,
hintText: 'Password'))
]));
},
'container1': (ctx, game) {
return ActionButtonWidget(
Colors.blueAccent, "Sign in", Alignment.bottomCenter, () {
print("=== This is Flutter widget inside Flutter Flame ===");
});
},
}, initialActiveOverlays: const [
'userArea',
'container1'
]))

Inside RainEffect, there is a class that extends FlameGame and has some mixins, such as HasGameRef, HasCollisionDetection, and HasTappables

The Flutter Flame lifecycle will start with onLoad(), so you can load assets or components there. I added a Sprite Sheet rain effect as the background of the Flutter Flame. Next, I added ScreenHitbox(), which is the widget that creates the boundary of the Flutter game and is used for collision detection. I also added DynamicIslandButton, which is a Flutter Flame button that I tried to mimic as a Flutter widget button. This way, it will appear as if it is part of the Flutter app rather than the Flutter Flame game itself.

class RainEffect extends FlameGame
with HasGameRef<FlameGame>, HasCollisionDetection, HasTappables {
late SpriteSheet rainSprite;
var isRaining = true;
@override
Color backgroundColor() => const Color(0x00000000);

@override
Future<void> onLoad() async {
rainSprite = SpriteSheet(
image: await images.load('rain_effect.png'),
srcSize: Vector2(1024.0, 60.0),
);
final spriteSize = Vector2(256.0, 240.0);

final animation = rainSprite.createAnimation(row: 0, stepTime: 0.1, to: 4);
final rainComponent = SpriteAnimationComponent(
animation: animation,
scale: Vector2(3.5, 3.5),
size: spriteSize,
);
add(rainComponent);

add(ScreenHitbox());
add(DynamicIslandButton()
..position = Vector2(size.x / 2, size.y / 2 + 80)
..size = Vector2(size.x - 160, 40)
..anchor = Anchor.center);

add(FakeArea(gameRef.size / 2, Vector2(gameRef.size.x - 160, 100)));
}

@override
Future<void> onTapDown(int pointerId, TapDownInfo info) async {
super.onTapDown(pointerId, info);

while (isRaining) {
final randomX = Random();
final xPos = randomX.nextDouble() * gameRef.size.x;
final position = Vector2(xPos, 0);
add(RainDrop(position));
await Future.delayed(const Duration(milliseconds: 200));
}
}
}
Rain Sprite Sheet

FakeArea is another important component that I use to fake collisions between Flutter widgets and Flutter Flame. I accomplish this by hiding it behind overlayBuilderMap.

HasTappables adds the onTapDown event inside the Flutter game. I added an event where tapping on the screen creates a RainDrop every 200ms at random locations within the Flutter game while using gameRef to check the game size. gameRef is similar to MediaQuery.of(context) in a Flutter app. To use gameRef, you need to add the HasGameRef mixin inside FlameGame.

HasCollisionDetection is needed when you want to have collision detection inside your game.

Now, let’s dig into RainDrop.

class RainDrop extends PositionComponent
with HasGameRef<RainEffect>, CollisionCallbacks {
late Vector2 velocity;
late ShapeHitbox hitbox;
final _defaultColor = Colors.blueAccent.shade100;
final gravity = 9.8;

RainDrop(Vector2 position)
: super(
position: position,
size: Vector2.all(5),
anchor: Anchor.center,
);

@override
Future<void> onLoad() async {
super.onLoad();
final defaultPaint = Paint()
..color = _defaultColor
..style = PaintingStyle.fill;

hitbox = CircleHitbox()
..paint = defaultPaint
..renderShape = true;

add(hitbox);

final center = gameRef.size / 2;
velocity = (center - position);
}

@override
void update(double dt) {
super.update(dt);
velocity.y += gravity;
position.y += velocity.y * dt;
}

@override
void onCollisionStart(
Set<Vector2> intersectionPoints,
PositionComponent other,
) {
super.onCollisionStart(intersectionPoints, other);
if (other is ScreenHitbox) {
final collisionPoint = intersectionPoints.first;
if (collisionPoint.x == 0) {
velocity.x = -velocity.x;
velocity.y = velocity.y;
}

if (collisionPoint.x.floor() == gameRef.size.x.floor()) {
removeFromParent();
return;
}

if (collisionPoint.y == 0) {
removeFromParent();
return;
}

if (collisionPoint.y.floor() == gameRef.size.y.floor()) {
removeFromParent();
gameRef.add(DropSplash(collisionPoint));

return;
}
} else if (other is FakeArea) {
removeFromParent();
final collisionPoint = intersectionPoints.first;
gameRef.add(RainSplash(collisionPoint));
return;
}
}
}

RainDrop

RainDrop is a PositionComponent and needs to have HasGameRef and CollisionCallbacks mixins to interact with its parent, which is RainEffect.

The update() function is an important function that is mostly used for moving objects in Flutter Flame. I used it as gravity in the app, so the rain will fall faster as it travels farther from the source.

In the onLoad() function, I added a CircleHitBox to represent the raindrop and used the add() function to add it to the widget.

onCollisionStart is called when two objects collide with each other. In this case, it will be ScreenHitBox and RainDrop itself. I added code to check if the other object they hit is ScreenHitBox. If so, the raindrop will be removed from the screen thanks to the removeFromParent() API. In this case, I used collisionPoint.y.floor() == gameRef.size.y.floor() to check if they really hit the floor yet. I added floor() to check the value because sometimes, when the rain hits the floor, the hitting position is slightly off, so I have to round down the number to make it the same value.

When the rain hits the ground, I added gameRef.add(DropSplash(collisionPoint));. This requires gameRef as a reference to its parent; otherwise, you can't add another component. It seems that you can only add components in the root parent.

Inside DropSplash is another FlameGame in which I loaded the splash animation when it hits the floor. This part is a little confusing, and I might have done it wrong, but I couldn't remove DropSplash when the animation finished. Even with removeOnFinish set to true, it still kept playing the animation. I hacked around it with a delay of about 1000ms and then removed it afterward.

class DropSplash extends FlameGame {
final Vector2 position;

DropSplash(this.position) : super();

@override
Future<void> onLoad() async {
final spriteSheet = SpriteSheet(
image: await images.load('drop_splash.png'),
srcSize: Vector2(90.0, 90.0),
);
final spriteSize = Vector2(90.0, 90.0);

final animation =
spriteSheet.createAnimation(row: 0, stepTime: 0.1, to: 12);
final component1 = SpriteAnimationComponent(
scale: Vector2(0.5, 0.5),
animation: animation,
position: position - Vector2(20, 40),
size: spriteSize,
removeOnFinish: true);

add(component1);

Future.delayed(const Duration(milliseconds: 1000))
.then((value) => removeFromParent());
}
}

I did the same with RainSplash and put these two different Sprite Sheet animations inside the app. These animations will be used in different situations. I use DropSplash when the raindrop hits the TextField area, while I use RainSplash when it hits the ground of the game.

Rain Drop Splash SpirteSheet

The final solution that I came up with is in the video below.

I admit the design may not look that great 😅 Sorry, as a programmer, I’m not very good at designs. I created the rain effects and raindrops using images I found on the internet, and put them together with my programming-level Photoshop skills 😆.

It still lacks particles and physics to make the raindrops more realistic. The particles can be used for rain splash, and some water that splashes out should be represented as particles. Another thing is physics. I should have added wind to the application so that the rain does not look like it’s falling straight down.

Finally, the big question is, is it possible to add a game to an app? Won’t it be too heavy, cause battery drain or consume a lot of memory?

“a picture is worth a thousand words”

I decided to incorporate this rain effect into my project, “MAKE by KBank,” to test it out. If we were to put it into production, would it be possible to run it? The results were unexpected; Flutter Flame had a better frame rate than a normal animation widget. It’s possible that I may have implemented something incorrectly in the Flutter animation or haven’t yet optimized it to its full potential. However, at least I know that Flutter Flame has high performance and can look similar to Flutter’s normal widgets.

In the app section where I incorporated this effect, I didn’t mean to put an actual game inside the app, but rather to get the best parts of the game, such as the animation and particles, into the app. Here’s how it looks.

This article can be a good starting point for anyone who wants to add more cool effects to their Flutter app. I still need to learn more about physics, ray casting, particles, and other techniques to improve my rain effect animation. In the meantime, I hope you enjoy this rainy effect 🌧️

Want to read more stories like this? Or catch up with the latest trends in the technology world? Be sure to check out our website for more at www.kbtg.tech

--

--

Amorn Apichattanakul
KBTG Life

Google Developer Expert for Flutter & Dart | Senior Flutter/iOS Software Engineer @ KBTG