30天计划第24天-游戏分数与UI

-
-
2025-12-15 13:32

比啊学习内容

  1. 游戏 UI:TextComponent(显示分数、生命值)

  2. UI 固定位置:用 PositionType.viewport 让 UI 不跟随场景移动

  • 实践任务

    1. 在游戏顶部显示 “分数:X”,玩家吃食物时分数实时更新

    2. 加一个 “重新开始” 按钮:点击后重置分数和玩家位置

一、核心知识点讲解

1. Flame中的文本组件:TextComponent

TextComponent是Flame专门用于显示文本的核心组件,相比Flutter原生的Text组件,它能无缝溶图Flame游戏循环,支持动态更新文本内容。

  • 关键属性

    属性作用示例
    text显示的文本内容(可动态修改)text="分数:100"
    textRenderer文本样式(字体、大小、颜色)TextPaint(style: TextStyle(fontSize: 24, color: Colors.white))
    position文本在屏幕中的位置(基于锚点)position:Vector2(20,20)
    anchor文本的锚点(基准点)anchor:Anchor.topLeft(左上角对齐)
  • 动态更新文本

    修改text属性即可实时更新显示内容,例如:

    // 分数组件中定义更新方法
    void updateScore(int newScore) {
      score = newScore;
      text = "分数:$score"; // 直接修改text属性,UI 实时刷新
    }

2. Flame中的按钮组件:ButtonComponent

ButtonComponent是一个用于创建可交互按钮的组件,它继承自PositionComponent并混入TapCallbacks,用于实现按钮点击事件。它的核心作用是封装按钮的视觉表现(常态、按下状态等)交互逻辑(点击回调),简化游戏汇中按钮的实现。

核心特性

  • 视觉表现管理

    通过buttonbuttonDown两个参数定义按钮的视觉组件

    • button:按钮常态下的视觉表现(如:背景、文本、图片等,通常是PositionComponent及其子类的组合)。
    • buttonDown(可选):按钮被按下时的视觉表现(如:改变背景颜色、缩放等,默认与button一致)。
  • 交互事件处理

    支持点击相关回调

    • onPressed:按钮被按下并释放时触发。
    • onReleased:按钮被释放时触发(无论是否在按钮范围内)。
    • onCancelled:交互被取消时触发(如:手指滑出按钮范围)
  • 布局与定位

    继承PositionComponent的特性,可通过:positionsizeanchor等参数控制按钮的位置、大小和锚点

关键事项

视觉组件必须赋值给button,否则运行会报错。

二、综合实战

  • 任务目标:在“30天计划第23天-碰撞检测”综合实战的基础上,使用ButtonComponent新增一个重新开始的按钮,按下后
    • 食物数量重置为5个,位置重置为随机位置
    • 玩家位置重置到中间
    • 分数重置为0
  • 代码实现
    • 新增一个重置按钮组件(reset_button_component.dart

      import 'package:flame/input.dart';
      import 'package:flame/components.dart';
      import 'package:flutter/material.dart';
      
      
      class RestartButton extends ButtonComponent {
        final VoidCallback onRestartPressed;
      
        RestartButton({
          required Vector2 position,
          required Vector2 size,
          required this.onRestartPressed,
        }) : super(
                position: position,
                size: size,
                anchor: Anchor.center,
              ){onPressed = onRestartPressed;}
      
        @override
        Future<void> onLoad() async {
          // 创建按钮组件
          final buttonContainer = PositionComponent(
            size: size,
          );
          
          // 添加背景
          final background = RectangleComponent(
            size: size,
            paint: Paint()..color = Colors.red,
          );
          buttonContainer.add(background);
      
          // 添加文本
          final text = TextComponent(
            text: "重新开始",
            textRenderer: TextPaint(
              style: const TextStyle(
                color: Colors.white,
                fontSize: 18,
                fontWeight: FontWeight.bold,
              ),
            ),
            anchor: Anchor.center,
            position: size / 2,
          );
          buttonContainer.add(text);
      
          // 必须设置button属性
          button = buttonContainer;
          
          // 调用父类的onLoad
          await super.onLoad();
        }
      }
    • 修改碰撞检测组件(collision_game.dart

      完整代码如下:

      import 'dart:ui' as ui;
      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';
      import 'restart_button_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();
      
              // 创建按钮组件
          final restartButton = RestartButton(
            position: Vector2(size.x - 80, 40),
            size: Vector2(140, 60),
            onRestartPressed: resetGame,
          );
          add(restartButton);
      
          // 方案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();
          }
        }
      
        // 重置游戏方法
        void resetGame() {
          // 移除所有食物
          final foodList = children.whereType<FoodComponent>().toList();
          for (final food in foodList) {
            food.removeFromParent();
          }
      
          // 重置分数
          scoreComponent.score = 0;
          scoreComponent.text = "分数:0";
      
          // 重置玩家位置
          player.position = Vector2(size.x / 2, size.y / 2);
      
          // 重新生成初始食物
          for (int i = 0; i < initialFoodCount; i++) {
            _spawnFood();
          }
      
          // 重置移动状态
          player.left = false;
          player.right = false;
          player.up = false;
          player.down = false;
        }
      
        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);
        }
      }
  • 运行效果

 

 


目录