Unleash full Flutter potential in the classic puzzle. Interactively.
What is it?
Given the task of reimagining the classic "game of 15" puzzle in Flutter, we decided to apply one of the key concepts in
the framework - composability - to the game itself.
You can make literally any piece of Flutter UI the content of the game without losing interactivity, animations or
compromising performance. Together with fun mechanics, beautiful visuals and 5 carefully selected levels, the game
redefines what's achievable in Flutter.
Features
- Real-world-inspired puzzle physics (move several tiles at once, snapping, board drift)
- Procedurally-generated initial configurations
- Fully interactive puzzle content
- 5 levels of varying difficulty, selected to reflect some of the Flutter's unique capabilities:
- Color Picker
- Custom Drawer
- Water Ripple
- AR Cam (not available on web, why?)
- Game x Game
How it works?
We started the development with simple question: can you create a puzzle which can use any flutter widget as a
content? As it turns out, with the right amount of digging around in the low-level flutter code, you can.
The final effect looks like this:
Widget build(BuildContext context) {
return Game(
controller: gameController,
child: const YourAwesomeFlutterWidget(),
);
}
Where Game handles the whole puzzle's logic.
Note that child can be any ordinary Flutter widget, and during the game it can alter its state, perform animations,
receive tap gestures (transformed according to current tile's positions). Below we'll discuss how is this possible.
Kaleidoscope
The heart of the puzzle is our custom-built Kaleidoscope widget. Generally speaking, it can display its child as an
arbitrary number of "shards". Each shard paints the child with different matrix transformations and clipping, as
specified by KaleidoscopeDelegate. Geometry of these shards can be changed without the need to trigger rebuilds - much
like the underappreciated Flow widget, but instead of painting multiple children once, we paint one child many
times.
Apart from limitations, there are no requirements on the child widget. It participates in layout and
lifecycle as usual. Additionally, it's available for hit testing (first matching shard receives the transformed
position).
Under the hood
It turns out that Flutter really doesn't expect anybody to paint one widget multiple times.
Long story short, painting in Flutter is carried out by the Layer tree, which is directly influenced by the shape of
current widget tree. Apart from sending the actual drawing commands, it's also responsible for managing hardware
resources allocated by the engine - EngineLayers.
One Layer can hold on to at most one EngineLayer. The problem arises when you want to draw a Layer more than once
per frame (EngineLayers cannot be reused during a single frame, so you need one of them for every shard).
To overcome this, KaleidoscopeLayer hooks into the low-level SceneBuilder API. Each of the child's Layers is "
tricked into thinking" that it manages a single, fake, EngineLayer, when it actually corresponds to as
many EngineLayers as needed.
See the KeyedEngineLayerStorage and SceneBuilderWithStorage classes for more details.
Limitations
Currently, there are some limitations on how the Kaleidoscope may be used, some of which can be removed in the future:
- Platform views are not supported - they are managed by the underlying platform, so we can't magically create multiple
instances of them (on the other side, platform textures used by e.g. the
camerapackage, do not cause such problems) - On the Web, only CanvasKit renderer is supported - seems to cause subtle errors in the HTML renderer, more research needed
- Child widget can't be repositioned outside the
KaleidoscopeusingGlobalKey- fakeEngineLayers do not work without customSceneBuilder; may be fixed by rewriting the fakeEngineLayersubstitution logic
Engine
Game mechanics during solving the puzzle are provided by the Engine class. It implements real-world-inspired physics
by allowing bodies (tiles and the whole board) to move in both axes, while handling snapping, collisions and other
features.
Game
Game manages the whole puzzle lifecycle (initialization, demo, shuffling, solving and winning). It acts as a bridge
between Engine and Kaleidoscope, scheduling physics updates and displaying tiles' state on every frame. Most of the
changes does not require rebuilding the widget tree which allows in to stay blazing-fast.
Game modes
There are 5 built-in interactive puzzles included to demonstrate endless creative possibilities of the Kaleidoscope.
Color Picker
The easiest yet one of the most satisfying modes to play. We've come across this idea in the Briefly app a while ago and after a few adjustments it turned out to be a perfect fit to showcase our game's capabilities. It's an efficiently implemented, elegant picker.
Custom Drawer
Inspired by Marcin Szalek's "Implementing complex UI" talk, it was one of first "outside the box" widgets we've ever created. It redefines the approach to basic, well known components and shows the simplicity of creating eye-catching layouts using basic Flutter features.
Water Ripple
The result of an experiment in which we used several layered widgets, modified with scaling and colored overlays to imitate the lensing effect of water ripples. Due to the puzzle transformations the level is not only extremely hard but also can make you dizzy - you're welcome and be careful.
AR Cam
That's the point where we really started to push boundaries. We wanted to include some sort of media widget, for
instance a video player, but we decided to give the camera package a go. As straightforward as the widget is, it's
still a hell of a lot of fun solving the puzzle made of your best bud's messed up face (or cat's if you don't like
humans).
Game x Game
Since we are able to use any widget as the game's content, we decided to use the Game widget itself. You must
translate between two boards in order to complete the puzzle. Taking into account additional degree of freedom it's the
hardest mode to play and might not be solvable in a reasonable amount of time. If that's the case, why did we even
bother creating it? Because we could

Log in or sign up for Devpost to join the conversation.