基于Flutter Canvas的飛機(jī)大戰(zhàn)(二)

回顧

昨天下午筆者已經(jīng)完成了背景動(dòng)畫的循環(huán)播放. 晚上筆者就開發(fā)中發(fā)現(xiàn)的問題在stackoverflow上進(jìn)行提問.
問題大概內(nèi)容:

如何在Canvas中, 將一個(gè)較小的圖片, 拉伸平鋪 問題鏈接

這個(gè)問題, 收到了二個(gè)有效的回答

  • Canvas.drawImageRect()
  • paintImage()

進(jìn)過筆者測(cè)試
<figure class="half">
<img src="https://user-gold-cdn.xitu.io/2019/1/25/168842541b92ee59?w=776&h=1538&f=png&s=27498" style="wdith: 200px">
<img src="https://user-gold-cdn.xitu.io/2019/1/25/168842e80e058bc9?w=776&h=1538&f=png&s=265695" style="wdith: 200px">
</figure>

二者視覺效果相似, 可是 paintImage 的性能問題, 嚴(yán)重消耗了GPU資源. 查看了paintImage的源碼, 發(fā)現(xiàn)這個(gè)函數(shù)實(shí)現(xiàn)的方式也是調(diào)用了 drawImageRect, 這個(gè)問題.有興趣的同學(xué)可以深入了解一下. 共同探討一下, 也行對(duì)于Flutter性能優(yōu)化有很大的幫助.

void paintImage(
  ...
  if (centerSlice == null) {
    for (Rect tileRect in _generateImageTileRects(rect, destinationRect, repeat))
      canvas.drawImageRect(image, sourceRect, tileRect, paint);
  } else {
    for (Rect tileRect in _generateImageTileRects(rect, destinationRect, repeat))
      canvas.drawImageNine(image, centerSlice, tileRect, paint);
  }
  if (needSave)
    canvas.restore();
}

開始

本篇我們的主要任務(wù)是, 在畫板上增加我們控制的飛機(jī), 可以操作飛機(jī)移動(dòng).

繪制飛機(jī)

考慮到我們未來要繪制玩家的戰(zhàn)機(jī). 還要繪制敵機(jī). 我們先抽象出一個(gè) Plan 的類, 方便以后我們的開發(fā).我們?cè)?src 下, 新建一個(gè)叫 plan.dart的文件. 定義他的方法.

abstract class Plan {
  void init() {}
  void moveTo(double x, double y) {}
  void destroy() {}
  void paint(Canvas canvas, Size size) async {}
}

接下來我們就可以定義的的 MainHero我們的主角了. 我們的src下新建一個(gè) hero.dart, 引用并繼承 Plan, 并實(shí)現(xiàn)在上邊定義的方法. 關(guān)于基本方法與屬性如下:


enum PlanStatus {stay, move, die}

class MainHero extends Plan {
  // 飛機(jī)的中心坐標(biāo)x
  double x = 100.0;
  // 飛機(jī)的中心坐標(biāo)y
  double y = 100.0;
  // 戰(zhàn)機(jī)寬度
  double width = 132.0;
  // 戰(zhàn)機(jī)高度
  double height = 160.0;

  ui.Image image;

  @override
  void init() async {
    // TODO: implement init
    image = await Utils.getImage('assets/images/hero.png');
  }
  @override
  void moveTo(double x, double y) {
    // TODO: implement moveTo
  }
  @override
  void destroy() {
    // TODO: implement destroy
    super.destroy();
  }
 
 
  @override
  void paint(Canvas canvas, Size size) {
    Rect paintArea = Offset(100, 100) & Size(width, height);
    Rect planArea = Offset(0, 0) & Size(image.width, image.height)
    canvas.save();
    // 將畫布向左上方偏移, 把繪圖點(diǎn), 遷移到飛機(jī)正中心
    canvas.translate( -width / 2, -height / 2);
    canvas.drawImageRect(image, planArea, paintArea, new Paint());
    frameIndex++;
    canvas.restore();
  }
}

