打造经典游戏:用 Flutter 实现贪吃蛇之旅

引言

在众多经典游戏中,贪吃蛇无疑是一个充满怀旧感的选择。通过简单的规则和直观的玩法,它吸引了无数玩家。今天,我将带您一步步用 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;
  }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值