基於Flutter Canvas的飛機大戰(二)
昨天下午筆者已經完成了背景動畫的迴圈播放. 晚上筆者就開發中發現的問題在stackoverflow上進行提問. 問題大概內容:
如何在 Canvas 中, 將一個較小的圖片, 拉伸平鋪問題連結
這個問題, 收到了二個有效的回答
- Canvas.drawImageRect()
- paintImage()
進過筆者測試


二者視覺效果相似, 可是 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 入口檔案中, 新建一個主角的例項, 完成初始化, 與繪圖的邏輯, 具體細節與背景圖類似, 我們就不細說了.
廢話不多說, 直接上效果圖

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

, 做過h5的同學可能比較瞭解, 在早期html介面中的動畫是由多幀拼接成一個膠片, 迴圈播放, 造成一種視覺停留的動畫效果. 這裡我們依然採用這種方式去實現本次的動態效果. 我們通過ps, 把每一幀拼接做成一個有2幀的132*80長幀圖;

接下來, 我們就要盤這張圖,對我們的 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); } 複製程式碼
效果圖如下:

飛機的控制
關於控制飛機飛行的思路是, 我們通過監聽螢幕, 手指的運動, 動態的更新飛機繪製 (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() {} 複製程式碼
我們在這裡用一張圖, 去展示新舊座標點之前的關係:

void moveTo(double x, double y) { // TODO: implement moveTo this._x = x; this._y = y; } void calculatePosition() { Pointp1 = Point(x, y); Pointp2 = 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 方法

增加一個飛機的飛行狀態, 當飛機與目標點及其接近時, 直接手動覆蓋(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(); } } 複製程式碼
通過以上改造, 我們看一下最終的效果.

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