以下是基于 Flutter 编写的一个简单的俄罗斯方块游戏。
在此过程中将使用以下 Flutter 包:
- flutter/material.dart (提供 Material 风格的组件和视觉效果)
- flutter/widgets.dart (各种基础的widget)
- flutter/animation.dart (用于动画处理)
- dart:math (用于生成随机数)
实现思路:
- 游戏分为游戏区和下一个方块区两个区域。
- 每次出现的方块为七种,而出现的形状和颜色随机,方块旋转时进行碰撞检测及旋转调整,碰撞时进行堆叠处理。
- 当满足整行的方块都完整时消行,累计得分。当游戏区完全被填满方块时,游戏结束。
代码实现如下:
import 'dart:math';
import 'package:flutter/material.dart';
void main() => runApp(Tetris());
class Tetris extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Tetris Game',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {
final int _numOfRows = 20;
final int _numOfColumns = 10;
final double _squareSize = 20.0;
late AnimationController _controller;
late Animation<double> _animation;
late List<Point<int>> _activePosition;
late List<Point<int>> _passivePosition;
late List<List<bool>> _filledPosition;
late List<Color> _colorList;
late List<int> _scoreList;
late int _currentScore = 0;
late int _highestScore = 0;
late int _numOfNextSquare;
late List<Point<int>> _nextSquarePosition;
late Timer _timer;
late Random _random;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 100),
vsync: this,
)..addListener(() {
setState(() {});
});
_animation = Tween<double>(begin: 0.0, end: 1.0).animate(_controller);
_activePosition = [Point(0, 3), Point(0, 4), Point(1, 3), Point(1, 4)];
_passivePosition = [];
_filledPosition = List.generate(
_numOfRows,
(_) => List<bool>.generate(_numOfColumns, (_) => false),
);
_colorList = [
Colors.redAccent,
Colors.lightGreenAccent,
Colors.yellowAccent,
Colors.blueAccent,
Colors.deepPurpleAccent,
Colors.tealAccent,
Colors.orangeAccent
];
_scoreList = [0, 100, 300, 500, 800];
_numOfNextSquare = _random.nextInt(7);
_nextSquarePosition = _getNextSquare(_numOfNextSquare);
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
if (_activePosition.any((p) => p.x == _numOfRows - 1) ||
_activePosition.any((p) => _filledPosition[p.x + 1][p.y])) {
_timer.cancel();
_passivePosition.addAll(_activePosition);
_activePosition.clear();
_checkIfLineIsComplete();
_currentScore += _scoreList[_scoreList.indexWhere((s) => s > _currentScore) - 1];
if (_currentScore > _highestScore) {
_highestScore = _currentScore;
}
_activePosition = _getNextSquare(_numOfNextSquare);
_fillActivePosition(true);
_numOfNextSquare = _random.nextInt(7);
_nextSquarePosition = _getNextSquare(_numOfNextSquare);
if (_activePosition.any((p) => _filledPosition[p.x][p.y])) {
_showGameOverDialog();
} else {
_animateMovement();
}
} else {
_fillActivePosition(false);
setState(() {
_activePosition = _activePosition.map((p) => p + Offset(1, 0) as Point<int>).toList();
});
_fillActivePosition(true);
}
});
}
@override
void dispose() {
_controller.dispose();
_timer.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter Tetris Game'),
centerTitle: true,
),
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Colors.amber.shade50, Colors.amber.shade900],
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(top: 50, left: 20),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
SizedBox(
width: 6 * _squareSize,
height: 6 * _squareSize,
child: GridView.builder(
physics: NeverScrollableScrollPhysics(),
itemCount: 4,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 1,
),
itemBuilder: (context, index) {
int row = _nextSquarePosition[index].x;
int column = _nextSquarePosition[index].y;
return Container(
color: _colorList[_numOfNextSquare],
margin: EdgeInsets.all(1),
decoration: BoxDecoration(
border: Border.all(width: 1, color: Colors.black),
borderRadius: BorderRadius.circular(5),
),
child: AnimatedOpacity(
opacity: _animation.value,
duration: Duration(milliseconds: 100),
child: _activePosition.any((p) => p == Point(row, column))
? Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withOpacity(0.7),
),
)
: null,
),
);
},
),
),
SizedBox(height: 50),
Text('Score: $_currentScore', style: TextStyle(fontSize: 24)),
SizedBox(height: 20),
Text('Highest Score: $_highestScore', style: TextStyle(fontSize: 24)),
SizedBox(height: 50),
FloatingActionButton(
onPressed: () {
setState(() {
_filledPosition = List.generate(
_numOfRows,
(_) => List<bool>.generate(_numOfColumns, (_) => false),
);
_activePosition.clear();
_passivePosition.clear();
_currentScore = 0;
_activePosition = _getNextSquare(_numOfNextSquare);
_fillActivePosition(true);
});
_timer.cancel();
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
if (_activePosition.any((p) => p.x == _numOfRows - 1) ||
_activePosition.any((p) => _filledPosition[p.x + 1][p.y])) {
_timer.cancel();
_passivePosition.addAll(_activePosition);
_activePosition.clear();
_checkIfLineIsComplete();
_currentScore +=
_scoreList[_scoreList.indexWhere((s) => s > _currentScore) - 1];
if (_currentScore > _highestScore) {
_highestScore = _currentScore;
}
_activePosition = _getNextSquare(_numOfNextSquare);
_fillActivePosition(true);
_numOfNextSquare = _random.nextInt(7);
_nextSquarePosition = _getNextSquare(_numOfNextSquare);
if (_activePosition.any((p) => _filledPosition[p.x][p.y])) {
_showGameOverDialog();
} else {
_animateMovement();
}
} else {
_fillActivePosition(false);
setState(() {
_activePosition =
_activePosition.map((p) => p + Offset(1, 0) as Point<int>).toList();
});
_fillActivePosition(true);
}
});
},
tooltip: 'Restart',
child: Icon(Icons.refresh),
),
],
),
),
Expanded(
child: Container(
height: _numOfRows * _squareSize,
child: GridView.builder(
physics: NeverScrollableScrollPhysics(),
itemCount: _numOfRows * _numOfColumns,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: _numOfColumns,
childAspectRatio: 1,
),
itemBuilder: (context, index) {
int row = index ~/ _numOfColumns;
int column = index % _numOfColumns;
if (_activePosition.any((p) => p.x == row && p.y == column)) {
return Container(
color: _colorList[_numOfNextSquare],
margin: EdgeInsets.all(1),
);
} else if (_passivePosition.any((p) => p.x == row && p.y == column)) {
return Container(
color: _colorList[_passivePosition
.indexWhere((p) => p.x == row && p.y == column)],
margin: EdgeInsets.all(1),
);
} else {
return Container(
color: _filledPosition[row][column] ? Colors.white : Colors.transparent,
margin: EdgeInsets.all(1),
);
}
},
),
),
),
],
),
),
);
}
void _showGameOverDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Game Over!'),
content: Text('Would You like to restart?'),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text(
'NO',
style: TextStyle(fontSize: 24),
),
),
TextButton(
onPressed: () {
Navigator.pop(context);
setState(() {
_filledPosition = List.generate(
_numOfRows,
(_) => List<bool>.generate(_numOfColumns, (_) => false),
);
_activePosition.clear();
_passivePosition.clear();
_currentScore = 0;
_activePosition = _getNextSquare(_numOfNextSquare);
_fillActivePosition(true);
});
_timer.cancel();
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
if (_activePosition.any((p) => p.x == _numOfRows - 1) ||
_activePosition.any((p) => _filledPosition[p.x + 1][p.y])) {
_timer.cancel();
_passivePosition.addAll(_activePosition);
_activePosition.clear();
_checkIfLineIsComplete();
_currentScore +=
_scoreList[_scoreList.indexWhere((s) => s > _currentScore) - 1];
if (_currentScore > _highestScore) {
_highestScore = _currentScore;
}
_activePosition = _getNextSquare(_numOfNextSquare);
_fillActivePosition(true);
_numOfNextSquare = _random.nextInt(7);
_nextSquarePosition = _getNextSquare(_numOfNextSquare);
if (_activePosition.any((p) => _filledPosition[p.x][p.y])) {
_showGameOverDialog();
} else {
_animateMovement();
}
} else {
_fillActivePosition(false);
setState(() {
_activePosition =
_activePosition.map((p) => p + Offset(1, 0) as Point<int>).toList();
});
_fillActivePosition(true);
}
});
},
child: Text(
'YES',
style: TextStyle(fontSize: 24),
),
),
],
),
);
}
List<Point<int>> _getNextSquare(int numOfSquare) {
switch (numOfSquare) {
case 0:
// Square
return [Point(0, 4), Point(0, 5), Point(1, 4), Point(1, 5)];
case 1:
// L1
return [Point(0, 3), Point(1, 3), Point(2, 3), Point(2, 4)];
case 2:
// L2
return [Point(0, 4), Point(1, 4), Point(2, 4), Point(2, 3)];
case 3:
// I
return [Point(0, 3), Point(1, 3), Point(2, 3), Point(3, 3)];
case 4:
// S1
return [Point(0, 4), Point(0, 5), Point(1, 3), Point(1, 4)];
case 5:
// S2
return [Point(0, 3), Point(0, 4), Point(1, 4), Point(1, 5)];
default:
// T
return [Point(0, 4), Point(1, 3), Point(1, 4), Point(1, 5)];
}
}
void _animateMovement() {
_controller.reset();
_controller.forward();
}
void _fillActivePosition(bool filled) {
_activePosition.forEach((p) {
_filledPosition[p.x][p.y] = filled;
});
}
void _checkIfLineIsComplete() {
for (int i = _numOfRows - 1; i >= 0; i--) {
if (_filledPosition[i].every((filled) => filled)) {
setState(() {
for (int j = i - 1; j >= 0; j--) {
_filledPosition[j + 1].clear();
_filledPosition[j + 1].addAll(_filledPosition[j]);
}
_filledPosition[0].fillRange(0, _numOfColumns, false);
});
_checkIfLineIsComplete();
break;
}
}
}
}
网友评论