在刚刚结束的 FlutterNFriends 大会上,Flame 展示了它们关于 3D 游戏的支持:flame_3d ,Flame 是一个以组件系统(Flame Component System, FCS)、游戏循环、碰撞检测和输入处理为核心的 Flutter 游戏框架,而这个架构的一个关键特点就是:纯 Dart 和 Flutter 的开发模式,在此之前 flame 在 2D 领域已经开发了不少小游戏。
而本次要聊的 flame_3d 属于 Flame 生态系统的一个官方扩展包,flame_3d 是在 Flame 已有的 FCS 上进行扩展的支持,比如:
- 在Flame 2D中,游戏对象通常是
Component
的子类 ,例如SpriteComponent
用于渲染图像 - 在 flame_3d 则是引入了新的三维组件类型,比如
MeshComponent
,它们也继承自Component
基类,但是在其之上有Component3D
的相关实现
这种继承关系主要是为了新的 3D 对象,能够无缝地融入已有的 Flame 游戏循环和组件管理,例如一个 MeshComponent
可以像 2D 的 SpriteComponent
一样被添加到 World
,并自动参与到游戏的更新(update
)和渲染(render
)循环,也能让原本熟悉 Flame 2D 的开发者更便捷进入到 3D 领域:

而在 flame_3d 里,三维场景的根节点是还是 FlameGame
类,它负责管理整个游戏 ,而所有 3D 对象都通常被添加到一个 World3D
组件:
World3D
组件作为一个逻辑容器来组织场景内容CameraComponent3D
它定义了三维世界的投影方式和视点位置
而在内部,flame_3d 会拦截 Flame 的渲染循环,从而利用 Flutter GPU 的低级API来执行三维渲染任务,例如:
将三维网格、材质和灯光信息发送到 GPU 进行着色和光栅化,这个过程发生在 Flutter 的
build
之外,也避免了造成 Flutter UI 层的性能瓶颈的可能。
所以目前而言,flame_3d 十分依赖于 Flutter GPU + Impeller , flutter_gpu 作为 Flutter 3.24 提供的一个实验性功能包,它为 Dart 语言暴露了 Impeller 渲染引擎的低级接口,它可以通过编写 Dart 代码和 GLSL 着色器在 Flutter 中构建和集成自定义渲染器,而无需 Native 平台代码,允许开发者直接访问 GPU 资源和执行自定义着色器。
简单来说就是,Flutter GPU 是 Impeller 对于 HAL 的一层很轻的包装,并搭配了关于着色器和管道编排的自动化能力,也通过 Flutter GPU 就可以使用 Dart 直接构建自定义渲染器。
Flutter GPU 和 Impeller 一样,它的着色器也是使用 impellerc 提前编译,所以 Flutter GPU 也只支持 Impeller 的平台上可用。
当然,实际上直接使用 Flutter GPU 十分复杂,比如一个简单的绘制就需要:
- 获取 GPUContext
GpuContext.createCommandBuffer
创建一个CommandBuffer
CommandBuffer.createRenderPass
创建一个RenderPass
- 使用各种方法设置状态/管道并绑定资源
RenderPass
- 附加绘图命令
RenderPass.draw
CommandBuffer
使用CommandBuffer.submit
(异步)提交绘制,所有RenderPass
会按照其创建顺序进行编码
而在 flame_3d 通过抽象出一系列核心三维组件来简化开发:
MeshComponent
: 这是最基本的可渲染三维组件,用于表示三维网格(Mesh),通过属性可以加载不同类型的网格,例如圆锥体(ConeMesh
)和圆柱体(CylinderMesh
),甚至支持复杂的模型解析和骨骼动画LightComponent
: 负责在场景中添加光源,影响 3D 物体的着色效果Material
: 材质定义了 3D 对象表面的外观特性,例如颜色和纹理,目前默认提供了一个SpatialMaterial
,开发者也可以编写自定义材质来使用自己的着色器Vector
和Quaternion
: 主要是用于方便进行三维空间的向量运算和旋转变换
这里需要注意的是,Flutter 目前并不原生支持着色器文件的打包,而为了解决这个问题,flame_3d 提供了一个自定义的 Dart 脚本,开发者可以将他们的顶点着色器(.vert
)和片段着色器(.frag
)文件存放在一个指定的shaders
目录下,并确保文件名称相同,,然后 :
通过运行命令
dart pub run flame_3d:build_shaders
自动编译并打包着色器,并放置到assets/shaders
目录中提供运行时加载 。
而针对 flame_3d 官方也提供了一些 demo ,例如 collect_the_donut 就是一个非常不错的例子,它很好的展示了 flame 如何 3D 领域的开发转变为大家熟悉的 Flutter 面向对象的开发模式:

例如在项目里,你可以通过 ModelParser
加载对应的模型资源,对应上面动图,在这里:
- rogue 就是我们操作的角色模型
- floor 是地板模型
- donut 是甜甜圈模型
- skeleton 是小兵模型
- walls 是墙体模型

而在实际使用上也并不复杂,比如对于我们操作的角色,在项目里对应的是 Player
封装,Player
类是一个继承自 ModelComponent
的自定义组件,并且通过混入 HasGameReference
获取对游戏实例的引用,并实现了 KeyboardHandler
和 TapCallbacks
接口,用于处理键盘输入和点击事件。

Player
类定义了一些关键属性,例如玩家的动作 _action
、武器 _weapon
、是否奔跑 _isRunning
、死亡计时器 _deathTimer
等,而构造函数中默认将玩家的武器设置为 knife
,并通过 _updateWeapon
方法隐藏其他武器节点,仅显示当前武器。

对于玩家动作,这里通过 action
属性管理,通过设置动作时启动计时器 _actionTimer
,并调用 stopAnimation
停止当前动画,这里的动画对应的是 flame_3d 里的 AnimationState
动画状态机:
dart
set action(PlayerAction? value) {
if (_actionTimer != 0.0) {
return;
}
_action = value;
_actionTimer = value?.timer ?? 0.0;
stopAnimation();
}
而玩家的视角可以通过 lookAngle
属性管理,设置时会更新模型的旋转,lookAt
属性返回玩家当前视角方向的向量,同时玩家位置通过 _input
和 _handleMovement
方法更新,支持基于键盘输入的移动逻辑:
dart
lookAngle += -_input.x * _rotationSpeed * dt;
final movement = lookAt.scaled(-_input.y * speed * dt);
position.add(movement);
_updateAnimation
方法根据玩家当前状态(动作、移动、奔跑等)播放对应的动画,例如攻击时播放攻击动画,移动时播放行走或奔跑动画,静止时播放待机动画:
dart
if (action != null) {
playAnimationByIndex(0, resetClock: false);
} else if (isMoving && _isRunning) {
playAnimationByName('Running_A', resetClock: false);
}
可以看到,很多底层操作 flame_3d 都帮我们做了隔离,在上层你只需要操作熟悉的对象和 API ,比如将 PlayerWeapon.knife
换成 PlayerWeapon.twoHandedCrossbow
:

而对于地板,在项目里对应的是Floor
类,它是一个自定义的地板组件,继承了 flame.Component
,用于在游戏场景中生成一个由多个地板段组成的地板网格。
Floor
的会接收一个 Vector2 size
参数,表示地板的宽度和深度,地板的生成逻辑基于网格划分,网格的单元大小由常量 _floorSegmentSize
定义,起始位置 start
是一个 Vector3
,计算方式确保地板网格居中于场景:

网格的生成逻辑是通过嵌套的 for
循环,遍历地板的宽度和深度,将每个网格单元的位置偏移量计算出来,并创建 _FloorSection
实例,每个实例的 position
属性设置为计算后的位置,并添加到当前组件中:
dart
final position = start.clone()
..x += x * _floorSegmentSize
..z += y * _floorSegmentSize;
add(_FloorSection()..position.setFrom(position));
而 _FloorSection
继承自 ModelComponent
,表示地板的单个网格单元,对应模型是通过 Loader.models.floor
加载,并设置了初始位置偏移量,使地板稍微低于默认高度:

同理,Demo 项目里的Wall
也是继承自 flame.Component
,用于在游戏场景中生成一段由多个墙段组成的墙体:

对于 Wall
的来说主要接收两个参数:start
和 end
,分别表示墙体的起点和终点,然后通过计算 end - start
得到方向向量 direction
,并将墙体的初始位置设置为起点加上方向向量的一半长度:
dart
final direction = end - start;
final position = start + direction.scaledTo(_wallSegmentSize / 2);
对于墙段生成逻辑,主要由多个固定大小的墙段组成,每段的大小由常量 _wallSegmentSize
定义,通过 while
循环,逐步减少剩余距离 totalDistance
,并在每次迭代中添加一个 _WallSection
实例,每个墙段的旋转通过 Quaternion.axisAngle
计算,使其与墙体方向对齐:
dart
final rotation = Quaternion.axisAngle(
up,
atan2(start.z - end.z, start.x - end.x),
);
add(
_WallSection(
wallIndex: randomInt(0, Loader.models.walls.length),
position: position,
rotation: rotation,
),
);
同样道理,这里的 _WallSection
也继承自 ModelComponent
,表示墙体的单个段,它的构造函数接收墙段的索引、位置和旋转,并从 Loader.models.walls
中加载对应的墙体模型:

另外还有光源演示, Demo 里的光源主要体现在几个随机移动的黑点,对应项目里的 Wisp
对象,它继承自 LightComponent
,并通过 HasGameRef
混入获取对游戏实例的引用,它的主要功能是创建一个动态移动的光源,模拟萤火虫的效果:

对于光源,同样有一个内部模型对象 _VisualLight
,它同样继承自 MeshComponent
,用于渲染光源的视觉效果,这里主要使用了一个小型球体网格 SphereMesh
,半径为 0.05
,材质为 SpatialMaterial
,颜色与光源一致:

最后少不了 Camera,在 Demo 里使用 ThirdPersonCamera
实现了一个自定义的 3D 摄像机组件,它主要是继承自 CameraComponent3D
,并通过 HasGameReference
混入获取对游戏实例的引用,而它的主要功能是实现第三人称视角,跟随玩家角色的移动和方向:

在 ThirdPersonCamera
里它主要是设置了摄像机的视野角度(fovY
)、初始位置(position
)、上方向向量(up
)以及目标点(target
),例如position
设置为 Vector3(-18, 6, -18)
,表示摄像机初始位于玩家后方偏左上方的位置。
dart
position: Vector3(-18, 6, -18),
up: Vector3(0.8, 1, 0.8),
target: Vector3(0, 0, 0),
update
方法在每帧调用,用于更新摄像机的位置和目标点,首先计算目标偏移量 targetOffset
和目标视角点 targetLookAt
,分别基于玩家的位置和视角方向进行偏移:
dart
final targetOffset = player.position + _positionOffset;
final targetLookAt = player.position + player.lookAt;
接着,使用线性插值公式更新摄像机的位置和目标点,使其平滑地跟随玩家移动和旋转,插值速度由 _cameraLinearSpeed
和 _cameraRotationSpeed
控制:
dart
position += (targetOffset - position) * _cameraLinearSpeed * dt;
target += (targetLookAt - target) * _cameraRotationSpeed * dt;
另外还有个值得聊的是 HUD,实际上也就是,用于在屏幕右上角显示当前分数,事实上其实就是使用 flame_3d 里的 TextPaint
,它可以把你需要的文本内容直接选入到屏幕:
dart
await camera.viewport.add(Hud());
static final textHuge = TextPaint(style: _style.copyWith(fontSize: 64));
class Hud extends Component with HasGameRef<CollectTheDonutGame> {
@override
void render(Canvas canvas) {
super.render(canvas);
Styles.textHuge.render(
canvas,
game.score.toString().padLeft(2, '0'),
Vector2(
game.size.x - _margin,
_margin,
),
anchor: Anchor.topRight,
);
}
}

可以看到,flame_3d 大大简化了 Flutter GPU 的使用,同时也给了沉寂这么久的 Flutter GPU 一个落地场景,由于需要 Flutter GPU 和 Impeller 支持,目前 flame_3d 只支持 Android、iOS 和 macOS ,同时由于 flame_3d 还是实验性阶段,所以 API 稳定性还没有保证。
对于 flame 而言,在理想情况下他们甚至希望 flame_3d 的用户完全不需要知道和理解 Flutter GPU,他们的目标是将 Flutter GPU 抽象为一个方便 3D 开发的 API,这不仅简化了创建渲染目标、设置颜色和深度纹理以及配置深度模板等操作,还包含支持更高级的 API,例如几何形状、纹理/材质渲染以及创建可以使用这些形状和材质的网格,最终把这一切和 有的 FCS 紧密结合。
另外本次 Flame 现场还在现在不是用 Flutter GPU 制作了一个小 Demo ship_game,通过覆盖 Raymarching 、 Volumetric Raymarching 、Weight maps 和 Ordered Dithering 来展示了 Flame 原生的能力:

可以看到,在 flame 在加持下,Flutter 在游戏领域的能力确实越来越强,也希望 Flutter GPU 可以早日发布稳定版本,把这个老饼给画完。