Flame 其他输入方式与辅助组件全指南:虚拟摇杆、按钮体系与事件忽略机制
【免费下载链接】flameA Flutter based game engine.项目地址: https://gitcode.com/GitHub_Trending/fl/flame
本文是 Flame 游戏引擎输入体系(inputs.md)中"其他输入与辅助"部分的深度指南,聚焦键盘、鼠标之外的游戏输入手段。文章围绕 other_inputs.md 展开,结合仓库源码深入讲解JoystickComponent虚拟摇杆、HudButtonComponent/SpriteButtonComponent/ButtonComponent/AdvancedButtonComponent/ToggleButtonComponent五级按钮体系,以及用于性能优化的IgnoreEvents事件忽略机制。读完后你将能够在自己的 Flame 游戏中快速实现移动端虚拟摇杆、屏幕固定 HUD 按钮、多状态皮肤按钮,并通过事件忽略机制优化组件树的输入分发。
概述:除键盘鼠标之外,Flame 还提供哪些输入能力
Flame 的输入 API 以 mixin 形式混入组件(详见 inputs.md 的事件坐标系统说明),但游戏往往还需要键盘、鼠标、触摸之外的交互手段:移动端常见的虚拟摇杆、模拟手柄按键的屏幕按钮、外部游戏手柄等。本文涉及的组件与 mixin 全部位于packages/flame/lib/src/components/input/目录,包括:
| 组件 / Mixin | 核心用途 | 源码位置 |
|---|---|---|
JoystickComponent | 虚拟摇杆,拖动旋钮产生方向与力度输入 | joystick_component.dart |
HudButtonComponent | 以边距定位、固定于屏幕(HUD)的按钮 | hud_button_component.dart |
SpriteButtonComponent | 由两张 Sprite 定义的按钮 | sprite_button_component.dart |
ButtonComponent | 由两个PositionComponent定义的通用按钮 | button_component.dart |
AdvancedButtonComponent | 为每个指针阶段提供独立皮肤的状态按钮 | advanced_button_component.dart |
ToggleButtonComponent | 可选中/未选中切换的按钮 | toggle_button_component.dart |
IgnoreEventsmixin | 让组件子树整体忽略事件 | ignore_events.dart |
其中游戏手柄(Gamepad)输入由独立插件包flame_gamepads提供,仓库中对应目录为 packages/flame_gamepads,下文单独说明。
虚拟摇杆 JoystickComponent
JoystickComponent是 Flame 提供的虚拟摇杆组件:创建一个摇杆组件,按需配置,然后加入游戏即可。它适合移动端触屏游戏的方向控制场景,也常用于桌面/网页演示。
基本用法与完整示例
以下示例来自 other_inputs.md,创建了MyGame与Player两个类:MyGame负责创建摇杆并传给Player,Player在update中根据摇杆当前状态驱动自身运动:
class MyGame extends FlameGame { @override Future<void> onLoad() async { super.onLoad(); final image = await images.load('assets/images/joystick.png'); final sheet = SpriteSheet.fromColumnsAndRows( image: image, columns: 6, rows: 1, ); final joystick = JoystickComponent( knob: SpriteComponent( sprite: sheet.getSpriteById(1), size: Vector2.all(100), ), background: SpriteComponent( sprite: sheet.getSpriteById(0), size: Vector2.all(150), ), margin: const EdgeInsets.only(left: 40, bottom: 40), ); final player = Player(joystick); add(player); add(joystick); } } class Player extends SpriteComponent with HasGameRef { Player(this.joystick) : super( anchor: Anchor.center, size: Vector2.all(100.0), ); /// Pixels/s double maxSpeed = 300.0; final JoystickComponent joystick; @override Future<void> onLoad() async { sprite = await gameRef.loadSprite('assets/images/layers/player.png'); position = gameRef.size / 2; } @override void update(double dt) { if (joystick.direction != JoystickDirection.idle) { position.add(joystick.relativeDelta * maxSpeed * dt); angle = joystick.delta.screenAngle(); } } }关键点拆解:
- 摇杆旋钮(knob)与底盘(background):两者都是
PositionComponent。可以从精灵表(SpriteSheet.fromColumnsAndRows)取图,也可以直接用CircleComponent等形状组件,见 joystick_example.dart 中的纯圆形实现:joystick = JoystickComponent( knob: CircleComponent(radius: 30, paint: knobPaint), background: CircleComponent(radius: 100, paint: backgroundPaint), margin: const EdgeInsets.only(left: 40, bottom: 40), ); - margin 定位:摇杆通过
margin(EdgeInsets)相对视口边缘定位(默认左下角),由ComponentViewportMarginmixin 实现,见 component_viewport_margin.dart。 - 构造约束:从源码(joystick_component.dart)可以看到构造函数含两条断言:
size与background必须至少提供其一;knob与background的位置不允许手动设置(必须保持零点,组件挂载时会自动计算摆放)。knobRadius默认取size.x / 2,即旋钮可拖动的最大半径默认为摇杆底盘的半宽。
摇杆状态字段:intensity、delta、relativeDelta
摇杆在不同状态下有一组字段,用于读取当前输入状态(other_inputs.md):
intensity:double,取值范围[0.0, 1.0],表示旋钮从圆心被拖到摇杆边缘(或knobRadius,若设置了该值)的百分比,可理解为"力度/行程比例"。delta:Vector2,旋钮相对圆心的绝对拖拽量(以像素为单位),其长度会被限制在knobRadius之内。relativeDelta:Vector2,delta / knobRadius的归一化结果,元素取值范围约[-1, 1],表示旋钮当前从基位被拉向边缘的百分比与方向,适合直接乘以速度参与位移计算(如示例中的position.add(joystick.relativeDelta * maxSpeed * dt))。
三者与源码的对应关系(joystick_component.dart):
double intensity = 0.0; // [0.0, 1.0] 拖拽行程比例 final Vector2 delta = Vector2.zero(); // 绝对拖拽量(被限制在 knobRadius 内) Vector2 get relativeDelta => delta / knobRadius; // 归一化拖拽向量在update(joystick_component.dart)中,每次更新都会将原始拖拽量_unscaledDelta限制到knobRadius圆内(delta.scaleTo(knobRadius)),再更新旋钮位置并计算intensity = delta.length2 / knobRadius2。也就是说,即使手指拖出摇杆底盘,delta与intensity也始终保持在合理范围内,不会产生越界值。
八方向判定:JoystickDirection
JoystickComponent还提供了direction属性,返回JoystickDirection枚举,用于快速判断当前摇杆方向(joystick_component.dart):
enum JoystickDirection { up, upLeft, upRight, right, down, downRight, downLeft, left, idle, }direction的判定逻辑(joystick_component.dart):当delta为零时返回idle;否则取delta.screenAngle(),将负角度转换为[0, 2π)区间,再按每π/8(22.5°)一个扇区映射为八方向之一。因此你可以像示例那样先用direction != JoystickDirection.idle判断是否有输入,再结合relativeDelta做连续位移、用delta.screenAngle()设置角色朝向。
进阶用法:摇杆 + HUD 按钮的组合示例
仓库的 joystick_advanced_example.dart 展示了更完整的实战组合:
- 摇杆驱动玩家移动,
HudButtonComponent作为"翻转"按钮(分别水平/垂直翻转玩家); - 用
ButtonComponent实现"缩放"按钮、用SpriteButtonComponent实现"透明度"按钮,这些按钮触发的是RotateEffect、ScaleEffect、OpacityEffect等效果(见 effects.md 相关文档); - 摇杆的实时状态(
intensity对应的速度、direction文本)通过TextComponent叠加在屏幕上显示。
同时注意该示例的组件挂载方式:摇杆和按钮通过camera.viewport.addAll([...])挂到视口上,从而始终固定在屏幕上;玩家则加入world,随游戏世界运动(相关概念可参考 camera.md)。
按钮体系:从 HUD 按钮到通用按钮
Flame 的按钮组件统一遵循"按下/释放两种外观 + 回调"的设计模式。HudButtonComponent、ButtonComponent、SpriteButtonComponent三者对应这一模式的不同变体。
HudButtonComponent:屏幕固定按钮
HudButtonComponent是用相对视口边缘的margin来定位(而不是用position)的按钮(other_inputs.md)。它接收两个PositionComponent:
button:按钮空闲(未按下)时显示的外观;buttonDown:按钮被按下时显示的外观,可选——若不希望在按下时改变外观,或由button组件自行处理按下态,可以省略。
由于默认是 HUD 组件,即使游戏相机移动,按钮也始终静止在屏幕上。若希望它参与相机变换,可设置hudButtonComponent.respectCamera = true;。
处理按下/释放有两种方式(other_inputs.md):
- 在构造时传入回调:
onPressed、onReleased(以及源码中支持的onCancelled); - 继承组件并重写
onTapDown、onTapUp、onTapCancel实现自定义逻辑。
从源码(hud_button_component.dart)看,HudButtonComponent extends ButtonComponent with HasGameRef, ComponentViewportMargin,margin定位与游戏引用(gameRef)均由 mixin 提供。进阶示例中的典型用法:
final flipButton = HudButtonComponent( button: SpriteComponent(sprite: sheet.getSpriteById(2), size: buttonSize), buttonDown: SpriteComponent(sprite: sheet.getSpriteById(4), size: buttonSize), margin: const EdgeInsets.only(right: 80, bottom: 60), onPressed: player.flipHorizontally, );HudButtonComponent的button也可以是形状组件(如CircleComponent、RectangleComponent),进阶示例中的旋转按钮就用CircleComponent作为默认外观、RectangleComponent作为按下外观,并配合margin定位。
ButtonComponent:两个 PositionComponent 的通用按钮
ButtonComponent与HudButtonComponent同源,但它用position定位而非边距,适合放在游戏世界的任意坐标。它由两个PositionComponent定义:一个表示按下状态,一个表示释放状态(other_inputs.md)。如果需要纯 Sprite 按钮,优先用SpriteButtonComponent;但若想用SpriteAnimationComponent或其他非纯 Sprite 的内容做按钮,ButtonComponent更合适。
源码行为(button_component.dart):
- 构造参数
size缺省时取button?.size;挂载时若size为零也会自动取button的尺寸; - 挂载断言
button必须提供(构造传入或onLoad中设置); onTapDown时若存在buttonDown,会从父级移除button并挂上buttonDown,同时调用onPressed;onTapUp时反向切换并调用onReleased;onTapCancel时恢复默认外观并调用onCancelled。
进阶示例中,ButtonComponent用描边矩形与填充矩形分别作为两种状态,配合position精确摆放:
final buttonComponent = ButtonComponent( button: RectangleComponent( size: Vector2(185, 50), paint: Paint() ..color = Colors.orange ..style = PaintingStyle.stroke, ), buttonDown: RectangleComponent( size: Vector2(185, 50), paint: BasicPalette.magenta.paint(), ), position: Vector2(20, size.y - 280), onPressed: () => player.add( ScaleEffect.by( Vector2.all(1.5), EffectController(duration: 1.0, reverseDuration: 1.0), ), ), );SpriteButtonComponent:双 Sprite 按钮
SpriteButtonComponent由两张Sprite定义按钮外观:一张表示按下,一张表示释放(other_inputs.md)。实现上它继承自SpriteGroupComponent<ButtonState>(ButtonState枚举含up与down两个状态,见 sprite_button_component.dart),因此可以手动修改current属性切换状态。
与ButtonComponent的差异值得注意(sprite_button_component.dart):
onTapDown仅切换外观(current = ButtonState.down),不触发任何回调;onPressed回调在onTapUp(手指抬起)时才触发,同时外观恢复为up;onTapCancel时仅恢复外观;buttonDown是可选的:不设置时按下态与释放态外观相同(get buttonDown => _buttonDown ?? button)。
进阶示例中从buttons.png精灵表(1 列 2 行)取上、下两张图作为两种状态:
final spriteButtonComponent = SpriteButtonComponent( button: buttonSheet.getSpriteById(0), buttonDown: buttonSheet.getSpriteById(1), position: Vector2(20, size.y - 360), size: Vector2(185, 50), onPressed: () => player.add( OpacityEffect.fadeOut( EffectController(duration: 0.5, reverseDuration: 0.5), ), ), );游戏手柄 Gamepad
外部游戏手柄(物理手柄)输入由 Flame 的独立插件提供:flame_gamepads包,仓库源码位于 packages/flame_gamepads。原文档(other_inputs.md)指出,如需支持手柄,请使用该专用插件,并在项目的pubspec.yaml中引入flame_gamepads依赖。手柄与本文的虚拟摇杆/屏幕按钮属于不同输入通道:前者是外部控制器事件流,后者是基于触摸/指针的组件级输入。
AdvancedButtonComponent:按指针阶段细分的多皮肤按钮
AdvancedButtonComponent为每一种指针阶段(pointer phase)提供了独立状态,每个状态皮肤都是一个PositionComponent(other_inputs.md)。其状态机由ButtonState枚举驱动(advanced_button_component.dart):up、down、hover、disabled。可用来自定义外观的字段:
| 字段 | 说明 |
|---|---|
defaultSkin | 按钮默认显示的外观(必须提供) |
downSkin | 按钮被点击/触摸时显示的外观 |
hoverSkin | 鼠标悬停时显示的外观(桌面与 Web 平台) |
defaultLabel | 显示在皮肤之上的文字组件,自动居中对齐 |
disabledSkin | 按钮禁用时显示的外观 |
disabledLabel | 按钮禁用时显示在皮肤之上的文字组件 |
源码实现要点(advanced_button_component.dart):
- 皮肤存储于
skinsMap、标签存储于labelsMap,均以ButtonState为键;defaultSkin是必须的(挂载时有断言),其余皮肤可选; - 标签通过
AlignComponent(alignment: Anchor.center)自动居中于按钮之上(labelAlignContainer); - 所有皮肤尺寸会跟随按钮的
size同步缩放(_updateSizes); - 状态切换优先级为:禁用(
isDisabled)> 按下(isPressed)> 悬停(isHovered)> 默认,见updateState(第 185-199 行); - 除
onPressed/onReleased回调外,还提供onChangeState(ButtonState state),在状态变化时触发; isDisabled为可读写属性,置为true后按钮将忽略点击/触摸事件。
ToggleButtonComponent:可选中切换的按钮
ToggleButtonComponent是AdvancedButtonComponent的子类,在选中与未选中两种状态间切换,适合做开关(switch)或可选中标签(tab)(other_inputs.md)。
在AdvancedButtonComponent已有皮肤之外,它额外提供以下选中态皮肤与标签(toggle_button_component.dart):
| 字段 | 说明 |
|---|---|
defaultSelectedSkin | 按钮处于选中状态时显示的外观 |
downAndSelectedSkin | 按钮选中且被按下时显示的外观 |
hoverAndSelectedSkin | 按钮选中且鼠标悬停时显示的外观(桌面与 Web) |
disabledAndSelectedSkin | 按钮选中且禁用时显示的外观 |
defaultSelectedLabel | 按钮选中时显示在皮肤之上的文字组件 |
源码实现要点(toggle_button_component.dart):
isSelected为读写属性;点击(onTapUp)时自动取反选中状态;- 挂载时断言
defaultSelectedSkin必须提供; - 提供
onSelectedChanged(bool selected)回调,在选中状态变化时触发; setSkin对downAndSelected、hoverAndSelected、disabledAndSelected等复合状态做了回退处理:若未提供对应的选中态皮肤,会自动回退到非选中态的同级皮肤(第 116-132 行);updateState会结合isDisabled、isPressed、isHovered、isSelected四者综合计算当前ButtonState。
使用示例(结合仓库测试 toogle_button_component_test.dart 中的构造方式):
final toggle = ToggleButtonComponent( defaultSkin: defaultSkin, defaultSelectedSkin: selectedSkin, onSelectedChanged: (selected) { // 处理选中/未选中变化 }, );IgnoreEvents:让组件子树忽略所有事件
如果某个组件子树不需要接收任何事件,可以给它混入IgnoreEventsmixin(other_inputs.md)。添加该 mixin 后,ignoreEvents默认即为true,此时该组件及其所有后代组件都会停止接收事件;将ignoreEvents置为false可恢复接收。
源码实现(ignore_events.dart):
mixin IgnoreEvents on Component { bool ignoreEvents = true; }使用要点:
- 运行时动态开关:可以随时读写
ignoreEvents属性,无需移除/重新添加组件; - 连带效果:忽略事件的同时,该组件及其后代在
Component.componentsAtLocation(坐标点命中查询)中也会被忽略——这意味着它既不会收到输入,也不会参与基于位置的组件查找; - 适用场景:当一棵较大的组件子树不需要接收任何事件时使用,例如纯装饰性的粒子层、静态背景层。文档明确指出这样做可以优化性能,因为当前所有事件都会遍历整棵组件树,忽略无用子树可以减少无谓的遍历开销。
小结与进一步探索
本文覆盖了 Flame 键盘鼠标之外的主要输入手段与辅助组件:
JoystickComponent:虚拟摇杆,通过intensity/delta/relativeDelta三个字段读取拖拽状态,direction提供八方向判定,margin实现视口边缘定位;- 按钮四件套:
HudButtonComponent(边距定位 + HUD)、ButtonComponent(坐标定位 + 任意组件外观)、SpriteButtonComponent(双 Sprite 外观)、AdvancedButtonComponent/ToggleButtonComponent(按指针阶段细分的多皮肤状态机,支持选中态与禁用态); IgnoreEvents:让组件子树整体忽略事件与坐标命中,用于输入性能优化;flame_gamepads:外部手柄输入由独立插件包提供。
如果希望继续深入,仓库内可参考的资源包括:
- 完整可运行示例:joystick_example.dart(基础摇杆)、joystick_advanced_example.dart(摇杆 + 各类按钮组合)、joystick_player.dart(碰撞 + 摇杆驱动的玩家);
- 单元测试:
packages/flame/test/components/下的 joystick_component_test.dart、hud_button_component_test.dart、advanced_button_component_test.dart、toogle_button_component_test.dart,以及 ignore_events_test.dart,可用于理解各组件的行为边界; - 输入体系总览:inputs.md,其中介绍了事件坐标系统(
devicePosition/canvasPosition/localPosition)与GestureHitboxes命中判定机制,是理解本页各组件事件来源的基础。
【免费下载链接】flameA Flutter based game engine.项目地址: https://gitcode.com/GitHub_Trending/fl/flame
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考