引言
在众多经典游戏中,贪吃蛇无疑是一个充满怀旧感的选择。通过简单的规则和直观的玩法,它吸引了无数玩家。今天,我将带您一步步用 Flutter 框架实现这个经典游戏。不论您是 Flutter 新手还是经验丰富的开发者,您都将在这个旅程中收获许多有趣的知识和实用的技巧。这段代码可以在全平台运行,包括macOS和iOS。
项目结构概览
在我们的项目中,我们将构建一个包含难度选择、模式选择以及完整贪吃蛇游戏逻辑的应用。具体的代码结构如下:
DifficultySelection:选择游戏难度。
ModeSelection:选择游戏模式(墙壁碰撞或忽略墙壁)。
SnakeGameHome:游戏主界面和游戏逻辑。
SnakePainter:负责绘制游戏中的蛇和食物。
第一步:设置应用框架
首先,我们通过 Flutter 的 MaterialApp 组件设置基本的应用框架。应用的主题、标题和初始路由都是在这里定义的:
class SnakeGame extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: '贪吃蛇',
theme: ThemeData(
primarySwatch: Colors.green,
),
home: DifficultySelection(),
);
}
}
第二步:添加难度选择界面
在 DifficultySelection 中,我们提供了三个难度选项(简单、中等、困难),并在用户选择后进入游戏模式选择界面:
class DifficultySelection extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('选择难度')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('选择你的难度:', style: TextStyle(fontSize: 24)),
SizedBox(height: 20),
_buildDifficultyButton(context, '简单', 300),
_buildDifficultyButton(context, '中等', 200),
_buildDifficultyButton(context, '困难', 100),
],
),
),
);
}
}
第三步:实现游戏逻辑
在 SnakeGameHome 类中,我们实现了蛇的移动逻辑和食物的生成。通过监听用户的手势(上下左右滑动),我们可以控制蛇的方向:
void _moveSnake() {
// ...移动逻辑
if (newHeadX == food.x && newHeadY == food.y) {
// 吃到食物
}
// ...检测碰撞
}
当蛇碰到墙壁或自身时,游戏会结束并提示用户。我们使用对话框功能来提示得分并提供重新开始的选项:
void _gameOver() {
timer?.cancel();
_showGameOverDialog();
}
第四步:绘制游戏界面
我们通过 CustomPainter 实现游戏界面的绘制,使用 Canvas 绘制蛇和食物。蛇用绿色表示,食物用红色表示:
class SnakePainter extends CustomPainter {
final List<Point> snake;
final Point food;
SnakePainter(this.snake, this.food);
void paint(Canvas canvas, Size size) {
// ...绘制蛇和食物
}
}
总结与展望
通过这篇文章,我们成功打造了一个简单的贪吃蛇游戏。项目展示了 Flutter 的基本用法,包括状态管理、动画、用户交互和画布绘制。在未来,您可以根据自己的需求扩展游戏功能,例如添加音效、动画、不同的食物效果等等。
希望您能从中获益,并在这个有趣的过程中发现更多 Flutter 的魅力!准备好开始您的开发之旅了吗?让我们共同实现更多精彩的游戏!
完整代码
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(SnakeGame());
class SnakeGame extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: '贪吃蛇',
theme: ThemeData(
primarySwatch: Colors.green,
),
home: DifficultySelection(),
);
}
}
class DifficultySelection extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('选择难度')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('选择你的难度:', style: TextStyle(fontSize: 24)),
SizedBox(height: 20),
_buildDifficultyButton(context, '简单', 300),
_buildDifficultyButton(context, '中等', 200),
_buildDifficultyButton(context, '困难', 100),
],
),
),
);
}
ElevatedButton _buildDifficultyButton(BuildContext context, String label, int speed) {
return ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => ModeSelection(speed: speed)),
);
},
child: Text(label, style: TextStyle(fontSize: 20)),
);
}
}
class ModeSelection extends StatelessWidget {
final int speed;
ModeSelection({required this.speed});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('选择游戏模式')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('选择你的游戏模式:', style: TextStyle(fontSize: 24)),
SizedBox(height: 20),
ElevatedButton(
onPressed: () => _startGame(context, speed, true),
child: Text('墙壁碰撞(碰撞则结束游戏)', style: TextStyle(fontSize: 16)),
),
ElevatedButton(
onPressed: () => _startGame(context, speed, false),
child: Text('忽略墙壁', style: TextStyle(fontSize: 16)),
),
],
),
),
);
}
void _startGame(BuildContext context, int speed, bool wallCollision) {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SnakeGameHome(speed: speed, wallCollision: wallCollision)),
);
}
}
class Point {
final int x;
final int y;
Point(this.x, this.y);
}
class SnakeGameHome extends StatefulWidget {
final int speed;
final bool wallCollision;
SnakeGameHome({required this.speed, required this.wallCollision});
_SnakeGameHomeState createState() => _SnakeGameHomeState();
}
class _SnakeGameHomeState extends State<SnakeGameHome> {
static const int squareSize = 20;
List<Point> snake = [Point(0, 0)];
late Point food;
String direction = 'right';
Timer? timer;
late int rows;
late int cols;
int score = 0;
bool isPaused = false;
void initState() {
super.initState();
timer = Timer.periodic(Duration(milliseconds: widget.speed), _updateGame);
}
void didChangeDependencies() {
super.didChangeDependencies();
_calculateDimensions();
_generateFood();
}
void dispose() {
timer?.cancel();
super.dispose();
}
void _updateGame(Timer timer) {
if (!isPaused) {
setState(() {
_moveSnake();
});
}
}
void _moveSnake() {
var head = snake.first;
int newHeadX = head.x;
int newHeadY = head.y;
switch (direction) {
case 'up':
newHeadY -= 1;
break;
case 'down':
newHeadY += 1;
break;
case 'left':
newHeadX -= 1;
break;
case 'right':
newHeadX += 1;
break;
}
if (widget.wallCollision) {
if (newHeadX < 0 || newHeadX >= cols || newHeadY < 0 || newHeadY >= rows) {
_gameOver();
return;
}
} else {
newHeadX = (newHeadX + cols) % cols;
newHeadY = (newHeadY + rows) % rows;
}
if (newHeadX == food.x && newHeadY == food.y) {
snake.insert(0, Point(newHeadX, newHeadY));
_generateFood();
score++;
} else {
snake.insert(0, Point(newHeadX, newHeadY));
snake.removeLast();
}
for (int i = 1; i < snake.length; i++) {
if (snake[i].x == snake.first.x && snake[i].y == snake.first.y) {
_gameOver();
}
}
}
void _generateFood() {
Random random = Random();
int foodX, foodY;
do {
foodX = random.nextInt(cols);
foodY = random.nextInt(rows);
} while (snake.any((segment) => segment.x == foodX && segment.y == foodY));
food = Point(foodX, foodY);
}
void _calculateDimensions() {
final double screenWidth = MediaQuery.of(context).size.width;
final double screenHeight = MediaQuery.of(context).size.height;
rows = (screenHeight * 0.6 / squareSize).floor();
cols = (screenWidth * 0.8 / squareSize).floor();
}
void _gameOver() {
timer?.cancel();
_showGameOverDialog();
}
void _showGameOverDialog() {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('游戏结束'),
content: Text('你的得分: $score\n下次好运!'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
_restartGame();
},
child: Text('重新开始'),
),
],
);
},
);
}
void _restartGame() {
snake = [Point(0, 0)];
direction = 'right';
score = 0;
isPaused = false;
_generateFood();
timer = Timer.periodic(Duration(milliseconds: widget.speed), _updateGame);
}
void _changeDirection(String newDirection) {
setState(() {
if (newDirection == 'up' && direction != 'down') direction = 'up';
if (newDirection == 'down' && direction != 'up') direction = 'down';
if (newDirection == 'left' && direction != 'right') direction = 'left';
if (newDirection == 'right' && direction != 'left') direction = 'right';
});
}
void _togglePause() {
setState(() {
isPaused = !isPaused;
if (isPaused) {
timer?.cancel();
} else {
timer = Timer.periodic(Duration(milliseconds: widget.speed), _updateGame);
}
});
}
Widget build(BuildContext context) {
return RawKeyboardListener(
focusNode: FocusNode()..requestFocus(), // 确保获取焦点
onKey: (RawKeyEvent event) {
if (event is RawKeyDownEvent) {
if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
_changeDirection('up');
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
_changeDirection('down');
} else if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
_changeDirection('left');
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
_changeDirection('right');
}
}
},
child: GestureDetector(
onVerticalDragUpdate: (details) {
if (details.delta.dy < 0) _changeDirection('up');
if (details.delta.dy > 0) _changeDirection('down');
},
onHorizontalDragUpdate: (details) {
if (details.delta.dx < 0) _changeDirection('left');
if (details.delta.dx > 0) _changeDirection('right');
},
child: Scaffold(
appBar: AppBar(
title: Text('贪吃蛇'),
actions: [
IconButton(
icon: Icon(isPaused ? Icons.play_arrow : Icons.pause),
onPressed: _togglePause,
),
],
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('得分: $score', style: TextStyle(fontSize: 24)),
SizedBox(height: 20),
SizedBox(
width: cols * squareSize.toDouble(),
height: rows * squareSize.toDouble(),
child: CustomPaint(
painter: SnakePainter(snake, food),
),
),
SizedBox(height: 20),
Text(
'滑动或使用方向键改变方向',
style: TextStyle(fontSize: 16, color: Colors.grey),
),
],
),
),
),
),
);
}
}
class SnakePainter extends CustomPainter {
final List<Point> snake;
final Point food;
SnakePainter(this.snake, this.food);
void paint(Canvas canvas, Size size) {
final paint = Paint();
paint.color = Colors.green;
for (var segment in snake) {
canvas.drawRect(
Rect.fromLTWH(segment.x * 20.0, segment.y * 20.0, 20, 20),
paint,
);
}
paint.color = Colors.red;
canvas.drawRect(
Rect.fromLTWH(food.x * 20.0, food.y * 20.0, 20, 20),
paint,
);
paint.color = Colors.black;
paint.style = PaintingStyle.stroke;
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), paint);
}
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}