A remake of the classic sliding puzzle. The goal is to navigate Cao Cao (the biggest piece) to the exit at the bottom, by carefully rearranging other pieces.
The project is written in Flutter and runs on desktop platforms including Windows, macOS, Linux, as well as mobile platforms including Android and iOS. The game is also available as a web app, which can be accessed from any desktop browsers.
A web version is available here.
Known issue: the web version has some rendering issues when viewed from mobile browsers (for example, Chrome on Android). For best experience, please use a desktop browser, or install the Android/iOS app.
Please watch the project overview video in English and video in Chinese.
At a high level, the app can be divided into 3 parts: the background layer, the game board, and the
puzzle pieces. Each puzzle piece also comes with a pair of interlocking attachments and a shadow for
some added depth. They are then stacked as different layers, to make sure they always appear in the
correct order. The color palette is configured as an InheritedWidget
so its values can be modified
in one place, and easily accessed elsewhere in the widget tree.
The puzzle pieces are made from AnimatedPositioned
. In Flutter, implicit animation widgets like
these are super easy to use - just give them a duration and an optional curve, and they’ll
automatically animate when their values are changed. For the duration, I first check if the window
size is different from before, and if so, I pass in zero. This is to skip the unwanted animation
when the app is being resized. And for the curve, I use EaseOut
to simulate physical objects being
slowed down by friction.
To handle user input, I added GestureDetector
. I used the onUpdate
event instead of the onEnd
event here. This reduces input lag, allowing the game to react before the gesture ends. For the
reset button, I also added a MouseRegion
widget to make it glow when being hovered.
The step counter is another example of utilizing implicit animations in Flutter. It’s also wrapped
in a ValueListenableBuilder
, so it can always keep up with the steps. I’ve published this widget
as a package on pub
and made a video explaining how it was made. If you like this animation, you
can import it to your own project.
The game board is mostly a Container
. I used a BackdropFilter
to blur everything behind it, and
a ShaderMask
to create a beam effect. Since we want the beam to keep dancing around, it’s easier
to use explicit animations with a controller. Also, the exit arrows at the bottom, and the
highlighting effect on texts, are all made with Flutter’s explicit animations.
Lastly, I used a CustomPaint
for the background. A text-painting version was first implemented but
later scrapped because having to layout texts every frame caused some performance issues on the web.
The simplified version looks similar but performs much better, even on low-end devices.
I've published lots of Chinese video tutorials on Flutter, covering a wide range of topics and popular questions from viewers. Here are a few of them.
- Get moving with just 2 lines of code
- Smooth transitioning between different widgets
- Curves, and more animation widgets
- DIY with a TweenAnimationBuilder
- Case study: make a flip counter
- Case study: flip counter continued
- A repeating animation
- What is an AnimationController
- Curves and Tween
- Staggered animations with intervals
- DIY with an AnimatedBuilder
- Case study: coordinating multiple animations
- Case study: multiple animation controllers
- Under the hood: animations and tickers
- Hero animations
- CustomPaint: do you wanna build a snowman
- Animate with Rive/Flare
- Bonus: create an asset with Rive tool
- Spooky stuff when you forget to use keys
- Widgets, Elements and their States
- Three types of LocalKeys
- Two purposes of GlobalKeys
- Case study: make a color sorting game
- Case study: use LocalKey in the game
- Case study: use GlobalKey in the game
- ListView and lazy loading
- Deep dive into the ListView widget
- RefreshIndicator and NotificationListener
- Swipe away with the Dismissible widget
- Case study: a GitHub repo browser
- GridView widget
- Even more scrollable widgets
- Event loop, queues, and microtasks
- Deep dive into the Future type
- FutureBuilder widget
- Stream and StreamBuilder widget
- Case study: event stream from user actions
- Case study: monitor event stream
- Case study: a good use for StreamTransformer
- Constraints, size, and positions
- LayoutBuilder and ConstraintBox
- Flex: Flexible and non-flexible
- Stack: Positioned and non-positioned
- What is a Container, really
- CustomMultiChildLayout widget
- Let's make a RenderObject
- Welcome to the world of Sliver
- All sorts of sliver lists
- SliverAppBar widget
- More sliver widgets and SliverLayoutBuilder
- Case study: convert a ListView into sliver
- SliverPersistentHeader
- Design a page with a SliverAppBar
- Different ways to detect screen rotation
- Different ways to implement this animation
- The new and the old material buttons
- Adaptive banner with Pythagorean Theorem
- Creative ways to achieve diagonal layout
- Ink, InkWell, and Material
- Hollowed Text
- Adaptive watermark with FittedBox
- A button that counts down with its border
- Pinch-to-zoom on a GridView, with animations!
- What is BuildContext?!
- What is SOUND null safety
- Hotkeys in Android Studio
- Weird tricks about Flutter Hot Reload
- WillPopScope and iOS swiping gesture
- Some super useful widgets in Flutter
- Some less known widgets in Flutter
- Flutter 2.0 is here!
- Common problems and solutions on Flutter Web
- Publish a package: animated flip counter
- Publish a package: interactive chart