Making 2048 Game in Flutter by using Explicit Animations — Part 3

Angjelko
9 min readJun 9, 2022

--

2048 Game in Flutter Logo

The source for the project is at Github:
https://github.com/angjelkom/flutter_2048

In this chapter we will finally add the AnimationControllers and the Explicit Animations.

Before we do that there is one more thing to do, open models/tile.dart and add the following 2 functions:

Methods to retrieve the next top/left positions in models/tile.dart

These 2 methods will be used to get the next position for the tile based on the nextIndex, so when moving the tile from point A to point B these two functions will be used to decide for the point B.

The complete tile.dart model looks like this now:

models/tile.dart

Adding AnimationController and CurvedAnimation

First we will add the AnimationControllers.

Open lib/game.dart and add the following controllers and Animations at the top of _GameState:

move and scale controllers and animations in game.dart

the _moveController will be used to control the move animation and the _scaleController will be used to control the pop effect the tiles will have when they get merged, and also each controller has a CurveAnimation as child.

Fun fact the AnimationController and CurvedAnimation both extend from the Animation<double> class which is why when adding a CurvedAnimation the AnimationController is added as the parent for the CurvedAnimation.

Now you probably will see an error, that’s because we need to add the TickerProviderStateMixin to the _GameState in order for the class to provide a tick which is used by the AnimationControllers, so replace this:

_GameState class in game.dart

with this:

_GameState class with TicketProviderStateMixin in game.dart

The Ticker is a special Timer that get’s called each time a new frame will be drawn, on standard devices with 60 FPS it means that the Ticket will be called 60 times in one second, on 120 FPS it means it will be called 120 times in one second. So the ticket “ticks” at each new frame.

Next we need to add listeners to these controllers in order to execute some logic when each animation is complete.

First for the _moveController, after the movement finishes what happens in the game?

If we look at the original game 2 things happen, the tiles get merged and a pop effect happens at the same time.

So on the _moveController we will add the following status listener:

move controller status listener in game.dart

So when the movement finishes (the animation status is completed) we will call the merge method of the BoardManager in order to merge the tiles that are at same position and at the same time start the scale animation which will give us a pop effect when the merge happens.

Now for the _scaleController we will add the following status listener:

scale controller status listener in game.dart

So when the scale animation finishes we will end the round by calling the endRound method of the BoardManager and also that same method will return a boolean whether a movement for the next direction was started or not and if was started we will start the _moveController again by calling forward.

The reason I’m calling forward(from: 0.0) and not just forward() is to make sure that the animation starts from beginning, else I would need to reset the animation by calling the reset() method and then call forward().

Next we need to dispose the animations when the widget is disposed in order to avoid memory leaks, so overwrite the dispose method of the Game widget and add the following code:

dispose method in game.dart

Next we need to pass the move and scale animation to the TileBoardWiget, so open the components/tile_board.dart and replace this line:

TileBoardWidget constructor in components/tile_board.dart

with this code:

TileBoardWidget constructor with move and scale animation as required parameters in components/tile_board.dart

This adds the moveAnimation and the scaleAnimation as the required parameter to the TileBoardWidget.
So go back to the lib/game.dart and pass the _moveAnimation and _scaleAnimation as parameters to the TileBoardWidget:

Passing move and scale animations to the TileBoardWidget in game.dart

Next we need to add the AnimatedTile widget which will render the tile itself, so under components folder create new file animated_tile.dart and add the following code:

components/animated_tile.dart

It might look too much code but don’t panic, let’s break it down.

So the AnimatedTile extends the AnimatedWidget which will listen to the 2 animations we passed to the listenable parameter and trigger a rebuild when needed and also it’s the most recommended approach when working with Animations.

Apart from the listenable and the key parameter the widget has few custom parameters included:

moveAnimation — The curved move animation we created earlier will be passed to each tile.
scaleAnimation — The curved scale animation we created earlier.
tile — The Tile object holding the index, number and etc. for the tile.
size — The size of the tile based on which the left/top position will be calculated

top/left — The current top/left position for the tile.
nextTop/nextLeft — The position to which the tile will be moved (if any)

In order to animate the movement using AnimationController (in this case the CurvedAnimation) we need to tell it how the animation needs to run, in this case we need to tell it how the animation should run from point A to point B and we can achieve that using Tweens. So tweens are used to interpolate a value across a range, in this case that would be to interpolate the movement from top/left to nextTop/nextLeft using the corresponding methods per tile, which is why we have top/left variables of type Animation<double> which represent the start position of the tile and nextTop/nextLeft variables which are also of type Animation<double> and the end position of the tile.

And for the scale animation we will also use Tweens more specifically TweenSequence which allow us to pass multiple tweens to be executed in sequence and in this case in order to achieve that pop effect we want the tile to scale up from 1.0 to 1.5 and then scale down from 1.5 back to 1.0 and all that should be executed 50/50 for the duration of the Sequenced Tween (which is why we have weight set tot 50% for each Tween).

And on the bottom we say that the scale animation should be used ONLY if the current tile is merged already which is why we’ve marked them as merged true in the BoardManager’s merge method.

Now open the components/tile_board.dart file and replace this code:

Basic Widget representing the Tile on the board in components/tile_board.dart

