一、本篇文章主要是来讲解下俄罗斯方块
游戏的开发思路(当然可能不是最好的思路),博客文章顶部有代码(仅供参考)
二、效果图
视频效果图地址
三、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
- “O”
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
下标插入一行,这样就完成了消除同时保证了二维数组的正确性