Use GridView to prepare a basic board for a puzzle game | Building a Puzzle App in Flutter – Part 1

This article focusses on building a GridView to represent a 2D-List of items. It is the first part of building a game described in the following section. More parts will follow as I worked on them.

GridView - Long Puzzle Board

As a developer and gamer, I surely have my favorite game for mobile devices. It is called Puzzle&Dragons and I love it for over 6 years now. As part of my learnings for Flutter development, I decided to try to do my own small version of the game. I want to build a gacha game with puzzle gameplay. My experiences with developing the Flutter App I will share with you on my blog. As the most interesting part for me is the battle gameplay I will start with that one. The goal is to have a board of objects from which I can drag one and move it around for some seconds. After that time all connected objects of the same time (for example fire objects) that are more than three in the row will be eliminated and counted as an attack. randomly new orbs spawn from the top and fill the board. If there are connections again they attack again until there is no more. Next, the opponent can attack and after it is me puzzling again.

The goal of Part 1

To puzzle some objects around the board I need the board first. This post will be about the creation of this board. The result you can see in the picture on top. It is a pretty simple UI. The board is just 5 rows x 6 columns big. Each field of the board will have a colored background to separate the fields visually. On each of the fields will be one object that represents an element of the set: Fire (Red), Water (Blue), Grass (Green), Light (yellow) and dark (purple). That’s it for the first part of the game building. In the first place, I got inspired for the board building by Sriram Thiagarajan but there are differences in some points.

Running the app

I will start at the outer UI elements and write about the deeper ones of the tree later. First of all, this game shall be a portrait game only. The design I am going to implement would break on horizontal views by overlapping the screen. I make sure that the app orientation is not changeable by setting the prefferedOrientations in the main function. After I run my App.

void main() { 
  SystemChrome.setPreferredOrientations([DeviceOrientation.portraitDown]);
  return runApp(BoardPuzzleApp()); 
}

For the first version, I just want to show the board. Nothing else is important for me – no fancy start animation, no awesome landing screen, just the board. Because of that, I do not require an awesome theme option. For now, I will use a standard MaterialApp with the default ThemeData. The home page of the application is the screen my battle will be on so that I can pass the BattlePage widget to home.

class BoardPuzzleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Board Puzzle App',
      theme: ThemeData(),
      home: BattlePage(),
    );
  }
}

Next, I will have a look at my BattlePage. This is structured pretty easy as well. There is a split into two vertical parts of the screen. The upper part may show some battle animations between my creatures and the enemy creatures. The bottom part is the puzzle board I want to create – so for user interaction. As I only concentrate on the puzzle part the code for this structure is as follow:

class BattlePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: <Widget>[
          Expanded(child: Container()),
          SafeArea(child: Board()),
        ],
      ),
    );
  }
}

So the upper part only fills the unneeded space with the Container widget. I wrap the Board in the bottom into a SafeArea widget because I want the complete board to be shown and touchable (especially because I am using an iPhone X and the board would be under a part of the phone I do not want to touch while playing).

The board structure as GridView

The board only represents some items as colored circles in a GridView. In the future, I want to drag those circles and move them around. Right now, I just want to present one state. I am handling my state as a List of Lists of Strings. By this, I get a 2d array-like structure with the wanted structure for the GridView. I know that Strings are not a good representation for what I want to display on my board but it is one of the easiest for now. Also, I do not need to care about state management right now. The state will always be the same. I want to write my next article about this game about the bloc pattern in this app. My first board has as state this list:

final List<List<String>> gridState = [
  ['R', 'B', 'G', 'Y', 'D', 'R'],
  ['R', 'Y', 'D', 'D', 'R', 'G'],
  ['B', 'G', 'B', 'Y', 'Y', 'B'],
  ['R', 'G', 'G', 'B', 'D', 'R'],
  ['D', 'B', 'R', 'D', 'R', 'G'],
];

The Strings are the first letter of the color I want to display on each field of the board. For example, ‘R’ should be mapped to red later. The board should now be a GridView which adopts this state. So technically I have 6 columns and 5 rows. This means that the height of my GridView has to be 5/6 of the width of my board as each field should have a 1:1 ratio. The following board class definition structure is used to implement this behavior:

