比啊学习内容:
游戏 UI:
TextComponent(显示分数、生命值)
UI 固定位置:用PositionType.viewport让 UI 不跟随场景移动
实践任务:
在游戏顶部显示 “分数:X”,玩家吃食物时分数实时更新
加一个 “重新开始” 按钮:点击后重置分数和玩家位置
一、核心知识点讲解※
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,用于实现按钮点击事件。它的核心作用是封装按钮的视觉表现(常态、按下状态等)和交互逻辑(点击回调),简化游戏汇中按钮的实现。
核心特性
视觉表现管理
通过
button和buttonDown两个参数定义按钮的视觉组件button:按钮常态下的视觉表现(如:背景、文本、图片等,通常是PositionComponent及其子类的组合)。buttonDown(可选):按钮被按下时的视觉表现(如:改变背景颜色、缩放等,默认与button一致)。
交互事件处理
支持点击相关回调
onPressed:按钮被按下并释放时触发。onReleased:按钮被释放时触发(无论是否在按钮范围内)。onCancelled:交互被取消时触发(如:手指滑出按钮范围)
布局与定位
继承
PositionComponent的特性,可通过:position、size、anchor等参数控制按钮的位置、大小和锚点
关键事项
视觉组件必须赋值给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); } }
运行效果
