扣丁書屋

使用 Flutter 與 Firebase 制作 I/O 彈球游戲

為了今年的 Google I/O 大會,Flutter 團隊使用 Flutter 以及 Firebase 構建了一款經典的彈球游戲。下面將會介紹我們是如何通過 Flame 游戲引擎將 I/O 彈球游戲[1] 帶到 Web 端的。

游戲開發要點

使用 Flutter 打造用戶交互類型的游戲是一個很棒的選擇,例如拼圖或者文字游戲這樣的游戲。Flame[2] 是一個在 Flutter 上構建的 2D 游戲引擎,當涉及到使用游戲循環的游戲時它會非常有用。I/O 彈球游戲使用了 Flame 提供的一系列特性,例如動畫、物理引擎、碰撞檢測等等,同時還借助了 Flutter 框架的基礎架構。如果你能用 Flutter 構建應用,你就獲得 Flame 構建游戲所需的基礎。

游戲循環

通常來說應用屏幕在沒有用戶交互事件的時候都會保持視覺靜止狀態。游戲中則是相反的—— UI 會持續的渲染,而且游戲狀態會不斷變化。Flame 提供了一個 game widget,它內部管理了一個游戲循環,所以能恒定且高效地進行渲染。Game 類包含了游戲組件以及其邏輯的實現,然后被交給 widget 樹中的 GameWidget。在 I/O 彈球游戲中,游戲循環反映了彈球在游戲場的位置以及狀態,然后如果球與物體碰撞或跌出比賽則需要給出必要的反饋。

@override
void update(double dt) {
  super.update(dt);
  final direction = -parent.body.linearVelocity.normalized();
  angle = math.atan2(direction.x, -direction.y);
  size = (_textureSize / 45) * 
    parent.body.fixtures.first.shape.radius;
}

使用 2D 組件渲染 3D 空間

在做 I/O 彈球游戲的時候,其中遇到的一個挑戰即是如何使用 2D 元素渲染一個 3D 的交互體驗。組件需要知道在屏幕上渲染的前后順序。例如,當小球發射到斜坡上時,它的順序會向前,這樣就會讓它看起來出現在斜坡的頂部。

彈球、彈射活塞、擋板以及 Chrome 小恐龍等等這些元素都是可活動的,這意味著它應該遵循真實世界的物理規則。而且彈球也需要根據它在板子上的位置改變其大小。當彈球滾到頂部時,它應該越來越小,以讓它看著離用戶更遠。此外,重力還會讓彈球調整角度,讓它能在斜坡上更快地落下。

/// Scales the ball's body and sprite according to its position on the board.
class BallScalingBehavior extends Component with ParentIsA<Ball> {
  @override
  void update(double dt) {
    super.update(dt);
    final boardHeight = BoardDimensions.bounds.height;
    const maxShrinkValue = BoardDimensions.perspectiveShrinkFactor;
    final standardizedYPosition = parent.body.position.y +   (boardHeight / 2);
    final scaleFactor = maxShrinkValue +
        ((standardizedYPosition / boardHeight) * (1 - maxShrinkValue));
parent.body.fixtures.first.shape.radius = (Ball.size.x / 2) * scaleFactor;
final ballSprite = parent.descendants().whereType<SpriteComponent>();
    if (ballSprite.isNotEmpty) {
      ballSprite.single.scale.setValues(
        scaleFactor,
        scaleFactor,
      );
    }
  }
}

Forge 2D 的物理引擎

I/O 彈球游戲很大程度依賴了 Flame 團隊維護的 forge2d[3] package。這個 package 將開源的 Box2D 物理引擎[4] 移植到 Dart 中,以便可以輕松集成到 Flutter。我們使用 forge2d 增強游戲中的物理特性,例如物體(夾板)在游戲場上的之間的碰撞檢測。使用 forge2D 能夠我們監聽夾板發生碰撞的時機。我們就可以在這里向夾板添加交互的回調,當兩個物體發生碰撞的時候我們就能收到通知。例如,彈球(它是圓形的)與彈簧(它是橢圓形的)接觸時,我們就會增加它的得分。在這些回調中,我們可以清楚地設置接觸開始和結束的位置,以便當兩個物體相互接觸時,會發生碰撞。

@override
Body createBody() {
  final shape = CircleShape()..radius = size.x / 2;
  final bodyDef = BodyDef(
    position: initialPosition,
    type: BodyType.dynamic,
    userData: this,
  );
  return world.createBody(bodyDef)
    ..createFixtureFromShape(shape, 1);
}