在本次我們的繪圖接口用的是 drawImageRect, 使用方法參考文檔, 我們?cè)谟螒虻?Enter入口文件中, 新建一個(gè)主角的實(shí)例, 完成初始化, 與繪圖的邏輯, 具體細(xì)節(jié)與背景圖類似, 我們就不細(xì)說了.

廢話不多說, 直接上效果圖

<img src="https://user-gold-cdn.xitu.io/2019/1/26/1688948e368cd8dc?w=388&h=769&f=png&s=13316" style="width: 200px" />

飛機(jī)的動(dòng)效

在我們玩過的飛機(jī)類游戲里邊. 我們控制的飛機(jī)通常都會(huì)有一個(gè)動(dòng)態(tài)效果, 這個(gè)動(dòng)態(tài)的效果會(huì)增強(qiáng)玩家的視覺體驗(yàn), 筆者從網(wǎng)上找到了一份游戲飛機(jī)的動(dòng)效如下:

image

這個(gè)飛機(jī)動(dòng)效是一個(gè) gif 類型的文件循環(huán)播放, 給人以動(dòng)態(tài)的感覺. 我查閱了 flutter 貌似沒有直接繪制gif的接口. 所以我們只能用繪制靜態(tài)圖的方式去想辦法讓飛機(jī)動(dòng)起來, 做過h5的同學(xué)可能比較了解, 在早期html界面中的動(dòng)畫是由多幀拼接成一個(gè)膠片, 循環(huán)播放, 造成一種視覺停留的動(dòng)畫效果. 這里我們依然采用這種方式去實(shí)現(xiàn)本次的動(dòng)態(tài)效果. 我們通過ps, 把每一幀拼接做成一個(gè)有2幀的132*80長(zhǎng)幀圖;
<Center>
<img src="https://user-gold-cdn.xitu.io/2019/1/26/1688938978fcd3f6?w=712&h=456&f=png&s=55383" style="width: 200px">
</Center>

接下來, 我們就要盤這張圖,對(duì)我們的 MainHero進(jìn)行改造, 把他動(dòng)態(tài)顯示在我們的屏幕上. 我們給它增加二個(gè)屬性和一個(gè)方法, 每一次屏幕刷新, 我們都把 frameIndex 進(jìn)行加1的操作, 當(dāng)達(dá)到最后一幀, 將 frameIndex重置為0, 這樣我們的飛機(jī)就可以動(dòng)起來了

// 總幀數(shù)
int frameNumber = 2;
// 當(dāng)前幀數(shù)
int frameIndex = 0;

// 動(dòng)態(tài)獲取飛機(jī)的長(zhǎng)幀圖的繪制區(qū)域
Rect getPlanAreaSize(int _frameIndex) {

double perFrameWidth = image.width / frameNumber;
double offsetX = perFrameWidth * _frameIndex;
double offsetY = 0;
if (offsetX >= image.width) {
  frameIndex = 0;
  return this.getPlanAreaSize(0);
}
return Offset(offsetX, offsetY) & Size(66.0, 80.0);
}

效果圖如下:


image

飛機(jī)的控制

關(guān)于控制飛機(jī)飛行的思路是, 我們通過監(jiān)聽屏幕, 手指的運(yùn)動(dòng), 動(dòng)態(tài)的更新飛機(jī)繪制 (x,y) 的坐標(biāo)點(diǎn), 從而達(dá)到我們想要的效果.

Flutter的文檔中, 我們找到了 GestureDetector 接口, 在 Enter 入口中 我們用GestureDetector控件包圍住我們的CustomPaint畫板 控件拂募。我們接下來的工作就是帕翻,使用 GestureDetector 控件來捕獲用戶的拖動(dòng)事件官辽。并更新我們 MainHero 的坐標(biāo)點(diǎn).

實(shí)現(xiàn)方式如下:

 Widget build(BuildContext context) build () {
    ...
    return GestureDetector(
      child: CustomPaint(
          painter: MainPainter(background: background, hero: hero)
      ),
      onPanStart: (DragDownDetails) {
        hero.moveTo(DragDownDetails.globalPosition.dx, DragDownDetails.globalPosition.dy);
      },
      onPanUpdate: (DragDownDetails) {
     
        hero.moveTo(DragDownDetails.globalPosition.dx, DragDownDetails.globalPosition.dy);
      }
    )
}

