搭游戲主循環(huán)
要Flutter做一個游戲溪窒,我們需要先把一個簡單的Flame游戲主循環(huán)腳手架給搭起來,這部分的內(nèi)容在前面的《開始用Flutter做游戲吧》里面有詳細(xì)的講解哦冯勉!
新建一個hit-game.dart
文件澈蚌,用以下代碼建立游戲主循環(huán),這個游戲主循環(huán)是我們游戲的核心灼狰,我們待會再擴(kuò)充里面的內(nèi)容宛瞄。
import 'dart:ui';
import 'package:flame/game.dart';
class HitGame extends Game {
Size screenSize;
void render(Canvas canvas) {}
void update(double t) {}
void resize(Size size) {}
}
然后修改main.dart
文件,創(chuàng)建一個游戲類的實(shí)例交胚,并調(diào)用runApp
函數(shù)份汗,而且因?yàn)?code>runApp函數(shù)需要一個Widget
對象伐厌,所以我們還要傳遞HitGame
實(shí)例的widget
屬性。
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flame/util.dart';
import 'package:hello_flame/hit-game.dart';
void main() async {
Util flameUtil = Util();
await flameUtil.fullScreen();
await flameUtil.setOrientation(DeviceOrientation.portraitUp);
HitGame game = HitGame();
runApp(game.widget);
}
再回到hit-game.dart
文件中裸影,編輯游戲類挣轨,確定屏幕的大小尺寸,便于后面的繪圖與對象移動操作轩猩。我們開始運(yùn)行游戲時卷扮,F(xiàn)lutter會計(jì)算其暴露給應(yīng)用程序的大小尺寸,但是呢均践,其他一些事件晤锹,例如翻轉(zhuǎn)手機(jī)等操作,會導(dǎo)致Flutter重新計(jì)算其暴露給應(yīng)用程序的大小尺寸彤委。
現(xiàn)在有些新手機(jī)支持多種分辨率鞭铆,讓玩家可以在玩游戲時更改屏幕分辨率,我們需要確保每次Flutter通知我們說焦影,應(yīng)用程序的畫布(Canvas
)已經(jīng)通過調(diào)整(resize
)方法調(diào)整大小時车遂,我們要重新計(jì)算。
在調(diào)整(resize
)方法添加下面的代碼斯辰。
void resize(Size size) {
screenSize = size;
}
上面加的那一行代碼只是將Flutter傳遞的新大小尺寸存儲到screenSize
實(shí)例變量舶担,這樣我們就可以在游戲主循環(huán)內(nèi)訪問它了。
繪制游戲背景
現(xiàn)在我們要為游戲繪制一個背景彬呻,為了簡單這里就是一個純色背景衣陶,這里可以使用任何一種顏色,但是有一點(diǎn)要注意闸氮,不能使用太亮眼的顏色剪况,例如紅色,因?yàn)槟菢訒ν婕业难劬Α?/p>
在渲染(render
)方法添加下面的代碼蒲跨。
void render(Canvas canvas) {
Rect bgRect = Rect.fromLTWH(0, 0, screenSize.width, screenSize.height);
Paint bgPaint = Paint();
bgPaint.color = Color(0xff576574);
canvas.drawRect(bgRect, bgPaint);
}
上面的代碼中译断,第1行代碼定義了一個矩形(Rect
)類實(shí)例bgRect
,坐標(biāo)(0,0)即左上角财骨,大小與屏幕大小一致镐作。第2、3行代碼定義了一個繪制(Paint
)類實(shí)例bgPaint
隆箩,隨后為其分配顏色。第4行代碼使用畫布(Canvas
)對象的畫矩形(drawRect
)方法繪制矩形羔杨。
兼容各種手機(jī)
現(xiàn)在市場上的Android設(shè)備奇奇怪怪的捌臊,各種型號加起來數(shù)千種,我們可以讓自己寫的游戲可以在所有設(shè)備上運(yùn)行嗎兜材?
要適配不同設(shè)備理澎,需要先了解“縱橫比”的概念逞力,縱橫比可以是設(shè)備寬度和高度之間的比例,也可以是高度和寬度糠爬,這兩者是可切換的寇荧,因?yàn)橥婕铱梢孕D(zhuǎn)手機(jī)。
目前市場上常用的手機(jī)屏幕有幾十種不同的縱橫比执隧,例如3:2揩抡、4:3、8:5镀琉、5:3峦嗤、16:9、18.5:9等屋摔,我們以目前最常見的16:9為基礎(chǔ)烁设。
由于我們的游戲是縱向模式運(yùn)行的,所以實(shí)際是以9:16為基礎(chǔ)的钓试,但是我們并不會把屏幕固定為9:16装黑,而是把重點(diǎn)放在一個維度上作為基礎(chǔ),比如現(xiàn)在我們使用寬度為9弓熏,那么屏幕的尺寸基礎(chǔ)為9:x的縱橫比曹体。
然后轉(zhuǎn)換9:x的縱橫比,把它變成9:13.5硝烂、9:12箕别、9:14.4、9:15滞谢、9:16串稀、9:18.5。這樣我們就只需要處理縱向時手機(jī)的寬度狮杨,手機(jī)高度越大母截,游戲?qū)ο罂梢砸苿拥目臻g就越多,反之亦然橄教。
這樣一來清寇,無論玩家用什么尺寸和縱橫比的手機(jī)玩我們的游戲,游戲?qū)ο罂偸蔷哂邢嗤某叽缁さp輕松松就解決了华烟。
接下來,我們在代碼上實(shí)現(xiàn)這個方案持灰,添加一個實(shí)例變量tileSize
盔夜,這個實(shí)例變量將保持屏幕寬度的值除以9。同時我們把它放在調(diào)整(resize
)方法里面,這樣每次屏幕尺寸發(fā)生變化時喂链,都能得到最新的大小返十。
class HitGame extends Game {
Size screenSize;
double tileSize;
...
void resize(Size size) {
screenSize = size;
tileSize = screenSize.width / 9;
}
}
組件與小精靈
在游戲中,組件椭微、對象洞坑、游戲?qū)ο筮@三個名字描述的是同一個概念,通常指在游戲中執(zhí)行某??些操作的對象蝇率,例如主角迟杂、敵人、陸地地形瓢剿、地圖逢慌、菜單、子彈等等间狂,下面我們用“組件”來指它攻泼。
精靈(sprite
)也是游戲中的一個重要概念,通常指游戲中的元素鉴象、主角忙菠,NPC之類的圖片、動畫纺弊、按鍵等牛欢。組件通常是和精靈耦合的,例如在敵人組件的位置上繪制某個精靈淆游,以便讓玩家知道敵人在哪里傍睹。
但是并非所有的組件都用于定位或繪制,有些組件沒有位置犹菱,也沒有在屏幕上繪制精靈拾稳。它們只是用于不同的功能,這些組件稱為控制器(controllers
)腊脱,控制器控制游戲的行為访得,同時不在屏幕上顯示。
舉個例子陕凹,有一個敵人產(chǎn)生者控制器悍抑,這個控制器就在等待產(chǎn)生敵人組件的時間,當(dāng)那個時間到來時杜耙,控制器會創(chuàng)建一個敵人對象搜骡,并將其提交給游戲主循環(huán),然后游戲主循環(huán)獲取到新的敵人對象并相應(yīng)地更新和渲染它泥技。
我們可以將組件視為迷你游戲循環(huán)或游戲主循環(huán)的子部分浆兰,它們有自己的更新(update
)和渲染(render
)方法磕仅,活用組件可以提高我們代碼的可維護(hù)性珊豹。
創(chuàng)建游戲組件
現(xiàn)在我們需要一個存放組件的地方簸呈,在lib
下創(chuàng)建一個components
文件夾,在該文件夾中店茶,再創(chuàng)建一個名為fly.dart
的新文件蜕便。
import 'dart:ui';
class Fly {
void render(Canvas c) {}
void update(double t) {}
}
上面代碼中,導(dǎo)入dart:ui
庫贩幻,這樣我們就可以使用畫布(Canvas
)類轿腺,就像主游戲類文件hit-game.dart
一樣。然后用兩個方法聲明一個Fly
類:更新(update
)和渲染(render
)丛楚。
是不是發(fā)現(xiàn)非常像游戲主循環(huán)族壳,因?yàn)楫?dāng)這個組件輪到更新(update
)和渲染(render
)時,游戲主循環(huán)會調(diào)用這些方法趣些。
Fly
組件應(yīng)該要保存它的位置和大小仿荆,所以我們要創(chuàng)建相關(guān)的實(shí)例變量。我們可以選擇創(chuàng)建double x
坏平、double y
拢操、double width
、double height
舶替,但是這種方式要4個變量令境。
更好的方式是,對于具有x/y
或width/height
值的數(shù)據(jù)類型顾瞪,我們有很多選擇舔庶,Dart中的大小(Size
)和偏移(Offset
)類陈醒,Dart的math庫的點(diǎn)(Point
)類和Flame的位置(Position
)類惕橙,但是這樣我們還是需要兩個變量,一個用于x/y
對孵延、一個用于width/height
對吕漂。
還有更好的方式是,在之前繪制背景時尘应,我們用到了矩形(Rect
)類惶凝,通過fromLTWH
構(gòu)造函數(shù)構(gòu)造實(shí)例時,要定義它的x
軸犬钢、y
軸苍鲜、寬度、高度玷犹。
這種方式也有缺點(diǎn)混滔,就是Rect
實(shí)例是不可變的,這就意味著我們無法通過直接設(shè)置值來更改其任何屬性,但是有解決方案坯屿,我們可以使用矩形(Rect
)實(shí)例的移動(shift
)和翻轉(zhuǎn)(translate
)方法來移動矩形油湖。
現(xiàn)在我們用代碼實(shí)現(xiàn)這個方案,添加一個實(shí)例變量flyRect
领跛,然后引用游戲類乏德,以便我們可以訪問像screenSize
這樣的屬性。最后再添加另一個實(shí)例變量game
吠昭,它將作為父游戲類的鏈接和引用喊括。
import 'dart:ui';
import 'package:hello_flame/hit-game.dart';
class Fly {
final HitGame game;
Rect flyRect;
上面的實(shí)例變量game
是最終變量,因?yàn)槲覀兊慕M件在其整個生命周期中只存在于一個游戲類中矢棚,因此我們不需要父游戲是動態(tài)的郑什。
然后,我們還需要初始化這些實(shí)例變量的值蒲肋,我們需要為這個Fly
類編寫一個構(gòu)造函數(shù)蘑拯,構(gòu)造函數(shù)會在創(chuàng)建類的實(shí)例時運(yùn)行,而且只運(yùn)行一次肉津,因此通常用于初始化强胰。
Fly(this.game, double x, double y) {
flyRect = Rect.fromLTWH(x, y, game.tileSize, game.tileSize);
}
上面的代碼中,默認(rèn)構(gòu)造函數(shù)接受三個參數(shù)妹沙,第1個參數(shù)this.game
指定傳遞給game
屬性的任何值偶洋,第2、3個參數(shù)x
和y
將是新構(gòu)造實(shí)例的初始位置距糖。
然后在默認(rèn)構(gòu)造函數(shù)里面的代碼中玄窝,我們?yōu)?code>flyRect分配了一個新的矩形,使用傳遞的x
和y
參數(shù)設(shè)置坐標(biāo)位置悍引,使用game.tileSize
分配寬度和高度恩脂。
到這里為止,我們的fly.dart
里面應(yīng)該有以下代碼趣斤。
import 'dart:ui';
import 'package:hello_flame/hit-game.dart';
class Fly {
final HitGame game;
Rect flyRect;
Fly(this.game, double x, double y) {
flyRect = Rect.fromLTWH(x, y, game.tileSize, game.tileSize);
}
void render(Canvas c) {}
void update(double t) {}
}
繪制游戲組件
上面我們已經(jīng)擁有了一個矩形(Rect
)俩块,但是這還不能繪制一個矩形,我們還需要一個繪制(Paint
)對象浓领。而且為了避免在渲染(render
)方法中重新初始化繪制(Paint
)對象玉凯,我們最好把它存儲在實(shí)例變量中。
現(xiàn)在我們還要在構(gòu)造函數(shù)中初始化flyPaint
联贩,添加以下代碼漫仆。
class Fly {
final HitGame game;
Rect flyRect;
Paint flyPaint;
Fly(this.game, double x, double y) {
flyRect = Rect.fromLTWH(x, y, game.tileSize, game.tileSize);
flyPaint = Paint();
flyPaint.color = Color(0xff6ab04c);
}
void render(Canvas c) {
c.drawRect(flyRect, flyPaint);
}
解決一些問題
在開始生產(chǎn)一個游戲組件之前,先分析一下有哪些要解決技術(shù)問題泪幌,當(dāng)我們的游戲運(yùn)行時盲厌,它并不知道屏幕有多大署照,游戲默認(rèn)認(rèn)為它在0x0
的屏幕上運(yùn)行,所以我們要依靠調(diào)整大小的方法讓游戲知道屏幕有多大吗浩。
在hit-game.dart
中建芙,當(dāng)渲染(render
)方法運(yùn)行時,實(shí)例變量screenSize
已經(jīng)設(shè)置好了拓萌,因?yàn)橛螒蜻\(yùn)行時會按照下面的順序調(diào)用方法岁钓。
- 通過構(gòu)造函數(shù)創(chuàng)建一個類的實(shí)例(這個例子里沒有構(gòu)造函數(shù)升略,所以跳過哈)微王。
- Flutter調(diào)用調(diào)整(
resize
)方法并設(shè)置實(shí)例變量screenSize
。 - 游戲主循環(huán)開始奴烙。
- 游戲主循環(huán):調(diào)用更新(
update
)方法均唉。 - 游戲主循環(huán):調(diào)用渲染(
render
)方法侠草。 - 游戲主循環(huán)結(jié)束,回到第3步罩旋,開始新的循環(huán)。
在理想情況下眶诈,初始化代碼是我們準(zhǔn)備和創(chuàng)建對象的地方涨醋,它應(yīng)該只運(yùn)行一次,我們可以使用調(diào)整(resize
)方法來啟動初始化代碼逝撬,這看起來很正常浴骂。
但是勒,如果手機(jī)改變了分辨率或縱向旋轉(zhuǎn)到橫向時宪潮,F(xiàn)lutter會再次調(diào)用調(diào)整(resize
)方法溯警,如果我們將初始化代碼放在調(diào)整(resize
)方法中,它會多次再次狡相。同樣的梯轻,初始化代碼應(yīng)該只運(yùn)行一次,假如屏幕上已經(jīng)有一個NPC了尽棕,玩家將手機(jī)翻轉(zhuǎn)180度喳挑,觸發(fā)調(diào)整大小的方法,然后再次運(yùn)行初始化代碼滔悉,屏幕上又出現(xiàn)了另一個NPC伊诵,真的讓人頭大。
我們可以解決這個問題氧敢,依然使用調(diào)整(resize
)作為初始化代碼的啟動器日戈,但是我們可以額外聲明一個布爾實(shí)例變量,可以取類似“isInitialized”的名字孙乖,然后默認(rèn)值為“false”浙炼,在調(diào)整(resize
)方法中份氧,可以先檢查“isInitialized”是否為“false”,如果是弯屈,就運(yùn)行初始化代碼并將值設(shè)置為“true”蜗帜。
上面的方法只能說是一個不完美的方案,因?yàn)樗肓艘粋€不必要的實(shí)例變量资厉,F(xiàn)lame為我們提供了更好的解決方式厅缺。
現(xiàn)在打開hit-game.dart
文件,在HitGame
類中編寫兩個方法:構(gòu)造函數(shù)和名為initialize
的方法宴偿,構(gòu)造函數(shù)里只包含一行代碼:調(diào)用initialize
方法湘捎。
我們將使用異步函數(shù)來等待屏幕大小,所以要使用到async
和initialize
關(guān)鍵字來實(shí)現(xiàn)異步方法窄刘。這同時也是為什么初始化代碼不能直接放在構(gòu)造函數(shù)中窥妇,并且必須放在單獨(dú)的方法上的原因,在Dart語法中娩践,構(gòu)造函數(shù)是不能是異步的活翩。
class HitGame extends Game {
Size screenSize;
double tileSize;
HitGame() {
initialize();
}
void initialize() async {}
接下來還需要調(diào)用Flame庫的util.initialDimensions
函數(shù),導(dǎo)入Flame庫并在文件hit-game.dart
文件里添加下面代碼翻伺。
import 'dart:ui';
import 'package:flame/game.dart';
import 'package:flame/flame.dart';
class HitGame extends Game {
Size screenSize;
double tileSize;
HitGame() {
initialize();
}
void initialize() async {
resize(await Flame.util.initialDimensions());
}
...
void resize(Size size) {
screenSize = size;
tileSize = screenSize.width / 9;
}
}
上面的代碼中材泄,調(diào)整(resize
)方法接受一個Size
類型的參數(shù),F(xiàn)lame的util.initialDimensions
函數(shù)返回一個Future<Size>
吨岭,所以我們等待(await
)未來(Future
)完成拉宗,這樣可以得到一個Size
。
一旦我們有一個大形疵谩(Size
)值簿废,就可以將其插入以調(diào)整(resize
)大小。我們現(xiàn)在可以直接將值插入screenSize
络它,但是也需要重新計(jì)算tileSize
族檬,另外后面我們還會計(jì)算其他東西,所以還是保存在調(diào)整(resize
)方法里化戳,我們只需要調(diào)用它來重新計(jì)算所有內(nèi)容单料。
生產(chǎn)準(zhǔn)備工作
到這一步,我們已經(jīng)為生產(chǎn)一個游戲組件做好了準(zhǔn)備工作点楼,為了讓游戲類可以訪問和創(chuàng)建Fly
類的實(shí)例扫尖,必須先在文件頂部導(dǎo)入它,并新建一個名為敵人(enemy
)的List
類型的實(shí)例變量掠廓。
...
import 'package:hello_flame/components/fly.dart';
class HitGame extends Game {
Size screenSize;
double tileSize;
List<Fly> enemy;
現(xiàn)在實(shí)例變量敵人(enemy
)是個null
值换怖,所以要在initialize
方法中為它分配一個實(shí)際的列表。
void initialize() async {
enemy = List<Fly>();
resize(await Flame.util.initialDimensions());
}
即使現(xiàn)在還沒有敵人(enemy
)蟀瞧,我們也要使用List
的forEach
方法循環(huán)遍歷敵人(enemy
)實(shí)例變量沉颂,并在更新(update
)和渲染(render
)方法上調(diào)用相應(yīng)的方法条摸。
因?yàn)槲覀冋{(diào)用對象的順序直接影響游戲在屏幕上的顯示方式,敵人本身的順序無關(guān)緊要铸屉,重要的是在敵人后面繪制背景钉蒲,所以現(xiàn)在添加下面代碼到更新(update
)和渲染(render
)方法上。
void render(Canvas canvas) {
Rect bgRect = Rect.fromLTWH(0, 0, screenSize.width, screenSize.height);
Paint bgPaint = Paint();
bgPaint.color = Color(0xff576574);
canvas.drawRect(bgRect, bgPaint);
enemy.forEach((Fly fly) => fly.render(canvas));
}
上面代碼中彻坛,forEach
方法使用一個函數(shù)作為參數(shù)顷啼,然后它為List
中的每個項(xiàng)目都調(diào)用一次該函數(shù),將當(dāng)前迭代中的項(xiàng)目作為參數(shù)傳遞昌屉。
生產(chǎn)游戲組件
我們的游戲最終會定期生產(chǎn)游戲組件钙蒙,所以需要一個可以重復(fù)使用的生產(chǎn)方法,創(chuàng)建一個produceFly
方法怠益,并在initialize
方法中調(diào)用這個方法仪搔。這樣的話,確定屏幕尺寸后蜻牢,就會開始生產(chǎn)游戲組件。
void initialize() async {
enemy = List<Fly>();
resize(await Flame.util.initialDimensions());
produceFly();
}
void produceFly() {
enemy.add(Fly(this, 50, 50));
}
在上面代碼的produceFly
方法中偏陪,創(chuàng)建了一個Fly
類的新實(shí)例抢呆,Fly
類的默認(rèn)構(gòu)造函數(shù)需要傳遞3個參數(shù),HitGame
的實(shí)例笛谦、x
初始位置抱虐、y
初始位置。對于第1個HitGame
實(shí)例饥脑,我們通過this
直接使用當(dāng)前正在操作的實(shí)例恳邀,對于第2、3個參數(shù)灶轰,我們暫時先通過硬編碼傳入(50,50
)谣沸。
接下來,我們要隨機(jī)化游戲組件的初始位置笋颤,為此需要導(dǎo)入Dart的數(shù)學(xué)(dart:math
)庫乳附,并創(chuàng)建另一個類型為隨機(jī)(Random
)的實(shí)例變量rnd
,這樣會使該變量可以重用伴澄,每次我們需要隨機(jī)的時候都不用再創(chuàng)建一個新的隨機(jī)(Random
)實(shí)例了赋除。
當(dāng)然,也不要忘了在initialize
方法中初始化這個實(shí)例變量非凌。
...
import 'dart:math';
class HitGame extends Game {
Size screenSize;
double tileSize;
List<Fly> enemy;
Random rnd;
...
void initialize() async {
enemy = List<Fly>();
rnd = Random();
resize(await Flame.util.initialDimensions());
produceFly();
}
現(xiàn)在举农,我們開始編輯produceFly
方法,使x
和y
位置隨機(jī)化敞嗡,Random
類有一個nextDouble
方法颁糟,其返回一個在0
(包括)和1
(不包括)之間的任何double
值祭犯。
接下來我們調(diào)用這個方法,并將它乘以屏幕的寬度滚停,再減去游戲組件的寬度沃粗,因?yàn)橛螒蚪M件位于其左上角,并將其分配給初始值x
键畴。然后再對初始值y
做同樣的操作最盅,但是使用屏幕的高度減去游戲組件的高度。
現(xiàn)在我們的游戲組件是一個正方形起惕,所以它的寬度和高度是相同的涡贱,而且寬高都是tileSize
,因此惹想,為了獲得最大值问词,我們需要將tileSize
減去屏幕的寬度或高度。
然后嘀粱,當(dāng)我們創(chuàng)建Fly
類的新實(shí)例時激挪,將這些x
和y
變量作為初始位置。
void produceFly() {
double x = rnd.nextDouble() * (screenSize.width - tileSize);
double y = rnd.nextDouble() * (screenSize.height - tileSize);
enemy.add(Fly(this, x, y));
}
讓組件可點(diǎn)擊
要開始讓游戲組件落下锋叨,我們的游戲就需要接受來自玩家的輸入垄分。然后我們明確一下玩家點(diǎn)擊游戲組件時會發(fā)生什么,游戲組件的顏色應(yīng)該變成紅色娃磺,并下落到屏幕的底部薄湿。再然后,當(dāng)游戲組件離開玩家視角的時候偷卧,我們要銷毀該游戲組件的實(shí)例豺瘤,讓玩家的設(shè)備不會浪費(fèi)CPU資源來更新它。
首先導(dǎo)入Flutter的手勢(flutter/gestures.dart
)庫听诸,在游戲類中添加一個處理函數(shù)坐求,該函數(shù)將負(fù)責(zé)處理點(diǎn)擊按下(onTapDown
)事件,同時接受TapDownDetails
類型的參數(shù)蛇更。
...
import 'package:flutter/gestures.dart';
class HitGame extends Game {
...
void onTapDown(TapDownDetails d) {}
}
然后再回到main.dart
文件中瞻赶,導(dǎo)入Flutter的手勢(flutter/gestures.dart
)庫,并創(chuàng)建一個手勢識別器派任,將onTapDown
屬性鏈接到游戲類的onTapDown
方法砸逊,最后還要使用Flame實(shí)用程序的添加手勢識別器(addGestureRecognizer
)方法注冊識別器。
...
import 'package:flutter/gestures.dart';
void main() async {
...
TapGestureRecognizer tapper = TapGestureRecognizer();
tapper.onTapDown = game.onTapDown;
flameUtil.addGestureRecognizer(tapper);
}
再然后呢掌逛,打開fly.dart
文件师逸,添加一個事件處理程序,只有點(diǎn)擊此Fly
實(shí)例時才會觸發(fā)該事件處理程序豆混。我們不用知道Fly
實(shí)例的位置篓像,反正如果我們?nèi)绻{(diào)用此處理程序动知,就會調(diào)用此Fly
實(shí)例。
class Fly {
...
void onTapDown() {}
}
現(xiàn)在打開hit-game.dart
文件员辩,再分接處理程序內(nèi)部盒粮,為此我們需要循環(huán)遍歷所有現(xiàn)有的游戲組件,并檢查分接位置是否在游戲組件的邊界矩形內(nèi)奠滑。
矩形(Rect
)類有一個包含(contains
)方法丹皱,此方法接受偏移(Offset
)作為參數(shù),如果偏移(Offset
)傳遞的是在調(diào)用它的矩形(Rect
)的邊界內(nèi)宋税,則返回true
摊崭,否則返回false
。
通過處理程序傳遞的TapDownDetails
實(shí)例杰赛,我們可以獲得globalPosition
屬性的值呢簸,這個屬性是點(diǎn)擊按下時偏移(Offset
)量。這樣我們就可以將globalPosition
屬性傳遞給Fly
實(shí)例的包含(contains
)方法乏屯,就知道點(diǎn)擊是否有效根时。
void onTapDown(TapDownDetails d) {
enemy.forEach((Fly fly) {
if (fly.flyRect.contains(d.globalPosition)) {
fly.onTapDown();
}
});
}
在上面的代碼中,循環(huán)遍歷了enemy
內(nèi)的所有Fly
實(shí)例瓶珊,與渲染(render
)和更新(update
)中使用的邏輯相似啸箫,我們傳入一個函數(shù),該函數(shù)針對當(dāng)前enemy
中的每個Fly
實(shí)例運(yùn)行伞芹。
Fly
類有一個名為flyRect
的實(shí)例變量,它是一個矩形(Rect
)蝉娜,因此它也有一個包含(contains
)方法唱较,我們使用包含(contains
)方法檢查傳遞的TapDownDetails
實(shí)例的globalPosition
是否在矩形內(nèi)。
如果它在里面召川,就可以確定在當(dāng)前的forEach
迭代中Fly
的實(shí)例被點(diǎn)擊了南缓,我們就可以通過調(diào)用它的onTapDown
方法來通知它已經(jīng)被點(diǎn)擊。
自由落體運(yùn)動
現(xiàn)在我們開始處理敵人(enemy
)的點(diǎn)擊荧呐,打開fly.dart
文件汉形。第一個要改變的是顏色,渲染敵人(enemy
)時倍阐,我們使用flyPaint
的繪制(Paint
)對象概疆,它有一個顏色(color
)屬性,如果敵人(enemy
)沒有被點(diǎn)擊峰搪,它會被分配綠色岔冀,如果我們改變它,它應(yīng)該會反映在下一次調(diào)用渲染時概耻,F(xiàn)lutter每秒60幀使套,也就是每秒渲染60次罐呼,從人類的角度來看,就是一瞬間的事情侦高。
void onTapDown() {
flyPaint.color = Color(0xffff4757);
}
上面的代碼把敵人(enemy
)的顏色更改為紅色〖挡瘢現(xiàn)在運(yùn)行游戲,我們可以看到奉呛,單玩家點(diǎn)擊飛行敵人時计螺,敵人從綠色變成紅色了。
當(dāng)敵人被點(diǎn)擊時侧馅,它會因重力而下落危尿,不會停留在空中。要實(shí)現(xiàn)這一點(diǎn)馁痴,我們就要開始用到之前一直忽略的游戲主循環(huán)的更新(update
)方法了谊娇。
更新(update
)方法通常用于更改游戲中未被玩家輸入觸發(fā)的任何內(nèi)容的代碼,此時fly.dart
中的更新(update
)方法已經(jīng)被游戲主循環(huán)的更新(update
)方法調(diào)用罗晕。
使敵人動畫看起來像是在下降济欢,就是一個需要更新(update
)的邏輯,但是也不能把動畫放在那里小渊,因?yàn)橹挥兴稽c(diǎn)擊了才會掉落法褥,所以我們需要定義一個保存此信息的實(shí)例變量isDead
。
class Fly {
final HitGame game;
Rect flyRect;
Paint flyPaint;
bool isDead = false;
然后現(xiàn)在需要編輯Fly
類的更新(update
)方法酬屉,當(dāng)判斷游戲組件已經(jīng)被點(diǎn)擊半等,就更改其邊界矩形,向其頂部屬性添加一定值以使其向下移動呐萨。然后在onTapDown
處理程序中杀饵,將值設(shè)置為true
,并在更新(update
)方法中添加落下動畫的代碼谬擦。
void update(double t) {
if (isDead) {
flyRect = flyRect.translate(0, game.tileSize * 12 * t);
}
}
void onTapDown() {
isDead = true;
flyPaint.color = Color(0xffff4757);
}
上面的代碼中切距,每次調(diào)用更新(update
)時,Fly
實(shí)例會檢查其isDead
屬性的值是否為true
惨远,如果為true
就調(diào)用其翻轉(zhuǎn)(translate
)方法谜悟,從現(xiàn)有的邊界矩形構(gòu)建一個新的矩形(Rect
),然后再將這個新創(chuàng)建的矩形(Rect
)實(shí)例分配回去給flyRect
北秽。
對于翻轉(zhuǎn)(translate
)方法的參數(shù)葡幸,x
部分保留為0
,因?yàn)槲覀儾幌胱層螒蚪M件向左或向右移動羡儿。y
部分呢礼患,出現(xiàn)了一個double
類型的變量t
,該變量的全名應(yīng)該是時間增量(timeDelta
),但是腳手架把它命名為t
缅叠。
當(dāng)我們說游戲當(dāng)幀頻率為每秒60幀時悄泥,就相當(dāng)用1000毫秒除于60秒等于16.666666666毫秒,即每幀占用16.666666666毫秒的時間跨度肤粱,我們可以基于這個t
值進(jìn)行計(jì)算弹囚。
玩家的手機(jī)上不僅是運(yùn)行游戲,還會運(yùn)行大量應(yīng)用领曼,后臺還會運(yùn)行其他程序鸥鹉,這些應(yīng)用程序可能正在做一些可能在每個周期中占用更多或更少時間的事情。而CPU會嘗試給予所有正在運(yùn)行的進(jìn)程相同的資源和時間庶骄,但是有些進(jìn)程可以做到花費(fèi)更少的資源和時間毁渗。
這就是時間增量(t
)有用的地方,它包含自上次運(yùn)行更新(update
)以來經(jīng)過的時間量单刁,該值以秒為單位灸异。使用時間增量(t
),我們可以計(jì)算應(yīng)該發(fā)生的移動量羔飞。
假如肺樟,游戲由于某種原因以每秒1
幀的恒定速度完美運(yùn)行,因此時間差值恰好為1
逻淌。如果我們打算以每秒10
個圖塊的速度移動一個對象么伯,那么我們要加/減去10
、乘以圖塊大小的值卡儒、再乘以時間增量值1
到我們希望對象移動的維度田柔。這將會出現(xiàn)每秒10
個圖塊的移動。
現(xiàn)在再假如一下骨望,游戲以每秒4
幀的恒定速度完美運(yùn)行凯楔,時間差值始終為0.25
,使用每秒10
個圖塊的速度移動锦募。每幀移動對象10
個圖塊大小乘以圖塊大小、乘以0.25
等于2.5
乘以圖塊大小邻遏。也就是說每秒4
幀的運(yùn)動仍然是每秒10
個圖塊大小糠亩。
所以,使用公式game.tileSize * 12 * t
應(yīng)用該邏輯准验,無論時間增量(t
)值是什么時間值赎线,我們都可以得到12次game.tileSize
每秒運(yùn)動值的恒定運(yùn)動。
上面公式中的12
是一個看起來不錯的值糊饱,實(shí)際可以根據(jù)需要修改它垂寥,使移動的速度更慢或更快。
讓游戲有趣些
只有一個游戲組件的話,點(diǎn)一下就沒了滞项,為了更好玩一些狭归,我們需要在游戲組件被點(diǎn)擊后在屏幕上產(chǎn)生更多的游戲組件,在onTapDown
中添加下面代碼文判。
void onTapDown() {
isDead = true;
flyPaint.color = Color(0xffff4757);
game.produceFly();
}
現(xiàn)在當(dāng)一個游戲組件被點(diǎn)擊后过椎,會產(chǎn)生更多的游戲組件了,但是還有問題戏仓。當(dāng)一個游戲組件落下時疚宇,它會一直下降,也就是其y
Y坐標(biāo)一直在增加值赏殃,直到玩家終止游戲?yàn)橹狗蟠_@樣的話,生產(chǎn)的游戲組件越多仁热,輕則游戲卡頓榜揖,重則出現(xiàn)數(shù)據(jù)溢出錯誤。
要解決這個問題股耽,就需要添加一些代碼來刪除所有已經(jīng)脫離屏幕的游戲組件根盒,首先添加一個實(shí)例變量isOffScreen
,然后在更新(update
)方法中物蝙,添加如下代碼炎滞。
class Fly {
...
bool isOffScreen = false;
...
void update(double t) {
if (isDead) {
flyRect = flyRect.translate(0, game.tileSize * 12 * t);
if (flyRect.top > game.screenSize.height) {
isOffScreen = true;
}
}
}
上面代碼中,我們檢查此游戲組件實(shí)例的矩形頂部是否大于屏幕高度诬乞,如果是就將isOffScreen
設(shè)置成true
册赛。因?yàn)槠聊坏钠矫嬖c(diǎn)為(0,0)
即左上角,所以屏幕的底部的y
值等于屏幕高度震嫉。
接下來我們還要銷毀所有isOffScreen
屬性為true
的Fly
實(shí)例森瘪,使用List
的removeWhere
方法可以刪除所有符合條件的項(xiàng)目,它和forEach
類似票堵,但是它需要一個返回布爾值(bool
)的方法扼睬。而正好的是,isOffScreen
就是一個布爾值(bool
)悴势,所以我們直接返回它就好了窗宇。
回到hit-game.dart
文件中,添加下面的代碼特纤。
void update(double t) {
enemy.forEach((Fly fly) => fly.update(t));
enemy.removeWhere((Fly fly) => fly.isOffScreen);
}
上面代碼中军俊,創(chuàng)建了一個匿名函數(shù),并將Fly
作為參數(shù)捧存,然后立即返回它作為參數(shù)獲得的Fly
實(shí)例的isOffScreen
屬性粪躬。然后將此匿名函數(shù)作為參數(shù)傳遞給enemy
列表的removeWhere
方法担败,該方法為列表中的每個Fly
實(shí)例運(yùn)行傳遞的方法,如果返回true
則刪除實(shí)例镰官。
現(xiàn)在運(yùn)行游戲提前,可以看到下面圖片所展示的效果。