class Board extends StatelessWidget {
  final List<List<String>> gridState = // see above ...

  @override
  Widget build(BuildContext context) {
    var gridStateLength = gridState.length * gridState.first.length;
    var boardWidth = MediaQuery.of(context).size.width;
    return Column(children: <Widget>[
      Container(
        height: gridState.length / gridState.first.length * boardWidth,
        width: boardWidth,
        child: GridView.count(
          physics: NeverScrollableScrollPhysics(), // disable scrolling
          crossAxisCount: gridState.first.length,
          childAspectRatio: 1,
          children: List.generate(gridStateLength, (index) {
            return BoardField(
              gridState: gridState,
              index: index,
            );
          }),
        ),
      ),
    ]);
  }
}

The number of items in my GridView should equal the amounts of items in my gridState. Because all rows do have the same length I can multiply the number of columns with the number of items in the first row. As mentioned the height of the board is calculated by the length of column / row (5/6) multiplied with the width of the board.

Fields of the board in the GridView

Each child of this GridView will be one field. Each field is defined for me as BoardField and calculates itself depending on the state and the index it shows. With this information, the BoardField can calculate the x and y position of the field. With this information, we can get the String identifier of the gridState for this field. This information will be passed to the visible circle of this field. Also, each field will have a background depending on the index of the field. The idea is to split the fields visually in a brighter and darker background. So the complete implementation of my field is:

class BoardField extends StatelessWidget {
  BoardField({Key key, @required this.gridState, this.index}) : super(key: key);

  final List<List<String>> gridState;
  final int index;

  @override
  Widget build(BuildContext context) {
    int x, y = 0;
    x = (index / gridState.first.length).floor();
    y = (index % gridState.first.length);
    var dark = x % 2 == 0 ? index % 2 == 0 : index % 2 == 1;

    return Container(
      decoration: BoxDecoration(
        border: Border.all(color: Colors.black, width: 0.5),
        color: dark ? Colors.brown[500] : Colors.brown[700]),
      child: Center(
        child: BoardItem(
          itemIdentifier: gridState[x][y],
        ),
      ),
    );
  }
}

FYI: I wrapped my fields into GestureDetectors in the public git commit that are not necessary right now.

Items on each field

The last part of the current app version is the circle Item. Other apps use candies at this point for example 😉 I like the basic idea of having simple circles. For this, I have to map each String identifier into a colored version of the circle. I can use a switch statement for this mapping (it will even get better if I do not use a String anymore but an enum). The returned widget will be the same except the color so that I can use a common function for the returning widget. This results in the following widget implementation of my BoardItem:

class BoardItem extends StatelessWidget {
  BoardItem({Key key, @required this.itemIdentifier}) : super(key: key);

  String itemIdentifier;

  @override
  Widget build(BuildContext context) {
    switch (itemIdentifier) {
      case 'R':
        return CircleItem(color: Colors.red,);
        break;
      case 'B':
        return CircleItem(color: Colors.blue);
        break;
      case 'G':
        return CircleItem(color: Colors.green[700]);
        break;
      case 'Y':
        return CircleItem(color: Colors.yellow[600]);
        break;
      case 'D':
        return CircleItem(color: Colors.deepPurple);
        break;
      default:
        return Text(itemIdentifier.toString());
    }
  }
}

class CircleItem extends StatelessWidget {
  const CircleItem({Key key, @required this.color}) : super(key: key);

  final Color color;
  
  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(builder: (context, constraint) {
      return Icon(
        Icons.blur_circular,
        size: constraint.biggest.height,
        color: color,
      );
    });
  }
}

That’s it. If I start the app now, I get the simple following view with the board on the bottom:

GridView - Ready Screen written in Flutter

Where to go from here

My next two goals in this project are better state management (probably with bloc pattern) and the movement of the items on the board: So drag one item, move it and if the movement passes another field I want the items on those fields to change. If I wrote those articles I will edit them in here later.

If you want to have a look at the complete source code of this article the repository is public on GitHub and the commit including everything is 0df2cbf.

Also if this just helped you a little bit I would be really happy if you tell me in the comments or on twitter.

And as this is only my second article on my blog the only other article I can recommend you right now is my very first one: Why start a Flutter blog?

Have a wonderful day!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.