接下來我們來改造我們的 MainHero 類, 完善他的 moveTo 方法. 在游戲過程中, 我們手指拖動(dòng), 飛機(jī)不可能以閃現(xiàn)的方式進(jìn)行閃動(dòng), 它需要一點(diǎn)點(diǎn)移動(dòng)到我們的想要的位置. 我們?cè)?MainHero中定義幾個(gè)屬性與方法

// 飛行目標(biāo)點(diǎn)坐標(biāo)
double _x;
double _y;
double speed = 20;
// 動(dòng)態(tài)計(jì)算新的坐標(biāo)點(diǎn)
void calculatePosition() {}

我們?cè)谶@里用一張圖, 去展示新舊坐標(biāo)點(diǎn)之前的關(guān)系:

image

通過以上這張圖, 我們要以明白在飛機(jī)在x與y軸上, 速度的矢量關(guān)系與運(yùn)算方法, 我們完善我們的 calculatePosition

 void moveTo(double x, double y) {
    // TODO: implement moveTo
    this._x = x;
    this._y = y;
 }
void calculatePosition() {
    Point  p1 = Point(x, y);
    Point  p2 = Point(_x, _y);
    double distance = p1.distanceTo(p2);
    double flyRadian = acos(((y - _y) / distance).abs());
    // 判斷位移方向
    if (_x < x) {
      x -= speed * sin(flyRadian);
    } else {
      x += speed * sin(flyRadian);
    }
    if (_y < y) {
      y -= speed * cos(flyRadian);
    } else {
      y += speed * cos(flyRadian);
    }
  }

通過以上改造, 我們進(jìn)行測(cè)試發(fā)現(xiàn), 在運(yùn)動(dòng)到終點(diǎn)時(shí),飛機(jī)會(huì)在終點(diǎn)發(fā)生抖動(dòng), 排查問題發(fā)現(xiàn), 是我們的calculatePosition方法, 在計(jì)算x值的時(shí)候, 會(huì)在最后一次計(jì)算中, 產(chǎn)生一個(gè) |x - _x| > 0的結(jié)果, 所以飛機(jī)會(huì)在坐標(biāo)點(diǎn)來回的跳動(dòng). 為了避免這種情況, 我們?cè)俅胃脑?calculatePosition 方法

image

我們?yōu)?MainHero 增加一個(gè)飛機(jī)的飛行狀態(tài), 當(dāng)飛機(jī)與目標(biāo)點(diǎn)及其接近時(shí), 直接手動(dòng)覆蓋(x, y), 并將飛機(jī)的狀態(tài)設(shè)為 stay.

// stay 無人控制, 自由飛行
// move 有人控制, 飛行運(yùn)動(dòng)狀態(tài)
// die  死了
enum PlanStatus {stay, move, die}

void calculatePosition() {
    ...
    // 避免抖動(dòng), 做一個(gè)判斷. 距離
    if (distance < 10) {
      x = _x;
      y = _y;
      status = PlanStatus.stay;
      return null;
    }
}
// 同時(shí)為了更好的優(yōu)化我們的Pain方法函數(shù), 我們?yōu)槠湓黾右粋€(gè)邏輯的判斷
void paint(Canvas canvas, Size size) {
    ...
    if (status == PlanStatus.move) {
      calculatePosition();
    }
}

通過以上改造, 我們看一下最終的效果.


image

總結(jié)

