【文章內容使用 Gemini 1.5 Pro 自動產生】
將 Flutter 遊戲開發提升到新的境界
為了 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 構建遊戲所需的基礎。
遊戲迴圈
在傳統應用程式中,螢幕通常在使用者發生事件或互動之前都是視覺上的靜態。對於遊戲來說,則相反 - 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 彈珠台的挑戰之一是弄清楚如何僅使用二維元素來建立三維效果。元件的順序決定了它們在螢幕上的渲染方式。例如,當彈珠向上發射到斜坡時,彈珠的順序會上升,因此它看起來像是位於斜坡的頂部。
彈珠、彈射器、兩個彈射器和 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` 會將所有圖像在迴圈中編譯在一起,使元素看起來像動畫。
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) 會將得分按降序排序,並移除任何不在前十名的得分。
/// 擷取前 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` 類別控制。根據角色選擇,彈珠顏色、背景和其他元素會更新。
/// {@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/#/)。這是在開發遊戲時的一個有用的工具,因為它允許您隔離開發遊戲元件,並確保它們在整合到遊戲中之前,看起來和行為符合預期。
接下來要做什麼
看看您是否可以在 [I/O 彈珠台](https://pinball.flutter.dev/) 中獲得高分!程式碼在 [這個 GitHub 儲存庫](https://github.com/flutter/pinball) 中是開源的。請關注排行榜,並在社交媒體上分享您的得分!
I/O 彈珠台由 Flutter 和 Firebase 提供支援 最初發佈在 Flutter 上的 Medium,人們在那裡透過突出顯示和回應這個故事來繼續討論。