1. 簡介
Flame 是以 Flutter 為基礎的 2D 遊戲引擎。在本程式碼研究室中,您將建構一款遊戲,靈感來自 1970 年代經典電玩遊戲之一,即 Steve Wozniak 的 Breakout。您將使用 Flame 的元件繪製球棒、球和磚塊。您將使用 Flame 的效果為蝙蝠的動作製作動畫,並瞭解如何將 Flame 與 Flutter 的狀態管理系統整合。
完成後,遊戲應如下列動畫 GIF 所示,但速度會稍慢。

課程內容
- 瞭解 Flame 的基本運作方式,首先請參閱
GameWidget。 - 如何使用遊戲迴圈。
- 瞭解 Flame 的
Component運作方式。這與 Flutter 的Widget類似。 - 如何處理衝突。
- 如何使用
Effect為Component設定動畫。 - 如何在 Flame 遊戲上疊加 Flutter
Widget。 - 如何將 Flame 與 Flutter 的狀態管理功能整合。
建構項目
在本程式碼研究室中,您將使用 Flutter 和 Flame 建構 2D 遊戲。完成後,遊戲應符合下列規定:
- 在 Flutter 支援的所有六個平台 (Android、iOS、Linux、macOS、Windows 和網頁) 上運作
- 使用 Flame 的遊戲迴圈,維持至少 60 fps。
- 使用
google_fonts套件和flutter_animate等 Flutter 功能,重現 80 年代電玩遊戲的感覺。
2. 設定 Flutter 環境
編輯者
為簡化本程式碼研究室,我們假設您使用 Visual Studio Code (VS Code) 做為開發環境。VS Code 免費提供,適用於所有主要平台。本程式碼研究室使用 VS Code,因為操作說明預設為 VS Code 專屬快速鍵。工作變得更簡單:「按一下這個按鈕」或「按下這個鍵來執行 X」,而不是「在編輯器中執行適當動作來執行 X」。
您可以使用任何喜歡的編輯器,例如 Android Studio、其他 IntelliJ IDE、Emacs、Vim 或 Notepad++,這些編輯器都支援 Flutter。

選擇開發目標
Flutter 可為多個平台製作應用程式。您的應用程式可以在下列任一作業系統上執行:
- iOS
- Android
- Windows
- macOS
- Linux
- 網路
一般來說,您會選擇一個作業系統做為開發目標。這個作業系統就是開發過程中用來執行應用程式的 OS。

舉例來說,假設您使用 Windows 筆電開發 Flutter 應用程式,然後選擇 Android 做為開發目標。如要預覽應用程式,請使用 USB 傳輸線將 Android 裝置連接至 Windows 筆電,然後在該 Android 裝置或 Android 模擬器上執行開發中的應用程式。您可能已選擇 Windows 做為開發目標,這會將開發中的應用程式與編輯器一起做為 Windows 應用程式執行。
請先選擇要繼續使用哪個帳戶。您日後隨時可以在其他作業系統上執行應用程式。選擇開發目標可讓下一個步驟更順利。
安裝 Flutter
如需安裝 Flutter SDK 的最新操作說明,請前往 docs.flutter.dev。
Flutter 網站上的操作說明涵蓋 SDK 安裝作業,以及開發目標相關工具和編輯器外掛程式。請安裝下列軟體,完成本程式碼研究室:
- Flutter SDK
- 安裝 Flutter 外掛程式的 Visual Studio Code
- 所選開發目標的編譯器軟體。(如要以 Windows 為目標,您需要 Visual Studio;如要以 macOS 或 iOS 為目標,則需要 Xcode)
在下一節中,您將建立第一個 Flutter 專案。
如需排解任何問題,您可能會發現以下 StackOverflow 的問答有助於疑難排解。
常見問題
- 如何找出 Flutter SDK 路徑?
- 如果找不到 Flutter 指令,該怎麼辦?
- 如何修正「Waiting for another flutter command to release the startup lock」(等待其他 Flutter 指令釋放啟動鎖定) 問題?
- 如何告知 Flutter Android SDK 的安裝位置?
- 執行
flutter doctor --android-licenses時發生 Java 錯誤,該如何處理? - 如何解決找不到 Android
sdkmanager工具的問題? - 如何處理「缺少『
cmdline-tools』元件」錯誤? - 如何在 Apple 晶片 (M1) 上執行 CocoaPods?
- 如何在 VS Code 中停用儲存時自動格式化功能?
3. 建立專案
建立第一個 Flutter 專案
包括開啟 VS Code,並在您選擇的目錄中建立 Flutter 應用程式範本。
- 啟動 Visual Studio Code。
- 開啟指令區塊面板 (
F1或Ctrl+Shift+P或Shift+Cmd+P),然後輸入「flutter new」。出現後,請選取「Flutter: New Project」指令。

- 選取「Empty Application」。選擇要建立專案的目錄。這個目錄不得需要提升權限,路徑中也不得有空格。例如主目錄或
C:\src\。

- 為專案命名
brick_breaker。本程式碼研究室的其餘部分會假設您將應用程式命名為brick_breaker。

Flutter 現在會建立專案資料夾,並在 VS Code 中開啟該資料夾。現在請使用應用程式的基本架構,覆寫兩個檔案的內容。
複製及貼上初始應用程式
這會將本程式碼研究室提供的範例程式碼新增至應用程式。
- 在 VS Code 的左側窗格中,按一下「Explorer」並開啟
pubspec.yaml檔案。

- 將這個檔案的內容替換成以下內容:
pubspec.yaml
name: brick_breaker
description: "Re-implementing Woz's Breakout"
publish_to: "none"
version: 0.1.0
environment:
sdk: ^3.8.0
dependencies:
flutter:
sdk: flutter
flame: ^1.28.1
flutter_animate: ^4.5.2
google_fonts: ^6.2.1
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
flutter:
uses-material-design: true
pubspec.yaml 檔案會指定應用程式的基本資訊,例如目前版本、依附元件,以及隨附的資產。
- 開啟
lib/目錄中的main.dart檔案。

- 將這個檔案的內容替換成以下內容:
lib/main.dart
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
void main() {
final game = FlameGame();
runApp(GameWidget(game: game));
}
- 執行這段程式碼,確認一切運作正常。畫面上應該會顯示新視窗,且只有空白的黑色背景。現在,全球最糟糕的電玩遊戲也能以 60 FPS 算繪!

