30天计划第23天-碰撞检测

-
-
2025-12-10 12:36
  • 学习内容

    1. 碰撞形状:RectangleHitbox(矩形碰撞)、CircleHitbox(圆形碰撞)

    2. 碰撞检测:collidesWith onCollisiononCollisionStartonCollisionEnd(判断两个组件是否碰撞)

  • 实践任务

    1. 给玩家添加矩形碰撞盒,再添加一个 “食物” 精灵(带碰撞盒)

    2. 实现 “吃食物”:玩家碰到食物时,食物消失,分数 + 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. 碰撞检测核心流程

  • 分别为碰撞对象绑定碰撞盒(默认collisionTypeactivate,消耗性能,建议将非必要检测碰撞的对象改为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),
              ),
            ),
          );
        }
      }
  • 运行效果

     


目录