Flutter实战:从零实现俄罗斯方块(三)交互控制与事件处理

Flutter实战:从零实现俄罗斯方块(三)交互控制与事件处理

文章目录

摘要

这是我用Flutter开发俄罗斯方块游戏的第三篇文章,主要讲解游戏的交互控制实现。我会分享如何使用RawKeyboardListener监听键盘事件、如何设计触摸控制按钮、如何管理游戏状态(暂停、结束),以及FocusNode焦点管理的技巧。通过这篇文章,你可以了解到Flutter事件处理的完整流程。

关键词:Flutter、事件处理、RawKeyboardListener、FocusNode、游戏控制、OpenHarmony

前言

在前两篇文章中,我已经实现了俄罗斯方块游戏的数据结构和绘制功能。但游戏还需要玩家能够控制方块才行!

这篇文章我主要解决三个问题:

  1. 如何让键盘控制方块移动和旋转?
  2. 如何添加屏幕按钮方便触摸屏操作?
  3. 如何处理暂停、游戏结束等状态?

系列说明:这是Flutter俄罗斯方块游戏开发系列教程的第3篇也是最后一篇。


一、键盘事件监听

1.1 RawKeyboardListener的基本用法

Flutter提供了RawKeyboardListener Widget来监听键盘事件:

dart 复制代码
RawKeyboardListener(
  focusNode: _focusNode,
  onKey: _handleKeyEvent,
  autofocus: true,
  child: Scaffold(
    // 游戏界面...
  ),
)

三个关键参数

  • focusNode:焦点控制器(必须有)
  • onKey:按键回调函数
  • autofocus:是否自动获取焦点

1.2 如何映射按键到游戏操作?

我在_handleKeyEvent函数中处理按键:

dart 复制代码
void _handleKeyEvent(RawKeyEvent event) {
  // 只处理按键按下事件
  if (event is! RawKeyDownEvent) return;

  if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
    _game.moveLeft();
  } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
    _game.moveRight();
  } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
    _game.moveDown();
  } else if (event.logicalKey == LogicalKeyboardKey.arrowUp ||
             event.logicalKey == LogicalKeyboardKey.space) {
    _game.rotate();
  } else if (event.logicalKey == LogicalKeyboardKey.keyP) {
    _game.togglePause();
  }
}

按键映射表

按键 LogicalKeyboardKey 功能
arrowLeft 左移一格
arrowRight 右移一格
arrowDown 加速下落
arrowUp 旋转方块
Space space 旋转方块
P keyP 暂停/继续

1.3 WASD键位支持

很多游戏玩家习惯用WASD控制,我也加上:

dart 复制代码
void _handleKeyEvent(RawKeyEvent event) {
  if (event is! RawKeyDownEvent) return;

  if (event.logicalKey == LogicalKeyboardKey.arrowLeft ||
      event.logicalKey == LogicalKeyboardKey.keyA) {
    _game.moveLeft();
  } else if (event.logicalKey == LogicalKeyboardKey.arrowRight ||
             event.logicalKey == LogicalKeyboardKey.keyD) {
    _game.moveRight();
  } else if (event.logicalKey == LogicalKeyboardKey.arrowDown ||
             event.logicalKey == LogicalKeyboardKey.keyS) {
    _game.moveDown();
  } else if (event.logicalKey == LogicalKeyboardKey.arrowUp ||
             event.logicalKey == LogicalKeyboardKey.keyW ||
             event.logicalKey == LogicalKeyboardKey.space) {
    _game.rotate();
  } else if (event.logicalKey == LogicalKeyboardKey.keyP) {
    _game.togglePause();
  }
}

二、触摸按钮控制

2.1 按钮布局设计

对于触摸屏设备,我设计了方向键布局:

dart 复制代码
Widget _buildControls(double buttonSize) {
  return Container(
    padding: const EdgeInsets.all(10),
    decoration: BoxDecoration(
      color: Colors.grey[850],
      borderRadius: BorderRadius.circular(8),
      border: Border.all(color: Colors.cyan[300]!, width: 2),
    ),
    child: Column(
      children: [
        // 第一行:↑键
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            _buildControlButton('↑', buttonSize, () => _game.rotate()),
          ],
        ),
        const SizedBox(height: 5),
        // 第二行:← ↓ →键
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            _buildControlButton('←', buttonSize, () => _game.moveLeft()),
            const SizedBox(width: 5),
            _buildControlButton('↓', buttonSize, () => _game.moveDown()),
            const SizedBox(width: 5),
            _buildControlButton('→', buttonSize, () => _game.moveRight()),
          ],
        ),
        const SizedBox(height: 10),
        // 第三行:暂停键
        _buildControlButton('⏸', buttonSize * 3 + 10, () => _game.togglePause()),
      ],
    ),
  );
}

布局效果

2.2 按钮交互反馈

dart 复制代码
Widget _buildControlButton(String label, double size, VoidCallback onPressed) {
  return SizedBox(
    width: size,
    height: size,
    child: ElevatedButton(
      onPressed: onPressed,
      style: ElevatedButton.styleFrom(
        backgroundColor: Colors.cyan[700],
        foregroundColor: Colors.white,
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(8),
        ),
        elevation: 4,  // 阴影效果
      ),
      child: FittedBox(
        child: Text(
          label,
          style: const TextStyle(
            fontSize: 20,
            fontWeight: FontWeight.bold,
          ),
        ),
      ),
    ),
  );
}

2.3 长按连续操作

为了方便操作,我实现了长按连续移动:

dart 复制代码
Widget _buildContinuousButton({
  required String label,
  required double size,
  required VoidCallback onAction,
}) {
  return GestureDetector(
    onLongPressStart: (_) {
      _repeatTimer = Timer.periodic(
        const Duration(milliseconds: 50),
        (timer) => onAction(),
      );
    },
    onLongPressEnd: (_) {
      _repeatTimer?.cancel();
    },
    onTap: onAction,
    child: Container(
      width: size,
      height: size,
      decoration: BoxDecoration(
        color: Colors.cyan[700],
        borderRadius: BorderRadius.circular(8),
      ),
      child: Center(
        child: Text(label, style: const TextStyle(fontSize: 20)),
      ),
    ),
  );
}

三、游戏状态管理

3.1 状态机设计

我用简单的状态变量管理游戏:

dart 复制代码
class TetrisGame {
  bool paused = false;
  bool gameOver = false;

  bool get isPlaying => !paused && !gameOver;
}

状态转换

复制代码
初始化 → playing → paused → playing
         ↓
      gameOver

3.2 暂停和继续功能

dart 复制代码
void togglePause() {
  paused = !paused;
  if (paused) {
    _timer?.cancel();
  } else {
    start();
  }
  updateCallback();
}

暂停UI

dart 复制代码
if (_game.paused && !_game.gameOver)
  Container(
    color: Colors.black.withValues(alpha: 0.8),
    child: Center(
      child: Text(
        'PAUSED',
        style: TextStyle(
          fontSize: 32,
          color: Colors.yellow[400],
        ),
      ),
    ),
  )

3.3 游戏结束处理

dart 复制代码
void _spawnPiece() {
  _currentPiece = _nextPiece;
  _nextPiece = _getRandomPiece();
  _currentX = (cols - _currentPiece![0].length) ~/ 2;
  _currentY = 0;

  // 检查是否立即碰撞
  if (_checkCollision(_currentX, _currentY, _currentPiece!)) {
    gameOver = true;
    _timer?.cancel();
  }
}



四、焦点管理

4.1 FocusNode的作用

RawKeyboardListener需要焦点才能接收键盘事件:

dart 复制代码
class _GamePageState extends State<GamePage> {
  final FocusNode _focusNode = FocusNode();

  @override
  void dispose() {
    _focusNode.dispose();  // 记得释放资源
    super.dispose();
  }
}

4.2 自动获取焦点

dart 复制代码
RawKeyboardListener(
  focusNode: _focusNode,
  onKey: _handleKeyEvent,
  autofocus: true,  // 自动获取焦点
  child: Scaffold(...),
)

4.3 点击重新获取焦点

dart 复制代码
GestureDetector(
  onTap: () {
    _focusNode.requestFocus();  // 点击时重新获取焦点
  },
  child: RawKeyboardListener(
    focusNode: _focusNode,
    onKey: _handleKeyEvent,
    child: Scaffold(...),
  ),
)

五、事件处理常见问题

问题1:按键没有响应

原因 :焦点丢失
解决:使用autofocus或点击重新获取焦点

问题2:按键触发了多次

原因 :没有区分KeyDown和KeyUp
解决:只处理KeyDownEvent

dart 复制代码
if (event is! RawKeyDownEvent) return;

问题3:触摸按钮太小

解决:根据屏幕大小动态调整

dart 复制代码
final buttonSize = isSmallScreen ? 50.0 : 60.0;

六、本文小结

这篇文章我讲解了游戏的交互控制实现:

  1. 键盘控制:RawKeyboardListener监听键盘事件
  2. 触摸控制:屏幕按钮布局和交互
  3. 状态管理:暂停、继续、游戏结束
  4. 焦点管理:FocusNode的正确使用

现在游戏已经完全可以玩了!

系列说明:这是Flutter俄罗斯方块游戏开发系列教程的第3篇,已经全部完结


参考资料

  1. Flutter事件处理官方文档
  2. RawKeyboardListener类API
  3. FocusNode类API
  4. 开源鸿蒙跨平台社区

社区支持

欢迎加入开源鸿蒙跨平台社区:

如果本文对你有帮助,欢迎点赞、收藏、评论!

相关推荐
方见华Richard18 分钟前
世毫九实验室(Shardy Lab)研究成果清单(2025版)
人工智能·经验分享·交互·原型模式·空间计算
WooaiJava30 分钟前
AI 智能助手项目面试技术要点总结(前端部分)
javascript·大模型·html5
微祎_30 分钟前
Flutter for OpenHarmony:构建一个 Flutter 平衡球游戏,深入解析动画控制器、实时物理模拟与手势驱动交互
flutter·游戏·交互
爱喝白开水a1 小时前
前端AI自动化测试:brower-use调研让大模型帮你做网页交互与测试
前端·人工智能·大模型·prompt·交互·agent·rag
Never_Satisfied1 小时前
在JavaScript / HTML中,关于querySelectorAll方法
开发语言·javascript·html
董世昌411 小时前
深度解析ES6 Set与Map:相同点、核心差异及实战选型
前端·javascript·es6
WeiXiao_Hyy2 小时前
成为 Top 1% 的工程师
java·开发语言·javascript·经验分享·后端
ZH15455891312 小时前
Flutter for OpenHarmony Python学习助手实战:面向对象编程实战的实现
python·学习·flutter
xjt_09012 小时前
基于 Vue 3 构建企业级 Web Components 组件库
前端·javascript·vue.js
renke33642 小时前
Flutter for OpenHarmony:构建一个 Flutter 色彩调和师游戏,RGB 空间探索、感知色差计算与视觉认知训练的工程实现
flutter·游戏