学习内容:
碰撞形状:
RectangleHitbox(矩形碰撞)、CircleHitbox(圆形碰撞)碰撞检测:
collidesWithonCollision、onCollisionStart、onCollisionEnd(判断两个组件是否碰撞)实践任务:
给玩家添加矩形碰撞盒,再添加一个 “食物” 精灵(带碰撞盒)
实现 “吃食物”:玩家碰到食物时,食物消失,分数 + 1
一、核心概念解析※
1. 碰撞盒(Hitbox)※
碰撞和是Flame中用于碰撞检测的“不可见区域”,核心作用是替代视觉图片的像素级碰撞(性能更高,逻辑更可控)。
| 碰撞盒类型 | 适用场景 | 核心参数 |
| RectangleHitbox | 方形/矩形组件(如:玩家、方块食物) | size(碰撞盒尺寸)、anchor(锚点,默认与组件锚点一致)、collisionType(碰撞类型) |
| CircleHitbox | 圆形组件(如:圆形食物、角色头部) | radius(半径)、anchor(锚点) |
核心属性
collisionType:碰撞类型activate:主动检测碰撞,非必要不进行主动碰撞检测,会消耗性能。passive:被动响应inactive:不参与碰撞
isSolid:是否为“固体”(碰撞后阻止穿透,默认false)
2. 碰撞检测核心回调方法※
要使用碰撞回调,需要在被碰撞对象类混入:CollisionCallbacks,且需要在碰撞检测类中混入HasCollisionDetection,否则无法触发回调。
| 方法名 | 触发时机 | 作用 |
| onCollisionStart | 首次与被碰撞对象发生碰撞时(1次) | 将被碰撞对象加入activeCollisions |
| onCollision | 与被碰撞对象持续碰撞的每帧(多次) | 处理碰撞过程中的实时逻辑(如:持续伤害) |
| onCollisionEnd | 与被碰撞对象结束碰撞时(1次) | 从activeCollisions移除被碰撞对象 |
3. 碰撞检测核心流程※
- 分别为碰撞对象绑定碰撞盒(默认
collisionType为activate,消耗性能,建议将非必要检测碰撞的对象改为passive) - 为被碰撞对象类混入
CollisionCallbacks,实现碰撞检测核心回调方法 - 为核心类(如:综合实战中的
CollisionGame类)混入:HasCollisionDetection才可以进行碰撞检测
二、综合实战※
- 任务目标
- 玩家:带矩形碰撞盒,支持触屏拖动/键盘方向键移动
- 食物:随机生成在屏幕内,带圆形/矩形碰撞盒
- 碰撞逻辑:玩家碰到食物→食物消失→分数+1
- 分数显示:屏幕顶部实时显示当前分数
- 资源配置
- 玩家图片:
assets/images/player/idle.png(尺寸:100X100) - 食物图片:
assets/images/food/apple.png(尺寸:50X50,圆形/矩形均可) pubspec.yaml声明flutter: uses-material-design: true assets: - assets/images/player/ - assets/images/food/执行
flutter pub get同步资源
- 玩家图片:
- 代码实现
分数显示组件(score_component.dart)
自定义文本组件,实时显示分数
import 'package:flame/components.dart'; import 'package:flutter/material.dart'; class ScoreComponent extends TextComponent{ int score = 0; ScoreComponent() : super( text: "分数:0", position: Vector2(20, 20), textRenderer: TextPaint( style: TextStyle( fontSize: 24, color: Colors.white, fontWeight: FontWeight.bold, ), ), ); /// 更新分数(纯内部逻辑,无外部依赖) void updateScore() { score += 1; text = "分数:$score"; } }食物组件(food_component.dart)
封装精灵组件+碰撞盒+碰撞响应回调,支持随机生成位置
import 'dart:math'; import 'package:flame/components.dart'; import 'package:flame/collisions.dart'; import 'player_component.dart'; class FoodComponent extends SpriteComponent with CollisionCallbacks { final Random random = Random(); final Vector2 gameSize; final Function() onConsumed; FoodComponent({ required Sprite sprite, required this.gameSize, required this.onConsumed, }) : super( sprite: sprite, size: Vector2(50, 50), anchor: Anchor.center, ) { position = Vector2( 25 + (gameSize.x - 50) * random.nextDouble(), 25 + (gameSize.y - 50) * random.nextDouble(), ); add(CircleHitbox( radius: size.x/2, anchor: Anchor.center, collisionType: CollisionType.passive, )); } @override void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) { super.onCollision(intersectionPoints, other); if (other is PlayerComponent) { removeFromParent(); onConsumed(); } } }玩家组件(player_component.dart)
import 'package:flame/components.dart'; import 'package:flame/collisions.dart'; class PlayerComponent extends SpriteComponent { final double moveSpeed; final Vector2 gameSize; bool left = false, right = false, up = false, down = false; PlayerComponent({ required Sprite sprite, required this.moveSpeed, required this.gameSize, }) : super( sprite: sprite, size: Vector2(100, 100), anchor: Anchor.center, position: Vector2(gameSize.x/2, gameSize.y/2), ) { add(RectangleHitbox( size: Vector2(80, 80), anchor: Anchor.center, collisionType: CollisionType.active, )); } void move(double dt) { final delta = Vector2.zero(); if (left) delta.x -= moveSpeed * dt; if (right) delta.x += moveSpeed * dt; if (up) delta.y -= moveSpeed * dt; if (down) delta.y += moveSpeed * dt; position += delta; position.x = position.x.clamp(50.0, gameSize.x - 50.0); position.y = position.y.clamp(50.0, gameSize.y - 50.0); } }游戏核心类(collision_game.dart)
集成玩家控制、碰撞检测、分数逻辑
import 'dart:ui' as ui; // 给dart:ui的Image起别名,避免冲突 import 'dart:async'; import 'package:flame/game.dart'; import 'package:flame/events.dart'; import 'package:flame/components.dart'; import 'package:flutter/services.dart'; import 'package:flutter/material.dart'; import 'score_component.dart'; import 'player_component.dart'; import 'food_component.dart'; class CollisionGame extends FlameGame with PanDetector, KeyboardEvents, HasCollisionDetection { late PlayerComponent player; late ScoreComponent scoreComponent; late Sprite playerSprite; late Sprite foodSprite; final double moveSpeed = 200; final int initialFoodCount = 5; @override Future<void> onLoad() async { await super.onLoad(); // 方案1:用外部素材(确保路径正确) // final playerUiImage = await images.load('player.png'); // Flame返回的是dart:ui的Image // final foodUiImage = await images.load('food.png'); // 方案2:生成纯色测试图(返回dart:ui的Image,无类型冲突) final playerUiImage = await _createSolidUiImage(const ui.Color(0xFF2196F3), 100, 100); // 蓝色 final foodUiImage = await _createSolidUiImage(const ui.Color(0xFFFF5722), 50, 50); // 红色 // Sprite必须接收dart:ui的Image(别名后明确类型) playerSprite = Sprite(playerUiImage); foodSprite = Sprite(foodUiImage); scoreComponent = ScoreComponent(); add(scoreComponent); player = PlayerComponent( sprite: playerSprite, moveSpeed: moveSpeed, gameSize: size, ); add(player); for (int i = 0; i < initialFoodCount; i++) { _spawnFood(); } } Future<ui.Image> _createSolidUiImage(ui.Color color, int width, int height) async { // 1. 创建Completer,用于将回调异步转为Future异步 final completer = Completer<ui.Image>(); // 2. 生成纯色像素数据 final pixels = Uint8List(width * height * 4); for (int i = 0; i < pixels.length; i += 4) { // 红色通道:r是0.0-1.0的double,转换为0-255的int pixels[i] = (color.r * 255).round() & 0xff; // 绿色通道 pixels[i + 1] = (color.g * 255).round() & 0xff; // 蓝色通道 pixels[i + 2] = (color.b * 255).round() & 0xff; // 透明通道 pixels[i + 3] = (color.a * 255).round() & 0xff; } // 3. 解码像素为ui.Image,通过Completer返回结果 ui.decodeImageFromPixels( pixels, width, height, ui.PixelFormat.rgba8888, (image) { // 回调函数:获取到image后完成Completer completer.complete(image); }, ); // 4. 返回Future<ui.Image>(匹配方法返回类型) return completer.future; } void _spawnFood() { add(FoodComponent( sprite: foodSprite, gameSize: size, onConsumed: () { scoreComponent.updateScore(); _spawnFood(); }, )); } @override KeyEventResult onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keysPressed) { player.left = keysPressed.contains(LogicalKeyboardKey.arrowLeft); player.right = keysPressed.contains(LogicalKeyboardKey.arrowRight); player.up = keysPressed.contains(LogicalKeyboardKey.arrowUp); player.down = keysPressed.contains(LogicalKeyboardKey.arrowDown); return KeyEventResult.handled; } @override void onPanUpdate(DragUpdateInfo info) { player.position += info.delta.global; player.position.x = player.position.x.clamp(50.0, size.x - 50.0); player.position.y = player.position.y.clamp(50.0, size.y - 50.0); } @override void update(double dt) { super.update(dt); player.move(dt); } }入口文件(main.dart)
import 'package:flutter/material.dart'; import 'package:flame/game.dart'; import 'collision_game.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flame 1.34.0 碰撞检测', theme: ThemeData(primarySwatch: Colors.blue), home: Scaffold( appBar: AppBar(title: const Text("Flame碰撞检测"), centerTitle: true, backgroundColor: Colors.blueAccent,), body: GameWidget( game: CollisionGame(), autofocus: true, backgroundBuilder: (context) => Container(color: Colors.black87), ), ), ); } }
运行效果
