学习内容:
触屏控制:
TapDetector(点击)、PanDetector(拖动)键盘控制:
KeyboardHandler(上下左右键移动)
实践任务:
实现 “拖动玩家”:触屏拖动时,玩家精灵跟随手指移动
桌面 / Web 端支持键盘:按方向键,玩家精灵向对应方向移动(速度可配置)
一、核心概念解析※
1. 触屏输入检测器※
Flame提供Detector系列mixin用于监听触屏事件,核心两类:
| 检测器 | 作用 | 核心回调方法 |
| TapDetector | 监听点击/双击事件(适合点击交互,如:按钮、攻击) | onTapDown(按下)、onTapUp(抬起) |
| PanDetector | 监听拖动事件(适合精灵随手指移动) | onPanUpdate(拖动中) |
- 使用方式:通过mixin混入到FlameGame或Component中,重写回调方法即可监听事件。
2. 键盘事件处理器※
KeyboardHandler是Flame提供的键盘事件处理mixin,适配桌面/Web端键盘输入
| 核心回调 | 作用 |
| onKeyEvent | 监听所有键盘事件(按下/抬起) |
| LogicalKeyboardKey | 枚举类,定义所有键盘按键(如:方向键:arrowLeft/arrowRight/arrowUp/arrowDown) |
- 关键特性:支持多按键同时按下(如:同时按上+右实现斜向移动)。
3. 移动速度控制逻辑※
为了保证不同帧率设备(如:30fps/120fps)移动速度一致,需基于dt(帧间隔时间,单位:秒)计算移动距离
// 核心公式:移动距离 = 移动速度(像素/秒) × 帧间隔时间(dt)
final moveDistance = speed * dt;
// 示例:速度 200 像素/秒,60fps 下 dt≈0.0167,每帧移动约 3.34 像素4. 坐标系统※
Flame以游戏容器左上角为原点,x轴向右为正方向,y轴向下为正方向。
二、综合实战※
- 任务目标
- 加载玩家精灵,初始显示在屏幕中间
- 移动端:手指拖动精灵,精灵跟随手指实时移动
- 桌面/Web端:按方向键(↑↓←→),精灵向对应方向移动(速度可配置)
- 边界限制:精灵不超出游戏容器范围(避免移出屏幕)
- 项目准备
- 图片路径:
项目根目录/assets/images/dog.png(推荐尺寸100X100) pubsepec.yaml依赖及资源声明dependencies: flutter: sdk: flutter flame: ^1.34.0 # 请使用官网最新版本 flutter: uses-material-design: true assets: - assets/images/
- 图片路径:
- 代码实现
游戏类(
player_control_game.dart)集成触屏检测器、键盘处理器,实现移动逻辑
import 'dart:async'; import 'package:flame/game.dart'; import 'package:flame/components.dart'; import 'package:flame/cache.dart'; import 'package:flame/input.dart'; import 'package:flame/events.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; // 自定义游戏类:混入PanDetector(拖动)、KeyboardHandler(键盘) class PlayerControlGame extends FlameGame with PanDetector, KeyboardEvents { final Images _images = Images(); late SpriteComponent _player; final double _moveSpeed = 200; bool _moveLeft = false; bool _moveRight = false; bool _moveUp = false; bool _moveDown = false; @override Future<void> onLoad() async { await super.onLoad(); final playerImage = await _images.load('dog.png'); final playerSprite = Sprite(playerImage); _player = SpriteComponent( sprite: playerSprite, size: Vector2(100, 100), anchor: Anchor.center, position: Vector2(size.x / 2, size.y / 2) ); add(_player); } // ------------- 触屏拖动逻辑 ---------------------------- @override void onPanUpdate(DragUpdateInfo info) { super.onPanUpdate(info); // 获取拖动的偏移量(屏幕坐标 -> 游戏场景坐标) final delta = info.delta.global; // 更新玩家位置(跟随手指移动) _player.position += delta; // 边界限制,防止精灵移出屏幕 _limitPlayerBounds(); } // ------------- 键盘控制逻辑 ---------------------------- @override KeyEventResult onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keysPressed) { // 更新方向键状态(按下/抬起) _moveLeft = keysPressed.contains(LogicalKeyboardKey.arrowLeft); _moveRight = keysPressed.contains(LogicalKeyboardKey.arrowRight); _moveUp = keysPressed.contains(LogicalKeyboardKey.arrowUp); _moveDown = keysPressed.contains(LogicalKeyboardKey.arrowDown); return super.onKeyEvent(event, keysPressed); } // ------------- 游戏循环:更新玩家位置 ------------------------ @override void update(double dt) { super.update(dt); // 基于方向键盘状态计算移动偏移量 Vector2 moveDelta = Vector2.zero(); if (_moveLeft) moveDelta.x -= _moveSpeed * dt; if (_moveRight) moveDelta.x += _moveSpeed * dt; if (_moveUp) moveDelta.y -= _moveSpeed * dt; if (_moveDown) moveDelta.y += _moveSpeed * dt; // 更新玩家位置(键盘控制) if (moveDelta != Vector2.zero()) { _player.position += moveDelta; _limitPlayerBounds(); } } // ------------- 辅助方法:边界限制 --------------------------- void _limitPlayerBounds() { // 左边界:精灵左边缘不小于0 if (_player.position.x - _player.size.x / 2 < 0) { _player.position.x = _player.size.x / 2; } // 右边界:精灵右边缘不大于容器宽度 if (_player.position.x + _player.size.x / 2 > size.x) { _player.position.x = size.x - _player.size.x / 2; } // 上边界:精灵上边缘不小于0 if (_player.position.y - _player.size.y / 2 < 0) { _player.position.y = _player.size.y / 2; } // 下边界:精灵下边缘不大于容器高度 if (_player.position.y + _player.size.y / 2 > size.y) { _player.position.y = size.y - _player.size.y / 2; } } }游戏页面(
game_screen.dart)import 'package:flutter/material.dart'; import 'package:flame/game.dart'; import 'player_control_game.dart'; class GameScreen extends StatelessWidget { const GameScreen({super.key}); @override Widget build(BuildContext context) { final game = PlayerControlGame(); return Scaffold( appBar: AppBar( title: const Text('Flame输入控制:触屏 + 键盘'), centerTitle: true, backgroundColor: Colors.blueAccent, ), body: GameWidget( game: game, backgroundBuilder: (context) => Container(color: Colors.black,), loadingBuilder: (context) => const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator(color: Colors.blueAccent,), SizedBox(height: 20,), Text( '游戏加载中...', style: TextStyle(color: Colors.white, fontSize: 16), ) ], ), ), errorBuilder: (context, error) => Center( child: Text( '加载失败:$error', style: TextStyle(color: Colors.red, fontSize: 16), textAlign: TextAlign.center, ), ), // 自动聚焦,监听键盘事件 autofocus: true, ), ); } }入口文件(
main.dart)import 'package:flutter/material.dart'; import 'game_screen.dart'; void main() => runApp(const App()); class App extends StatelessWidget { const App({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flame输入控制示例', theme: ThemeData(primarySwatch: Colors.blue), home: const GameScreen(), ); } }
运行效果

三、拓展※
1. mixin核心定义※
mixin是Dart中的一种特殊类型,用于定义“可复用的方法和属性集合”,通过with关键字可以将一个或多个mixin混入到类中,被混入的类会自动拥有这些方法和属性,无需继承。
2. mixin解决的核心问题※
Dart是单继承语言(即:一个类只能extends一个父类),如果用传统继承实现“触屏+键盘”功能,会面临以下问题:
// 错误:Dart 不支持多继承
class PlayerControlGame extends FlameGame, PanDetector, KeyboardHandler { ... }3. mixin基础语法※
(1)定义※
用mixin关键字定义(可包含属性、方法,甚至异步方法)
// 定义一个“移动能力”的 mixin
mixin Movable {
// 可复用属性
double speed = 200;
// 可复用方法
void move(double dx, double dy) {
print("移动偏移:x=$dx, y=$dy");
}
}
// 定义一个“边界检测”的 mixin
mixin Bounded {
void checkBounds(double x, double y, double maxX, double maxY) {
if (x < 0 || x > maxX || y < 0 || y > maxY) {
print("超出边界!");
}
}
}(2)使用mixin(with关键字)※
通过with将mixin混入类中,可混入多个,用逗号分隔
// 主类继承 FlameGame,同时混入 Movable + Bounded
class PlayerControlGame extends FlameGame with Movable, Bounded {
@override
void update(double dt) {
super.update(dt);
// 直接使用 mixin 中的属性和方法
move(10 * dt, 20 * dt); // 调用 Movable 的 move 方法
checkBounds(0, 0, size.x, size.y); // 调用 Bounded 的 checkBounds 方法
print("当前速度:$speed"); // 访问 Movable 的 speed 属性
}
}(3)带约束的mixin(on关键字)※
有时希望mixin只能被特定类/父类的子类混入(如:Flame的PanDector只能混入FlameGame或Component),用on约束
// 约束:该 mixin 只能被 FlameGame 或其子类混入
mixin PanDetector on FlameGame {
// 触屏拖动回调(只有 FlameGame 子类能使用)
void onPanUpdate(DragUpdateInfo info) {
// 核心逻辑...
}
}
// 合法:PlayerControlGame 继承 FlameGame
class PlayerControlGame extends FlameGame with PanDetector { ... }
// 非法:MyWidget 不是 FlameGame 子类,编译报错
class MyWidget extends StatelessWidget with PanDetector { ... }4. mixin、继承、接口的核心区别※
| 特性 | mixin(混入) | 继承(extends) | 接口(implements) |
| 核心目的 | 复用多组独立的功能逻辑 | 复用父类的核心逻辑(单继承) | 约束类的方法签名(无实现) |
| 方法实现 | 可包含完整实现(直接用) | 可包含完整实现(可重写) | 仅定义方法签名(必须重写) |
| 多重复用 | 支持(多个with) | 不支持(单继承) | 支持(多个implements) |
| 与原类的关系 | 注入功能,不改变继承关系 | 是“is-a”关系(子类是父类) | 是“has-a”关系(类实现接口) |