Inspiration

"Planets" was inspired by the idea of being able to play a slide puzzle of animations instead of texts, numbers, or images. I knew I could use CustomClipper to clip any Flutter widgets using just a mathematical formula, all was now needed to make an animation. But, make what?

As I am no designer or animator, it would be tough for me to learn first, and then fabricate a beautiful animation to go with the puzzle. But since the start, I desired to build the puzzle around a specific theme. A few days into brainstorming and playing around with the Rive editor, I discovered I could make a planet rotate.

Thus, was the idea of making a slide puzzle based on the planets of our solar system.

What it does

The core gameplay of "Planets" is a side puzzle game of an animating planet. To play with, the player can choose any planets of our solar system and more (Pluto), with varying difficulty levels - easy, medium & hard. The different playable planet also represents different themes for the puzzle.

The game comes with an auto-solver. At anytime user can choose to auto solve the puzzle. The solver solves the puzzle in a human-friendly way, thus making it easy to follow and learn.

How I built it

The entirety of the project can be broadly classified into 3 parts - Loading Page, Dashboard Page, and of course the main Puzzle Page.

Loading Page

The main purpose of the loading page is to initialize the audio assets and cache image/animation assets. The most challenging part of this screen was to build the Loading Flutter animation that we show while caching the assets. The loading animation is built using implicit animations namely - AnimatedPositioned & AnimatedOpacity.

Dashboard Page

The dashboard page presents the user with all the planets, the user can choose to play with. The user is also given options to choose puzzle level - easy, medium, or hard. There are other settings, related buttons as well for sound effect/music playback volume, playing/pausing the orbiting animation. The main challenging part of this screen was to build the orbiting animation around the Sun. We have the PlanetOrbitalAnimationCubit that positions and animates the planets.

We show the SunWidget, the orbits and the PlanetWidget inside a Stack, as follows

Stack(
  alignment: Alignment.center,
  children: [
    // sun
    const SunWidget(
      key: Key('Sun'),
    ),

    // orbits
    ...state.orbits.map<Widget>((orbit) => orbit.widget).toList(),

    // planets
    ...(state).orbits.map<Widget>((orbit) => orbit.planet.widget).toList(),
  ],
);

And in PlanetOrbitalAnimationCubit, we run an AnimationController for every planet, which determines the angle the planet should make with the horizontal.

We have defined min and max angles to bound the planet's movement along its orbit. This is done to avoid the planet from spending time outside the visibility of the user.

The following code snippet shows instantiation of the AnimationControllers

final controller = AnimationController(
  value: math.max(math.min(_random.nextDouble(), 0.60), 0.30),
  vsync: _tickerProvider,
)..repeat(period: _getDuration(planet.type));

Every planet's revolution duration is different. Interestingly, on hovering, the planet slows down to let the user interact with it.

Puzzle Page

The Puzzle page mainly consists of - a stats part, the main puzzle board, the control buttons & the background landscape. The puzzle tiles are AnimatedPositioned, whose states are controlled from the PuzzleBloc and the PlanetPuzzleBloc. The following method builds the child of the puzzle tiles

_buildChild() {
  final theme = themeBloc.state.theme;

  if (context.read<PuzzleHelperCubit>().state.optimized) {
    // if we need to play optimized puzzle, just show images, instead of animations
    childVn.value = Image.asset(theme.placeholderAssetForTile);
    puzzleInitCubit.onInit(widget.tile.value);
  } else {
    // show animations if we don't wanna play optimized puzzle
    childVn.value = RiveAnimation.asset(
      theme.assetForTile,
      controllers: [puzzleInitCubit.getRiveControllerFor(widget.tile.value)],
      onInit: (_) => puzzleInitCubit.onInit(widget.tile.value),
      fit: BoxFit.cover,
      placeHolder: Image.asset(
        theme.placeholderThumbnail,
        fit: BoxFit.fill,
        height: size,
        width: size,
      ),
    );
  }
}

Depending upon, if we are running an optimized puzzle version, we show static images, otherwise show the planet's rotation animation. The following code snippet is responsible for syncing up all the different RiveAnimation widgets.

controllers: [puzzleInitCubit.getRiveControllerFor(widget.tile.value)],
onInit: (_) => puzzleInitCubit.onInit(widget.tile.value),