Sprite sheet 動畫

在彈球游戲場中有一些小東西,例如 Android、Dash(Dart 吉祥物)、Sparky(Firebase 吉祥物)以及 Chrome 小恐龍,這些都是動畫。對于這些東西,我們使用了 sprite sheets,它已經包含在 Flame 引擎中了,叫做 SpriteAnimationComponent。對于每個元素,我們都有一個文件,其中包含不同方向的圖像、文件中的幀數以及幀之間的時間。使用這些數據,Flame 中的 SpriteAnimationComponent 能夠在一個循環中將所有圖像編在一起,使元素看起來在運動。

△ Sprite sheet 示例

△ Sprite sheet 示例

final spriteSheet = gameRef.images.fromCache(
  Assets.images.android.spaceship.animatronic.keyName,
);
const amountPerRow = 18;
const amountPerColumn = 4;
final textureSize = Vector2(
  spriteSheet.width / amountPerRow,
  spriteSheet.height / amountPerColumn,
);
size = textureSize / 10;
animation = SpriteAnimation.fromFrameData(
  spriteSheet,
  SpriteAnimationData.sequenced(
    amount: amountPerRow * amountPerColumn,
    amountPerRow: amountPerRow,
    stepTime: 1 / 24,
    textureSize: textureSize,
  ),
);

接下來詳細解析 I/O 彈球游戲代碼。

來自 Firebase 的實時積分排行榜

I/O 彈球排行榜實時地顯示世界各地玩家的最高分數,玩家還可以在 Twitter 和 Facebook 上分享他們的分數。我們使用 Firebase Cloud Firestore[5] 記錄排名前十的分數,將其顯示在排行榜上。當一個新的分數計入排行榜時,一個 Cloud Function[6] 會將分數按降序排列并刪除目前不在前十的分數。

/// Acquires top 10 [LeaderboardEntryData]s.
Future<List<LeaderboardEntryData>> fetchTop10Leaderboard() async {
  try {
    final querySnapshot = await _firebaseFirestore
      .collection(_leaderboardCollectionName)
      .orderBy(_scoreFieldName, descending: true)
      .limit(_leaderboardLimit)
      .get();
    final documents = querySnapshot.docs;
    return documents.toLeaderboard();
  } on LeaderboardDeserializationException {
    rethrow;
  } on Exception catch (error, stackTrace) {
    throw FetchTop10LeaderboardException(error, stackTrace);
  }
}

構建 Web 應用

與傳統應用相比,制作響應式的游戲更容易。彈球游戲只需要根據設備的大小進行縮放即可。對于 I/O 彈球游戲,我們基于固定比例的設備大小進行縮放。確保了無論在何種顯示大小,坐標系統總是相同的。保證組件在不同設備之間的一致顯示和交互非常重要。

I/O 彈球游戲也適配了移動和桌面瀏覽器。在移動瀏覽器上,用戶可以點擊啟動按鈕開始播放,也可以點擊屏幕左右兩側來控制相應的擋板。在桌面瀏覽器上,用戶可以使用鍵盤來發射彈球和控制擋板。

代碼架構

彈球代碼遵循分層架構,每個功能都在自己的文件夾中。在這個項目中,游戲邏輯也與視覺組件分離。讓我們能獨立于游戲邏輯輕松地更新視覺元素。彈球游戲的主題取決于玩家在游戲開始前選擇的角色。主題是由 CharacterThemeCubit 類控制的。根據角色的選擇,球的顏色、背景和其他元素都會更新。

/// {@template character_theme}
/// Base class for creating character themes.
///
/// Character specific game components should have a getter specified here to
/// load their corresponding assets for the game.
/// {@endtemplate}
abstract class CharacterTheme extends Equatable {
  /// {@macro character_theme}
  const CharacterTheme();
/// Name of character.
  String get name;
/// Asset for the ball.
  AssetGenImage get ball;
/// Asset for the background.
  AssetGenImage get background;
/// Icon asset.
  AssetGenImage get icon;
/// Icon asset for the leaderboard.
  AssetGenImage get leaderboardIcon;
/// Asset for the the idle character animation.
  AssetGenImage get animation;
@override
  List<Object> get props => [
        name,
        ball,
        background,
        icon,
        leaderboardIcon,
        animation,
      ];
}