with this line of code:

AnimatedTile Widget in components/tile_board.dart

As you’ve noticed already the actual tile (the Container with the decoration) is being passed as child to the AnimatedTile, the reason for this is to optimise the performances as the Tile won’t change for the duration of its movement (apart from its position of course). So the styling will stay same except for the position and the number of the tile.

Now before we build and test the game we need to make sure the move method get’s called when user swipes on the screen. So go ahead and open lib/game.dart and replace this line:

onSwipe event in game.dart

with this line of code:

Call move method on swipe on Android and iOS in game.dart

And again here we see similar approach like for the endRound method, the move method returns a boolean whether a movement has happened or not, and if so, we will start the move animation.

Now we can finally build and run the application. We can do that by either pressing F5 in vscode which would run the app in debug mode on the device or run the following command in terminal:

Run the flutter app from terminal.

Probably you’ve noticed that even in debug mode the game and animations run smoothly that’s how powerful Flutter is. But still running it in release mode the performances are even grater.

Now let’s see how the flow works:

  • The user Swipes in some direction
  • We call the move method passing the direction
  • The logic calculates the nextIndex and assigns it to the tile
  • The state gets updated
  • Based on the current screen size and the size of the tile and the index, the top/left position are retrieved (point A) and the top/left position for the the nextIndex are retrieved (point B)
  • Having the positions for which the tile will move from point A to point B, we use Tweens which will interpolate those values and do the transitions.
  • When the movement finishes we need to merge the tiles that are on the same index and mark them merged: true
  • We need to mark them merged: true, so that we can start the pop effect only for the tiles that have been merged.
  • After the move animation finishes and the tiles are merged we update the state and at the same time start the scale animation which gives us the pop effect.
  • After the pop effect ends we need to end the round.
  • We end the round by setting the merged tiles to merged: false
  • And then the user either: won the game if a tile with number 2048 exists, lost the game if there are no more tiles to add on the board, do a merge if possible and then end the round or we start the next round right away in case the user swiped too soon while the current round wasn’t finished and we have a movement “queued” using the NextDirectionManager

NOTE: It’s good to keep in mind that any visual change in an app regardless if it’s a color change, position change, text changes, animation running, any widget change, all these means that a state change has happened either in the current widget or it’s parent.

Before we end this post there are few smaller things left to do.

  • We need to connect the BoardManager to the score board
  • Show game won/lost message with a new game/try again button.
  • Start new game when the restart button is pressed
  • Add the undo functionality
  • Make it possible to move the tiles with the arrow keys on the keyboard for Desktop

First let’s connect the score board. Open the components/score_board.dart file and inside the ScoreBoard build method at the very top add the following code:

Watch for state changes for score and best score in components/score_board.dart

Make sure the boardManager is imported:

imports in components/score_board.dart

Next replace this lines of code:

Score Widget in components/score_board.dart

with this line:

Connect Score Widget with Riverpod for the current Score in components/score_board.dart

And this lines of code:

Score Widget for the Best Score in components/score_board.dart

With this lines of code:

Connect the Score Widget with Riverpod for the best Score in components/score_board.dart

this will basically show the value of the current score and best score from the BoardManager.

Next let’s add a message to be shown when the game is over and the user either won or lost the game.

Go ahead and open the components/tile_board.dart file, and as last child of stack widget add the following widget:

Show overlay over the tile board when game is over in components/tile_board.dart

This will show a Widget on top of the board with the message “You win!” or “Game over!” and a button bellow it, with the text “New Game” or “Try again” depending whether the user won or lost the game, and if you’ve noticed at the top of the widget we have an if statement, this means that the widget bellow it will ONLY be rendered if the statement is true in this case if the game is over.

And now when you win or loose a game you will see this message shown covering the whole board (which is why we use the Position.fill widget).

Next we will add the new code to start a new game when user presses the restart button on the board that’s located bellow the score board. We will use the same method we used above.
So open the lib/game.dart file and replace this line:

Restart the game Button in game.dart

With this line:

Call newGame method when Restart Button is pressed in game.dart

This will start a new game for us.

Next we will add the undo functionality. So first open managers/board.dart file and bellow the endRound method add the following code:

Undo method in managers/board.dart

This will “undo” one round only, that is why we keep the last state under the undo parameter of the Board model.
Now let’s use this method, open the lib/game.dart file again and replace this line:

Undo one round button in game.dart

With this line:

Call the undo method when the Undo button is pressed in game.dart

One last thing we need to do is make the tiles be moveable using the arrow keys on the keyboard for Desktop.

So open managers/board.dart again and bellow the undo method add the following code:

onKey method in managers/board.dart

And make sure the needed imports are added:

Imports in managers/board.dart

Now let’s use this function. Open the lib/game.dart file again and replace this line:

Listen to key presses on the keyboard on Desktop in game.dart

With this line:

Call onKey method when key is pressed on the keyboard on Desktop in game.dart

So we are using the KeyboardListener to listen to the key presses, and now when you run the app on desktop or web you will be able to use the arrow keys to move the tiles!

And we are done for this part which was the most important part, you can go ahead and run the game and play few rounds.

In the next part we will see how we can save the state of the game so that we don’t loose progress when we close the game. So go ahead and click the link bellow.

--

--