Flame 其他输入方式与辅助组件全指南:虚拟摇杆、按钮体系与事件忽略机制
2026/9/15 22:50:40 网站建设 项目流程

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,创建了MyGamePlayer两个类:MyGame负责创建摇杆并传给PlayerPlayerupdate中根据摇杆当前状态驱动自身运动:

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 定位:摇杆通过marginEdgeInsets)相对视口边缘定位(默认左下角),由ComponentViewportMarginmixin 实现,见 component_viewport_margin.dart。
  • 构造约束:从源码(joystick_component.dart)可以看到构造函数含两条断言:sizebackground必须至少提供其一;knobbackground的位置不允许手动设置(必须保持零点,组件挂载时会自动计算摆放)。knobRadius默认取size.x / 2,即旋钮可拖动的最大半径默认为摇杆底盘的半宽。

摇杆状态字段:intensity、delta、relativeDelta

摇杆在不同状态下有一组字段,用于读取当前输入状态(other_inputs.md):

  • intensitydouble,取值范围[0.0, 1.0],表示旋钮从圆心被拖到摇杆边缘(或knobRadius,若设置了该值)的百分比,可理解为"力度/行程比例"。
  • deltaVector2,旋钮相对圆心的绝对拖拽量(以像素为单位),其长度会被限制在knobRadius之内。
  • relativeDeltaVector2delta / 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。也就是说,即使手指拖出摇杆底盘,deltaintensity也始终保持在合理范围内,不会产生越界值。

八方向判定: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实现"透明度"按钮,这些按钮触发的是RotateEffectScaleEffectOpacityEffect等效果(见 effects.md 相关文档);
  • 摇杆的实时状态(intensity对应的速度、direction文本)通过TextComponent叠加在屏幕上显示。

同时注意该示例的组件挂载方式:摇杆和按钮通过camera.viewport.addAll([...])挂到视口上,从而始终固定在屏幕上;玩家则加入world,随游戏世界运动(相关概念可参考 camera.md)。

按钮体系:从 HUD 按钮到通用按钮

Flame 的按钮组件统一遵循"按下/释放两种外观 + 回调"的设计模式。HudButtonComponentButtonComponentSpriteButtonComponent三者对应这一模式的不同变体。

HudButtonComponent:屏幕固定按钮

HudButtonComponent是用相对视口边缘的margin来定位(而不是用position)的按钮(other_inputs.md)。它接收两个PositionComponent

  • button:按钮空闲(未按下)时显示的外观;
  • buttonDown:按钮被按下时显示的外观,可选——若不希望在按下时改变外观,或由button组件自行处理按下态,可以省略。

由于默认是 HUD 组件,即使游戏相机移动,按钮也始终静止在屏幕上。若希望它参与相机变换,可设置hudButtonComponent.respectCamera = true;

处理按下/释放有两种方式(other_inputs.md):

  1. 在构造时传入回调:onPressedonReleased(以及源码中支持的onCancelled);
  2. 继承组件并重写onTapDownonTapUponTapCancel实现自定义逻辑。

从源码(hud_button_component.dart)看,HudButtonComponent extends ButtonComponent with HasGameRef, ComponentViewportMarginmargin定位与游戏引用(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, );

HudButtonComponentbutton也可以是形状组件(如CircleComponentRectangleComponent),进阶示例中的旋转按钮就用CircleComponent作为默认外观、RectangleComponent作为按下外观,并配合margin定位。

ButtonComponent:两个 PositionComponent 的通用按钮

ButtonComponentHudButtonComponent同源,但它用position定位而非边距,适合放在游戏世界的任意坐标。它由两个PositionComponent定义:一个表示按下状态,一个表示释放状态(other_inputs.md)。如果需要纯 Sprite 按钮,优先用SpriteButtonComponent;但若想用SpriteAnimationComponent或其他非纯 Sprite 的内容做按钮,ButtonComponent更合适。

源码行为(button_component.dart):

  • 构造参数size缺省时取button?.size;挂载时若size为零也会自动取button的尺寸;
  • 挂载断言button必须提供(构造传入或onLoad中设置);
  • onTapDown时若存在buttonDown,会从父级移除button并挂上buttonDown,同时调用onPressedonTapUp时反向切换并调用onReleasedonTapCancel时恢复默认外观并调用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枚举含updown两个状态,见 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):updownhoverdisabled。可用来自定义外观的字段:

字段说明
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:可选中切换的按钮

ToggleButtonComponentAdvancedButtonComponent的子类,在选中与未选中两种状态间切换,适合做开关(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)回调,在选中状态变化时触发;
  • setSkindownAndSelectedhoverAndSelecteddisabledAndSelected等复合状态做了回退处理:若未提供对应的选中态皮肤,会自动回退到非选中态的同级皮肤(第 116-132 行);
  • updateState会结合isDisabledisPressedisHoveredisSelected四者综合计算当前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),仅供参考

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

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

立即咨询