在剛剛結束的 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
動畫狀態機:
set action(PlayerAction? value) {if (_actionTimer != 0.0) {return;}_action = value;_actionTimer = value?.timer ?? 0.0;stopAnimation();
}
而玩家的視角可以通過 lookAngle
屬性管理,設置時會更新模型的旋轉,lookAt
屬性返回玩家當前視角方向的向量,同時玩家位置通過 _input
和 _handleMovement
方法更新,支持基于鍵盤輸入的移動邏輯:
lookAngle += -_input.x * _rotationSpeed * dt;
final movement = lookAt.scaled(-_input.y * speed * dt);
position.add(movement);
_updateAnimation
方法根據玩家當前狀態(動作、移動、奔跑等)播放對應的動畫,例如攻擊時播放攻擊動畫,移動時播放行走或奔跑動畫,靜止時播放待機動畫:
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
屬性設置為計算后的位置,并添加到當前組件中:
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
,并將墻體的初始位置設置為起點加上方向向量的一半長度:
final direction = end - start;
final position = start + direction.scaledTo(_wallSegmentSize / 2);
對于墻段生成邏輯,主要由多個固定大小的墻段組成,每段的大小由常量 _wallSegmentSize
定義,通過 while
循環,逐步減少剩余距離 totalDistance
,并在每次迭代中添加一個 _WallSection
實例,每個墻段的旋轉通過 Quaternion.axisAngle
計算,使其與墻體方向對齊:
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)
,表示攝像機初始位于玩家后方偏左上方的位置。
position: Vector3(-18, 6, -18),
up: Vector3(0.8, 1, 0.8),
target: Vector3(0, 0, 0),
update
方法在每幀調用,用于更新攝像機的位置和目標點,首先計算目標偏移量 targetOffset
和目標視角點 targetLookAt
,分別基于玩家的位置和視角方向進行偏移:
final targetOffset = player.position + _positionOffset;
final targetLookAt = player.position + player.lookAt;
接著,使用線性插值公式更新攝像機的位置和目標點,使其平滑地跟隨玩家移動和旋轉,插值速度由 _cameraLinearSpeed
和 _cameraRotationSpeed
控制:
position += (targetOffset - position) * _cameraLinearSpeed * dt;
target += (targetLookAt - target) * _cameraRotationSpeed * dt;
另外還有個值得聊的是 HUD,實際上也就是,用于在屏幕右上角顯示當前分數,事實上其實就是使用 flame_3d 里的 TextPaint
,它可以把你需要的文本內容直接選入到屏幕:
await camera.viewport.add(Hud());static final textHuge = TextPaint(style: _style.copyWith(fontSize: 64));class Hud extends Component with HasGameRef<CollectTheDonutGame> {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 可以早日發布穩定版本,把這個老餅給畫完。