0%

【文章翻譯】I/O Pinball Powered by Flutter and Firebase

【文章內容使用 Gemini 1.5 Pro 自動產生】

將 Flutter 遊戲開發提升到新的境界

Flutter’s Dash, Android Jetpack, Chrome Dino, and Firebase’s Sparky gathering around a pinball machine.

為了 Google I/O,我們與 Flutter 團隊合作,重新構想了一款經典彈珠台遊戲,該遊戲使用 Flutter 和 Firebase 構建。以下是如何在 Flame 遊戲引擎的幫助下將 [I/O 彈珠台](https://pinball.flutter.dev/) 在網頁上呈現。

遊戲開發基礎

對於構建由使用者互動驅動的遊戲(例如益智遊戲和文字遊戲)來說,Flutter 架構是一個很好的選擇。當談到使用遊戲迴圈的遊戲時,[Flame](https://docs.flame-engine.org/)(一個建立在 Flutter 之上的二維遊戲引擎)可以成為一個有用的工具。I/O 彈珠台使用 Flame 的開箱即用功能,例如動畫、物理、碰撞偵測等等,同時也利用 Flutter 架構的基礎設施。如果您可以使用 Flutter 構建應用程式,那麼您已經擁有了使用 Flame 構建遊戲所需的基礎。

Flame engine logo

遊戲迴圈

在傳統應用程式中,螢幕通常在使用者發生事件或互動之前都是視覺上的靜態。對於遊戲來說,則相反 - UI 是持續渲染的,遊戲的狀態也在不斷變化。Flame 提供了一個遊戲 Widget,它在內部管理遊戲迴圈,以便 UI 能夠以高效能的方式持續渲染。`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;
}

使用二維元件渲染三維空間

構建 I/O 彈珠台的挑戰之一是弄清楚如何僅使用二維元素來建立三維效果。元件的順序決定了它們在螢幕上的渲染方式。例如,當彈珠向上發射到斜坡時,彈珠的順序會上升,因此它看起來像是位於斜坡的頂部。

I/O Pinball playfield featuring Flutter’s Dash, Android Jetpack, Chrome’s Dino, and Firebase’s Sparky, and other Google-themed elements. Toward the bottom of the board there are two flippers with two bumpers above and to the bottom right is the ball ready to be launched.

彈珠、彈射器、兩個彈射器和 Chrome 恐龍都是具有動態主體的元素,這些元素會受到世界物理的影響。彈珠的大小也會根據它在遊戲場上的位置而變化。當彈珠移動到遊戲場的頂部時,它的尺寸會縮小,從使用者的角度來看,它看起來離使用者更遠。此外,彈珠上的重力會根據彈珠台的角度進行調整,因此彈珠在斜坡上會下降得更快。

/// 根據彈珠在遊戲場上的位置縮放彈珠的主體和 Sprite。
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](https://pub.dev/packages/forge2d) 套件。此套件將開源 [Box2D 物理引擎](https://box2d.org/) 移植到 Dart 中,以便可以輕鬆地與 Flutter 整合。我們使用 forge2d 來為遊戲的物理提供動力,例如遊戲場上物體(Fixture)之間的碰撞偵測。

forge2D 允許我們監聽 Fixture 之間的碰撞事件。然後,我們為 Fixture 添加 `ContactCallbacks`,以便在兩個元素之間發生接觸時收到通知。例如,當彈珠(具有具有 `CircleShape` 的 Fixture)與彈珠台(具有具有 `EllipseShape` 的 Fixture)發生接觸時,得分會增加。在這些回呼函式中,我們可以精確設定接觸的開始和結束位置,因此當兩個元素與另一個元素發生接觸時,就會發生碰撞。

@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 表格動畫

彈珠台遊戲場上有一些元素,例如 Android、Dash、Sparky 和 Chrome 恐龍,它們是動畫的。對於這些元素,我們使用了 Sprite 表格,這些表格包含在 Flame 引擎中,並帶有 `SpriteAnimationComponent`。對於每個元素,我們都有一個檔案,其中包含圖像的不同方向、檔案中的幀數以及幀之間的時間。使用這些資料,Flame 中的 `SpriteAnimationComponent` 會將所有圖像在迴圈中編譯在一起,使元素看起來像動畫。

Sprite sheet showing the Android in various orientations so that if played on a loop, it will appear to be spinning in a circle.
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](https://firebase.google.com/docs/firestore) 來追蹤前十名得分,並將其擷取以顯示在排行榜上。當新的得分寫入排行榜時,[Cloud Function](https://firebase.google.com/docs/functions) 會將得分按降序排序,並移除任何不在前十名的得分。

Leaderboard for I/O pinball with 10 top scores displayed.
/// 擷取前 10 個 [LeaderboardEntryData]。
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);
}
}

為網頁構建

與傳統應用程式相比,構建響應式的遊戲可能會更容易。彈珠台遊戲場只需要縮放到設備的大小即可。對於 I/O 彈珠台,我們根據設備的大小以固定比例進行縮放。這確保了無論顯示大小如何,座標系始終保持一致,這對於確保元件在設備之間一致地顯示和互動非常重要。

I/O 彈珠台還可以適應行動或桌面瀏覽器。在行動瀏覽器中,使用者可以點擊發射按鈕開始遊戲,也可以點擊螢幕的左右兩側來控制對應的彈射器。在桌面瀏覽器中,使用者可以使用鍵盤來發射彈珠和控制彈射器。

程式碼庫架構

彈珠台程式碼庫遵循分層架構,每個功能都在其自己的資料夾中。此專案中,遊戲邏輯也與視覺元件分開。這可以確保我們能夠輕鬆地獨立更新視覺元素,而與遊戲邏輯無關,反之亦然。

彈珠台的主題會根據使用者在開始遊戲之前選擇的角色而有所不同。主題由 `CharacterThemeCubit` 類別控制。根據角色選擇,彈珠顏色、背景和其他元素會更新。

Displays the different I/O Pinball themes. The top left shows Sparky, carpet with prominent flame decorations and neon orange lighting. The top right shows Dash, a carpet with prominent egg decorations and neon blue lighting. The bottom left shows Android, carpet with prominent Android Jetpack decorations and neon green lighting. The bottom right shows Chrome Dino, carpet with prominent cactus decorations, and neon white lighting.
/// {@template character_theme}
/// 用於建立角色主題的基類。
///
/// 角色特定的遊戲元件應在此處指定一個 getter,以
/// 載入其對應的遊戲資產。
/// {@endtemplate}
abstract class CharacterTheme extends Equatable {
/// {@macro character_theme}
const CharacterTheme();
/// 角色名稱。
String get name;
/// 彈珠的資產。
AssetGenImage get ball;
/// 背景的資產。
AssetGenImage get background;
/// 圖示資產。
AssetGenImage get icon;
/// 排行榜的圖示資產。
AssetGenImage get leaderboardIcon;
/// 閒置角色動畫的資產。
AssetGenImage get animation;
@override
List<Object> get props => [
name,
ball,
background,
icon,
leaderboardIcon,
animation,
];
}

I/O 彈珠台的遊戲狀態由 [flame_bloc](https://pub.dev/packages/flame_bloc) 處理,flame_bloc 是一個將 bloc 與 Flame 元件連接起來的套件。例如,我們使用 flame_bloc 來追蹤剩餘的遊戲回合數、透過遊戲獲得的任何獎金以及目前的遊戲得分。此外,Widget 樹的頂部還有一個 Widget,其中包含載入頁面的邏輯,包括如何玩遊戲的說明。我們還遵循 [行為模式](https://en.wikipedia.org/wiki/Behavioral_pattern) 來封裝和隔離遊戲功能的某些元素,這些元素基於其元件。例如,彈珠台在被彈珠擊中時會發出聲音,因此我們實作了 `BumperNoiseBehavior` 類別來處理此問題。

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

程式碼庫還包含全面的單元、Widget 和黃金測試。有時,由於單個元件可能具有多個責任,這使得它們難以隔離測試,因此測試遊戲會帶來一些挑戰。結果,我們最終定義了一些模式來更好地隔離和測試元件。我們還將改進整合到 [flame_test](https://pub.dev/packages/flame_test) 套件中。

元件沙箱

此專案很大程度上依賴於 Flame 元件,以將彈珠台體驗呈現出來。程式碼庫附帶了一個元件沙箱,它類似於 [UI 元件庫](https://gallery.flutter.dev/#/)。這是在開發遊戲時的一個有用的工具,因為它允許您隔離開發遊戲元件,並確保它們在整合到遊戲中之前,看起來和行為符合預期。

Chrome Dino is animated, moving left to right and opening its mouth to shoot out the pinball ball.

接下來要做什麼

看看您是否可以在 [I/O 彈珠台](https://pinball.flutter.dev/) 中獲得高分!程式碼在 [這個 GitHub 儲存庫](https://github.com/flutter/pinball) 中是開源的。請關注排行榜,並在社交媒體上分享您的得分!


I/O 彈珠台由 Flutter 和 Firebase 提供支援 最初發佈在 Flutter 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。