The emergence and development of Flutter has led to the development of cross-platform game design. The design and logic of the Flutter game can be created in just a few lines of code while maintaining a good UI/UX.

The Flutter is capable of rendering at speeds of up to 60FPS. You can use this ability to build a simple 2D or even 3D game. Keep in mind that developing more complex games in Flutter is not a good idea as most developers will prefer to develop complex applications locally.

In this tutorial, we will recreate one of the earliest computer games of all time. Table tennis. Ping pong is a simple game, so it’s a good place to start. This article is divided into two main sections: the game logic and the user interface, making the build process clearer by focusing on the important parts separately.

Before we get into the build, let’s look at prerequisites and Settings.

A prerequisite for

To understand and write the content of this lesson, you will need the following.

  • Install the Flutter on your machine
  • Working knowledge of Dart and Flutter
  • A text editor

Begin to learn

In this article, we will use alignments (x,y) as vectors (x, Y) representing the positions of the X and y axes on the screen, which will help develop the physics of the game. We’ll also create stateless widgets for some of our variables and declare them in homepage.dart to make the code less bloated and easy to understand.

First, create a Flutter project. Clear the default code in the main.dart file and import the Material. Dart package to include Material Widgets in your application.

Next, create a class named MyApp() and return the MaterialApp(), then create a statefulWidget HomePage() and pass it into the MaterialApp() parameter Home, as shown in the figure below.

import 'package:flutter/material.dart'; import 'package:pong/homePage.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner:false, home: HomePage(), ); }}Copy the code

The game logic

In HomePage(), we need to write some functions and methods to handle math and physics related operations. These include handling collisions, acceleration or deceleration, and in-game navigation.

But first, we need to declare some parameters that will represent the placement of the ball, the players, and the initial score of each player. The code for these parameters should be placed in _HomePageState, which we will refer to in a later article.

// Player variations double playerX = -0.2; Double brickWidth = 0.4; int playerScore = 0; // enemy variable double enemyX = -0.2; int enemyScore = 0; //ball double ballx = 0; double bally = 0; var ballYDirection = direction.DOWN; var ballXDirection = direction.RIGHT; bool gameStarted = false; .Copy the code

We then provide an enumeration for the direction of motion of the ball and brick.

enum direction { UP, DOWN, LEFT, RIGHT }
...

Copy the code

For this game to work, we need to create artificial gravity so that when the ball hits the top brick (0.9) or the bottom brick (-0.9), it will move in the opposite direction. Otherwise, if it doesn’t hit any bricks and instead goes to the top (1) or bottom (-1) of the playing field, it records a loss for the player.

When the ball hits the left (1) or right (-1) wall, it goes in the opposite direction.

