一、本篇文章主要是来讲解下俄罗斯方块游戏的开发思路(当然可能不是最好的思路),博客文章顶部有代码(仅供参考)

二、效果图

视频效果图地址

三、UI页面思路拆解

  • 游戏的主界面两部分组成,上面为15*10的格子用来放置方块,下面为操作按钮和显示当前分数(也就是消失了多少行方块)
  • 每个方块的大小计算:根据当前屏幕的宽度、显示的列数、方块之间的间隙
 Size _calcRectSize(double screenWidth, int count) {
    double remainderWidth = screenWidth - ((count - 1) * widget.gap);
    return Size.square(remainderWidth / count);
  }

计算出每个方块的大小,也就可以计算出格子所占的高度了,接下来通过CustomPaint进行绘制游戏背景即可,如下

class GameBgWidget extends StatelessWidget {
  ///省略部分代码...
  
  Widget build(BuildContext context) {
    return CustomPaint(
      size: Size.fromHeight(height),
      painter: _GameBgPainter(parent: this, size: rectSize),
    );
  }
}
  • 同理,绘制游戏过程中显示的UI也应当是和背景一模一样的大小,最后将这两个上下层叠 就达到了方块在格子中移动的效果,整体UI布局如下:
class GameWidget extends StatefulWidget {
  final int colCount;
  final int rowCount;
  final double gap;

  const GameWidget({
    Key? key,
    required this.colCount,
    required this.rowCount,
    required this.gap,
  }) : super(key: key);

  
  State<StatefulWidget> createState() => _GameWidgetState();
}

class _GameWidgetState extends BaseState<GameWidget> {

  
  Widget build(BuildContext context) {
    return Column(
      children: [
        LayoutBuilder(
          builder: (_, constrains) {
            //计算方块的大小
            final size = _calcRectSize(constrains.maxWidth, widget.colCount);
            //计算所占的高度
            final height = _calcCanvasHeight(size);
            return Stack(
              children: [
              	//游戏背景组件
                GameBgWidget(
                  colCount: widget.colCount,
                  rowCount: widget.rowCount,
                  gap: widget.gap,
                  rectSize: size,
                  height: height,
                ),
                //游戏进行中的数据组件
                GameDataWidget(
                  colCount: widget.colCount,
                  rowCount: widget.rowCount,
                  gap: widget.gap,
                  rectSize: size,
                  height: height,
                  scoreCallback: (line) {
                  },
                ),
              ],
            );
          },
        ),
        Expanded(
          child: Container(
            color: Colors.blueGrey,
            ///省略操作按钮代码...
            ),
        ),
      ],
    );
  }

四、接下来就是重点了游戏逻辑的开发思路,一个怎样的方法会比较好处理数据,下面将为大家说说我的思路

1、 这里可以将整个游戏界面(格子)看成一个15*10的二维数组,当某一个格子内有方块的时候,那么对应的二维数组位置就不为空,如下表示:

  • 当假设开始加入一个“O”型方块的时候,就会变成如下这样

还有一点:这里为什么二维数组里面装的是Color、null而不是0、1呢,原因就是每个方块的颜色会随机生成,同时当方块消失的时候上面的方块要进行下移,所以就需要知道每个格子需要绘制什么颜色的方块

2、 游戏中会产生的所有方块类型如下:

可以讲如上七种方块大致形象称为"O,Z,S,T,J,L,I"类型

  • 那现在的重点就是又该如何表示这些方块了?其实和上面同理也可以使用一个二维数组来进行表示,当某个位置没有方块则为0有则为1如下:
    • “O” 2*2
    • “Z” 2*3
    • “S” 2*3
    • “T” 2*3
    • “J” 3*2
    • “L” 3*2
    • “I” 4*1

3、现在就可以抽象出一个方块的模板来了

abstract class BaseBlock {
  List<Color> allColors = [
    Colors.amber,
    Colors.lightBlue,
    Colors.red,
    Colors.blue,
    Colors.pink,
    Colors.lightGreen,
    Colors.purpleAccent,
  ];

  ///方块数据
  List<List<int>> block;

  ///方向
  BlocDirection direction;

  ///颜色
  late Color color;
  
  BaseBlock({
    required this.block,
    this.direction = BlocDirection.top,
  }) {
    color = allColors[Random().nextInt(allColors.length)];
  }

  ///宽度
  int get width => block.first.length;

  ///高度
  int get height => block.length;

  ///旋转
  void rotate() {
    int nextIndex = (direction.index + 1) % BlocDirection.values.length;
    direction = BlocDirection.values[nextIndex];
  }
}
  • 有了模板实现起来就很快了,举几个例子
    • "O"型方块
    class OBlock extends BaseBlock {
      OBlock()
          : super(block: [
              [1, 1],
              [1, 1]
            ]);
    }
    
    • "T"型方块
    class TBlock extends BaseBlock {
      TBlock()
          : super(block: [
              [1, 1, 1],
              [0, 1, 0]
            ]);
    
      
      void rotate() {
        var currDirection = direction;
        super.rotate();
        if (currDirection == BlocDirection.top) {
          block = [
            [0, 1],
            [1, 1],
            [0, 1]
          ];
        } else if (currDirection == BlocDirection.right) {
          block = [
            [0, 1, 0],
            [1, 1, 1]
          ];
        } else if (currDirection == BlocDirection.bottom) {
          block = [
            [1, 0],
            [1, 1],
            [1, 0]
          ];
        } else {
          block = [
            [1, 1, 1],
            [0, 1, 0]
          ];
        }
      }
    }
    

    因为每一个方块可以进行无限制旋转,所以T型方块重写rotate函数进行实现,也就是将旋转后将新block数据进行更新即可。

五、剩下的就是对方块进行向下移动,左移、右移动同时需要进行判断是否可以向左、向右、向下移动

  • 对于生成的方块我们可以使用(x,y)来进行标记位置,向下移动y+1,向左移动x-1,向右移动x+1,同时对边界进行处理,也要判断要移动的位置是否已经有方块了
  • 最后对于某一行是否填满可以进行消除的判断就更简单些了,需要判断每一行是否都不为null,然后对应的行删除,然后在 在0下标插入一行,这样就完成了消除同时保证了二维数组的正确性
Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