回顧
昨天下午筆者已經完成了背景動畫的迴圈播放. 晚上筆者就開發中發現的問題在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入口檔案中, 新建一個主角的例項, 完成初始化, 與繪圖的邏輯, 具體細節與背景圖類似, 我們就不細說了.
廢話不多說, 直接上效果圖
飛機的動效
在我們玩過的飛機類遊戲裡邊. 我們控制的飛機通常都會有一個動態效果, 這個動態的效果會增強玩家的視覺體驗, 筆者從網上找到了一份遊戲飛機的動效如下:
這個飛機動效是一個 gif 型別的檔案迴圈播放, 給人以動態的感覺. 我查閱了 flutter 貌似沒有直接繪製gif的介面. 所以我們只能用繪製靜態圖的方式去想辦法讓飛機動起來, 做過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() {}
複製程式碼
我們在這裡用一張圖, 去展示新舊座標點之前的關係:
通過以上這張圖, 我們要以明白在飛機在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 方法
我們為 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();
}
}
複製程式碼
通過以上改造, 我們看一下最終的效果.
總結
第二部份, 大工告成, 內容可能會有錯別字, 請大家指出, 我將進行改正, 剩下的邏輯. 我會一點點補上, 如果覺得本篇內容對您有幫助, 期待您的贊~ git傳送門