Flame 太空射击教程(二):用 DragCallbacks 与 SpriteComponent 实现玩家拖拽控制与精灵渲染
2026/9/16 22:05:21 网站建设 项目流程

Flame 太空射击教程(二):用 DragCallbacks 与 SpriteComponent 实现玩家拖拽控制与精灵渲染

【免费下载链接】flameA Flutter based game engine.项目地址: https://gitcode.com/GitHub_Trending/fl/flame

本篇是 Flame 游戏引擎「Space Shooter」系列教程的第二步,紧接 第一步:搭建游戏骨架 的FlameGameGameWidgetPositionComponent基础,重点解决两个核心问题:如何用拖拽手势(鼠标/触摸)控制玩家移动,以及如何用SpriteComponent用真实美术素材替换调试用的白色矩形。读完本文,你将掌握 Flame 的事件监听 mixin(DragCallbacks)、事件对象DragUpdateEvent的位移语义、SpriteComponentloadSprite资源加载,以及HasGameRef这一组件与游戏实例解耦的惯用写法——这些都是后续步骤(动画、子弹、敌人、碰撞)反复依赖的基础能力。

回顾:当前项目的起点

在进入本文之前,SpaceShooterGame是一个继承自FlameGame的空游戏,通过GameWidget挂载进 Flutter 组件树;Player继承自PositionComponent,重写render用白色Paint画了一个矩形,并在FlameGame.onLoad中把它以Anchor.center居中添加到游戏里:

import 'package:flame/components.dart'; import 'package:flutter/material.dart'; class Player extends PositionComponent { static final _paint = Paint()..color = Colors.white; @override void render(Canvas canvas) { canvas.drawRect(size.toRect(), _paint); } } class SpaceShooterGame extends FlameGame { @override Future<void> onLoad() async { await super.onLoad(); add( Player() ..position = size / 2 ..width = 50 ..height = 100 ..anchor = Anchor.center, ); } }

运行后屏幕中央只会出现一个白色矩形。本步的目标就是:让它能被手指/鼠标拖着走,并换上飞船贴图。

让游戏响应拖拽:DragCallbacksonDragUpdate

Flame 的输入系统通过mixin挂在组件上实现,每种手势对应一组回调。拖拽对应的就是DragCallbacks,它定义于 drag_callbacks.dart:

mixin DragCallbacks on Component implements PointerInputCallbacks { bool _isDragged = false; /// Returns true while the component is being dragged. bool get isDragged => _isDragged; ... }

从源码看,该 mixin 约束在Component上,并实现PointerInputCallbacks接口,内部通过MultiDragScaleDispatcher路由事件(源码注释明确说明它是旧版Draggablemixin 的替代品)。关键点在于:

  • FlameGame本身也是一个Component,所以可以直接with DragCallbacks混入游戏类;
  • 混入后即可覆写onDragStartonDragUpdateonDragEnd等监听方法,本文用到的是onDragUpdate
  • mixin 自带isDragged状态,可判断组件是否正处于拖拽中。

第一步改造非常简单,只需导入package:flame/events.dart,让游戏类混入DragCallbacks,并覆写onDragUpdate

import 'package:flame/events.dart'; class SpaceShooterGame extends FlameGame with DragCallbacks { late Player player; @override void onLoad() { // omitted } @override void onDragUpdate(DragUpdateEvent event) { } }

此时游戏已经能收到所有拖拽更新事件,但回调体为空,玩家还不会动。接下来要做的是把「事件」与「玩家位移」连接起来。

认识DragUpdateEvent:localDelta 的语义

onDragUpdate收到的DragUpdateEvent定义于 drag_update_event.dart,它继承自DisplacementEvent<DragUpdateDetails>。而DisplacementEvent(见 displacement_event.dart)是「包含两个屏幕点与位移增量」的事件基类,它提供:

  • deviceStartPosition/deviceEndPosition:设备坐标系(手机、浏览器窗口或应用)下的起止位置;
  • canvasStartPosition/canvasEndPosition:游戏画布坐标系下的起止位置;
  • localStartPosition/localEndPosition:事件接收组件本地坐标系下的起止位置;
  • localDeltaend - start计算出的位移增量(DisplacementContextDelta扩展提供deltagetter)。

之所以用localDelta而非绝对坐标,是因为拖拽移动只关心「手指/鼠标动了多少」,与起点无关——这天然适合做「跟随式」操控:玩家每次只需把增量累加到自身位置上。

连接事件与玩家:给 Player 加 move 方法

先把Player实例保存到游戏类的成员变量(由late Player player声明),然后在Player上新增move(Vector2 delta)方法,将增量累加到positionposition本身就是Vector2,支持add原地相加):

class Player extends PositionComponent { static final _paint = Paint()..color = Colors.white; @override void render(Canvas canvas) { canvas.drawRect(size.toRect(), _paint); } void move(Vector2 delta) { position.add(delta); } } class SpaceShooterGame extends FlameGame with DragCallbacks { late Player player; @override Future<void> onLoad() async { await super.onLoad(); player = Player() ..position = size / 2 ..width = 50 ..height = 100 ..anchor = Anchor.center; add(player); } @override void onDragUpdate(DragUpdateEvent event) { player.move(event.localDelta); } }

player.move(event.localDelta)放入onDragUpdate后,拖拽屏幕时白色矩形就会跟随手势移动——至此,你已经完成了第一个可交互的 Flame 游戏:手势输入 → 事件分发 → 组件位移。

需要留意的是:size是游戏类的Vector2x为宽、y为高;Flame 默认遵循 Flutter 的 Canvas 锚点约定,(0,0) 位于左上角。把anchor设为Anchor.center后,position表示组件中心的坐标,size / 2即可将组件精确居中(这些约定在第一步中已有说明)。

用 SpriteComponent 替换白色矩形

交互已经可用,但一个白方块远不够「酷」。Flame 为图形渲染提供了大量类,本步选用Sprite:它用于在游戏中渲染静态图片或其局部区域;而SpriteComponent则是把Sprite包装进组件体系的封装。

源码中SpriteComponent定义于 sprite_component.dart,声明为class SpriteComponent extends PositionComponent with HasPaint,即它是PositionComponent的子类,因此具备位置、尺寸、锚点等全部能力,并额外维护一个Sprite? _sprite用于渲染,还支持autoResize等选项。

准备素材:保存玩家精灵图片

在写代码前,先准备飞船图片。将下图保存为player-sprite.png放入项目的assets/images/目录(若尚未在pubspec.yaml中声明assets/images/,请参照第一步的「Preparing the assets folder」小节配置):

本仓库的教程配套工程 doc/tutorials/space_shooter/app 中已包含全部素材与各步骤完整代码,可作为对照参考。该工程pubspec.yaml声明的依赖为flame: ^1.38.0

从 PositionComponent 迁移到 SpriteComponent

由于SpriteComponent继承自PositionComponent,迁移十分顺滑:只需把父类换成SpriteComponent,删除原先的render覆写(SpriteComponent已自带渲染实现),并在onLoad中加载精灵:

class Player extends SpriteComponent { void move(Vector2 delta) { position.add(delta); } } class SpaceShooterGame extends FlameGame with DragCallbacks { late Player player; @override Future<void>? onLoad() async { await super.onLoad(); final playerSprite = await loadSprite('assets/images/player-sprite.png'); player = Player() ..sprite = playerSprite ..x = size.x / 2 ..y = size.y / 2 ..width = 50 ..height = 100 ..anchor = Anchor.center; add(player); } @override void onDragUpdate(DragUpdateEvent event) { player.move(event.localDelta); } }

这里有几个值得记住的要点:

  • SpriteComponent自带render实现,所以上一步白矩形阶段的render覆写必须删除,否则会覆盖精灵绘制;
  • FlameGame提供了loadSprite等一系列资源加载方法,它们会自动缓存图片,并且在游戏从 Flutter 组件树移除时负责清理缓存,避免内存泄漏;
  • 可以给组件显式设置width/height(缩放精灵到目标尺寸)与anchor

运行后,屏幕中央就会出现一艘小飞船,并且同样可以拖拽移动。

更优雅的架构:用 HasGameRef 让组件自己加载资源

上面代码虽然能工作,但存在一个扩展性问题:资源加载逻辑全堆在游戏类的onLoad。当一个游戏有大量组件时,游戏类会迅速膨胀成「什么都管的上帝类」。更合理的做法是让每个组件在自己的onLoad中加载自己所需的资源。

组件与FlameGame一样拥有可覆写的onLoad方法。但组件内部如何拿到游戏实例?Flame 提供了HasGameRef<T>mixin,其定义见 has_game_ref.dart:

mixin HasGameRef<T extends FlameGame> on Component { T? _game; /// Reference to the top-level Game instance that owns this component. T get gameRef => _game ??= (game as T); ... }

从源码可见:HasGameRef<T>约束在Component上,T是你游戏类的具体类型;它向组件注入一个类型安全的gameRef属性,指向组件运行所在的游戏实例,并且在运行时还会校验实际类型与泛型是否匹配。有了它,组件在onLoad中就可以直接调用gameRef.loadSprite(...)、读取gameRef.size等游戏级 API。

重构后的最终代码(与仓库 step2/main.dart 完全一致):

class Player extends SpriteComponent with HasGameRef<SpaceShooterGame> { Player() : super( size: Vector2(100, 150), anchor: Anchor.center, ); @override Future<void> onLoad() async { await super.onLoad(); sprite = await gameRef.loadSprite('assets/images/player-sprite.png'); position = gameRef.size / 2; } void move(Vector2 delta) { position.add(delta); } } class SpaceShooterGame extends FlameGame with DragCallbacks { late Player player; @override Future<void> onLoad() async { await super.onLoad(); player = Player(); add(player); } @override void onDragUpdate(DragUpdateEvent event) { player.move(event.localDelta); } }

这次重构有两个显著变化:

  1. 尺寸与锚点在构造器初始化Player()通过super(size: Vector2(100, 150), anchor: Anchor.center)在构造时固定了100×150的尺寸与居中锚点(SpriteComponent的构造函数支持这些参数,源码中sizeanchor均为其可配置属性);
  2. 加载与定位移入组件自身的onLoad:精灵通过gameRef.loadSprite加载,位置通过gameRef.size / 2计算。游戏类只负责创建与添加组件,不再关心资源细节。

运行后视觉效果与之前完全一致,但代码结构已经为「大量组件」的游戏做好了准备——这正是后续步骤中子弹、敌人等组件各自加载贴图时所采用的标准模式。

小结与源码对照

本步完成的三件事,构成了 Flame 交互式游戏的最小闭环:

能力实现方式对应源码
接收拖拽输入FlameGame with DragCallbacks并覆写onDragUpdatedrag_callbacks.dart
事件位移增量DragUpdateEvent.localDeltaDisplacementEvent提供)drag_update_event.dart
渲染静态贴图SpriteComponent+loadSpritesprite_component.dart
组件访问游戏实例HasGameRef<T>注入gameRefhas_game_ref.dart

仓库中的可对照资源:

  • 本步完整可运行代码:step2/main.dart
  • 教程工程配置(依赖flame: ^1.38.0、资源声明):app/pubspec.yaml
  • 玩家精灵素材:player-sprite.png
  • 本系列教程总览与全部步骤:space_shooter.md

进入下一步 Adding animations and depth 前,玩家已经可以响应拖拽并用真实贴图渲染。下一步将在此基础上为玩家加入动画与层次感。

【免费下载链接】flameA Flutter based game engine.项目地址: https://gitcode.com/GitHub_Trending/fl/flame

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询