手寫一個在Flutter裡展示”精靈圖“的Widget

大帥老猿發表於2021-04-08

前言

之前用Flutter裡的遊戲引擎Flare做了一個“是男人就堅持100秒”的遊戲,文章請看這裡

使用Flare引擎之後,完全沒有了Flutter應用特有的程式碼風格。雖然更適應我這類有過遊戲開發經驗的開發者,但並不利於我們學習Flutter框架。所以我在那篇文章最後也說了,要抽空用Widget重寫一次這個遊戲。

首要任務,就是得有一個支援”精靈圖“的Widget,既然是學習,那就不能用別人開發好的,必須得自己親手造輪子。

什麼是”精靈圖“

image.png

精靈圖的英文是spritesheet(精靈表單),就是在一張圖上放置多個圖形,只需要載入到記憶體裡一次。在展示的時候,僅展示單個圖形的區域。一般多個圖形多用來放置連續動畫的多個關鍵幀。除了在遊戲引擎裡很常見以外,為了減少web請求,在前端領域也很常見。

原理拆解

載入一張大圖,但每次只展示圖片的特定區域

image.png

比如這張飛機的精靈圖,尺寸是330x82(畫素),橫向排布5個畫面,那麼單個畫面的尺寸就是330/5 = 66。我們每次展示的區域為x=66*畫面序號,y=0,width=66,height=82

可以設定橫向排布或縱向排布

精靈圖可以橫向或縱向排布,有些遊戲引擎的貼圖最大尺寸為4096x4096,所以還有些情況是需要我們換行切換的,但原理差異並不大,這裡就不過多討論了。

可以設定播放時間間隔,自動切換多個連續區域

2021-04-08 09_45_16.gif

大部分時候我們是需要用精靈圖來展示動畫的,比如這個飛機的精靈圖。其中第1,2幅畫面用於展示飛機飛行狀態的動畫,需要迴圈播放。

2021-04-08 09_48_43.gif

第3,4,5幅畫面用於展示飛機爆炸的動畫,只需播放一次。

思考應該用哪些Widget來搭建

通過一個動畫演示來看看我們需要哪些Widget

2021-04-08 10_00_37.gif

  • 可以控制顯示區域的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為迴圈播放
)
複製程式碼

關注大帥

一個熱愛前端開發的老程式猿,只在三個平臺分享內容

相關文章