4. 建立遊戲
評估遊戲
以 2D 形式進行的遊戲需要遊戲區。您將建構特定維度的區域,然後使用這些維度來調整遊戲的其他層面。
在遊戲區中,座標的排列方式有很多種。根據慣例,您可以從畫面中心測量方向,原點 (0,0) 位於畫面中心,正值會沿著 x 軸將項目向右移動,沿著 y 軸向上移動。這項標準適用於現今大多數遊戲,尤其是涉及三維空間的遊戲。
當初建立原始 Breakout 遊戲時,慣例是將原點設在左上角。正 x 方向維持不變,但 y 翻轉了。x 正向 x 方向為向右,y 正向為向下。為了忠實呈現當時的風格,這個遊戲將原點設為左上角。
在名為 lib/src 的新目錄中,建立名為 config.dart 的檔案。這個檔案會在後續步驟中加入更多常數。
lib/src/config.dart
const gameWidth = 820.0;
const gameHeight = 1600.0;
這個遊戲的寬度為 820 像素,高度為 1600 像素。遊戲區域會縮放以配合顯示視窗,但新增至畫面的所有元件都會符合這個高度和寬度。
建立 PlayArea
在打磚塊遊戲中,球會從遊戲區的牆壁彈開。如要處理碰撞情形,您必須先使用 PlayArea 元件。
- 在名為
lib/src/components的新目錄中,建立名為play_area.dart的檔案。 - 將下列內容加入這個檔案。
lib/src/components/play_area.dart
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
class PlayArea extends RectangleComponent with HasGameReference<BrickBreaker> {
PlayArea() : super(paint: Paint()..color = const Color(0xfff2e8cf));
@override
FutureOr<void> onLoad() async {
super.onLoad();
size = Vector2(game.width, game.height);
}
}
Flutter 有 Widget,Flame 則有 Component。Flutter 應用程式是由小工具樹狀結構組成,而 Flame 遊戲則是由元件樹狀結構組成。
這就是 Flutter 和 Flame 之間有趣的差異。Flutter 的小工具樹狀結構是暫時性說明,用於更新持續性且可變動的 RenderObject 層。Flame 的元件是持續且可變動的,開發人員應將這些元件做為模擬系統的一部分。
Flame 的元件經過最佳化,可表達遊戲機制。本程式碼研究室將從遊戲迴圈開始,請見下一個步驟。
- 為避免雜亂,請新增包含這個專案中所有元件的檔案。在
lib/src/components中建立components.dart檔案,並加入以下內容。
lib/src/components/components.dart
export 'play_area.dart';
export 指令的作用與 import 相反。這個檔案會宣告匯入其他檔案時公開的功能。在後續步驟中新增元件時,這個檔案會增加更多項目。
建立 Flame 遊戲
如要消除上一個步驟中的紅色波浪線,請為 Flame 的 FlameGame 衍生新的子類別。
- 在
lib/src中建立名為brick_breaker.dart的檔案,並加入下列程式碼。
lib/src/brick_breaker.dart
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'components/components.dart';
import 'config.dart';
class BrickBreaker extends FlameGame {
BrickBreaker()
: super(
camera: CameraComponent.withFixedResolution(
width: gameWidth,
height: gameHeight,
),
);
double get width => size.x;
double get height => size.y;
@override
FutureOr<void> onLoad() async {
super.onLoad();
camera.viewfinder.anchor = Anchor.topLeft;
world.add(PlayArea());
}
}
這個檔案會協調遊戲動作。建構遊戲執行個體時,這段程式碼會將遊戲設定為使用固定解析度轉譯。遊戲會調整大小,填滿包含遊戲的畫面,並視需要新增上下黑邊。
您會公開遊戲的寬度和高度,讓子項元件 (例如 PlayArea) 可以自行設定適當大小。
在覆寫的 onLoad 方法中,程式碼會執行兩項動作。
- 將左上角設為觀景窗的錨點。根據預設,
viewfinder會使用區域中間做為(0,0)的錨點。 - 將
PlayArea新增至world。世界代表遊戲世界。它會透過CameraComponent的檢視區塊轉換投影所有子項。
在螢幕上顯示遊戲畫面
如要查看您在這個步驟中進行的所有變更,請更新 lib/main.dart 檔案,並進行下列變更。
lib/main.dart
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'src/brick_breaker.dart'; // Add this import
void main() {
final game = BrickBreaker(); // Modify this line
runApp(GameWidget(game: game));
}
完成這些變更後,請重新啟動遊戲。遊戲畫面應如下圖所示。

在下一個步驟中,您將在世界中新增球體,並讓球體移動!
5. 顯示球
建立球體元件
如要在畫面上放置移動中的球,需要建立另一個元件並新增至遊戲世界。
- 按照下列方式編輯
lib/src/config.dart檔案的內容。
lib/src/config.dart
const gameWidth = 820.0;
const gameHeight = 1600.0;
const ballRadius = gameWidth * 0.02; // Add this constant
在本程式碼研究室中,您會多次看到將具名常數定義為衍生值的設計模式。您可以藉此修改頂層 gameWidth 和 gameHeight,瞭解遊戲外觀和風格的變化。
- 在
lib/src/components中名為ball.dart的檔案中,建立Ball元件。
lib/src/components/ball.dart
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
class Ball extends CircleComponent {
Ball({
required this.velocity,
required super.position,
required double radius,
}) : super(
radius: radius,
anchor: Anchor.center,
paint: Paint()
..color = const Color(0xff1e6091)
..style = PaintingStyle.fill,
);
final Vector2 velocity;
@override
void update(double dt) {
super.update(dt);
position += velocity * dt;
}
}
您先前使用 RectangleComponent 定義 PlayArea,因此存在更多形狀是合理的。CircleComponent (例如 RectangleComponent) 衍生自 PositionedComponent,因此您可以在畫面上放置球。更重要的是,你可以更新其位置。
這個元件會介紹 velocity 的概念,也就是位置隨時間的變化。速度是 Vector2 物件,因為速度包含速率和方向。如要更新位置,請覆寫 update 方法,遊戲引擎會為每個影格呼叫這個方法。dt 是指前一個影格和這個影格之間的時間長度。這有助於您因應不同影格速率 (60 Hz 或 120 Hz) 或因運算量過大而導致影格過長等因素。
請密切注意 position += velocity * dt 更新。以下說明如何實作,以便更新一段時間內的動作離散模擬。
- 如要在元件清單中加入
Ball元件,請按照下列方式編輯lib/src/components/components.dart檔案。
lib/src/components/components.dart
export 'ball.dart'; // Add this export
export 'play_area.dart';
將球體新增至世界
你有一顆球。將其放置在世界中,並設定在遊戲區移動。
按照下列方式編輯 lib/src/brick_breaker.dart 檔案。
lib/src/brick_breaker.dart
import 'dart:async';
import 'dart:math' as math; // Add this import
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'components/components.dart';
import 'config.dart';
class BrickBreaker extends FlameGame {
BrickBreaker()
: super(
camera: CameraComponent.withFixedResolution(
width: gameWidth,
height: gameHeight,
),
);
final rand = math.Random(); // Add this variable
double get width => size.x;
double get height => size.y;
@override
FutureOr<void> onLoad() async {
super.onLoad();
camera.viewfinder.anchor = Anchor.topLeft;
world.add(PlayArea());
world.add(
Ball( // Add from here...
radius: ballRadius,
position: size / 2,
velocity: Vector2(
(rand.nextDouble() - 0.5) * width,
height * 0.2,
).normalized()..scale(height / 4),
),
);
debugMode = true; // To here.
}
}
這項變更會將 Ball 元件新增至 world。如要將球的 position 設為顯示區域的中心,程式碼會先將遊戲大小減半,因為 Vector2 具有運算子多載 (* 和 /),可依純量值縮放 Vector2。
設定球的velocity涉及更多複雜性。目的是以合理的速度,將球體移到螢幕下方,方向隨機。呼叫 normalized 方法會建立 Vector2 物件,並將其設為與原始 Vector2 相同的方向,但縮放比例會降至 1。無論球往哪個方向移動,速度都會保持一致。接著,球的速度會放大至遊戲高度的 1/4。
如要取得這些正確值,需要經過幾次疊代,也就是業界所謂的「遊戲測試」。
最後一行會開啟偵錯顯示畫面,在顯示畫面中加入額外資訊,協助您進行偵錯。
現在執行遊戲時,畫面應如下所示。