I/O 彈球的游戲狀態是用 flam_bloc[7] 這個 package 處理的,這是一個組合了 bloc 和 Flame 組件的 package。例如,我們使用 flame_bloc 來記錄剩余的游戲回合數、游戲中獲得的獎勵以及當前的游戲分數。另外,在 wdget 樹頂層有一個 widget,它包含加載頁面的邏輯以及玩游戲的說明。我們還遵循 行為型模式[8] 來封裝和隔離基于組件的游戲功能元素。例如,保險杠在被球擊中時會發出聲音,所以我們實現了 BumperNoiseBehavior 類來處理這個問題。

class BumperNoiseBehavior extends ContactBehavior {
  @override
  void beginContact(Object other, Contact contact) {
    super.beginContact(other, contact);
    readProvider<PinballPlayer>().play(PinballAudio.bumper);
  }
}

代碼庫還包含全面的單元測試、組件測試和黃金測試。測試游戲會帶來一些挑戰,因為一個組件可能具有多個職責,使得它們很難單獨地進行測試。最終我們定義了更好的隔離和測試組件的模式,并將其改進整合到 flame_test[9] 這個 package 中。

組件沙盒

這個項目高度依賴于 Flame 組件帶來的仿真彈球體驗。代碼附帶了一個組件沙盒,它類似于 Flutter Gallery 中展示的 UI 組件庫[10]。在開發游戲時,這是一個很有用的工具,因為它提供了獨立的每個游戲組件以確保它們的外觀和行為符合預期,然后再將它們整合到游戲中。

接下來

邀請你來 I/O Pinball[11] 試玩并獲取高分!關注積分排行榜并且在社交媒體上分享你的分數,也可以在 GitHub repo[12] 訪問并學習項目的開源代碼。


原文鏈接:

https://medium.com/flutter/i-o-pinball-powered-by-flutter-and-firebase-d22423f3f5d

本地化: CFUG 團隊

中文鏈接: https://flutter.cn/posts/i-o-pinball

文內鏈接

[1]I/O 彈球游戲主頁: https://pinball.flutter.dev/

[2]Flame 開發文檔主頁: https://docs.flame-engine.org/

[3]Flame 團隊維護的 package: forge2d: https://pub.flutter-io.cn/packages/forge2d

[4]Box2D 物理引擎官網: https://box2d.org/

[5]Firebase Cloud Firestore 文檔: https://firebase.google.cn/docs/firestore

[6]Firebase Cloud Function 文檔: https://firebase.google.cn/docs/functions

[7]Flutter package: flam_bloc 頁面: https://pub.flutter-io.cn/packages/flame_bloc

[8]百度百科: 設計模式行為型模式: http://baike.baidu.com/l/i4znnfCN

[9]Flutter package: flame_test 頁面: https://pub.flutter-io.cn/packages/flame_test

[10]Flutter Gallery 網頁版: https://gallery.flutter.cn/#/

[11]I/O Pinball 彈球游戲: https://pinball.flutter.dev/

[12]彈球游戲源代碼倉庫: https://github.com/flutter/pinball


https://mp.weixin.qq.com/s/P7yBzPvkdKoirpJ8Mk-DNQ

最多閱讀

如何有效定位Flutter內存問題? 2年以前  |  12971次閱讀
Flutter的手勢GestureDetector分析詳解 3年以前  |  9239次閱讀
Flutter插件詳解及其發布插件 3年以前  |  8162次閱讀
在Flutter中添加資源和圖片 4年以前  |  6312次閱讀
發布Flutter開發的iOS程序 4年以前  |  5482次閱讀
Flutter 狀態管理指南之 Provider 3年以前  |  5436次閱讀
Flutter for Web詳細介紹 3年以前  |  5236次閱讀
在Flutter中發起HTTP網絡請求 4年以前  |  4981次閱讀
使用Inspector檢查用戶界面 4年以前  |  4852次閱讀
Flutter路由詳解 3年以前  |  4427次閱讀
Flutter Widget框架概述 4年以前  |  3953次閱讀
為Flutter應用程序添加交互 4年以前  |  3918次閱讀
JSON和序列化 4年以前  |  3806次閱讀
推薦5個Flutter重磅開源項目! 2年以前  |  3725次閱讀
Flutter框架概覽 4年以前  |  3659次閱讀
處理文本輸入 4年以前  |  3575次閱讀
使用自定義字體 4年以前  |  3542次閱讀

手機掃碼閱讀
18禁止午夜福利体验区,人与动人物xxxx毛片人与狍,色男人窝网站聚色窝
<蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>