void startGame() { gameStarted = true; Timer.periodic(Duration(milliseconds: 1), (timer) { updatedDirection(); moveBall(); moveEnemy(); if (isPlayerDead()) { enemyScore++; timer.cancel(); _showDialog(false); // resetGame(); } if (isEnemyDead()) { playerScore++; timer.cancel(); _showDialog(true); // resetGame(); }}); }...Copy the code

In the above code, we start with a function startGame(), changing the Boolean gameStarted to true, and then we call a Timer() with a duration of one second.

Within the timer, functions like updatedDirection(), moveBall(), moveEnemy() are passed along with an if statement to check if any player has failed. If so, points are accumulated, the timer is cancelled, and a dialog box is displayed.

The following function ensures that the ball is no more than 0.9 aligned, and that when it comes into contact with the brick, it only moves in the opposite direction.

Void updatedDirection() {setState() {//update vertical Dirction if (bally >= 0.9&& playerX + brickWidth>= ballx && playerX <= ballx) { ballYDirection = direction.UP; } else if (bally <= -0.9) {ballYDirection = direction.down; } // update horizontal directions if (ballx >= 1) { ballXDirection = direction.LEFT; } else if (ballx <= -1) { ballXDirection = direction.RIGHT; }}); } void moveBall() {// Vertical movement setState(() {if (ballYDirection == direction.down) {bally += 0.01; } else if (ballYDirection == direction.up) {bally -= 0.01; }}); // Horizontal movement setState(() {if (ballXDirection == direction.left) {ballx -= 0.01; } else if (ballXDirection == direction.right) {ballx += 0.01; }}); }...Copy the code

In addition, if the ball hits the left or right side of the court, it will go in the opposite direction.

void moveLeft() { setState(() { if (! (playerx-0.1 <= -1)) {playerX -= 0.1; }}); } void moveRight() { if (! (playerX + brickWidth >= 1)) {playerX += 0.1; }}...Copy the code

The moveLeft() and moveRight() functions help to control the movement of our bricks from left to right using keyboard arrows. These functions are used in conjunction with the if statement to ensure that the brick does not exceed the width of the two axes of the field.

The function resetGame() returns the player and ball to their default positions.

void resetGame() { Navigator.pop(context); setState(() { gameStarted = false; ballx = 0; bally = 0; PlayerX = 0.2; EnemyX = 0.2; }); }...Copy the code

Next, we create two functions, isEnemyDead() and isPlayerDead(), which return a Boolean value. They check if either player has lost (if the ball hits the vertical part behind the brick).

bool isEnemyDead(){ if (bally <= -1) { return true; } return false; } bool isPlayerDead() { if (bally >= 1) { return true; } return false; }...Copy the code

Finally, when either side wins, the function _showDialog displays a dialog box. It passes a Boolean value, enemyDied, to distinguish when a player loses a match. It then announces that the non-loser has won the round and uses the winner’s color to display the text “Play again:”

void _showDialog(bool enemyDied) { showDialog( context: context, barrierDismissible: false, builder: // Return object of type Dialog return AlertDialog(elevation: 0.0, shape: Rectangleborder (borderRadius: borderRadius. Circular (10.0)), backgroundColor: Colors. Purple, title: rectangleBorder (borderRadius: borderRadius. Circular (10.0)), backgroundColor: Colors. Center( child: Text( enemyDied?" Pink Wins": "Purple Wins", style: TextStyle(color: Colors.white), ), ), actions: [ GestureDetector( onTap: resetGame, child: ClipRRect( borderRadius: BorderRadius.circular(5), child: Container( padding: EdgeInsets.all(7), color: Colors.purple[100], child: Text( "Play Again", style: TextStyle(color:enemyDied?Colors.pink[300]: Colors.purple[000]), )), ), ) ], ); }); }Copy the code

The user interface

Now, we’ll start developing the user interface.

In the widgetBuild in homepage.dart file, add the following code.

return RawKeyboardListener( focusNode: FocusNode(), autofocus: false, onKey: (event) { if (event.isKeyPressed(LogicalKeyboardKey.arrowLeft)) { moveLeft(); } else if (event.isKeyPressed(LogicalKeyboardKey.arrowRight)) { moveRight(); } }, child: GestureDetector( onTap: startGame, child: Scaffold( backgroundColor: Colors.grey[900], body: Center( child: Stack( children: [gameStarted], // Top brick brick (enemyX, -0.9, brickWidth, true), //scoreboard Score(gameStarted,enemyScore,playerScore), // ball Ball(ballx, bally), // //bottom brick Brick(enemyX, 0.9, brickWidth, false)],))),),);Copy the code

In the code, we return RawKeyboardListener(), which will provide left-to-right motion as we build on the network. This can also be replicated on touch-screen devices.

The GestureDetector() widget provides onTap functionality for calling the function startGame written in the logic above. A Scaffold() subroutine was also written to specify the background color and body of the application.

Next, create a class called Welcome and pass in a Boolean value to check if the game has started. If the game has not yet started, the “click to play” text will become visible.

class Welcome extends StatelessWidget { final bool gameStarted; Welcome(this.gameStarted); Override Widget build(BuildContext context) {return Container(alignment: alignment (0, -0.2), child: Text( gameStarted ? "": "T A P T O P L A Y", style: TextStyle(color: Colors.white), )); }}Copy the code

Now we can create another class, Ball, that deals with Alignment(x,y) to the Ball’s design and its position at each point in the field. We pass these parameters through a constructor to get liquidity, like this.

class Ball extends StatelessWidget { final x; final y; Ball(this.x, this.y); @override Widget build(BuildContext context) { return Container( alignment: Alignment(x, y), child: Container( decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.white), width: 20, height: 20, ), ); }}Copy the code

Now let’s design the Brick class to handle the design, color, position, and player type of the Brick.

Here, we use a mathematical equation (Alignment((2* x +brickWidth)/(2-BrickWidth), y)) to convey the position of the X and y axes.

class Brick extends StatelessWidget {
 final x;
 final y;
 final brickWidth;
 final isEnemy;
 Brick( this.x, this.y, this.brickWidth, this.isEnemy);
 @override
 Widget build(BuildContext context) {
  return Container(
    alignment: Alignment((2* x +brickWidth)/(2-brickWidth), y),
    child: ClipRRect(
     borderRadius: BorderRadius.circular(10),
     child: Container(
       alignment: Alignment(0, 0),
       color: isEnemy?Colors.purple[500]: Colors.pink[300],
       height: 20,
       width:MediaQuery.of(context).size.width * brickWidth/ 2,
       ),
    ));
 }
}

Copy the code

Finally, the Score class should be placed directly below the build widget in homepage.dart; It shows the score of each player.

Create a constructor for the variables enemyScore and playerScore to handle each player’s score and check for gameStarted to see if the game has started. This will display the contents of Stack(), or an empty Container().

class Score extends StatelessWidget { final gameStarted; final enemyScore; final playerScore; Score(this.gameStarted, this.enemyScore,this.playerScore, ); @override Widget build(BuildContext context) { return gameStarted? Stack(children: [ Container( alignment: Alignment(0, 0), child: Container( height: 1, width: Of (context).size. Width / 3, color: color.grey [800],)), Container(alignment: alignment (0, -0.3), child: Text( enemyScore.toString(), style: TextStyle(color: Colors.grey[800], fontSize: 100), )), Container( alignment: Alignment(0, 0.3), Child: Text(playerScore.toString(), style: TextStyle(color: color.grey [800], fontSize: 100), )), ]): Container(); }}Copy the code

The GIF below shows a test of the game.

conclusion

In this article, we’ve covered alignments, RawKeyboardListener, Widgets, Booleans, ClipRect for containers, and mathematical functions in our code to recreate the game Pong. The game can also be improved by increasing the number of balls or reducing the length of bricks to make it more complex.

I hope this article is as helpful and interesting as the process of building and documenting it. Feel free to use the principles in this article to recreate another classic game, or invent a new one. You can find a link to this article’s code on GitHub.

The postBuilding a 2D game with Flutterappeared first onLogRocket Blog.