PlayArea 和 Ball 元件都有偵錯資訊,但背景遮罩會裁剪 PlayArea 的數字。之所以會顯示所有項目的偵錯資訊,是因為您已為整個元件樹狀結構開啟 debugMode。如果這樣比較有用,您也可以只為所選元件開啟偵錯功能。
如果重新啟動遊戲幾次,可能會發現球與牆壁的互動不如預期。如要達成這個效果,您需要新增碰撞偵測功能,這會在下一個步驟中完成。
6. 隨意跳動
新增碰撞偵測功能
碰撞偵測功能會新增行為,讓遊戲辨識兩個物件何時發生接觸。
如要為遊戲新增碰撞偵測功能,請將 HasCollisionDetection 混入 BrickBreaker 遊戲,如下列程式碼所示。
lib/src/brick_breaker.dart
import 'dart:async';
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'components/components.dart';
import 'config.dart';
class BrickBreaker extends FlameGame with HasCollisionDetection { // Modify this line
BrickBreaker()
: super(
camera: CameraComponent.withFixedResolution(
width: gameWidth,
height: gameHeight,
),
);
final rand = math.Random();
double get width => size.x;
double get height => size.y;
@override
FutureOr<void> onLoad() async {
super.onLoad();
camera.viewfinder.anchor = Anchor.topLeft;
world.add(PlayArea());
world.add(
Ball(
radius: ballRadius,
position: size / 2,
velocity: Vector2(
(rand.nextDouble() - 0.5) * width,
height * 0.2,
).normalized()..scale(height / 4),
),
);
debugMode = true;
}
}
這會追蹤元件的碰撞箱,並在每個遊戲刻度上觸發碰撞回呼。
如要開始填入遊戲的命中框,請修改 PlayArea 元件,如下所示:
lib/src/components/play_area.dart
import 'dart:async';
import 'package:flame/collisions.dart'; // Add this import
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
class PlayArea extends RectangleComponent with HasGameReference<BrickBreaker> {
PlayArea()
: super(
paint: Paint()..color = const Color(0xfff2e8cf),
children: [RectangleHitbox()], // Add this parameter
);
@override
FutureOr<void> onLoad() async {
super.onLoad();
size = Vector2(game.width, game.height);
}
}
將 RectangleHitbox 元件新增為 RectangleComponent 的子項,即可建構碰撞偵測的點擊區塊,大小與父項元件相符。如果希望熱區小於或大於父項元件,可以使用 RectangleHitbox 的工廠建構函式 relative。
彈跳球
到目前為止,新增碰撞偵測功能對遊戲體驗沒有任何影響。但修改 Ball 元件後,系統就會變更該值。當球體與 PlayArea 發生碰撞時,必須改變的是球體的行為。
按照下列方式修改 Ball 元件。
lib/src/components/ball.dart
import 'package:flame/collisions.dart'; // Add this import
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart'; // And this import
import 'play_area.dart'; // And this one too
class Ball extends CircleComponent
with CollisionCallbacks, HasGameReference<BrickBreaker> { // Add these mixins
Ball({
required this.velocity,
required super.position,
required double radius,
}) : super(
radius: radius,
anchor: Anchor.center,
paint: Paint()
..color = const Color(0xff1e6091)
..style = PaintingStyle.fill,
children: [CircleHitbox()], // Add this parameter
);
final Vector2 velocity;
@override
void update(double dt) {
super.update(dt);
position += velocity * dt;
}
@override // Add from here...
void onCollisionStart(
Set<Vector2> intersectionPoints,
PositionComponent other,
) {
super.onCollisionStart(intersectionPoints, other);
if (other is PlayArea) {
if (intersectionPoints.first.y <= 0) {
velocity.y = -velocity.y;
} else if (intersectionPoints.first.x <= 0) {
velocity.x = -velocity.x;
} else if (intersectionPoints.first.x >= game.width) {
velocity.x = -velocity.x;
} else if (intersectionPoints.first.y >= game.height) {
removeFromParent();
}
} else {
debugPrint('collision with $other');
}
} // To here.
}
這個範例新增了 onCollisionStart 回呼,因此有重大變更。先前範例中新增至 BrickBreaker 的碰撞偵測系統會呼叫這個回呼。
首先,程式碼會測試 Ball 是否與 PlayArea 發生碰撞。目前遊戲世界中沒有其他元件,因此這似乎是多餘的。在下一個步驟中,您將蝙蝠加入世界時,這項設定就會變更。接著,它還會新增 else 條件,處理球與球拍以外物體碰撞的情況。溫馨提醒您實作其餘邏輯 (如有)。
當球撞到底牆時,球會從比賽場地消失,但仍可清楚看見。您將在後續步驟中運用 Flame 的特效功能處理這項構件。
現在球體會與遊戲牆壁發生碰撞,如果能讓玩家使用球拍擊球,一定會很有用...
7. 擊中球
建立 bat 檔案
如要在遊戲中加入球棒,讓球保持在場上,請按照下列步驟操作:
- 在
lib/src/config.dart檔案中插入一些常數,如下所示。
lib/src/config.dart
const gameWidth = 820.0;
const gameHeight = 1600.0;
const ballRadius = gameWidth * 0.02;
const batWidth = gameWidth * 0.2; // Add from here...
const batHeight = ballRadius * 2;
const batStep = gameWidth * 0.05; // To here.
batHeight 和 batWidth 常數的意義很明確。另一方面,batStep 常數則需要稍微說明。如要與遊戲中的球互動,玩家可以使用滑鼠或手指拖曳球拍 (視平台而定),或使用鍵盤。batStep 常數會設定球棒在每次按下向左或向右鍵時移動的距離。
- 請按照下列方式定義
Bat元件類別。
lib/src/components/bat.dart
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame/events.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
class Bat extends PositionComponent
with DragCallbacks, HasGameReference<BrickBreaker> {
Bat({
required this.cornerRadius,
required super.position,
required super.size,
}) : super(anchor: Anchor.center, children: [RectangleHitbox()]);
final Radius cornerRadius;
final _paint = Paint()
..color = const Color(0xff1e6091)
..style = PaintingStyle.fill;
@override
void render(Canvas canvas) {
super.render(canvas);
canvas.drawRRect(
RRect.fromRectAndRadius(Offset.zero & size.toSize(), cornerRadius),
_paint,
);
}
@override
void onDragUpdate(DragUpdateEvent event) {
super.onDragUpdate(event);
position.x = (position.x + event.localDelta.x).clamp(0, game.width);
}
void moveBy(double dx) {
add(
MoveToEffect(
Vector2((position.x + dx).clamp(0, game.width), position.y),
EffectController(duration: 0.1),
),
);
}
}
這個元件推出幾項新功能。
首先,Bat 元件是 PositionComponent,不是 RectangleComponent 也不是 CircleComponent。也就是說,這段程式碼需要在畫面上算繪 Bat。為達成這個目標,它會覆寫 render 回呼。
仔細查看 canvas.drawRRect (繪製圓角矩形) 呼叫,您可能會問:「矩形在哪裡?」Offset.zero & size.toSize() 會利用 dart:ui Offset 類別的 operator & 多載,建立 Rect。這個簡寫一開始可能會讓您感到困惑,但您會在較低層級的 Flutter 和 Flame 程式碼中經常看到這個簡寫。
其次,這個 Bat 元件可使用手指或滑鼠拖曳 (視平台而定)。如要實作這項功能,請新增 DragCallbacks 混合,並覆寫 onDragUpdate 事件。
最後,Bat 元件需要回應鍵盤控制。moveBy 函式可讓其他程式碼指示蝙蝠向左或向右移動特定數量的虛擬像素。這項函式會導入 Flame 遊戲引擎的新功能:Effect。將 MoveToEffect 物件新增為這個元件的子項後,球員就會看到球棒動畫移動到新位置。Flame 提供一系列 Effect,可執行各種效果。
Effect 的建構函式引數包含對 game 擷取器的參照。因此您要在這個類別中加入 HasGameReference mixin。這個 mixin 會將型別安全 game 存取子新增至這個元件,以便存取元件樹狀結構頂端的 BrickBreaker 執行個體。
- 如要讓
BrickBreaker能夠使用Bat,請按照下列方式更新lib/src/components/components.dart檔案。
lib/src/components/components.dart
export 'ball.dart';
export 'bat.dart'; // Add this export
export 'play_area.dart';
將蝙蝠新增至世界
如要將 Bat 元件新增至遊戲世界,請更新 BrickBreaker,如下所示。
lib/src/brick_breaker.dart
import 'dart:async';
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame/events.dart'; // Add this import
import 'package:flame/game.dart';
import 'package:flutter/material.dart'; // And this import
import 'package:flutter/services.dart'; // And this
import 'components/components.dart';
import 'config.dart';
class BrickBreaker extends FlameGame
with HasCollisionDetection, KeyboardEvents { // Modify this line
BrickBreaker()
: super(
camera: CameraComponent.withFixedResolution(
width: gameWidth,
height: gameHeight,
),
);
final rand = math.Random();
double get width => size.x;
double get height => size.y;
@override
FutureOr<void> onLoad() async {
super.onLoad();
camera.viewfinder.anchor = Anchor.topLeft;
world.add(PlayArea());
world.add(
Ball(
radius: ballRadius,
position: size / 2,
velocity: Vector2(
(rand.nextDouble() - 0.5) * width,
height * 0.2,
).normalized()..scale(height / 4),
),
);
world.add( // Add from here...
Bat(
size: Vector2(batWidth, batHeight),
cornerRadius: const Radius.circular(ballRadius / 2),
position: Vector2(width / 2, height * 0.95),
),
); // To here.
debugMode = true;
}
@override // Add from here...
KeyEventResult onKeyEvent(
KeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
super.onKeyEvent(event, keysPressed);
switch (event.logicalKey) {
case LogicalKeyboardKey.arrowLeft:
world.children.query<Bat>().first.moveBy(-batStep);
case LogicalKeyboardKey.arrowRight:
world.children.query<Bat>().first.moveBy(batStep);
}
return KeyEventResult.handled;
} // To here.
}
新增的 KeyboardEvents mixin 和覆寫的 onKeyEvent 方法會處理鍵盤輸入。回想您先前新增的程式碼,將球棒移動適當的步數。
其餘新增的程式碼區塊會將蝙蝠加入遊戲世界,並放在適當位置,比例也正確。這個檔案會顯示所有這些設定,方便您調整球棒和球的相對大小,找出最適合遊戲的感覺。
如果此時玩遊戲,你會發現可以移動球棒攔截球,但除了留在 Ball 碰撞偵測程式碼中的偵錯記錄外,沒有任何可見的回應。
現在就來修正這個問題。請編輯 Ball 元件,如下所示。
lib/src/components/ball.dart
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/effects.dart'; // Add this import
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
import 'bat.dart'; // And this import
import 'play_area.dart';
class Ball extends CircleComponent
with CollisionCallbacks, HasGameReference<BrickBreaker> {
Ball({
required this.velocity,
required super.position,
required double radius,
}) : super(
radius: radius,
anchor: Anchor.center,
paint: Paint()
..color = const Color(0xff1e6091)
..style = PaintingStyle.fill,
children: [CircleHitbox()],
);
final Vector2 velocity;
@override
void update(double dt) {
super.update(dt);
position += velocity * dt;
}
@override
void onCollisionStart(
Set<Vector2> intersectionPoints,
PositionComponent other,
) {
super.onCollisionStart(intersectionPoints, other);
if (other is PlayArea) {
if (intersectionPoints.first.y <= 0) {
velocity.y = -velocity.y;
} else if (intersectionPoints.first.x <= 0) {
velocity.x = -velocity.x;
} else if (intersectionPoints.first.x >= game.width) {
velocity.x = -velocity.x;
} else if (intersectionPoints.first.y >= game.height) {
add(RemoveEffect(delay: 0.35)); // Modify from here...
}
} else if (other is Bat) {
velocity.y = -velocity.y;
velocity.x =
velocity.x +
(position.x - other.position.x) / other.size.x * game.width * 0.3;
} else { // To here.
debugPrint('collision with $other');
}
}
}
這些程式碼變更可修正兩個不同的問題。
首先,修正了球體在觸及螢幕底部時消失的問題。如要修正這個問題,請將 removeFromParent 呼叫替換為 RemoveEffect。RemoveEffect 會在球離開可檢視的遊戲區域後,將球從遊戲世界中移除。
其次,這些變更修正了球棒和球之間發生碰撞的處理方式。這段處理程式碼對玩家非常有利。只要球員用球棒觸碰球,球就會回到畫面頂端。如果覺得這樣太寬鬆,想要更貼近現實的感覺,請變更這項處理方式,讓遊戲更符合您的期望。
值得一提的是,velocity 更新相當複雜。這不只是反轉速度的 y 分量,就像牆壁碰撞一樣。此外,系統也會根據球棒和球接觸時的相對位置,更新 x 元件。這可讓玩家進一步掌控球的動作,但除了實際操作,遊戲不會以任何方式向玩家說明具體做法。
現在您有了球拍可以擊球,如果能用球擊破一些磚塊,那就太棒了!
8. 拆除牆壁
建立積木
如要在遊戲中新增積木,請按照下列步驟操作:
- 在
lib/src/config.dart檔案中插入一些常數,如下所示。
lib/src/config.dart
import 'package:flutter/material.dart'; // Add this import
const brickColors = [ // Add this const
Color(0xfff94144),
Color(0xfff3722c),
Color(0xfff8961e),
Color(0xfff9844a),
Color(0xfff9c74f),
Color(0xff90be6d),
Color(0xff43aa8b),
Color(0xff4d908e),
Color(0xff277da1),
Color(0xff577590),
];
const gameWidth = 820.0;
const gameHeight = 1600.0;
const ballRadius = gameWidth * 0.02;
const batWidth = gameWidth * 0.2;
const batHeight = ballRadius * 2;
const batStep = gameWidth * 0.05;
const brickGutter = gameWidth * 0.015; // Add from here...
final brickWidth =
(gameWidth - (brickGutter * (brickColors.length + 1))) / brickColors.length;
const brickHeight = gameHeight * 0.03;
const difficultyModifier = 1.03; // To here.
- 插入
Brick元件,如下所示。
lib/src/components/brick.dart
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
import '../config.dart';
import 'ball.dart';
import 'bat.dart';
class Brick extends Rectangl<eComponent>span>
with CollisionCallbacks, HasGameReferenceBrickBreaker {
Brick({required super.position, required Color color})
: super(
size: Vector2(brickWidth, brickHeight),
anchor: Anchor.center,
paint: Paint()
..color = color
..style = PaintingStyle.fill,
children: [RectangleHitbox()]<,
);
@override
void onCollisionStart(
SetVector2 intersectionPoints,
PositionComponent other,
) {
super.onCollisionStart(intersectionPoints, other);
< rem>oveFromParent();
if (game.world.children.queryBrick().length == 1<) {
> game.world.removeAll(game.world.children.queryBall(<));>
game.world.removeAll(game.world.children.queryBat());
}
}
}
到目前為止,您應該已熟悉大部分的程式碼。這個程式碼使用 RectangleComponent,並在元件樹狀結構頂端,同時進行碰撞偵測和 BrickBreaker 遊戲的型別安全參照。
這段程式碼導入最重要的全新概念,就是玩家如何達成勝利條件。勝出條件檢查會查詢世界中的積木,並確認只剩一個積木。這可能有點令人困惑,因為前一行會從父項移除這個磚塊。
請務必瞭解,移除元件是排入佇列的指令。這個程式碼會在執行後移除積木,但會在遊戲世界下一個刻度之前移除。
如要讓 BrickBreaker 存取 Brick 元件,請按照下列方式編輯 lib/src/components/components.dart。
lib/src/components/components.dart
export 'ball.dart';
export 'bat.dart';
export 'brick.dart'; // Add this export
export 'play_area.dart';
在世界中新增積木
將 Ball 元件更新如下。
lib/src/components/ball.dart
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
import 'bat.dart';
import 'brick.dart'; // Add this import
import 'play_area.dart';
class Ball exten<ds CircleCom>ponent
with CollisionCallbacks, HasGameReferenceBrickBreaker {
Ball({
required this.velocity,
required super.position,
required double radius,
required this.difficultyModifier, // Add this parameter
}) : super(
radius: radius,
anchor: Anchor.center,
paint: Paint()
..color = const Color(0xff1e6091)
..style = PaintingStyle.fill,
children: [CircleHitbox()],
);
final Vector2 velocity;
final double difficultyModifier; // Add this member
@override
void update(double dt) {
super.update(dt);
position += <velocit>y * dt;
}
@override
void onCollisionStart(
SetVector2 intersectionPoints,
PositionComponent other,
) {
super.onCollisionStart(intersectionPoints, other);
< if (other is PlayArea) {
if (intersectionPoints.first.y = 0) {
veloci<ty.y = -velocity.y;
} else if (intersectionPoints.first.x = 0) {
veloci>ty.x = -velocity.x;
} else if (intersectionPoints.first.x = game.width) {
veloci>ty.x = -velocity.x;
} else if (intersectionPoints.first.y = game.height) {
add(RemoveEffect(delay: 0.35));
}
} else if (other is Bat) {
velocity.y = -velocity.y;
velocity.x =
velocity.x +
(position.x - other.position.x) / other.size.x * game.width * 0.3;
} else if (other is Brick) { < // Modify from here...
if (position.y other.position.y - other.size.y / 2) >{
velocity.y = -velocity.y;
} else if (position.y other.position.y + other.size.y / 2) <{
velocity.y = -velocity.y;
} else if (position.x other.position.x) >{
velocity.x = -velocity.x;
} else if (position.x other.position.x) {
velocity.x = -velocity.x;
}
velocity.setFrom(velocity * difficultyModifier); // To here.
}
}
}
這項功能是唯一的新元素,也就是難度修飾符,每次撞到磚塊後都會增加球速。您需要進行遊戲測試,找出適合遊戲的適當難度曲線,才能調整這個參數。
按照下列方式編輯 BrickBreaker 遊戲。
lib/src/brick_breaker.dart
import 'dart:async';
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'components/components.dart';
import 'config.dart';
class BrickBreaker extends FlameGame
with HasCollisionDetection, KeyboardEvents {
BrickBreaker()
: super(
camera: CameraComponent.withFixedResolution(
width: gameWidth,
height: gameHeight,>
),
);
final >rand = math.Random();
double g<et w>idth = size.x;
double get height = size.y;
@override
FutureOrvoid onLoad() async {
super.onLoad();
camera.viewfinder.anchor = Anchor.topLeft;
world.add(PlayArea());
world.add(
Ball(
difficultyModifier: difficultyModifier, // Add this argument
radius: ballRadius,
position: size / 2,
velocity: Vector2(
(rand.nextDouble() - 0.5) * width,
height * 0.2,
).normalized()..scale(height / 4),
),
);
world.add(
Bat(
size: Vector2(batWidth, batHeight),
cornerRadius: const Radius.circular(ballRadius / 2),
position: Vector2(width / 2, height * 0.95),
),
);
await world.addAll([ < // Add from here...
< for (var i = 0; i brickColors.length; i++)
for (var j = 1; j = 5; j++)
Brick(
position: Vector2(
(i + 0.5) * brickWidth + (i + 1) * brickGutter,
(j + 2.0) * brickHeight + j * brickGutter,
),
color: brickColors[i],
),
]); // To here.
debugMode = true;
< }
@override
>KeyEventResult onKeyEvent(
KeyEvent event,
SetLogicalKeyboardKey keysPressed,
) {
super.onKeyEvent(event, keysPressed);
switch (event.logicalKey)< {
> case LogicalKeyboardKey.arrowLeft:
world.children.queryBat().first.moveBy(-batStep)<;>span>
case LogicalKeyboardKey.arrowRight:
world.children.queryBat().first.moveBy(batStep);
}
return KeyEventResult.handled;
}
}
執行遊戲時,系統會顯示所有主要遊戲機制。您可以關閉偵錯功能並視為完成,但總覺得少了些什麼。

