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:
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:
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:
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:
with this:
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:
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:
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:
Next we need to pass the move and scale animation to the TileBoardWiget, so open the components/tile_board.dart and replace this line:
with this code:
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:
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:
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:
with this line of code:
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:
with this line of code:
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:
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:
Make sure the boardManager is imported:
Next replace this lines of code:
with this line:
And this lines of code:
With this lines of code:
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:
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:
With this line:
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:
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:
With this line:
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:
And make sure the needed imports are added:
Now let’s use this function. Open the lib/game.dart file again and replace this line:
With this line:
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.