基於Flutter Canvas的飛機大戰(二)

美味的小布丁發表於2019-01-26

回顧

昨天下午筆者已經完成了背景動畫的迴圈播放. 晚上筆者就開發中發現的問題在stackoverflow上進行提問. 問題大概內容:

如何在Canvas中, 將一個較小的圖片, 拉伸平鋪 問題連結

這個問題, 收到了二個有效的回答

  • Canvas.drawImageRect()
  • paintImage()

進過筆者測試

基於Flutter Canvas的飛機大戰(二) 基於Flutter Canvas的飛機大戰(二)

二者視覺效果相似, 可是 paintImage 的效能問題, 嚴重消耗了GPU資源. 檢視了paintImage的原始碼, 發現這個函式實現的方式也是呼叫了 drawImageRect, 這個問題.有興趣的同學可以深入瞭解一下. 共同探討一下, 也行對於Flutter效能優化有很大的幫助.

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();
}
複製程式碼

開始

本篇我們的主要任務是, 在畫板上增加我們控制的飛機, 可以操作飛機移動.

繪製飛機

考慮到我們未來要繪製玩家的戰機. 還要繪製敵機. 我們先抽象出一個 Plan 的類, 方便以後我們的開發.我們在 src 下, 新建一個叫 plan.dart的檔案. 定義他的方法.

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

接下來我們就可以定義的的 MainHero我們的主角了. 我們的src下新建一個 hero.dart, 引用並繼承 Plan, 並實現在上邊定義的方法. 關於基本方法與屬性如下:


enum PlanStatus {stay, move, die}

class MainHero extends Plan {
  // 飛機的中心座標x
  double x = 100.0;
  // 飛機的中心座標y
  double y = 100.0;
  // 戰機寬度
  double width = 132.0;
  // 戰機高度
  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();
    // 將畫布向左上方偏移, 把繪圖點, 遷移到飛機正中心
    canvas.translate( -width / 2, -height / 2);
    canvas.drawImageRect(image, planArea, paintArea, new Paint());
    frameIndex++;
    canvas.restore();
  }
}

複製程式碼

在本次我們的繪圖介面用的是 drawImageRect, 使用方法參考文件, 我們在遊戲的 Enter入口檔案中, 新建一個主角的例項, 完成初始化, 與繪圖的邏輯, 具體細節與背景圖類似, 我們就不細說了.

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

基於Flutter Canvas的飛機大戰(二)

飛機的動效

在我們玩過的飛機類遊戲裡邊. 我們控制的飛機通常都會有一個動態效果, 這個動態的效果會增強玩家的視覺體驗, 筆者從網上找到了一份遊戲飛機的動效如下:

基於Flutter Canvas的飛機大戰(二)
這個飛機動效是一個 gif 型別的檔案迴圈播放, 給人以動態的感覺. 我查閱了 flutter 貌似沒有直接繪製gif的介面. 所以我們只能用繪製靜態圖的方式去想辦法讓飛機動起來, 做過h5的同學可能比較瞭解, 在早期html介面中的動畫是由多幀拼接成一個膠片, 迴圈播放, 造成一種視覺停留的動畫效果. 這裡我們依然採用這種方式去實現本次的動態效果. 我們通過ps, 把每一幀拼接做成一個有2幀的132*80長幀圖;

基於Flutter Canvas的飛機大戰(二)

接下來, 我們就要盤這張圖,對我們的 MainHero進行改造, 把他動態顯示在我們的螢幕上. 我們給它增加二個屬性和一個方法, 每一次螢幕重新整理, 我們都把 frameIndex 進行加1的操作, 當達到最後一幀, 將 frameIndex重置為0, 這樣我們的飛機就可以動起來了

// 總幀數
int frameNumber = 2;
// 當前幀數
int frameIndex = 0;

// 動態獲取飛機的長幀圖的繪製區域
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);
}
複製程式碼

效果圖如下:

基於Flutter Canvas的飛機大戰(二)

飛機的控制

關於控制飛機飛行的思路是, 我們通過監聽螢幕, 手指的運動, 動態的更新飛機繪製 (x,y) 的座標點, 從而達到我們想要的效果.

Flutter的文件中, 我們找到了 GestureDetector 介面, 在 Enter 入口中 我們用GestureDetector控制元件包圍住我們的CustomPaint畫板 控制元件。我們接下來的工作就是,使用 GestureDetector 控制元件來捕獲使用者的拖動事件。並更新我們 MainHero 的座標點.

實現方式如下:

 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 方法. 在遊戲過程中, 我們手指拖動, 飛機不可能以閃現的方式進行閃動, 它需要一點點移動到我們的想要的位置. 我們在 MainHero中定義幾個屬性與方法

// 飛行目標點座標
double _x;
double _y;
double speed = 20;
// 動態計算新的座標點
void calculatePosition() {}
複製程式碼

我們在這裡用一張圖, 去展示新舊座標點之前的關係:

基於Flutter Canvas的飛機大戰(二)
通過以上這張圖, 我們要以明白在飛機在x與y軸上, 速度的向量關係與運算方法, 我們完善我們的 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);
    }
  }
複製程式碼

通過以上改造, 我們進行測試發現, 在運動到終點時,飛機會在終點發生抖動, 排查問題發現, 是我們的calculatePosition方法, 在計算x值的時候, 會在最後一次計算中, 產生一個 |x - _x| > 0的結果, 所以飛機會在座標點來回的跳動. 為了避免這種情況, 我們再次改造 calculatePosition 方法

基於Flutter Canvas的飛機大戰(二)
我們為 MainHero 增加一個飛機的飛行狀態, 當飛機與目標點及其接近時, 直接手動覆蓋(x, y), 並將飛機的狀態設為 stay.

// stay 無人控制, 自由飛行
// move 有人控制, 飛行運動狀態
// die  死了
enum PlanStatus {stay, move, die}

void calculatePosition() {
    ...
    // 避免抖動, 做一個判斷. 距離
    if (distance < 10) {
      x = _x;
      y = _y;
      status = PlanStatus.stay;
      return null;
    }
}
// 同時為了更好的優化我們的Pain方法函式, 我們為其增加一個邏輯的判斷
void paint(Canvas canvas, Size size) {
    ...
    if (status == PlanStatus.move) {
      calculatePosition();
    }
}
複製程式碼

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

基於Flutter Canvas的飛機大戰(二)

總結

第二部份, 大工告成, 內容可能會有錯別字, 請大家指出, 我將進行改正, 剩下的邏輯. 我會一點點補上, 如果覺得本篇內容對您有幫助, 期待您的贊~ git傳送門

相關文章