例如歡迎畫面、遊戲結束畫面,或許還有分數?Flutter 可在遊戲中新增這些功能,這也是您接下來要關注的重點。
9. 贏得遊戲
新增播放狀態
在這個步驟中,您會在 Flutter 包裝函式中嵌入 Flame 遊戲,然後為歡迎、遊戲結束和獲勝畫面新增 Flutter 疊加層。
首先,您要修改遊戲和元件檔案,實作可反映是否顯示疊加層的播放狀態,以及要顯示哪個疊加層。
- 按照下列方式修改
BrickBreaker遊戲。
lib/src/brick_breaker.dart
import 'dart:async';
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'components/components.dart';
import 'config.dart';
enum PlayState { welcome, playing, gameOver, won } // Add this enumeration
class BrickBreaker extends FlameGame
with HasCollisionDetection, KeyboardEvents, TapDetector { // Modify this line
BrickBreaker()
: super(
camera: CameraComponent.withFixedResolution(
width: gameWidth,
height: gameHeight,>
),
);
final >rand = math.Random();
double get width = size.x;
double get height = size.y;
late PlayState _playState; > // Add from here...
PlayState get playState = _playState;
set playState(PlayState playState) {
_playState = playState;
switch (playState) {
case PlayState.welcome:
case PlayState.gameOver:
case PlayState.won:
overlays.add(playState.name);
case PlayState.playing:
overlays.remove(PlayState.welcome.name);
overlays.remove(PlayState.gameOver.name);
overlays.remove(PlayState.won.name);
}
} < > // To here.
@override
FutureOrvoid onLoad() async {
super.onLoad();
camera.viewfinder.anchor = Anchor.topLeft;
world.add(PlayArea());
playState = PlayState.welcome; // Add from here...
}
void startGame() {
if (playState< == >PlayState.playing) return;
world.removeA<ll(>world.children.queryBall());
world.remove<All(w>orld.children.queryBat());
world.removeAll(world.children.queryBrick());
playState = PlayState.playing; // To here.
world.add(
Ball(
difficultyModifier: difficultyModifier,
radius: ballRadius,
position: size / 2,
velocity: Vector2(
(rand.nextDouble() - 0.5) * width,
height * 0.2,
).normalized()..scale(height / 4),
),
);
world.add(
Bat(
size: Vector2(batWidth, batHeight),
cornerRadius: const Radius.circular(ballRadius / 2),
position: Vector2(width / 2, height * 0.95),
),
);
world.addAll([ < // Drop the await
< for (var i = 0; i brickColors.length; i++)
for (var j = 1; j = 5; j++)
Brick(
position: Vector2(
(i + 0.5) * brickWidth + (i + 1) * brickGutter,
(j + 2.0) * brickHeight + j * brickGutter,
),
color: brickColors[i],
),
]);
} // Drop the debugMode
@override // Add from here...
void onTap() {
super.onTap();
startGame();
} // To her<e.
@override
>KeyEventResult onKeyEvent(
KeyEvent event,
SetLogicalKeyboardKey keysPressed,
) {
super.onKeyEvent(event, keysPressed);
switch (event.logicalKey)< {
> case LogicalKeyboardKey.arrowLeft:
world.children.queryBat().first.moveBy(-batStep)<;>span>
case LogicalKeyboardKey.arrowRight:
world.children.queryBat().first.moveBy(batStep);
case LogicalKeyboardKey.space: // Add from here...
case LogicalKeyboardKey.enter:
startGame(); // To here.
}
ret>urn KeyEventResult.handled;
}
@override
Color backgroundColor() = const Color(0xfff2e8cf); // Add this override
}
這段程式碼會大幅變更 BrickBreaker 遊戲。新增 playState 列舉需要花費大量心力。這會記錄玩家進入、玩遊戲,以及輸贏的過程。在檔案頂端定義列舉,然後將其例項化為隱藏狀態,並搭配相符的 getter 和 setter。當遊戲的各個部分觸發播放狀態轉換時,這些 getter 和 setter 可修改疊加層。
接著,您要將 onLoad 中的程式碼分割成 onLoad 和新的 startGame 方法。在此之前,您只能透過重新啟動遊戲來開始新遊戲。有了這些新功能,玩家現在不必採取如此極端的措施,就能開始新遊戲。
您為遊戲設定了兩個新的處理常式,允許玩家開始新遊戲。您新增了輕觸事件處理常式,並擴充鍵盤事件處理常式,讓使用者能以多種模式開始新遊戲。建立播放狀態模型後,更新元件以在玩家獲勝或輸掉時觸發播放狀態轉換,就是合情合理的做法。
- 按照下列方式修改
Ball元件。
lib/src/components/ball.dart
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
import 'bat.dart';
import 'brick.dart';
import 'play_area.dart';
class Ball exten<ds CircleCom>ponent
with CollisionCallbacks, HasGameReferenceBrickBreaker {
Ball({
required this.velocity,
required super.position,
required double radius,
required this.difficultyModifier,
}) : super(
radius: radius,
anchor: Anchor.center,
paint: Paint()
..color = const Color(0xff1e6091)
..style = PaintingStyle.fill,
children: [CircleHitbox()],
);
final Vector2 velocity;
final double difficultyModifier;
@override
void update(double dt) {
super.update(dt);
position += <velocit>y * dt;
}
@override
void onCollisionStart(
SetVector2 intersectionPoints,
PositionComponent other,
) {
super.onCollisionStart(intersectionPoints, other);
< if (other is PlayArea) {
if (intersectionPoints.first.y = 0) {
veloci<ty.y = -velocity.y;
} else if (intersectionPoints.first.x = 0) {
veloci>ty.x = -velocity.x;
} else if (intersectionPoints.first.x = game.width) {
veloci>ty.x = -velocity.x;
} else if (intersectionPoints.first.y = game.height) {
add(
RemoveEffect(
delay: 0.35,
onComplete: () { // Modify from here
game.playState = PlayState.gameOver;
},
),
); // To here.
}
} else if (other is Bat) {
velocity.y = -velocity.y;
velocity.x =
velocity.x +
(position.x - other.position.x) / other.size.x * game.wid<th * 0.3;
} else if (other is Brick) {
if (position.y other.position.y - other.size.y / 2) >{
velocity.y = -velocity.y;
} else if (position.y other.position.y + other.size.y / 2) <{
velocity.y = -velocity.y;
} else if (position.x other.position.x) >{
velocity.x = -velocity.x;
} else if (position.x other.position.x) {
velocity.x = -velocity.x;
}
velocity.setFrom(velocity * difficultyModifier);
}
}
}
這項小變更會在 RemoveEffect 中新增 onComplete 回呼,觸發 gameOver 播放狀態。如果播放器允許球從畫面底部脫離,這應該就差不多了。
- 請編輯
Brick元件,如下所示。
lib/src/components/brick.dart
impimport 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
import '../config.dart';
import 'ball.dart';
import 'bat.dart';
class Brick extends Rectangl<eComponent>span>
with CollisionCallbacks, HasGameReferenceBrickBreaker {
Brick({required super.position, required Color color})
: super(
size: Vector2(brickWidth, brickHeight),
anchor: Anchor.center,
paint: Paint()
..color = color
..style = PaintingStyle.fill,
children: [RectangleHitbox()]<,
);
@override
void onCollisionStart(
SetVector2 intersectionPoints,
PositionComponent other,
) {
super.onCollisionStart(intersectionPoints, other);
< rem>oveFromParent();
if (game.world.children.queryBrick().length == 1) {
game.playState = PlayState.won; // Add this l<ine<>/span>
game.world.removeAll(game.world.children.queryBall(<));>
game.world.removeAll(game.world.children.queryBat());
}
}
}
反之,如果玩家能擊碎所有磚塊,就會看到「遊戲獲勝」畫面。做得好,玩家!
新增 Flutter 封裝函式
如要提供遊戲的嵌入位置並新增播放狀態疊加層,請新增 Flutter Shell。
- 在
lib/src下建立widgets目錄。 - 新增
game_app.dart檔案,並在該檔案中插入下列內容。
lib/src/widgets/game_app.dart
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../brick_breaker.dart';
import '../config.dart';
class GameApp extends StatelessWidget {
const GameApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
textTheme: GoogleFonts.pressStart2pTextTheme().apply(
bodyColor: const Color(0xff184e77),
displayColor: const Color(0xff184e77),
),
),
home: Scaffold(
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xffa9d6e5), Color(0xfff2e8cf)],
),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: FittedBox(
child: SizedBox(
width: gameWidth,
height: gameHeight,
child: GameWidget.controlled(
gameFactory: BrickBreaker.new,
overlayBuilderMap: {
P>layState.welcome.name: (context, game) = Center(
child: Text(
'TAP TO PLAY',
style: Theme.of(context).textTheme.headlineLarge,
),
),
> PlayState.gameOver.name: (context, game) = Center(
child: Text(
'G A M E O V E R',
style: Theme.of(context).textTheme.headlineLarge,
),
),
> PlayState.won.name: (context, game) = Center(
child: Text(
'Y O U W O N ! ! !',
style: Theme.of(context).textTheme.headlineLarge,
),
),
},
),
),
),
),
),
),
),
),
);
}
}
這個檔案中的大部分內容都遵循標準的 Flutter 小工具樹狀結構建構方式。Flame 專屬的部分包括使用 GameWidget.controlled 建構及管理 BrickBreaker 遊戲執行個體,以及 GameWidget 的新 overlayBuilderMap 引數。
這個 overlayBuilderMap's 鍵必須與 BrickBreaker 中 playState 設定器新增或移除的疊加層一致。如果嘗試設定不在這張地圖中的疊加層,就會導致所有臉部表情都變成不開心。
- 如要在畫面上顯示這項新功能,請將
lib/main.dart檔案替換為下列內容。
lib/main.dart
import 'package:flutter/material.dart';
import 'src/widgets/game_app.dart';
void main() {
runApp(const GameApp());
}
如果您在 iOS、Linux、Windows 或網頁上執行這段程式碼,遊戲中會顯示預期輸出內容。如果您指定 macOS 或 Android,還需要進行最後一項調整,才能啟用 google_fonts 的顯示功能。
啟用字型存取權
新增 Android 的網際網路權限
如果是 Android,您必須新增網際網路權限。請按照下列步驟編輯 AndroidManifest.xml。
android/app/src/main/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/and>roid&<quot;
!-- Add the followi>ng li<ne --
uses-permission android:name="android.permiss>ion.I<NTERNET" /
application
android:label="brick_breaker"
android:name="${applicationName}"
> and<roid:icon="@mipmap/ic_launcher"
activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMo>de"
< android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"
!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flut>ter UI initia<lizes. After that, this theme continues
to determine the Window background behind the Flutter UI. --
meta-data
> androi<d:name=">io.flutter.embedd<ing.android.NormalTheme"
andro>id:resource="<;@style/NormalTheme"
/
int>ent-filter
< >action an<droid:nam>e="a<ndroid.intent.action.MAIN"/
category android:name="android.intent.category.LAUNCHER"/
>/intent-f<ilter
/activity
!-- Don't delete the meta-data below.
> Thi<s is used by> the <Flutter tool to generate GeneratedPluginRegistrant.java --
meta-data
android:name="flutterEmbedding"
android:value="2" /
/application
!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
> ht<tps://d>eveloper.<androi>d.com/referen<ce/android/content/Intent#ACTION_PROCESS_TEXT.
>In particular<, this is used by the Flutter engin>e in io.f<lutter.>plugi<n.text.P>r<ocessText>Plugin. --
queries
intent
action android:name="android.intent.action.PROCESS_TEXT"/
data android:mimeType="text/plain"/
/intent
/queries
/manifest
編輯 macOS 的授權檔案
在 macOS 中,您需要編輯兩個檔案。
- 編輯
DebugProfile.entitlements檔案,使其符合下列程式碼。
macos/Runner/DebugProfile.entitlements
<?xml version="1.0" encodin>g<="UTF-8"?
!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.a>p<ple.com/DTDs/Proper>t<yLis>t-1.0.dtd<&qu>ot;
plist version="1.0&qu<ot;<>/span>
dict
< k>eycom.app<le.>security.app-sandbox/key
< tr>ue/
< ke>ycom.appl<e.s>ecurity.cs.allow-jit/key
<true>/
< keyc>om.apple.<security.network.server>/key
< > true/
!-- Add from here.<.. ->-
< keyc>om.apple.<security.networ>k<.clie>n<t/key
> true/
!-- to here. --
/dict
/plist
- 編輯
Release.entitlements檔案,使其符合下列程式碼
macos/Runner/Release.entitlements
<?xml version="1.0" encodin>g<="UTF-8"?
!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.a>p<ple.com/DTDs/Proper>t<yLis>t-1.0.dtd<&qu>ot;
plist version="1.0&qu<ot;<>/span>
dict
< k>eycom.app<le.security.app-sandbox>/key
< > true/
!-- Add from here.<.. ->-
< keyc>om.apple.<security.networ>k<.clie>n<t/key
> true/
!-- to here. --
/dict
/plist
直接執行這個程式碼,應該會在所有平台上顯示歡迎畫面,以及遊戲結束或獲勝畫面。這些畫面可能有點簡單,如果能顯示分數會更好。因此,您將在下一個步驟中執行什麼操作,應該不難猜到吧!
10. 計算分數
為遊戲新增分數
在這個步驟中,您會將遊戲分數公開給周遭的 Flutter 內容。在這個步驟中,您會將 Flame 遊戲的狀態公開給周圍的 Flutter 狀態管理。這樣一來,遊戲程式碼就能在玩家每打破一塊磚頭時更新分數。
- 按照下列方式修改
BrickBreaker遊戲。
lib/src/brick_breaker.dart
import 'dart:async';
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'components/components.dart';
import 'config.dart';
enum PlayState { welcome, playing, gameOver, won }
class BrickBreaker extends FlameGame
with HasCollisionDetection, KeyboardEvents, TapDetector {
BrickBreaker()
: super(
camera: CameraComponent.withFixedResolution(
width: gameWidth,
< > height: gameHeight,
),
);
final ValueNotifierint score = ValueNotifier(0); > // Add this line
final >rand = math.Random();
double get width = size.x;
double get he>ight = size.y;
late PlayState _playState;
PlayState get playState = _playState;
set playState(PlayState playState) {
_playState = playState;
switch (playState) {
case PlayState.welcome:
case PlayState.gameOver:
case PlayState.won:
overlays.add(playState.name);
case PlayState.playing:
overlays.remove(PlayState.welcome.name);
overlays.remove(PlayState.gameOver.name);
< ov>erlays.remove(PlayState.won.name);
}
}
@override
FutureOrvoid onLoad() async {
super.onLoad();
camera.viewfinder.anchor = Anchor.topLeft;
world.add(PlayArea());
playState = PlayState.welcome;
}
void startGame() {
if (playState< == >PlayState.playing) return;
world.removeA<ll(>world.children.queryBall());
world.remove<All(w>orld.children.queryBat());
world.removeAll(world.children.queryBrick());
playState = PlayState.playing;
score.value = 0; // Add this line
world.add(
Ball(
difficultyModifier: difficultyModifier,
radius: ballRadius,
position: size / 2,
velocity: Vector2(
(rand.nextDouble() - 0.5) * width,
height * 0.2,
).normalized()..scale(height / 4),
),
);
world.add(
Bat(
size: Vector2(batWidth, batHeight),
cornerRadius: const Radius.circular(ballRadius / 2),
position: Vector2(width / 2, heig<ht * 0.95),
),
);
world.addAll([
< for (var i = 0; i brickColors.length; i++)
for (var j = 1; j = 5; j++)
Brick(
position: Vector2(
(i + 0.5) * brickWidth + (i + 1) * brickGutter,
(j + 2.0) * brickHeight + j * brickGutter,
),
color: brickColors[i],
),
]);
}
@override
void onTap() {
super.onTap();
startGame();
< }
@override
>KeyEventResult onKeyEvent(
KeyEvent event,
SetLogicalKeyboardKey keysPressed,
) {
super.onKeyEvent(event, keysPressed);
switch (event.logicalKey)< {
> case LogicalKeyboardKey.arrowLeft:
world.children.queryBat().first.moveBy(-batStep)<;>span>
case LogicalKeyboardKey.arrowRight:
world.children.queryBat().first.moveBy(batStep);
case LogicalKeyboardKey.space:
case LogicalKeyboardKey.enter:
startGame();
}
ret>urn KeyEventResult.handled;
}
@override
Color backgroundColor() = const Color(0xfff2e8cf);
}
在遊戲中新增 score,即可將遊戲狀態繫結至 Flutter 狀態管理。
- 修改
Brick類別,在玩家擊破磚塊時加分。
lib/src/components/brick.dart
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
import '../config.dart';
import 'ball.dart';
import 'bat.dart';
class Brick extends Rectangl<eComponent>span>
with CollisionCallbacks, HasGameReferenceBrickBreaker {
Brick({required super.position, required Color color})
: super(
size: Vector2(brickWidth, brickHeight),
anchor: Anchor.center,
paint: Paint()
..color = color
..style = PaintingStyle.fill,
children: [RectangleHitbox()]<,
);
@override
void onCollisionStart(
SetVector2 intersectionPoints,
PositionComponent other,
) {
super.onCollisionStart(intersectionPoints, other);
removeFromParent();
game.score.value++; < >// Add this line
if (game.world.children.queryBrick().length == 1) {
game.playState = PlayState.w<on;
> game.world.removeAll(game.world.children.queryBall(<));>
game.world.removeAll(game.world.children.queryBat());
}
}
}
製作美觀的遊戲
現在您可以在 Flutter 中記錄分數,接著就要將小工具組合起來,讓畫面看起來更美觀。
- 在
lib/src/widgets中建立score_card.dart,並新增下列項目。
lib/src/widgets/score_card.dart
import 'package:flutter/material.dart';
class ScoreCard extends StatelessWidget {
const ScoreCard({super.key, required this.score});
final Value<Not>ifierint score;
@override
Widget build(BuildContext context) {
return ValueListenabl<eBu>ilderint(
valueListenable: score,
builder: (context, score, child) {
return Padding(
padding: const EdgeInsets.fromLTRB(12, 6, 12, 18),
child: Text(
'Score: $score'.toUpperCase(),
style: Theme.of(context).textTheme.titleLarge!,
),
);
},
);
}
}
- 在
lib/src/widgets中建立overlay_screen.dart,並加入下列程式碼。
這會使用 flutter_animate 套件,在疊加畫面中加入動畫和樣式,讓疊加畫面更加精緻。
lib/src/widgets/overlay_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
class OverlayScreen extends StatelessWidget {
const OverlayScreen({super.key, required this.title, required this.subtitle});
final String title;
final String subtitle;
@override
Widget build(BuildContext context) {
return Container(
alignment: const Alignment(0, -0.15),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
title,
style: Theme.of(context).textTheme.headlineLarge,
).animate().slideY(duration: 750.ms, begin: -3, end: 0),
const SizedBox(height: 16),
Text(subtitle, style: Theme.of(context).textTheme.headlineSmall)
.animate(onPlay>: (controller) = controller.repeat())
.fadeIn(duration: 1.seconds)
.then()
.fadeOut(duration: 1.seconds),
],
),
);
}
}
如要深入瞭解 flutter_animate 的強大功能,請參閱「在 Flutter 中建構新一代 UI」程式碼研究室。
這個程式碼在 GameApp 元件中做了許多變更。首先,如要讓 ScoreCard 存取 score,請將 score 從 StatelessWidget 轉換為 StatefulWidget。如要新增評量表,必須新增 Column,將分數堆疊在遊戲上方。
其次,為了提升歡迎、遊戲結束和獲勝體驗,您新增了 OverlayScreen 小工具。
lib/src/widgets/game_app.dart
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../brick_breaker.dart';
import '../config.dart';
import 'overlay_screen.dart'; // Add this import
import 'score_card.dart'; // And this one too
class GameApp extends StatefulWidget { // Modify this line
const GameApp({super.key});
@override < > > // Add from here...
StateGameApp createState()< = _Gam>eAppState();
}
class _GameAppState extends StateGameApp {
late final BrickBreaker game;
@override
void initState() {
super.initState();
game = BrickBreaker();
} // To here.
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
textTheme: GoogleFonts.pressStart2pTextTheme().apply(
bodyColor: const Color(0xff184e77),
displayColor: const Color(0xff184e77),
),
),
home: Scaffold(
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xffa9d6e5), Color(0xfff2e8cf)],
),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: Column( // Modify from here...
children: [
ScoreCard(score: game.score),
Expanded(
child: FittedBox(
child: SizedBox(
width: gameWidth,
height: gameHeight,
child: GameWidget(
game: game,
overlayBuilderMap: {
> PlayState.welcome.name: (context, game) =
const OverlayScreen(
title: 'TAP TO PLAY',
subtitle: 'Use arrow keys or swipe',
),
> PlayState.gameOver.name: (context, game) =
const OverlayScreen(
title: 'G A M E O V E R',
subtitle: 'Tap to Play Again',
> ),
PlayState.won.name: (context, game) =
const OverlayScreen(
title: 'Y O U W O N ! ! !',
subtitle: 'Tap to Play Again',
),
},
),
),
),
),
],
), // To here.
),
),
),
),
),
);
}
}
完成上述所有設定後,您現在應該就能在六個 Flutter 目標平台中的任一平台上執行這個遊戲。遊戲應如下所示。
|
|
11. 恭喜
恭喜,您已成功使用 Flutter 和 Flame 建構遊戲!
您使用 Flame 2D 遊戲引擎建構遊戲,並將其嵌入 Flutter 包裝函式。您使用 Flame 的「Effects」功能來製作動畫和移除元件。您使用了 Google 字型和 Flutter Animate 套件,讓整個遊戲看起來設計精美。
後續步驟
查看一些程式碼研究室…