第二部份, 大工告成, 內(nèi)容可能會(huì)有錯(cuò)別字, 請(qǐng)大家指出, 我將進(jìn)行改正, 剩下的邏輯. 我會(huì)一點(diǎn)點(diǎn)補(bǔ)上, 如果覺得本篇內(nèi)容對(duì)您有幫助, 期待您的贊~ git傳送門

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末酪耳,一起剝皮案震驚了整個(gè)濱河市睦尽,隨后出現(xiàn)的幾起案子铁材,更是在濱河造成了極大的恐慌宇攻,老刑警劉巖惫叛,帶你破解...
    沈念sama閱讀 211,496評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異逞刷,居然都是意外死亡嘉涌,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,187評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門夸浅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來仑最,“玉大人,你說我怎么就攤上這事帆喇【剑” “怎么了?”我有些...
    開封第一講書人閱讀 157,091評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)预皇。 經(jīng)常有香客問我侈玄,道長(zhǎng),這世上最難降的妖魔是什么吟温? 我笑而不...
    開封第一講書人閱讀 56,458評(píng)論 1 283
  • 正文 為了忘掉前任序仙,我火速辦了婚禮,結(jié)果婚禮上鲁豪,老公的妹妹穿的比我還像新娘潘悼。我一直安慰自己,他們只是感情好爬橡,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,542評(píng)論 6 385
  • 文/花漫 我一把揭開白布治唤。 她就那樣靜靜地躺著,像睡著了一般堤尾。 火紅的嫁衣襯著肌膚如雪肝劲。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,802評(píng)論 1 290
  • 那天郭宝,我揣著相機(jī)與錄音辞槐,去河邊找鬼。 笑死粘室,一個(gè)胖子當(dāng)著我的面吹牛榄檬,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播衔统,決...
    沈念sama閱讀 38,945評(píng)論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼鹿榜,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了锦爵?” 一聲冷哼從身側(cè)響起舱殿,我...
    開封第一講書人閱讀 37,709評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎险掀,沒想到半個(gè)月后沪袭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,158評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡樟氢,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,502評(píng)論 2 327
  • 正文 我和宋清朗相戀三年冈绊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片埠啃。...
    茶點(diǎn)故事閱讀 38,637評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡死宣,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出碴开,到底是詐尸還是另有隱情毅该,我是刑警寧澤博秫,帶...
    沈念sama閱讀 34,300評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站眶掌,受9級(jí)特大地震影響台盯,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜畏线,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,911評(píng)論 3 313
  • 文/蒙蒙 一静盅、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧寝殴,春花似錦蒿叠、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,744評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至抵蚊,卻和暖如春施绎,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背贞绳。 一陣腳步聲響...
    開封第一講書人閱讀 31,982評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工谷醉, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人冈闭。 一個(gè)月前我還...
    沈念sama閱讀 46,344評(píng)論 2 360
  • 正文 我出身青樓俱尼,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親萎攒。 傳聞我的和親對(duì)象是個(gè)殘疾皇子遇八,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,500評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • 說明 小編也是初學(xué)者,為了了解flutter動(dòng)畫的使用與效果, 決定親自定手用flutte寫一款小游戲出來. 并...
    ontow閱讀 5,013評(píng)論 0 8
  • canvas元素的基礎(chǔ)知識(shí) 在頁面上放置一個(gè)canvas元素,就相當(dāng)于在頁面上放置了一塊畫布耍休,可以在其中進(jìn)行圖形的...
    oWSQo閱讀 10,274評(píng)論 0 19
  • 1 CALayer IOS SDK詳解之CALayer(一) http://doc.okbase.net/Hell...
    Kevin_Junbaozi閱讀 5,133評(píng)論 3 23
  • 前言 什么值得買 是一家網(wǎng)購(gòu)?fù)扑]網(wǎng)站刃永,主要推薦「高性價(jià)比」產(chǎn)品,范圍包括了國(guó)內(nèi)及國(guó)外的多個(gè)電商網(wǎng)站羊精。 在網(wǎng)頁版的基...
    Ziya閱讀 2,429評(píng)論 2 15
  • 今天去了谷德佳樂家斯够,發(fā)現(xiàn)了一些端倪 首先,超市入口幾個(gè)大字非常明顯园匹,超市標(biāo)語也非常明朗 其次雳刺,幾乎所有的價(jià)格都在同...
    峰之與你若只如初見閱讀 207評(píng)論 0 0