站長阿川

站長阿川私房教材:
學 JavaScript 前端,帶作品集去面試!

站長精心設計,帶你實作 63 個小專案,得到作品集!

立即開始免費試讀!

Flutter 真 3D 遊戲引擎來了,flame_3d 了解一下

image

在剛剛結束的 FlutterNFriends 大會上,Flame 展示了它們關於 3D 遊戲的支持:flame_3d ,Flame 是一個以元件系統(Flame Component System, FCS)、遊戲循環、碰撞檢測和輸入處理為核心的 Flutter 遊戲框架,而這個架構的一個關鍵特點就是:純 Dart 和 Flutter 的開發模式,在此之前 flame 在 2D 領域已經開發了不少小遊戲。

image image

而本次要聊的 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 領域:

image

而在 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 直接構建自定義渲染器。

image

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,開發者也可以編寫自定義材質來使用自己的著色器
  • VectorQuaternion: 主要是用於方便進行三維空間的向量運算和旋轉變換

這裡需要注意的是,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 面向物件的開發模式

image

例如在專案裡,你可以通過 ModelParser 加載對應的模型資源,對應上面動圖,在這裡:

  • rogue 就是我們操作的角色模型
  • floor 是地板模型
  • donut 是甜甜圈模型
  • skeleton 是小兵模型
  • walls 是牆體模型

image

而在實際使用上也並不複雜,比如對於我們操作的角色,在專案裡對應的是 Player 封裝,Player 類是一個繼承自 ModelComponent 的自定義元件,並且通過混入 HasGameReference 獲取對遊戲實例的引用,並實現了 KeyboardHandlerTapCallbacks 接口,用於處理鍵盤輸入和點擊事件。

image

Player 類定義了一些關鍵屬性,例如玩家的動作 _action、武器 _weapon、是否奔跑 _isRunning、死亡計時器 _deathTimer 等,而構造函數中默認將玩家的武器設置為 _knife,並通過 _updateWeapon 方法隱藏其他武器節點,僅顯示當前武器。

image

對於玩家動作,這裡通過 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

image

而對於地板,在專案裡對應的是Floor 類,它是一個自定義的地板元件,繼承了 flame.Component,用於在遊戲場景中生成一個由多個地板段組成的地板網格。

Floor 的會接收一個 Vector2 size 參數,表示地板的寬度和深度,地板的生成邏輯基於網格劃分,網格的單元大小由常量 _floorSegmentSize 定義,起始位置 start 是一個 Vector3,計算方式確保地板網格居中於場景:

image

網格的生成邏輯是通過嵌套的 for 循環,遍歷地板的寬度和深度,將每個網格單元的位置偏移量計算出來,並創建 _FloorSection 實例,每個實例的 position 屬性設置為計算後的位置,並添加到當前元件中:

final position = start.clone()
  ..x += x * _floorSegmentSize
  ..z += y * _floorSegmentSize;
add(_FloorSection()..position.setFrom(position));

_FloorSection 繼承自 ModelComponent,表示地板的單個網格單元,對應模型是通過 Loader.models.floor 加載,並設置了初始位置偏移量,使地板稍微低於默認高度:

image

同理,Demo 專案裡的Wall 也是繼承自 flame.Component,用於在遊戲場景中生成一段由多個牆段組成的牆體:

image

對於 Wall 的來說主要接收兩個參數:startend,分別表示牆體的起點和終點,然後通過計算 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 中加載對應的牆體模型:

image

另外還有光源演示, Demo 裡的光源主要體現在幾個隨機移動的黑點,對應專案裡的 Wisp 物件,它繼承自 LightComponent,並通過 HasGameRef 混入獲取對遊戲實例的引用,它的主要功能是創建一個動態移動的光源,模擬螢火蟲的效果:

image

對於光源,同樣有一個內部模型物件 _VisualLight ,它同樣繼承自 MeshComponent,用於渲染光源的視覺效果,這裡主要使用了一個小型球體網格 SphereMesh,半徑為 0.05,材質為 SpatialMaterial,顏色與光源一致:

image

最後少不了 Camera,在 Demo 裡使用 ThirdPersonCamera 實現了一個自定義的 3D 攝像機元件,它主要是繼承自 CameraComponent3D,並通過 HasGameReference 混入獲取對遊戲實例的引用,而它的主要功能是實現第三人稱視角,跟隨玩家角色的移動和方向:

image

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> {
  @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,
    );
  }
}

image

可以看到,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 原生的能力:

image

可以看到,在 flame 在加持下,Flutter 在遊戲領域的能力確實越來越強,也希望 Flutter GPU 可以早日發布穩定版本,把這個老餅給畫完。


原文出處:https://juejin.cn/post/7545699564176719914


共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
站長阿川

站長阿川私房教材:
學 JavaScript 前端,帶作品集去面試!

站長精心設計,帶你實作 63 個小專案,得到作品集!

立即開始免費試讀!