Finally, there is a visibility feature, that a user can toggle on, to show tile numbers, thus aiding the user in solving the puzzle.

Challenges I ran into

There were two major challenges that I had to overcome, I have discussed them as follows

  • The first major challenge was instantiating and running n number of RiveAnimation (that are shown as tiles). To show 9 puzzle pieces, 9 RiveAnimation widgets are running and rendering frames separately. The first challenge is to sync up all the animations, whenever the user resizes screens, start/stop puzzles. This is done by the PuzzleInitCubit, which resets the rive animation controllers whenever we need to sync up all the animations. Also for mobile browsers or hard-level puzzles, that can have 25 puzzle pieces, we run an optimized version of the puzzle to avoid frame dropping. In the case of the optimized version, instead of showing the RiveAnimation widgets, we show an Image widget displaying a static image of the planet.

  • The second challenge was building the auto solver for automatically solving any n x n slide puzzle. I wanted to write an algorithm that always works predictably and is easy to follow for a user. The current PuzzleSolver algorithm solves the puzzle in first row - first column manner, thus reducing the puzzle size by one after solving every row - column. And at each iteration, the previously placed tiles are kept undisturbed. I took great advantage of flutter's hot reload to quickly make fixes and check my algorithm against the puzzle.

Accomplishments that I'm proud of

Many small accomplishments made me happy while working on the "Planets" project

  • Being able to use only implicit animations to achieve my desired output was always exciting. For reference, the Loading widget just used AnimatedPositioned & AnimatedOpacity to achieve the loading animation.

  • While making the ShakeAnimator widget, which notifies the user if a non-moveable tile is tapped, I had learned about writing my Curve class to achieve the spring effect.

  • This was my first try at making vector illustrations (planets) and animating them (to make them look like they are rotating). Though it doesn't look like rotation, I am happy with what I was able to achieve in a short time using Rive and Rive's web editor.

  • Writing the puzzle auto solver was my biggest accomplishment in the whole project. I took no reference and had written the whole PuzzleSolver from scratch. The algorithm is pretty simple to understand, the solver first solves the first row, then the first column, after that the puzzle size decreases to (n-1) x (n-1), and the following algorithm continues, until the whole puzzle is solved.

  • Taking advantage of CustomCliper to slice up the RiveAnimation widgets into n pieces, with round corners was another accomplishment, I would like to list.

What I learned

I have been using flutter since July 2020. It's always interesting to learn something new, or a different approach to a problem, that I have been dealing with inefficiently. This project had taught me a plethora of useful features and functionalities of flutter, I have summed up in the following points

  • I learned to use the dev tools to find unnecessary widget rebuilds, understood the flutter outline while navigating the widget tree, learned the importance of delivering frames at 60 fps, thus optimizing minute code blocks for better performance.

  • "Planets" is my first app with internalization. I took a lot of references from the demo slide puzzle app and had read through online blogs and documentation to have a better understanding of supporting various locales in Flutter.

  • I learned the usefulness of using delegate interfaces for theming or other purposes. I will be using the delegate pattern now in more of my other flutter projects.

  • I learned the benefit of using Stateless / Stateful widgets, instead of writing build functions to build large chunks of UI. I understood the performance implications it has and how it's best Flutter maintains the tree when using Widgets, instead of functions.

  • I got familiarized with Rive editor, learned to make animations and designs there. Most importantly, I learned to use Rive in a Flutter project, though I haven't used Rive for what it's intended to be used for, I am going to include it in my future Flutter project.

  • While making the loading screen animation, I appreciated how powerful the implicit built-in animating widgets are. We can achieve somewhat complex animations without even going to the AnimationController territory.

Overall it was such a great experience, I am always amazed by Flutter and its efficiency and beauty.

What's next for Planets

Going forward, I have the following points that I would love to add in "Planets"

  • Adding an efficient auto solver, using a heuristic search algorithm - as the current solver algorithm, though easy to understand, is too slow.
  • Improve the image and animation assets, namely the planets, the landscape images, and the overall theme of the app.
  • Support for extreme(6x6) & pro(7x7) puzzle levels
  • Add support for a different language.
  • Most importantly, discovering a different way to slice up the puzzle pieces to improve performance.

Built With

+ 11 more
Share this project:

Updates