前言
之前用Flutter裡的遊戲引擎Flare做了一個“是男人就堅持100秒”的遊戲,文章請看這裡
使用Flare引擎之後,完全沒有了
Flutter
應用特有的程式碼風格。雖然更適應我這類有過遊戲開發經驗的開發者,但並不利於我們學習Flutter
框架。所以我在那篇文章最後也說了,要抽空用Widget重寫一次這個遊戲。
首要任務,就是得有一個支援”精靈圖“的Widget
,既然是學習,那就不能用別人開發好的,必須得自己親手造輪子。
什麼是”精靈圖“
精靈圖的英文是spritesheet
(精靈表單),就是在一張圖上放置多個圖形,只需要載入到記憶體裡一次。在展示的時候,僅展示單個圖形的區域。一般多個圖形多用來放置連續動畫的多個關鍵幀。除了在遊戲引擎裡很常見以外,為了減少web請求,在前端領域也很常見。
原理拆解
載入一張大圖,但每次只展示圖片的特定區域
比如這張飛機的精靈圖,尺寸是330x82(畫素),橫向排布5個畫面,那麼單個畫面的尺寸就是330/5 = 66
。我們每次展示的區域為x=66*畫面序號,y=0,width=66,height=82
。
可以設定橫向排布或縱向排布
精靈圖可以橫向或縱向排布,有些遊戲引擎的貼圖最大尺寸為4096x4096,所以還有些情況是需要我們換行切換的,但原理差異並不大,這裡就不過多討論了。
可以設定播放時間間隔,自動切換多個連續區域
大部分時候我們是需要用精靈圖來展示動畫的,比如這個飛機的精靈圖。其中第1,2幅畫面用於展示飛機飛行狀態的動畫,需要迴圈播放。
第3,4,5幅畫面用於展示飛機爆炸的動畫,只需播放一次。
思考應該用哪些Widget來搭建
通過一個動畫演示來看看我們需要哪些Widget
- 可以控制顯示區域的Widget(Container)
- 需要可以指定座標的Widget(Stack+Positioned)
原理也清楚了,也知道該用什麼Widget,那麼接下來的程式碼就很容易了
將思路轉變為程式碼
@override
Widget build(BuildContext context) {
return Container(
width: 66,
height: 82,
child: Stack(
children: [
Positioned(
left: 66*currentIndex,
top: 0,
child: widget.image
)
],
),
);
}
複製程式碼
加入定時器,根據設定的時間間隔改變currentIndex
,那麼圖片看上去就動起來了。
Timer.periodic(widget.duration, (timer) {
setState(() {
if(currentIndex>=4){
currentIndex=0;
}
else currentIndex++;
});
}
});
複製程式碼
我們再進一步封裝成一個自己原創的Widget
,下面是這個Widget的全部程式碼
import 'dart:async';
import 'package:flutter/widgets.dart';
class AnimatedSpriteImage extends StatefulWidget {
final Image image;
final Size spriteSize;
final int startIndex;
final int endIndex;
final int playTimes;
final Duration duration;
final Axis axis;
AnimatedSpriteImage({
Key? key,
required this.image,
required this.spriteSize,
required this.duration,
this.axis = Axis.horizontal,
this.startIndex = 0,
this.endIndex = 0,
this.playTimes = 0,//0 = loop
}) : super(key: key);
@override
_AnimatedSpriteImageState createState() => _AnimatedSpriteImageState();
}
class _AnimatedSpriteImageState extends State<AnimatedSpriteImage> {
int currentIndex = 0;
int currentTimes = 0;
@override
void initState() {
currentIndex = widget.startIndex;
Timer.periodic(widget.duration, (timer) {
if(currentTimes<=widget.playTimes){
setState(() {
if(currentIndex>=widget.endIndex){
if(widget.playTimes!=0)currentTimes++;
if(currentTimes<widget.playTimes||widget.playTimes==0)currentIndex=widget.startIndex;
else currentIndex = widget.endIndex;
}
else currentIndex++;
});
}
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Container(
width: widget.spriteSize.width,
height: widget.spriteSize.height,
child: Stack(
children: [
Positioned(
left: widget.axis==Axis.horizontal?-widget.spriteSize.width*currentIndex:0,
top: widget.axis==Axis.vertical?-widget.spriteSize.height*currentIndex:0,
child: widget.image
)
],
),
);
}
}
複製程式碼
封裝得好,使用起來也尤其方便。
//播放飛機飛行狀態動畫
AnimatedSpriteImage(
duration: Duration(milliseconds: 200),//動畫的間隔
image: Image.asset("assets/images/player.png"),//精靈圖
spriteSize: Size(66, 82),//單畫面尺寸
startIndex: 0,//動畫起始畫面序號
endIndex: 1,//動畫結束畫面序號
playTimes: 0,//播放次數,0為迴圈播放
)
//播放飛機爆炸動畫
AnimatedSpriteImage(
duration: Duration(milliseconds: 200),//動畫的間隔
image: Image.asset("assets/images/player.png"),//精靈圖
spriteSize: Size(66, 82),//單畫面尺寸
startIndex: 2,//動畫起始畫面序號
endIndex: 4,//動畫結束畫面序號
playTimes: 1,//播放次數,0為迴圈播放
)
複製程式碼
關注大帥
一個熱愛前端開發的老程式猿,只在三個平臺分享內容
- 掘金
- B站:大帥老猿
- 微信公眾號:大帥老猿