回顧
昨天下午筆者已經(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)效如下:
這個(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);
}
效果圖如下:
飛機(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)系:
通過以上這張圖, 我們要以明白在飛機(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 方法
我們?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();
}
}
通過以上改造, 我們看一下最終的效果.
總結(jié)
第二部份, 大工告成, 內(nèi)容可能會(huì)有錯(cuò)別字, 請(qǐng)大家指出, 我將進(jìn)行改正, 剩下的邏輯. 我會(huì)一點(diǎn)點(diǎn)補(bǔ)上, 如果覺得本篇內(nèi)容對(duì)您有幫助, 期待您的贊~ git傳送門