1. 事前準備
Flame 是以 Flutter 為基礎的 2D 遊戲引擎。在本程式碼研究室中,您將建構遊戲,在 Box2D 的幾行程式碼 (名為 Forge2D) 上進行 2D 物理模擬。您使用 Flame 的元件,在螢幕上繪製模擬真實實境,讓使用者玩遊戲。完成後,遊戲看起來應該會像以下 GIF 動畫:
必要條件
- 完成「Flame 搭配 Flutter 簡介」程式碼研究室
課程內容
- Forge2D 的基本運作方式,從不同類型的物理身體開始。
- 如何設定 2D 的實體模擬。
需求條件
- Flutter SDK
- Visual Studio Code (VS Code),含 Flutter 和 Dart 外掛程式
適用於所選開發目標的編譯器軟體。本程式碼研究室適用於 Flutter 支援的全部六個平台。您需要 Visual Studio 指定 Windows、Xcode 來指定 macOS 或 iOS,並且需要 Android Studio 指定 Android。
2. 建立專案
建立 Flutter 專案
建立 Flutter 專案的方法有很多種。為求簡潔,在本節中,您將使用指令列。
若要暫停或刪除廣告,請先按照下列步驟進行:
- 在指令列中建立 Flutter 專案:
$ flutter create --empty forge2d_game Creating project forge2d_game... Resolving dependencies in forge2d_game... (4.7s) Got dependencies in forge2d_game. Wrote 128 files. All done! You can find general documentation for Flutter at: https://docs.flutter.dev/ Detailed API documentation is available at: https://api.flutter.dev/ If you prefer video documentation, consider: https://www.youtube.com/c/flutterdev In order to run your empty application, type: $ cd forge2d_game $ flutter run Your empty application code is in forge2d_game/lib/main.dart.
- 修改專案的依附元件,以新增 Flame 和 Forge2D:
$ cd forge2d_game $ flutter pub add characters flame flame_forge2d flame_kenney_xml xml Resolving dependencies... Downloading packages... characters 1.3.0 (from transitive dependency to direct dependency) collection 1.18.0 (1.19.0 available) + flame 1.18.0 + flame_forge2d 0.18.1 + flame_kenney_xml 0.1.0 flutter_lints 3.0.2 (4.0.0 available) + forge2d 0.13.0 leak_tracker 10.0.4 (10.0.5 available) leak_tracker_flutter_testing 3.0.3 (3.0.5 available) lints 3.0.0 (4.0.0 available) material_color_utilities 0.8.0 (0.12.0 available) meta 1.12.0 (1.15.0 available) + ordered_set 5.0.3 (6.0.1 available) + petitparser 6.0.2 test_api 0.7.0 (0.7.3 available) vm_service 14.2.1 (14.2.4 available) + xml 6.5.0 Changed 8 dependencies! 10 packages have newer versions incompatible with dependency constraints. Try `flutter pub outdated` for more information.
您熟悉 flame
套件,但其他三個套件可能需要一些說明。characters
套件的用途是以符合 UTF8 標準的方式操控檔案路徑。flame_forge2d
套件能以適合 Flame 的方式公開 Forge2D 功能。最後,xml
套件會在許多地方使用及修改 XML 內容。
開啟專案,然後將 lib/main.dart
檔案的內容替換成以下內容:
lib/main.dart
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
void main() {
runApp(
GameWidget.controlled(
gameFactory: FlameGame.new,
),
);
}
這會透過將 FlameGame
例項例項化的 GameWidget
啟動應用程式。在本程式碼研究室中,沒有任何 Flutter 程式碼使用遊戲執行個體的狀態顯示執行中遊戲的資訊,因此這個簡化的 Bootstrap 可以正常運作。
選用:進行 macOS 專屬的側面任務
這項專案的螢幕截圖來自 macOS 電腦版應用程式的遊戲。為避免應用程式的標題列影響整體體驗,您可以修改 macOS 執行器的專案設定,藉此省略標題列。
步驟如下:
- 建立
bin/modify_macos_config.dart
檔案並新增下列內容:
bin/modify_macos_config.dart
import 'dart:io';
import 'package:xml/xml.dart';
import 'package:xml/xpath.dart';
void main() {
final file = File('macos/Runner/Base.lproj/MainMenu.xib');
var document = XmlDocument.parse(file.readAsStringSync());
document.xpath('//document/objects/window').first
..setAttribute('titlebarAppearsTransparent', 'YES')
..setAttribute('titleVisibility', 'hidden');
document
.xpath('//document/objects/window/windowStyleMask')
.first
.setAttribute('fullSizeContentView', 'YES');
file.writeAsStringSync(document.toString());
}
這個檔案不屬於遊戲的執行階段程式碼集,因此不在 lib
目錄中。這是一種指令列工具,可用來修改專案。
- 從專案基本目錄中執行這項工具,如下所示:
$ dart bin/modify_macos_config.dart
如果一切都沒問題,該程式就不會在指令列上產生任何輸出內容。不過,這會修改 macos/Runner/Base.lproj/MainMenu.xib
設定檔,以便在沒有顯示標題列的情況下執行遊戲,並讓 Flame 遊戲佔滿整個視窗。
執行遊戲,確認一切運作正常。應該會顯示只有空白背景的新視窗。
3. 新增圖片素材資源
新增圖片
任何遊戲都需要藝術素材資源,才能以有趣的方式繪製畫面。本程式碼研究室會使用 Kenney.nl 的物理素材資源套件。這些素材資源具有授權的創用 CC CC0,但我還是強烈建議您捐款給肯尼團隊,讓他們繼續自己的傑出表現。我知道了。
您需要修改 pubspec.yaml
設定檔,才能啟用 Kenney 的素材資源。請依下列方式修改:
pubspec.yaml
name: forge2d_game
description: "A new Flutter project."
publish_to: 'none'
version: 0.1.0
environment:
sdk: '>=3.3.3 <4.0.0'
dependencies:
characters: ^1.3.0
flame: ^1.17.0
flame_forge2d: ^0.18.0
flutter:
sdk: flutter
xml: ^6.5.0
dev_dependencies:
flutter_lints: ^3.0.0
flutter_test:
sdk: flutter
flutter:
uses-material-design: true
assets: # Add from here
- assets/
- assets/images/ # To here.
雖然您可以採用不同的設定方式,但 Flame 預期圖片素材資源位於 assets/images
。詳情請參閱 Flame 的圖片說明文件。設定路徑後,請接著將路徑新增至專案本身。其中一個方法是使用指令列,如下所示:
$ mkdir -p assets/images
mkdir
指令應該沒有任何輸出內容,但新目錄應該會顯示在編輯器或檔案總管中。
展開下載的 kenney_physics-assets.zip
檔案,您應該會看到如下內容:
在 PNG/Backgrounds
目錄中,將 colored_desert.png
、colored_grass.png
、colored_land.png
和 colored_shroom.png
檔案複製到專案的 assets/images
目錄中。
另外還有 Sprite 工作表。由 PNG 圖片與 XML 檔案結合,說明 Sprite 工作表圖片中可在哪些位置找到較小的圖片。Spritesheet 是一種縮短載入時間的技術。這種技術只載入單一檔案,而不是只載入成千上萬個圖片檔 (而非上百個圖片檔案)。
從 spritesheet_aliens.png
、spritesheet_elements.png
和 spritesheet_tiles.png
複製到專案的 assets/images
目錄。屆時,請將 spritesheet_aliens.xml
、spritesheet_elements.xml
和 spritesheet_tiles.xml
檔案複製到專案的 assets
目錄。您的專案應如下所示。
繪製背景
專案已新增圖片素材資源,現在可以將素材資源放到畫面上了。先在畫面上顯示一張圖片,進一步完成更多步驟。
在名為 lib/components
的新目錄中建立名為 background.dart
的檔案,並新增下列內容。
lib/components/background.dart
import 'dart:math';
import 'package:flame/components.dart';
import 'game.dart';
class Background extends SpriteComponent with HasGameReference<MyPhysicsGame> {
Background({required super.sprite})
: super(
anchor: Anchor.center,
position: Vector2(0, 0),
);
@override
void onMount() {
super.onMount();
size = Vector2.all(max(
game.camera.visibleWorldRect.width,
game.camera.visibleWorldRect.height,
));
}
}
這個元件是特殊的 SpriteComponent
。負責顯示 Kenney.nl 四張背景圖片中的其中一張。這段程式碼有一些簡化的假設。第一種是正方形圖片,而且全部四張來自 Kenney 的背景圖片。其次,可見世界的大小永遠不會改變,否則這個元件就需要處理遊戲大小調整事件。第三個假設是位置 (0,0) 位於畫面中央。這些假設需要遊戲 CameraComponent
的特定設定。
在 lib/components
目錄中再建立另一個新檔案,名為 game.dart
。
lib/components/game.dart
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_kenney_xml/flame_kenney_xml.dart';
import 'background.dart';
class MyPhysicsGame extends Forge2DGame {
MyPhysicsGame()
: super(
gravity: Vector2(0, 10),
camera: CameraComponent.withFixedResolution(width: 800, height: 600),
);
late final XmlSpriteSheet aliens;
late final XmlSpriteSheet elements;
late final XmlSpriteSheet tiles;
@override
FutureOr<void> onLoad() async {
final backgroundImage = await images.load('colored_grass.png');
final spriteSheets = await Future.wait([
XmlSpriteSheet.load(
imagePath: 'spritesheet_aliens.png',
xmlPath: 'spritesheet_aliens.xml',
),
XmlSpriteSheet.load(
imagePath: 'spritesheet_elements.png',
xmlPath: 'spritesheet_elements.xml',
),
XmlSpriteSheet.load(
imagePath: 'spritesheet_tiles.png',
xmlPath: 'spritesheet_tiles.xml',
),
]);
aliens = spriteSheets[0];
elements = spriteSheets[1];
tiles = spriteSheets[2];
await world.add(Background(sprite: Sprite(backgroundImage)));
return super.onLoad();
}
}
這裡有許多精彩活動。首先,我們來談談 MyPhysicsGame
類別。與先前的程式碼研究室不同,這擴充了 Forge2DGame
,而非 FlameGame
。Forge2DGame
本身會擴充 FlameGame
,略做一些有趣的調整。第一種是 zoom
的預設值為 10。這項 zoom
設定是用來處理 Box2D
樣式的物理模擬引擎適用於哪些實用值的範圍。引擎是使用 MKS 系統編寫,而系統假設單位是以公尺、公斤和秒為單位。你所看到物體的數學誤差範圍介於 0.1 公尺到 10 公尺之間。如果直接提供像素維度,而缺乏一定程度的縮減規模,就會導致 Forge2D 成為實用信封之外的部分。因此,我們建議你想像要模擬在汽水範圍內發生的物體,直到可以搭公車。
將 CameraComponent
的解析度修正為 800x600 虛擬像素,即可滿足在背景元件中所做的假設。這表示遊戲區域可寬 80 個單位,高度為 60 個單位,以 (0,0) 為中心。這不會影響顯示的解析度,但會影響我們在遊戲場景中放置物件的位置。
除了 camera
建構函式引數外,另一個稱為 gravity
的物理對齊引數。重力已設為 Vector2
,其中 x
為 0,y
為 10。10 代表對於重力而言,每秒一般可接受的值約為每秒 9.81 公尺。重力設定為正 10 時,表示這個系統中 Y 軸的方向下降。和 Box2D 一般不同,但與 Flame 的一般設定方式一致。
接下來介紹 onLoad
方法。此方法並非同步執行,因為它負責從磁碟載入圖片素材資源,因此適用。呼叫 images.load
會傳回 Future<Image>
,並做為連帶效果在遊戲物件中快取載入的圖片。這些 Future 會一起收集,並用 Futures.wait
靜態方法以單一單元的形式等待。傳回的圖片清單隨後會比對模式與個別名稱。
接著,Sprite 工作表圖片會動態饋給到一系列的 XmlSpriteSheet
物件,這些物件負責擷取 Sprite 工作表中個別命名的 Sprites。flame_kenney_xml
套件中已定義 XmlSpriteSheet
類別。
除此之外,您只需要稍微調整 lib/main.dart
的幾項小幅,即可在畫面上顯示圖片。
lib/main.dart
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'components/game.dart'; // Add this import
void main() {
runApp(
GameWidget.controlled(
gameFactory: MyPhysicsGame.new, // Modify this line
),
);
}
只需變更這個簡單的變更,即可再次執行遊戲,查看畫面背景。請注意,CameraComponent.withFixedResolution()
相機執行個體會視需要加上黑邊,讓遊戲工作的比例為 800 x 600。
4. 新增地面
建構基礎
如果我們有重力,就需要在遊戲中取出物體,才能從畫面底部擷取物件。當然,除非在遊戲設計過程中從螢幕掉落的情形。在 lib/components
目錄中建立新的 ground.dart
檔案,並加入下列內容:
lib/components/ground.dart
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
const groundSize = 7.0;
class Ground extends BodyComponent {
Ground(Vector2 position, Sprite sprite)
: super(
renderBody: false,
bodyDef: BodyDef()
..position = position
..type = BodyType.static,
fixtureDefs: [
FixtureDef(
PolygonShape()..setAsBoxXY(groundSize / 2, groundSize / 2),
friction: 0.3,
)
],
children: [
SpriteComponent(
anchor: Anchor.center,
sprite: sprite,
size: Vector2.all(groundSize),
position: Vector2(0, 0),
),
],
);
}
這個 Ground
元件衍生自 BodyComponent
。在 Forge2D 體中,它們是 2D 物理模擬中最重要的物件。此元件的 BodyDef
已指定為包含 BodyType.static
。
在 Forge2D 中,身體有三種不同的型別。身體不會移動。它們實際上都有 0 個質量 - 不會對重力產生反應,而且不會發生無限質量,因此無論物體高度為何,它們不會在其他物體上移動時移動。因此,靜止的身體不會隨著地面的地面而受損。
另外兩種身體則是運動和動態身體。動態身體是完全模擬的身體,對重力和撞擊物體產生反應。本程式碼研究室的其他部分將會顯示許多動態主體。運動體是介於靜態和動態之間的半房屋。它們會移動,但無法對重力或其他攻擊物體產生反應。很實用,但不屬於本程式碼研究室的範圍。
身體本身並沒有什麼作用。身體需要相關聯的形狀才能具有物質。在本例中,此主體有 1 個相關聯的形狀,也就是 PolygonShape
設為 BoxXY
。不同於可繞旋轉點旋轉點的 PolygonShape
設為 BoxXY
,這個類型的方塊會對齊世界的軸。這些範例同樣實用,但也不屬於本程式碼研究室的範圍內。形狀和主體會與固定項目一起連結,這在將 friction
等項目新增至系統時相當實用。
根據預設,主體會以有利於偵錯的方式算繪連接的形狀,但不會打造優質的遊戲過程。將 super
引數 renderBody
設為 false
會停用此偵錯轉譯功能。子項 SpriteComponent
負責為此內文提供遊戲內算繪。
如要在遊戲中新增 Ground
元件,請按照下列方式編輯 game.dart
檔案。
lib/components/game.dart
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_kenney_xml/flame_kenney_xml.dart';
import 'background.dart';
import 'ground.dart'; // Add this import
class MyPhysicsGame extends Forge2DGame {
MyPhysicsGame()
: super(
gravity: Vector2(0, 10),
camera: CameraComponent.withFixedResolution(width: 800, height: 600),
);
late final XmlSpriteSheet aliens;
late final XmlSpriteSheet elements;
late final XmlSpriteSheet tiles;
@override
FutureOr<void> onLoad() async {
final backgroundImage = await images.load('colored_grass.png');
final spriteSheets = await Future.wait([
XmlSpriteSheet.load(
imagePath: 'spritesheet_aliens.png',
xmlPath: 'spritesheet_aliens.xml',
),
XmlSpriteSheet.load(
imagePath: 'spritesheet_elements.png',
xmlPath: 'spritesheet_elements.xml',
),
XmlSpriteSheet.load(
imagePath: 'spritesheet_tiles.png',
xmlPath: 'spritesheet_tiles.xml',
),
]);
aliens = spriteSheets[0];
elements = spriteSheets[1];
tiles = spriteSheets[2];
await world.add(Background(sprite: Sprite(backgroundImage)));
await addGround(); // Add this line
return super.onLoad();
}
Future<void> addGround() { // Add from here...
return world.addAll([
for (var x = camera.visibleWorldRect.left;
x < camera.visibleWorldRect.right + groundSize;
x += groundSize)
Ground(
Vector2(x, (camera.visibleWorldRect.height - groundSize) / 2),
tiles.getSprite('grass.png'),
),
]);
} // To here.
}
這項編輯動作會在 List
結構定義內使用 for
迴圈,將 Ground
元件的結果清單傳遞至 world
的 addAll
方法,藉此將一系列 Ground
元件新增至世界。
現在執行遊戲會顯示背景和地面。
5. 加上磚塊
打造牆壁
地面提供了靜態主體的範例。接著是第一個動態元件。Forge2D 中的動態元件是玩家體驗的基石,也是影響他們周遭世界與互動的動力。這個步驟中,您將引入「磚塊」,這類磚塊會隨機選擇在一連串的磚塊上顯示在畫面上。然後會發現它們會互相撞擊。
系統會從元素 Sprite 工作表中建立草稿。當您查看 assets/spritesheet_elements.xml
中的 Sprite 工作表說明,會發現我們有個有趣的問題。這些名稱似乎沒有幫助。選擇磚塊的材質、大小和損害程度時,我們比較會有幫助。幸好,有一位實用的小精靈花了一些時間找出檔案命名的模式,並打造了一個讓所有人更容易使用的工具。在 bin
目錄中建立新檔案 generate_brick_file_names.dart
,並新增下列內容:
bin/generate_brick_file_names.dart
import 'dart:io';
import 'package:equatable/equatable.dart';
import 'package:xml/xml.dart';
import 'package:xml/xpath.dart';
void main() {
final file = File('assets/spritesheet_elements.xml');
final rects = <String, Rect>{};
final document = XmlDocument.parse(file.readAsStringSync());
for (final node in document.xpath('//TextureAtlas/SubTexture')) {
final name = node.getAttribute('name')!;
rects[name] = Rect(
x: int.parse(node.getAttribute('x')!),
y: int.parse(node.getAttribute('y')!),
width: int.parse(node.getAttribute('width')!),
height: int.parse(node.getAttribute('height')!),
);
}
print(generateBrickFileNames(rects));
}
class Rect extends Equatable {
final int x;
final int y;
final int width;
final int height;
const Rect(
{required this.x,
required this.y,
required this.width,
required this.height});
Size get size => Size(width, height);
@override
List<Object?> get props => [x, y, width, height];
@override
bool get stringify => true;
}
class Size extends Equatable {
final int width;
final int height;
const Size(this.width, this.height);
@override
List<Object?> get props => [width, height];
@override
bool get stringify => true;
}
String generateBrickFileNames(Map<String, Rect> rects) {
final groups = <Size, List<String>>{};
for (final entry in rects.entries) {
groups.putIfAbsent(entry.value.size, () => []).add(entry.key);
}
final buff = StringBuffer();
buff.writeln('''
Map<BrickDamage, String> brickFileNames(BrickType type, BrickSize size) {
return switch ((type, size)) {''');
for (final entry in groups.entries) {
final size = entry.key;
final entries = entry.value;
entries.sort();
for (final type in ['Explosive', 'Glass', 'Metal', 'Stone', 'Wood']) {
var filtered = entries.where((element) => element.contains(type));
if (filtered.length == 5) {
buff.writeln('''
(BrickType.${type.toLowerCase()}, BrickSize.size${size.width}x${size.height}) => {
BrickDamage.none: '${filtered.elementAt(0)}',
BrickDamage.some: '${filtered.elementAt(1)}',
BrickDamage.lots: '${filtered.elementAt(4)}',
},''');
} else if (filtered.length == 10) {
buff.writeln('''
(BrickType.${type.toLowerCase()}, BrickSize.size${size.width}x${size.height}) => {
BrickDamage.none: '${filtered.elementAt(3)}',
BrickDamage.some: '${filtered.elementAt(4)}',
BrickDamage.lots: '${filtered.elementAt(9)}',
},''');
} else if (filtered.length == 15) {
buff.writeln('''
(BrickType.${type.toLowerCase()}, BrickSize.size${size.width}x${size.height}) => {
BrickDamage.none: '${filtered.elementAt(7)}',
BrickDamage.some: '${filtered.elementAt(8)}',
BrickDamage.lots: '${filtered.elementAt(13)}',
},''');
}
}
}
buff.writeln('''
};
}''');
return buff.toString();
}
編輯器應該會指出缺少依附元件的警告或錯誤訊息。新增如下:
$ flutter pub add equatable
現在,您可以按照下列步驟執行這個程式:
$ dart run bin/generate_brick_file_names.dart Map<BrickDamage, String> brickFileNames(BrickType type, BrickSize size) { return switch ((type, size)) { (BrickType.explosive, BrickSize.size140x70) => { BrickDamage.none: 'elementExplosive009.png', BrickDamage.some: 'elementExplosive012.png', BrickDamage.lots: 'elementExplosive050.png', }, (BrickType.glass, BrickSize.size140x70) => { BrickDamage.none: 'elementGlass010.png', BrickDamage.some: 'elementGlass013.png', BrickDamage.lots: 'elementGlass048.png', }, [Content elided...] (BrickType.wood, BrickSize.size140x220) => { BrickDamage.none: 'elementWood020.png', BrickDamage.some: 'elementWood025.png', BrickDamage.lots: 'elementWood052.png', }, }; }
這項工具非常實用,能剖析 Sprite 工作表說明檔案,並將其轉換為 Dart 程式碼,方便我們為要放在畫面上的每個積木選取正確的圖片檔。有幫助!
請使用以下內容建立 brick.dart
檔案:
lib/components/brick.dart
import 'dart:math';
import 'dart:ui' as ui;
import 'package:flame/components.dart';
import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
const brickScale = 0.5;
enum BrickType {
explosive(density: 1, friction: 0.5),
glass(density: 0.5, friction: 0.2),
metal(density: 1, friction: 0.4),
stone(density: 2, friction: 1),
wood(density: 0.25, friction: 0.6);
final double density;
final double friction;
const BrickType({required this.density, required this.friction});
static BrickType get randomType => values[Random().nextInt(values.length)];
}
enum BrickSize {
size70x70(ui.Size(70, 70)),
size140x70(ui.Size(140, 70)),
size220x70(ui.Size(220, 70)),
size70x140(ui.Size(70, 140)),
size140x140(ui.Size(140, 140)),
size220x140(ui.Size(220, 140)),
size140x220(ui.Size(140, 220)),
size70x220(ui.Size(70, 220));
final ui.Size size;
const BrickSize(this.size);
static BrickSize get randomSize => values[Random().nextInt(values.length)];
}
enum BrickDamage { none, some, lots }
Map<BrickDamage, String> brickFileNames(BrickType type, BrickSize size) {
return switch ((type, size)) {
(BrickType.explosive, BrickSize.size140x70) => {
BrickDamage.none: 'elementExplosive009.png',
BrickDamage.some: 'elementExplosive012.png',
BrickDamage.lots: 'elementExplosive050.png',
},
(BrickType.glass, BrickSize.size140x70) => {
BrickDamage.none: 'elementGlass010.png',
BrickDamage.some: 'elementGlass013.png',
BrickDamage.lots: 'elementGlass048.png',
},
(BrickType.metal, BrickSize.size140x70) => {
BrickDamage.none: 'elementMetal009.png',
BrickDamage.some: 'elementMetal012.png',
BrickDamage.lots: 'elementMetal050.png',
},
(BrickType.stone, BrickSize.size140x70) => {
BrickDamage.none: 'elementStone009.png',
BrickDamage.some: 'elementStone012.png',
BrickDamage.lots: 'elementStone047.png',
},
(BrickType.wood, BrickSize.size140x70) => {
BrickDamage.none: 'elementWood011.png',
BrickDamage.some: 'elementWood014.png',
BrickDamage.lots: 'elementWood054.png',
},
(BrickType.explosive, BrickSize.size70x70) => {
BrickDamage.none: 'elementExplosive011.png',
BrickDamage.some: 'elementExplosive014.png',
BrickDamage.lots: 'elementExplosive049.png',
},
(BrickType.glass, BrickSize.size70x70) => {
BrickDamage.none: 'elementGlass011.png',
BrickDamage.some: 'elementGlass012.png',
BrickDamage.lots: 'elementGlass046.png',
},
(BrickType.metal, BrickSize.size70x70) => {
BrickDamage.none: 'elementMetal011.png',
BrickDamage.some: 'elementMetal014.png',
BrickDamage.lots: 'elementMetal049.png',
},
(BrickType.stone, BrickSize.size70x70) => {
BrickDamage.none: 'elementStone011.png',
BrickDamage.some: 'elementStone014.png',
BrickDamage.lots: 'elementStone046.png',
},
(BrickType.wood, BrickSize.size70x70) => {
BrickDamage.none: 'elementWood010.png',
BrickDamage.some: 'elementWood013.png',
BrickDamage.lots: 'elementWood045.png',
},
(BrickType.explosive, BrickSize.size220x70) => {
BrickDamage.none: 'elementExplosive013.png',
BrickDamage.some: 'elementExplosive016.png',
BrickDamage.lots: 'elementExplosive051.png',
},
(BrickType.glass, BrickSize.size220x70) => {
BrickDamage.none: 'elementGlass014.png',
BrickDamage.some: 'elementGlass017.png',
BrickDamage.lots: 'elementGlass049.png',
},
(BrickType.metal, BrickSize.size220x70) => {
BrickDamage.none: 'elementMetal013.png',
BrickDamage.some: 'elementMetal016.png',
BrickDamage.lots: 'elementMetal051.png',
},
(BrickType.stone, BrickSize.size220x70) => {
BrickDamage.none: 'elementStone013.png',
BrickDamage.some: 'elementStone016.png',
BrickDamage.lots: 'elementStone048.png',
},
(BrickType.wood, BrickSize.size220x70) => {
BrickDamage.none: 'elementWood012.png',
BrickDamage.some: 'elementWood015.png',
BrickDamage.lots: 'elementWood047.png',
},
(BrickType.explosive, BrickSize.size70x140) => {
BrickDamage.none: 'elementExplosive017.png',
BrickDamage.some: 'elementExplosive022.png',
BrickDamage.lots: 'elementExplosive052.png',
},
(BrickType.glass, BrickSize.size70x140) => {
BrickDamage.none: 'elementGlass018.png',
BrickDamage.some: 'elementGlass023.png',
BrickDamage.lots: 'elementGlass050.png',
},
(BrickType.metal, BrickSize.size70x140) => {
BrickDamage.none: 'elementMetal017.png',
BrickDamage.some: 'elementMetal022.png',
BrickDamage.lots: 'elementMetal052.png',
},
(BrickType.stone, BrickSize.size70x140) => {
BrickDamage.none: 'elementStone017.png',
BrickDamage.some: 'elementStone022.png',
BrickDamage.lots: 'elementStone049.png',
},
(BrickType.wood, BrickSize.size70x140) => {
BrickDamage.none: 'elementWood016.png',
BrickDamage.some: 'elementWood021.png',
BrickDamage.lots: 'elementWood048.png',
},
(BrickType.explosive, BrickSize.size140x140) => {
BrickDamage.none: 'elementExplosive018.png',
BrickDamage.some: 'elementExplosive023.png',
BrickDamage.lots: 'elementExplosive053.png',
},
(BrickType.glass, BrickSize.size140x140) => {
BrickDamage.none: 'elementGlass019.png',
BrickDamage.some: 'elementGlass024.png',
BrickDamage.lots: 'elementGlass051.png',
},
(BrickType.metal, BrickSize.size140x140) => {
BrickDamage.none: 'elementMetal018.png',
BrickDamage.some: 'elementMetal023.png',
BrickDamage.lots: 'elementMetal053.png',
},
(BrickType.stone, BrickSize.size140x140) => {
BrickDamage.none: 'elementStone018.png',
BrickDamage.some: 'elementStone023.png',
BrickDamage.lots: 'elementStone050.png',
},
(BrickType.wood, BrickSize.size140x140) => {
BrickDamage.none: 'elementWood017.png',
BrickDamage.some: 'elementWood022.png',
BrickDamage.lots: 'elementWood049.png',
},
(BrickType.explosive, BrickSize.size220x140) => {
BrickDamage.none: 'elementExplosive019.png',
BrickDamage.some: 'elementExplosive024.png',
BrickDamage.lots: 'elementExplosive054.png',
},
(BrickType.glass, BrickSize.size220x140) => {
BrickDamage.none: 'elementGlass020.png',
BrickDamage.some: 'elementGlass025.png',
BrickDamage.lots: 'elementGlass052.png',
},
(BrickType.metal, BrickSize.size220x140) => {
BrickDamage.none: 'elementMetal019.png',
BrickDamage.some: 'elementMetal024.png',
BrickDamage.lots: 'elementMetal054.png',
},
(BrickType.stone, BrickSize.size220x140) => {
BrickDamage.none: 'elementStone019.png',
BrickDamage.some: 'elementStone024.png',
BrickDamage.lots: 'elementStone051.png',
},
(BrickType.wood, BrickSize.size220x140) => {
BrickDamage.none: 'elementWood018.png',
BrickDamage.some: 'elementWood023.png',
BrickDamage.lots: 'elementWood050.png',
},
(BrickType.explosive, BrickSize.size70x220) => {
BrickDamage.none: 'elementExplosive020.png',
BrickDamage.some: 'elementExplosive025.png',
BrickDamage.lots: 'elementExplosive055.png',
},
(BrickType.glass, BrickSize.size70x220) => {
BrickDamage.none: 'elementGlass021.png',
BrickDamage.some: 'elementGlass026.png',
BrickDamage.lots: 'elementGlass053.png',
},
(BrickType.metal, BrickSize.size70x220) => {
BrickDamage.none: 'elementMetal020.png',
BrickDamage.some: 'elementMetal025.png',
BrickDamage.lots: 'elementMetal055.png',
},
(BrickType.stone, BrickSize.size70x220) => {
BrickDamage.none: 'elementStone020.png',
BrickDamage.some: 'elementStone025.png',
BrickDamage.lots: 'elementStone052.png',
},
(BrickType.wood, BrickSize.size70x220) => {
BrickDamage.none: 'elementWood019.png',
BrickDamage.some: 'elementWood024.png',
BrickDamage.lots: 'elementWood051.png',
},
(BrickType.explosive, BrickSize.size140x220) => {
BrickDamage.none: 'elementExplosive021.png',
BrickDamage.some: 'elementExplosive026.png',
BrickDamage.lots: 'elementExplosive056.png',
},
(BrickType.glass, BrickSize.size140x220) => {
BrickDamage.none: 'elementGlass022.png',
BrickDamage.some: 'elementGlass027.png',
BrickDamage.lots: 'elementGlass054.png',
},
(BrickType.metal, BrickSize.size140x220) => {
BrickDamage.none: 'elementMetal021.png',
BrickDamage.some: 'elementMetal026.png',
BrickDamage.lots: 'elementMetal056.png',
},
(BrickType.stone, BrickSize.size140x220) => {
BrickDamage.none: 'elementStone021.png',
BrickDamage.some: 'elementStone026.png',
BrickDamage.lots: 'elementStone053.png',
},
(BrickType.wood, BrickSize.size140x220) => {
BrickDamage.none: 'elementWood020.png',
BrickDamage.some: 'elementWood025.png',
BrickDamage.lots: 'elementWood052.png',
},
};
}
class Brick extends BodyComponent {
Brick({
required this.type,
required this.size,
required BrickDamage damage,
required Vector2 position,
required Map<BrickDamage, Sprite> sprites,
}) : _damage = damage,
_sprites = sprites,
super(
renderBody: false,
bodyDef: BodyDef()
..position = position
..type = BodyType.dynamic,
fixtureDefs: [
FixtureDef(
PolygonShape()
..setAsBoxXY(
size.size.width / 20 * brickScale,
size.size.height / 20 * brickScale,
),
)
..restitution = 0.4
..density = type.density
..friction = type.friction
]);
late final SpriteComponent _spriteComponent;
final BrickType type;
final BrickSize size;
final Map<BrickDamage, Sprite> _sprites;
BrickDamage _damage;
BrickDamage get damage => _damage;
set damage(BrickDamage value) {
_damage = value;
_spriteComponent.sprite = _sprites[value];
}
@override
Future<void> onLoad() {
_spriteComponent = SpriteComponent(
anchor: Anchor.center,
scale: Vector2.all(1),
sprite: _sprites[_damage],
size: size.size.toVector2() / 10 * brickScale,
position: Vector2(0, 0),
);
add(_spriteComponent);
return super.onLoad();
}
}
現在您可以看到上述產生的 Dart 程式碼如何整合到這個程式碼集,方便您根據材質、尺寸和狀況,輕鬆快速地選擇磚塊圖片。查看 enum
後,在 Brick
元件本身上,您應該會發現,在上一個步驟中的 Ground
元件,大部分的程式碼似乎都很熟悉。這裡的可變動狀態可讓磚塊受損,不過使用此狀態是讀者練習用的。
該將磚塊放到螢幕上顯示了。按照下列方式編輯 game.dart
檔案:
lib/components/game.dart
import 'dart:async';
import 'dart:math'; // Add this import
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_kenney_xml/flame_kenney_xml.dart';
import 'background.dart';
import 'brick.dart'; // Add this import
import 'ground.dart';
class MyPhysicsGame extends Forge2DGame {
MyPhysicsGame()
: super(
gravity: Vector2(0, 10),
camera: CameraComponent.withFixedResolution(width: 800, height: 600),
);
late final XmlSpriteSheet aliens;
late final XmlSpriteSheet elements;
late final XmlSpriteSheet tiles;
@override
FutureOr<void> onLoad() async {
final backgroundImage = await images.load('colored_grass.png');
final spriteSheets = await Future.wait([
XmlSpriteSheet.load(
imagePath: 'spritesheet_aliens.png',
xmlPath: 'spritesheet_aliens.xml',
),
XmlSpriteSheet.load(
imagePath: 'spritesheet_elements.png',
xmlPath: 'spritesheet_elements.xml',
),
XmlSpriteSheet.load(
imagePath: 'spritesheet_tiles.png',
xmlPath: 'spritesheet_tiles.xml',
),
]);
aliens = spriteSheets[0];
elements = spriteSheets[1];
tiles = spriteSheets[2];
await world.add(Background(sprite: Sprite(backgroundImage)));
await addGround();
unawaited(addBricks()); // Add this line
return super.onLoad();
}
Future<void> addGround() {
return world.addAll([
for (var x = camera.visibleWorldRect.left;
x < camera.visibleWorldRect.right + groundSize;
x += groundSize)
Ground(
Vector2(x, (camera.visibleWorldRect.height - groundSize) / 2),
tiles.getSprite('grass.png'),
),
]);
}
final _random = Random(); // Add from here...
Future<void> addBricks() async {
for (var i = 0; i < 5; i++) {
final type = BrickType.randomType;
final size = BrickSize.randomSize;
await world.add(
Brick(
type: type,
size: size,
damage: BrickDamage.some,
position: Vector2(
camera.visibleWorldRect.right / 3 +
(_random.nextDouble() * 5 - 2.5),
0),
sprites: brickFileNames(type, size).map(
(key, filename) => MapEntry(
key,
elements.getSprite(filename),
),
),
),
);
await Future<void>.delayed(const Duration(milliseconds: 500));
}
} // To here.
}
這段程式碼的附加內容與您用來新增 Ground
元件的程式碼略有不同。這次 Brick
會隨時間新增到隨機叢集中。這有兩個部分,第一種是新增 Brick
await
Future.delayed
(與 sleep()
呼叫的非同步對等的方法)。不過,進行這項作業還有第二部分,系統不會對 onLoad
方法中對 addBricks
的呼叫執行 await
呼叫。如果答案為肯定,onLoad
方法必須等到所有磚塊都顯示在螢幕上後才能執行。將對 addBricks
的呼叫納入 unawaited
呼叫中,會讓 Linter 感到開心,並讓未來的程式設計人員瞭解我們的意圖。請勿等待這個方法傳回結果。
執行遊戲後,您就會看到磚塊出現、互相碰撞,並撞擊地面。
6. 新增玩家
在磚塊上浮出外星人
前幾次看磚塊的玩法是有趣的,但我想我們猜只要能讓玩家獲得與全世界互動的虛擬角色,這款遊戲就能更有趣。想想看,牠們可以把外星人拉進磚牆上,去尋找外星人吧?
在 lib/components
目錄中建立新的 player.dart
檔案,並加入下列內容:
lib/components/player.dart
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame/events.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
const playerSize = 5.0;
enum PlayerColor {
pink,
blue,
green,
yellow;
static PlayerColor get randomColor =>
PlayerColor.values[Random().nextInt(PlayerColor.values.length)];
String get fileName =>
'alien${toString().split('.').last.capitalize}_round.png';
}
class Player extends BodyComponent with DragCallbacks {
Player(Vector2 position, Sprite sprite)
: _sprite = sprite,
super(
renderBody: false,
bodyDef: BodyDef()
..position = position
..type = BodyType.static
..angularDamping = 0.1
..linearDamping = 0.1,
fixtureDefs: [
FixtureDef(CircleShape()..radius = playerSize / 2)
..restitution = 0.4
..density = 0.75
..friction = 0.5
],
);
final Sprite _sprite;
@override
Future<void> onLoad() {
addAll([
CustomPainterComponent(
painter: _DragPainter(this),
anchor: Anchor.center,
size: Vector2(playerSize, playerSize),
position: Vector2(0, 0),
),
SpriteComponent(
anchor: Anchor.center,
sprite: _sprite,
size: Vector2(playerSize, playerSize),
position: Vector2(0, 0),
)
]);
return super.onLoad();
}
@override
void update(double dt) {
super.update(dt);
if (!body.isAwake) {
removeFromParent();
}
if (position.x > camera.visibleWorldRect.right + 10 ||
position.x < camera.visibleWorldRect.left - 10) {
removeFromParent();
}
}
Vector2 _dragStart = Vector2.zero();
Vector2 _dragDelta = Vector2.zero();
Vector2 get dragDelta => _dragDelta;
@override
void onDragStart(DragStartEvent event) {
super.onDragStart(event);
if (body.bodyType == BodyType.static) {
_dragStart = event.localPosition;
}
}
@override
void onDragUpdate(DragUpdateEvent event) {
if (body.bodyType == BodyType.static) {
_dragDelta = event.localEndPosition - _dragStart;
}
}
@override
void onDragEnd(DragEndEvent event) {
super.onDragEnd(event);
if (body.bodyType == BodyType.static) {
children
.whereType<CustomPainterComponent>()
.firstOrNull
?.removeFromParent();
body.setType(BodyType.dynamic);
body.applyLinearImpulse(_dragDelta * -50);
add(RemoveEffect(
delay: 5.0,
));
}
}
}
extension on String {
String get capitalize =>
characters.first.toUpperCase() + characters.skip(1).toLowerCase().join();
}
class _DragPainter extends CustomPainter {
_DragPainter(this.player);
final Player player;
@override
void paint(Canvas canvas, Size size) {
if (player.dragDelta != Vector2.zero()) {
var center = size.center(Offset.zero);
canvas.drawLine(
center,
center + (player.dragDelta * -1).toOffset(),
Paint()
..color = Colors.orange.withOpacity(0.7)
..strokeWidth = 0.4
..strokeCap = StrokeCap.round);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
這是與上一個步驟中 Brick
元件相比的進展。這個 Player
元件包含兩個子元件,分別是 SpriteComponent
和新的 CustomPainterComponent
。CustomPainter
概念來自 Flutter,可用來在畫布上繪圖。玩家可透過此標記提供意見,指出圓形外星人將前進的方向。
玩家如何開始快速滑過外星人?使用拖曳手勢,讓播放器元件透過 DragCallbacks
回呼偵測到。眼睛會一眼注意到這裡的東西。
如果 Ground
元件是靜態主體,那麼板塊元件會是動態的主體。在這個畫面中,玩家是兩者的組合。玩家一開始會以靜態方式顯示,等待玩家拖曳;拖曳時,將本身從靜態轉成動態畫面,並依拖曳方式加入直線衝刺,讓外星人虛擬化像可以飛舞!
Player
元件中也有程式碼,可在超出邊界、入睡或逾時時從螢幕中移除。這裡的用意是讓玩家快速滑過外星人來看看會發生什麼事,然後再次嘗試。
如要將 Player
元件整合至遊戲中,請編輯 game.dart
,如下所示:
lib/components/game.dart
import 'dart:async';
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_kenney_xml/flame_kenney_xml.dart';
import 'background.dart';
import 'brick.dart';
import 'ground.dart';
import 'player.dart'; // Add this import
class MyPhysicsGame extends Forge2DGame {
MyPhysicsGame()
: super(
gravity: Vector2(0, 10),
camera: CameraComponent.withFixedResolution(width: 800, height: 600),
);
late final XmlSpriteSheet aliens;
late final XmlSpriteSheet elements;
late final XmlSpriteSheet tiles;
@override
FutureOr<void> onLoad() async {
final backgroundImage = await images.load('colored_grass.png');
final spriteSheets = await Future.wait([
XmlSpriteSheet.load(
imagePath: 'spritesheet_aliens.png',
xmlPath: 'spritesheet_aliens.xml',
),
XmlSpriteSheet.load(
imagePath: 'spritesheet_elements.png',
xmlPath: 'spritesheet_elements.xml',
),
XmlSpriteSheet.load(
imagePath: 'spritesheet_tiles.png',
xmlPath: 'spritesheet_tiles.xml',
),
]);
aliens = spriteSheets[0];
elements = spriteSheets[1];
tiles = spriteSheets[2];
await world.add(Background(sprite: Sprite(backgroundImage)));
await addGround();
unawaited(addBricks());
await addPlayer(); // Add this line
return super.onLoad();
}
Future<void> addGround() {
return world.addAll([
for (var x = camera.visibleWorldRect.left;
x < camera.visibleWorldRect.right + groundSize;
x += groundSize)
Ground(
Vector2(x, (camera.visibleWorldRect.height - groundSize) / 2),
tiles.getSprite('grass.png'),
),
]);
}
final _random = Random();
Future<void> addBricks() async {
for (var i = 0; i < 5; i++) {
final type = BrickType.randomType;
final size = BrickSize.randomSize;
await world.add(
Brick(
type: type,
size: size,
damage: BrickDamage.some,
position: Vector2(
camera.visibleWorldRect.right / 3 +
(_random.nextDouble() * 5 - 2.5),
0),
sprites: brickFileNames(type, size).map(
(key, filename) => MapEntry(
key,
elements.getSprite(filename),
),
),
),
);
await Future<void>.delayed(const Duration(milliseconds: 500));
}
}
Future<void> addPlayer() async => world.add( // Add from here...
Player(
Vector2(camera.visibleWorldRect.left * 2 / 3, 0),
aliens.getSprite(PlayerColor.randomColor.fileName),
),
);
@override
void update(double dt) {
super.update(dt);
if (isMounted && world.children.whereType<Player>().isEmpty) {
addPlayer();
}
} // To here.
}
將玩家新增至遊戲的方式與先前元件類似,只是一個怪物。玩家的外星人可在特定情況下將自身從遊戲中移除,因此這裡使用更新處理常式來檢查遊戲中是否不存在 Player
元件,如果有,則會加回一個元件。看起來就像執行遊戲一樣。
7. 回應影響
新增敵人
您已看過靜態和動態物件彼此的互動。然而,如要真正前往某處,您必須在程式碼發生衝突時,從程式碼中取得回呼。我們來看看具體操作。您將介紹一些敵人,讓玩家有所突破。這將提供獲勝條件的機會,從遊戲中移除所有敵人!
在 lib/components
目錄中建立 enemy.dart
檔案,並新增下列指令:
lib/components/enemy.dart
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter/material.dart';
import 'body_component_with_user_data.dart';
const enemySize = 5.0;
enum EnemyColor {
pink(color: 'pink', boss: false),
blue(color: 'blue', boss: false),
green(color: 'green', boss: false),
yellow(color: 'yellow', boss: false),
pinkBoss(color: 'pink', boss: true),
blueBoss(color: 'blue', boss: true),
greenBoss(color: 'green', boss: true),
yellowBoss(color: 'yellow', boss: true);
final bool boss;
final String color;
const EnemyColor({required this.color, required this.boss});
static EnemyColor get randomColor =>
EnemyColor.values[Random().nextInt(EnemyColor.values.length)];
String get fileName =>
'alien${color.capitalize}_${boss ? 'suit' : 'square'}.png';
}
class Enemy extends BodyComponentWithUserData with ContactCallbacks {
Enemy(Vector2 position, Sprite sprite)
: super(
renderBody: false,
bodyDef: BodyDef()
..position = position
..type = BodyType.dynamic,
fixtureDefs: [
FixtureDef(
PolygonShape()..setAsBoxXY(enemySize / 2, enemySize / 2),
friction: 0.3,
)
],
children: [
SpriteComponent(
anchor: Anchor.center,
sprite: sprite,
size: Vector2.all(enemySize),
position: Vector2(0, 0),
),
],
);
@override
void beginContact(Object other, Contact contact) {
var interceptVelocity =
(contact.bodyA.linearVelocity - contact.bodyB.linearVelocity)
.length
.abs();
if (interceptVelocity > 35) {
removeFromParent();
}
super.beginContact(other, contact);
}
@override
void update(double dt) {
super.update(dt);
if (position.x > camera.visibleWorldRect.right + 10 ||
position.x < camera.visibleWorldRect.left - 10) {
removeFromParent();
}
}
}
extension on String {
String get capitalize =>
characters.first.toUpperCase() + characters.skip(1).toLowerCase().join();
}
根據您先前的互動情形與播放器和積木元件,這個檔案的大部分內容應該都很熟悉。不過,由於新的未知基礎類別,編輯器中會顯示一些紅色底線。如要立即新增此類別,請將名為 body_component_with_user_data.dart
的檔案新增至 lib/components
,並在其中加入以下內容:
lib/components/body_component_with_user_data.dart
import 'package:flame_forge2d/flame_forge2d.dart';
class BodyComponentWithUserData extends BodyComponent {
BodyComponentWithUserData({
super.key,
super.bodyDef,
super.children,
super.fixtureDefs,
super.paint,
super.priority,
super.renderBody,
});
@override
Body createBody() {
final body = world.createBody(super.bodyDef!)..userData = this;
fixtureDefs?.forEach(body.createFixture);
return body;
}
}
此基礎類別與 Enemy
元件中新 beginContact
回呼合併,構成透過程式輔助方式接收身體之間影響的通知。其實,你必須編輯所有要接收相關通知的元件。因此,請編輯 Brick
、Ground
和 Player
元件,以使用這個 BodyComponentWithUserData
取代這些元件目前使用的 BodyComponent
基礎類別。舉例來說,以下說明如何編輯 Ground
元件:
lib/components/ground.dart
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'body_component_with_user_data.dart'; // Add this import
const groundSize = 7.0;
class Ground extends BodyComponentWithUserData { // Edit this line
Ground(Vector2 position, Sprite sprite)
: super(
renderBody: false,
bodyDef: BodyDef()
..position = position
..type = BodyType.static,
fixtureDefs: [
FixtureDef(
PolygonShape()..setAsBoxXY(groundSize / 2, groundSize / 2),
friction: 0.3,
)
],
children: [
SpriteComponent(
anchor: Anchor.center,
sprite: sprite,
size: Vector2.all(groundSize),
position: Vector2(0, 0),
),
],
);
}
如要進一步瞭解 Forge2d 如何處理聯絡資料,請參閱「關於聯絡回呼的 Forge2D 說明文件」。
贏得遊戲
現在您有了敵人,也能夠輕鬆移除世界中的敵人,現在有一種簡單的方式,能將這些模擬結果變成遊戲。將目標放在移除所有敵人!請依照以下方式編輯 game.dart
檔案:
lib/components/game.dart
import 'dart:async';
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_kenney_xml/flame_kenney_xml.dart';
import 'package:flutter/material.dart'; // Add this import
import 'background.dart';
import 'brick.dart';
import 'enemy.dart'; // Add this import
import 'ground.dart';
import 'player.dart';
class MyPhysicsGame extends Forge2DGame {
MyPhysicsGame()
: super(
gravity: Vector2(0, 10),
camera: CameraComponent.withFixedResolution(width: 800, height: 600),
);
late final XmlSpriteSheet aliens;
late final XmlSpriteSheet elements;
late final XmlSpriteSheet tiles;
@override
FutureOr<void> onLoad() async {
final backgroundImage = await images.load('colored_grass.png');
final spriteSheets = await Future.wait([
XmlSpriteSheet.load(
imagePath: 'spritesheet_aliens.png',
xmlPath: 'spritesheet_aliens.xml',
),
XmlSpriteSheet.load(
imagePath: 'spritesheet_elements.png',
xmlPath: 'spritesheet_elements.xml',
),
XmlSpriteSheet.load(
imagePath: 'spritesheet_tiles.png',
xmlPath: 'spritesheet_tiles.xml',
),
]);
aliens = spriteSheets[0];
elements = spriteSheets[1];
tiles = spriteSheets[2];
await world.add(Background(sprite: Sprite(backgroundImage)));
await addGround();
unawaited(addBricks().then((_) => addEnemies())); // Modify this line
await addPlayer();
return super.onLoad();
}
Future<void> addGround() {
return world.addAll([
for (var x = camera.visibleWorldRect.left;
x < camera.visibleWorldRect.right + groundSize;
x += groundSize)
Ground(
Vector2(x, (camera.visibleWorldRect.height - groundSize) / 2),
tiles.getSprite('grass.png'),
),
]);
}
final _random = Random();
Future<void> addBricks() async {
for (var i = 0; i < 5; i++) {
final type = BrickType.randomType;
final size = BrickSize.randomSize;
await world.add(
Brick(
type: type,
size: size,
damage: BrickDamage.some,
position: Vector2(
camera.visibleWorldRect.right / 3 +
(_random.nextDouble() * 5 - 2.5),
0),
sprites: brickFileNames(type, size).map(
(key, filename) => MapEntry(
key,
elements.getSprite(filename),
),
),
),
);
await Future<void>.delayed(const Duration(milliseconds: 500));
}
}
Future<void> addPlayer() async => world.add(
Player(
Vector2(camera.visibleWorldRect.left * 2 / 3, 0),
aliens.getSprite(PlayerColor.randomColor.fileName),
),
);
@override
void update(double dt) {
super.update(dt);
if (isMounted && // Modify from here...
world.children.whereType<Player>().isEmpty &&
world.children.whereType<Enemy>().isNotEmpty) {
addPlayer();
}
if (isMounted &&
enemiesFullyAdded &&
world.children.whereType<Enemy>().isEmpty &&
world.children.whereType<TextComponent>().isEmpty) {
world.addAll(
[
(position: Vector2(0.5, 0.5), color: Colors.white),
(position: Vector2.zero(), color: Colors.orangeAccent),
].map(
(e) => TextComponent(
text: 'You win!',
anchor: Anchor.center,
position: e.position,
textRenderer: TextPaint(
style: TextStyle(color: e.color, fontSize: 16),
),
),
),
);
}
}
var enemiesFullyAdded = false;
Future<void> addEnemies() async {
await Future<void>.delayed(const Duration(seconds: 2));
for (var i = 0; i < 3; i++) {
await world.add(
Enemy(
Vector2(
camera.visibleWorldRect.right / 3 +
(_random.nextDouble() * 7 - 3.5),
(_random.nextDouble() * 3)),
aliens.getSprite(EnemyColor.randomColor.fileName),
),
);
await Future<void>.delayed(const Duration(seconds: 1));
}
enemiesFullyAdded = true; // To here.
}
}
請選擇接受挑戰,挑戰就是執行遊戲並進入這個畫面。
8. 恭喜
恭喜,您成功運用 Flutter 和 Flame 打造遊戲!
您已使用 Flame 2D 遊戲引擎打造遊戲,並將該遊戲嵌入 Flutter 包裝函式。你使用了 Flame 的效果來為元件製作動畫和移除元件。你使用了 Google Fonts 和 Flutter Animate 套件,打造整體遊戲美觀體驗。
後續步驟
查看一些程式碼研究室…