Flutter游戲:萬有引力定律

網(wǎng)絡(luò)配圖

搭游戲主循環(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 widthdouble height舶替,但是這種方式要4個變量令境。

更好的方式是,對于具有x/ywidth/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ù)xy將是新構(gòu)造實(shí)例的初始位置距糖。

然后在默認(rèn)構(gòu)造函數(shù)里面的代碼中玄窝,我們?yōu)?code>flyRect分配了一個新的矩形,使用傳遞的xy參數(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)用方法岁钓。

  1. 通過構(gòu)造函數(shù)創(chuàng)建一個類的實(shí)例(這個例子里沒有構(gòu)造函數(shù)升略,所以跳過哈)微王。
  2. Flutter調(diào)用調(diào)整(resize)方法并設(shè)置實(shí)例變量screenSize
  3. 游戲主循環(huán)開始奴烙。
  4. 游戲主循環(huán):調(diào)用更新(update)方法均唉。
  5. 游戲主循環(huán):調(diào)用渲染(render)方法侠草。
  6. 游戲主循環(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ù)來等待屏幕大小,所以要使用到asyncinitialize關(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)蟀瞧,我們也要使用ListforEach方法循環(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方法,使xy位置隨機(jī)化敞嗡,Random類有一個nextDouble方法颁糟,其返回一個在0(包括)和1(不包括)之間的任何double值祭犯。

接下來我們調(diào)用這個方法,并將它乘以屏幕的寬度滚停,再減去游戲組件的寬度沃粗,因?yàn)橛螒蚪M件位于其左上角,并將其分配給初始值x键畴。然后再對初始值y做同樣的操作最盅,但是使用屏幕的高度減去游戲組件的高度。

現(xiàn)在我們的游戲組件是一個正方形起惕,所以它的寬度和高度是相同的涡贱,而且寬高都是tileSize,因此惹想,為了獲得最大值问词,我們需要將tileSize減去屏幕的寬度或高度。

然后嘀粱,當(dāng)我們創(chuàng)建Fly類的新實(shí)例時激挪,將這些xy變量作為初始位置。

  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)一個游戲組件落下時疚宇,它會一直下降,也就是其yY坐標(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屬性為trueFly實(shí)例森瘪,使用ListremoveWhere方法可以刪除所有符合條件的項(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)行游戲提前,可以看到下面圖片所展示的效果。

萬有引力定律GIF圖地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末朋魔,一起剝皮案震驚了整個濱河市岖研,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌警检,老刑警劉巖孙援,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異扇雕,居然都是意外死亡拓售,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門镶奉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來础淤,“玉大人,你說我怎么就攤上這事哨苛「胄祝” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵建峭,是天一觀的道長玻侥。 經(jīng)常有香客問我,道長亿蒸,這世上最難降的妖魔是什么凑兰? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮边锁,結(jié)果婚禮上姑食,老公的妹妹穿的比我還像新娘。我一直安慰自己茅坛,他們只是感情好音半,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著贡蓖,像睡著了一般祟剔。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上摩梧,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天,我揣著相機(jī)與錄音宣旱,去河邊找鬼仅父。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的笙纤。 我是一名探鬼主播耗溜,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼省容!你這毒婦竟也來了抖拴?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤腥椒,失蹤者是張志新(化名)和其女友劉穎阿宅,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體笼蛛,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡洒放,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了滨砍。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片往湿。...
    茶點(diǎn)故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖惋戏,靈堂內(nèi)的尸體忽然破棺而出领追,到底是詐尸還是另有隱情,我是刑警寧澤响逢,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布绒窑,位于F島的核電站,受9級特大地震影響龄句,放射性物質(zhì)發(fā)生泄漏回论。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一分歇、第九天 我趴在偏房一處隱蔽的房頂上張望傀蓉。 院中可真熱鬧,春花似錦职抡、人聲如沸葬燎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽谱净。三九已至,卻和暖如春擅威,著一層夾襖步出監(jiān)牢的瞬間壕探,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工郊丛, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留李请,地道東北人瞧筛。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像导盅,于是被迫代替她去往敵國和親较幌。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評論 2 345