加載游戲資源
在開始下面的內(nèi)容之前哗讥,最好的話是先把《開始用Flutter做游戲吧》過一遍嚷那,然后再完成《Flutter游戲:萬有引力定律》里的游戲,因為下面的內(nèi)容是在該游戲的基礎(chǔ)上開發(fā)的杆煞。
首先下載這個游戲要用到的游戲資源文件魏宽,然后在項目目錄下建立assets/images
目錄,在該目錄下再分別建立bg
和flies
目錄决乎,用于存放背景圖片和組件圖片队询。
資源文件就位后,在pubspec.yaml
文件里添加對這些資源文件的引用构诚。
flutter:
uses-material-design: true
assets:
- assets/images/bg/backyard.png
- assets/images/flies/agile-fly-1.png
- assets/images/flies/agile-fly-2.png
- assets/images/flies/agile-fly-dead.png
- assets/images/flies/drooler-fly-1.png
- assets/images/flies/drooler-fly-2.png
- assets/images/flies/drooler-fly-dead.png
- assets/images/flies/mosquito-fly-1.png
- assets/images/flies/mosquito-fly-2.png
- assets/images/flies/mosquito-fly-dead.png
- assets/images/flies/hungry-fly-1.png
- assets/images/flies/hungry-fly-2.png
- assets/images/flies/hungry-fly-dead.png
- assets/images/flies/macho-fly-1.png
- assets/images/flies/macho-fly-2.png
- assets/images/flies/macho-fly-dead.png
下面我們要在游戲開始時加載所有資源娘摔,這會花費幾毫秒的時間,但是又有誰會注意到這幾毫秒內(nèi)的黑屏顯示呢唤反。打開main.dart
文件凳寺,在頂部添加代碼以導(dǎo)入flame/flame.dart
包,然后就可以預(yù)加載游戲資源了彤侍。
...
import 'package:flame/flame.dart';
void main() async {
Util flameUtil = Util();
await flameUtil.fullScreen();
await flameUtil.setOrientation(DeviceOrientation.portraitUp);
Flame.images.loadAll(<String>[
'bg/backyard.png',
'flies/agile-fly-1.png',
'flies/agile-fly-2.png',
'flies/agile-fly-dead.png',
'flies/drooler-fly-1.png',
'flies/drooler-fly-2.png',
'flies/drooler-fly-dead.png',
'flies/mosquito-fly-1.png',
'flies/mosquito-fly-2.png',
'flies/mosquito-fly-dead.png',
'flies/hungry-fly-1.png',
'flies/hungry-fly-2.png',
'flies/hungry-fly-dead.png',
'flies/macho-fly-1.png',
'flies/macho-fly-2.png',
'flies/macho-fly-dead.png',
]);
...
}
上面的代碼中肠缨,使用一個String
列表作為參數(shù)傳遞給images
的loadAll
方法,該方法用于預(yù)加載String
列表指向的圖像文件盏阶。這些圖像將緩存在Flame的靜態(tài)變量中晒奕,以便以后可以重復(fù)使用。
設(shè)置游戲背景
現(xiàn)在游戲的背景是一個灰藍純色背景,看起來還不錯脑慧,但是我們接下來還是要改變它魄眉。我們預(yù)加載的資源中有一個bg/backyard.png
,這是一個高度很高的垂直圖片闷袒,因為我們的游戲目前只關(guān)心寬度坑律,不管手機的縱橫比如何,這張背景圖都可以覆蓋整個屏幕囊骤。
接下來晃择,創(chuàng)建一個組件文件components/backyard.dart
,將背景邏輯分開來也物,該文件聲明了一個Backyard
類宫屠,有一個構(gòu)造函數(shù)和另外渲染(render
)、更新(update
)方法滑蚯。
這個Backyard
類還有一個最終(final
)的HitGame
實例變量浪蹂,它將作為包含該組件的游戲?qū)嵗捌鋵傩缘逆溄樱梢詤⒖?code>components/fly.dart中的實現(xiàn)告材。另一個實例變量是一個名為bgSprite
的精靈(Sprite
)乌逐,它會保存我們稍后將繪制在屏幕上的精靈(Sprite
)數(shù)據(jù)。
import 'dart:ui';
import 'package:flame/sprite.dart';
import 'package:hello_flame/hit-game.dart';
class Backyard {
final HitGame game;
Sprite bgSprite;
Backyard(this.game) {
bgSprite = Sprite('bg/backyard.png');
}
void render(Canvas c) {}
void update(double t) {}
}
在構(gòu)造函數(shù)中创葡,通過創(chuàng)建一個新的精靈(Sprite
)并傳遞要使用的資源文件名來初始化bgSprite
變量浙踢。文件bg/backyard.png
已經(jīng)在main.dart
中被預(yù)加載,因此無需任何加載時間即可使用灿渴。
文件頂部的import
語句導(dǎo)入了3個內(nèi)容洛波,dart:ui
允許我們訪問畫布(Canvas
)類,flame/sprite.dart
允許我們使用精靈(Sprite
)類骚露,hello_flame/hit-game.dart
使我們可以訪問HitGame
類蹬挤。
那么在添加背景圖以后,我們怎么定位游戲組件的位置勒棘幸?如果打開bg/backyard.png
文件焰扳,可以看到它的大小為1080 x 2760
像素,我們不用關(guān)注它的物理像素或邏輯像素误续,我們只要關(guān)心我們的背景有9個圖塊的寬度就好了吨悍。
1080(像素) ÷ 9(圖塊) = 120(每個圖塊的像素)
2760(像素) ÷ 120(每個圖塊的像素) = 23(圖塊)
如同上面的計算結(jié)果所示,我們當(dāng)前使用的背景圖像寬為9個圖塊蹋嵌、高為23個圖塊育瓜。
繪制游戲背景
現(xiàn)在我們可以開始繪制游戲背景了,將背景圖像底部錨定在屏幕的底部栽烂,為此需要定義一個包含背景尺寸的矩形(Rect
)躏仇,這里需要正確計算大小恋脚,以便在渲染過程中保留背景圖像的寬高比。
在components/backyard.dart
文件中添加一個名為bgRect
的矩形(Rect
)實例變量焰手,并在構(gòu)造函數(shù)內(nèi)部糟描,初始化這個矩形(Rect
)。
class Backyard {
...
Rect bgRect;
Backyard(this.game) {
bgSprite = Sprite('bg/backyard.png');
bgRect = Rect.fromLTWH(
0,
game.screenSize.height - (game.tileSize * 23),
game.tileSize * 9,
game.tileSize * 23,
);
}
...
}
上面代碼中书妻,矩形(Rect
)構(gòu)造函數(shù)fromLTWH
的4個參數(shù)分別對應(yīng)于x
坐標(biāo)船响、y
坐標(biāo)、寬度和高度的值驻子。我們以最大寬度繪制背景,因此估灿,它的寬度從x
開始延伸到game.tileSize * 9
為止崇呵,我們也可以在這里使用game.screenSize.width
,因為game.tileSize
是等于game.screenSize.width ÷ 9
的馅袁。
在前面的計算中域慷,我們已知背景圖像為9 x 23
的圖塊大小,因此要繪制整個背景圖像的話汗销,只需要設(shè)置game.tileSize * 23
的高度即可犹褒。最后,y
坐標(biāo)是一個負數(shù)弛针,對應(yīng)于屏幕大小和背景圖像的差異叠骑。
如果設(shè)備屏幕的寬高比為9:16
,則屏幕的高度為16 * 圖塊大小
削茁,如果從中減去23 * 圖塊大小
宙枷,我們就可以得到-7 * 圖塊大小
的值,這意味著背景圖片是使用屏幕頂部邊緣上方的7
個圖塊大小的地方開始繪制的茧跋。
通過上面的計算慰丛,背景圖像將始終錨定在設(shè)備屏幕的底部,最后瘾杭,我們在調(diào)用此組件的渲染(render
)方法時就繪制背景圖像诅病。
void render(Canvas c) {
bgSprite.renderRect(c, bgRect);
}
到這里為止,我們的components/backyard.dart
里面應(yīng)該有以下代碼粥烁。
import 'dart:ui';
import 'package:flame/sprite.dart';
import 'package:hello_flame/hit-game.dart';
class Backyard {
final HitGame game;
Sprite bgSprite;
Rect bgRect;
Backyard(this.game) {
bgSprite = Sprite('bg/backyard.png');
bgRect = Rect.fromLTWH(
0,
game.screenSize.height - (game.tileSize * 23),
game.tileSize * 9,
game.tileSize * 23,
);
}
void render(Canvas c) {
bgSprite.renderRect(c, bgRect);
}
void update(double t) {}
}
添加游戲背景
上面我們已經(jīng)完成了背景組件贤笆,現(xiàn)在讓我們把它添加到游戲邏輯中,打開hit-game.dart
文件讨阻,導(dǎo)入hello_flame/components/backyard.dart
苏潜,然后添加一個名為background
的Backyard
類型的新實例變量。
然后在initialize
方法中变勇,實例化一個新的Backyard
對象恤左,并將其分配給實例變量background
贴唇,而且,必須在確定屏幕大小后再執(zhí)行此操作飞袋,因為Backyard
類的構(gòu)造函數(shù)中使用到了屏幕大小和圖塊大小值戳气。
還有就是,要像我們創(chuàng)建游戲組件Fly
一樣巧鸭,使用關(guān)鍵字this
傳遞當(dāng)前的HitGame
實例瓶您。
...
import 'package:hello_flame/components/backyard.dart';
class HitGame extends Game {
...
Backyard background;
...
void initialize() async {
enemy = List<Fly>();
rnd = Random();
resize(await Flame.util.initialDimensions());
background = Backyard(this);
produceFly();
}
...
}
然后在渲染(render
)方法中,調(diào)用background
的渲染(render
)方法并將畫布(Canvas
)傳遞給它纲仍。同時呀袱,刪除我們之前繪制的一個純色矩形背景。
void render(Canvas canvas) {
// 刪除以下內(nèi)容
// Rect bgRect = Rect.fromLTWH(0, 0, screenSize.width, screenSize.height);
// Paint bgPaint = Paint();
// bgPaint.color = Color(0xff576574);
// canvas.drawRect(bgRect, bgPaint);
background.render(canvas);
enemy.forEach((Fly fly) => fly.render(canvas));
}
當(dāng)我們現(xiàn)在運行游戲時郑叠,應(yīng)該可以看到游戲背景在不同手機上會有不同的高度夜赵。
改變游戲組件
目前,文件的預(yù)加載資源中乡革,有五種不同的蚊子素材寇僧,我們現(xiàn)在就重點看下它們的視覺差異。具體可以通過子類來實現(xiàn)沸版,就是創(chuàng)建一個類作為子類擴展現(xiàn)有的父類嘁傀。
我們的蚊子素材的大小相對于實例變量flyRect
的矩形來說,會占用更大的圖塊大小视粮,要解釋清楚的話细办,可能要借助于下面的示例圖。
在上面的示例圖中蕾殴,精靈(sprite
)將被繪制在藍色圖塊內(nèi)蟹腾,我們就稱它為精靈(sprite
)矩形。但是點擊需要發(fā)生在紅色圖塊內(nèi)区宇,我們就稱它為命中矩形娃殖,而在代碼中它被命名為flyRect
。
在開始創(chuàng)建第一個子類之前议谷,我們要先準(zhǔn)備好一個可以進行擴展的父類炉爆。打開components/fly.dart
文件,Fly
類將具有所有蚊子種類共享的常用方法和變量卧晓。首先芬首,刪除畫矩形(drawRect
)方法,因為我們不需要繪制矩形了逼裆,再清空渲染(render
)方法郁稍。
然后,刪除所有對flyPaint
的引用胜宇,因為該對象僅用于繪制矩形耀怜,從實例變量恢着、onTapDown
處理方法和構(gòu)造函數(shù)中都刪除它。但是我們?nèi)匀粫褂?code>flyRect作為命中矩形财破,所以讓它留在文件中掰派。
class Fly {
final HitGame game;
Rect flyRect;
// 刪除內(nèi)容
// Paint flyPaint;
bool isDead = false;
bool isOffScreen = false;
Fly(this.game, double x, double y) {
flyRect = Rect.fromLTWH(x, y, game.tileSize, game.tileSize);
// 刪除內(nèi)容
// flyPaint = Paint();
// flyPaint.color = Color(0xff6ab04c);
}
void render(Canvas c) {
// 刪除內(nèi)容
// c.drawRect(flyRect, flyPaint);
}
...
void onTapDown() {
isDead = true;
// 刪除內(nèi)容
// flyPaint.color = Color(0xffff4757);
game.produceFly();
}
}
對于Fly
類或其子類的每個實例,都需要準(zhǔn)備和存儲2組精靈(sprite
)左痢,1組將由2個精靈(sprite
)組成靡羡,這些精靈將1個接1個地顯示,以繪制出“飛行”的動畫效果俊性,我們需要一個List
略步。
另一組將只有1個精靈(sprite
)將在蚊子死亡時顯示,這里需要另一個實例變量來存儲將為“飛行”動畫顯示的精靈(sprite
)定页。
在文件頂部導(dǎo)入flame/sprite.dart
趟薄,并在實例變量部分中添加下面代碼。
...
import 'package:flame/sprite.dart';
class Fly {
...
List<Sprite> flyingSprite;
Sprite deadSprite;
double flyingSpriteIndex = 0;
但是這些精靈(sprite
)變量不會在Fly
類中初始化拯勉,因為每個子類都會使用不同的精靈(sprite
)竟趾,在渲染(render
)方法中憔购,我們會根據(jù)實例的狀態(tài)(死或生)來渲染精靈(sprite
)宫峦。
void render(Canvas c) {
if (isDead) {
deadSprite.renderRect(c, flyRect.inflate(2));
} else {
flyingSprite[flyingSpriteIndex.toInt()].renderRect(c, flyRect.inflate(2));
}
}
在上面的代碼中,渲染(render
)方法通過檢查isDead
變量來決定顯示哪個精靈(sprite
)玫鸟,如果當(dāng)前實例已死导绷,則渲染deadSprite
,如果沒有屎飘,則渲染flyingSprite
列表中的第0個下標(biāo)項妥曲。
對于flyingSpriteIndex.toInt()
來說,List
的精靈(sprite
)項由整數(shù)索引訪問钦购,而flyingSpriteIndex
是雙精度(double
)類型的檐盟,所以需要先轉(zhuǎn)換為整型(int
)。那么為啥它是雙精度(double
)類型的呢押桃,因為我們將使用更新(update
)方法中的時間增量(t
)來遞增它葵萎。
最后一部分的.inflate(2)
只是創(chuàng)建了一個被調(diào)用的矩形的副本,但是從中心開始按乘數(shù)膨脹唱凯,這里我們把乘數(shù)設(shè)置為2
羡忘,因為從蚊子素材的大小來看,精靈(sprite
)矩形的大小約是命中矩形的2倍磕昼。
到這里為止卷雕,我們的fly.dart
里面應(yīng)該有以下代碼。
import 'dart:ui';
import 'package:hello_flame/hit-game.dart';
import 'package:flame/sprite.dart';
class Fly {
final HitGame game;
List<Sprite> flyingSprite;
Sprite deadSprite;
double flyingSpriteIndex = 0;
Rect flyRect;
bool isDead = false;
bool isOffScreen = false;
Fly(this.game, double x, double y) {
flyRect = Rect.fromLTWH(x, y, game.tileSize, game.tileSize);
}
void render(Canvas c) {
if (isDead) {
deadSprite.renderRect(c, flyRect.inflate(2));
} else {
flyingSprite[flyingSpriteIndex.toInt()].renderRect(c, flyRect.inflate(2));
}
}
void update(double t) {
if (isDead) {
flyRect = flyRect.translate(0, game.tileSize * 12 * t);
if (flyRect.top > game.screenSize.height) {
isOffScreen = true;
}
}
}
void onTapDown() {
isDead = true;
game.produceFly();
}
}
創(chuàng)建組件子類
現(xiàn)在創(chuàng)建第一個蚊子種類票从,這是一個“正陈瘢”的種類滨嘱,就將它命名為MosquitoFly
,一只正常飛行的蚊子蝎亚。在components
文件夾下新建一個mosquito-fly.dart
文件并打開它九孩,創(chuàng)建基本的組件類,但這次我們擴展了Fly
類发框。
import 'package:flame/sprite.dart';
import 'package:hello_flame/components/fly.dart';
import 'package:hello_flame/hit-game.dart';
class MosquitoFly extends Fly {
MosquitoFly(HitGame game, double x, double y) : super(game, x, y) {
flyingSprite = List<Sprite>();
flyingSprite.add(Sprite('flies/mosquito-fly-1.png'));
flyingSprite.add(Sprite('flies/mosquito-fly-2.png'));
deadSprite = Sprite('flies/mosquito-fly-dead.png');
}
}
上面的代碼中躺彬,先導(dǎo)入該類所依賴的包和類,然后聲明一個名為MosquitoFly
的類梅惯,并使其擴展Fly
類宪拥,從而有效地創(chuàng)建一個Fly
子類,其可以訪問和覆蓋Fly
類的變量和方法铣减。
構(gòu)造函數(shù)中調(diào)用super
她君,這樣在構(gòu)造函數(shù)執(zhí)行代碼當(dāng)前類代碼之前,就會先運行父類的構(gòu)造函數(shù)葫哗。構(gòu)造函數(shù)只映射父類構(gòu)造函數(shù)所需的參數(shù)缔刹,并在調(diào)用super
期間轉(zhuǎn)發(fā)它們。
在構(gòu)造函數(shù)中劣针,通過創(chuàng)建一個精靈(Sprite
)列表的新實例來初始化此子類從Fly
類繼承的flyingSprite
變量校镐,然后我們在這個列表中添加兩個精靈(Sprite
),它們對應(yīng)于飛行動畫的2個幀捺典。
然后我們將“正衬窭”蚊子的掉落圖加載到精靈(Sprite
)中并將其分配給deadSprite
。
我們現(xiàn)在不會覆蓋更新(update
)和渲染(render
)方法襟己,因為目前沒有針對這類蚊子的特定內(nèi)容引谜,所有蚊子都相同。
生產(chǎn)正常蚊子
現(xiàn)在回到hit-game.dart
文件中擎浴,編輯produceFly
方法以一個MosquitoFly
而不是父類Fly
员咽。在文件頂部導(dǎo)入剛剛創(chuàng)建的子類,然后替換之前生成Fly
的代碼贮预。
...
import 'package:hello_flame/components/mosquito-fly.dart';
class HitGame extends Game {
...
void produceFly() {
double x = rnd.nextDouble() * (screenSize.width - tileSize);
double y = rnd.nextDouble() * (screenSize.height - tileSize);
// 刪除內(nèi)容
// enemy.add(Fly(this, x, y));
enemy.add(MosquitoFly(this, x, y));
}
現(xiàn)在運行游戲贝室,應(yīng)該可以看到下面圖片所示的效果。