flutter開發遊戲入門(仿谷歌瀏覽器小恐龍Chrome dino)二

前言

這篇文章是接着上一章寫的,若是沒有看過上一章,能夠經過查看公衆號"bugporter"的歷史記錄獲取上一章的內容,或者經過如下連接查看。bash

juejin.im/post/5ea1a5…框架

優化上一章的代碼

上一章全部須要用到屏幕尺寸的 組件(Component)類都是在resize方法中接收到包含屏幕尺寸的Size參數後才構建的。可是每一個類都這樣寫,有點不友好,因此我把構造方法改了一下,讓它直接接收Size參數,而後在MyGame類的resize方法中,把接收到Size參數給到組件後再實例化這些組件。ide

以前的地面(Horizon)組件類示例:函數

lib/sprite/horizon.dartoop

class Horizon ...{
  ...
  Horizon(this.spriteImage);
  
  @override
  void resize(ui.Size size) {
    super.resize(size);
    if(components.isEmpty){
      init();
      return;
    }
  }
  ...
}
複製代碼

更改後post

class Horizon ...{
  ...
  ui.Size size;

  Horizon(this.spriteImage, this.size){
    init();
  }
  //再也不須要重寫resize了
複製代碼

其它組件類也這樣改,而後咱們在MyGame類的resize中,才實例化這些組件優化

lib/game.dart動畫

Class MyGame...
  @override
  void resize(ui.Size size) {
    if(components.isEmpty){
      gameBg = GameBg(Color.fromRGBO(245, 243, 245, 1));
      horizon = Horizon(spriteImage, size);
      cloud = Cloud(spriteImage, size);
      obstacle = Obstacle(spriteImage, size);
    }
    super.resize(size);
  }
  ...
複製代碼

建立小恐龍

在上一章已經完成了遊戲背景、地面、和天空(雲朵),如今來建立遊戲最重要的一部分,遊戲主角,那個會跳不會rap也不會籃球的 小恐龍(dino)ui

除了跳小恐龍還會什麼?this

這裏面有兩個狀態我解釋一下:

  1. 等待: 遊戲未開始時小恐龍的樣子,開始後它須要跑到屏幕的必定距離,咱們才能控制它

  2. 驚訝: 這圖像中的小恐龍很驚訝,由於它碰到障礙物,Game Over了!

知道這些狀態後,須要測量出這些狀態對應的圖像位置和大小,而後把它寫到配置中。

lib/config.dart

...
class DinoConfig{
  static double h = 94.0;
  static double y = 2.0;
}
class DinoJumpConfig{
  static double w = 88.0;
  static double x = 1336.5;
}
class DinoWaitConfig{
  static double w = 88.0;
  static double x = 1336.5+88;
}
class DinoRunConfig{
  static double w = 88;
  final double x;

  const DinoRunConfig._internal({this.x});

  static List<DinoRunConfig> list = [
    DinoRunConfig._internal(
      x: 1336.5+(88*2)
    ),
    DinoRunConfig._internal(
      x: 1336.5+(88*3)
    ),
  ];
}
class DinoDieConfig{
  static double w = 88;
  static double x = 1336.5+(88*4);
}
class DinoDownConfig{
  static double w = 118;
  final double x;

  const DinoDownConfig._internal({this.x});

  static List<DinoDownConfig> list = [
    DinoDownConfig._internal(
        x: 1866.0
    ),
    DinoDownConfig._internal(
        x: 1866.0+118
    ),
  ];
}
複製代碼

上面代碼中,我爲小恐龍每一個狀態的圖像位置都建立了一個配置類。在這些配置中,它們的h(高)和y軸有些不是同樣的,因此我把它放到DinoConfig中,把這些狀態的高和y軸都強制同樣,能夠方便控制它的y軸實現跳躍。否則的話,須要計算每一個狀態的跳躍高度,還有站在地面上的高度。

裏面的蹲和站兩個跑步狀態是由多個圖像組成的動畫,因此我爲它們寫了一個私有的構造方法,並經過一個靜態的List返回每一個圖像不一樣的地方。

爲何要這樣返回呢?是由於在flame這個框架中,它爲咱們提供了一個動畫Animation類來建立動畫,咱們能夠經過它的spriteList構造方法來建立。在這個方法中,須要一個Sprite類型的List,因此咱們能夠經過遍歷配置中的List,把建立的Sprite對象加入到動畫組件的List中。

栗子

List<Sprite> runSpriteList = [];
DinoRunConfig.list.forEach((DinoRunConfig config){
  runSpriteList.add(Sprite.fromImage(spriteImage,
        x: config.x,
        y: DinoConfig.y,
        width: DinoRunConfig.w,
        height: DinoConfig.h),
    );
});
//AnimationComponent 動畫組件,須要3個參數,寬、高和動畫對象。
//stepTime每幀的時間,loop是否循環播放
AnimationComponent(
    DinoRunConfig.w,
    DinoConfig.h,
    Animation.spriteList(runSpriteList, stepTime: 0.1, loop: true));
複製代碼

這裏面有個地方須要注意一下,若是在父組件中把這個動畫組件添加進去了,可是重寫了父的update方法時,還須要在父的update中調用動畫組件的update方法,這個動畫纔會播放。

配置寫好了,如今來建立主角的組件。打開lib/script目錄,在這個目錄下建立一個dino.dart

在dino.dart中,先建立一個枚舉,把小恐龍在整個遊戲中的狀態寫上

enum DinoStatus {
  waiting,
  running,
  jumping,
  downing,
  die,
}
複製代碼

五個狀態,分別是:等待中、跑步中、跳躍中、正在蹲着和game over了

建立好了以後,在枚舉代碼的下邊,咱們建立一個組件類dino。在這個類中定義一個list屬性,並把上面枚舉對應狀態的組件都添加進去,最後還須要一個status屬性來記錄小恐龍當前的狀態。

enum DinoStatus...
class Dino extends Component{
  List<PositionComponent> actualDinoList = List(5);
  DinoStatus status = DinoStatus.waiting; //默認是等待中
  
  Dino(ui.Image spriteImage, this.size) {
    final double height = DinoConfig.h;
    final double yPos = DinoConfig.y;
    
    //建立枚舉對應的組件,加進list屬性
    //waiting
    actualDinoList[0] = SpriteComponent.fromSprite(
        DinoWaitConfig.w,
        height,
        Sprite.fromImage(spriteImage,
            x: DinoWaitConfig.x,
            y: yPos,
            width: DinoWaitConfig.w,
            height: height));

    //running
    List<Sprite> runSpriteList = [];
    DinoRunConfig.list.forEach((DinoRunConfig config){
      runSpriteList.add(Sprite.fromImage(spriteImage,
            x: config.x,
            y: yPos,
            width: DinoRunConfig.w,
            height: height),
        );
    });
    actualDinoList[1] = AnimationComponent(
        DinoRunConfig.w,
        height,
        Animation.spriteList(runSpriteList,
            stepTime: 0.1,
            loop: true));


    //jumping
    actualDinoList[2] = SpriteComponent.fromSprite(
        DinoJumpConfig.w,
        height,
        Sprite.fromImage(spriteImage,
            x: DinoJumpConfig.x,
            y: yPos,
            width: DinoJumpConfig.w,
            height: height));

    //downing
    List<Sprite> downSpriteList = [];
    DinoDownConfig.list.forEach((DinoDownConfig config){
      downSpriteList.add(Sprite.fromImage(spriteImage,
          x: config.x,
          y: yPos,
          width: DinoDownConfig.w,
          height: height),
      );
    });
    actualDinoList[3] = AnimationComponent(
        DinoDownConfig.w,
        height,
        Animation.spriteList(downSpriteList,
            stepTime: 0.1,
            loop: true));

    //die
    actualDinoList[4] = SpriteComponent.fromSprite(
        DinoDieConfig.w,
        height,
        Sprite.fromImage(spriteImage,
            x: DinoDieConfig.x,
            y: yPos,
            width: DinoDieConfig.w,
            height: height));
  }
}
複製代碼

狀態對應的組件加到list了,咱們還須要根據當前的狀態來渲染不一樣的組件。

首先在類中定義一個獲取器,返回當前的狀態對應的組件

Dino(ui.Image spriteImage, this.size)...

//獲取當前狀態對應的組件
PositionComponent get actualDino => actualDinoList[status.index];

複製代碼

而後重寫render方法,把當前狀態的組件渲染出來

...
  @override
  void render(ui.Canvas c) {
    actualDino.render(c);
  }
...

複製代碼

如今,小恐龍組件已經被建立好了,咱們回到MyGame這個類中,把它添加進去

class MyGame...
  ...
  Dino dino;
  
  @override
  void resize(ui.Size size) {
    if(components.isEmpty){
      ...
      dino = Dino(spriteImage, size);
      this
        ..add(gameBg)..add(horizon)..add(cloud)..add(dino)
    ...
複製代碼

ps: ... 是省略以前的代碼的意思

打包運行:

恐龍飛起來了,是由於在添加時,還沒給它設置y軸的位置,因此默認是0的。

如今咱們給它添加一個y軸的位置,屏幕高-(地面高+恐龍高-再站下一點點的距離)

class Dino...
  ...
  double maxY;
  double x,y;
  
  Dino(ui.Image spriteImage, this.size) {
    final double height = DinoConfig.h;
    final double yPos = DinoConfig.y;
    maxY = size.height - (HorizonConfig.h + height - 22);
    x = 0;
    y = maxY;
    
    //waiting
    actualDinoList[0] = SpriteComponent.fromSprite(
        DinoWaitConfig.w,
        height,
        Sprite.fromImage(spriteImage,
            x: DinoWaitConfig.x,
            y: yPos,
            width: DinoWaitConfig.w,
            height: height))
    ..x=x..y=y;
    
   ... 其餘組件也這樣設置一下x和y。
  }
複製代碼

上面代碼的maxY: 地面的位置,也就是恐龍最大的y軸位置。

dino類不須要添加子組件,由於它每次都是根據狀態來渲染一個組件的,只是起到了調度的做用,因此沒有繼承PositionComponent,而是繼承了基礎的Component類。這樣作的話,須要給它一個x和y屬性,咱們在渲染子組件的時候,把子組件的x和y設置成dino類的,能夠方便外面控制或者獲取,後面進行破撞檢測的時候會用到。

如今再運行:

給遊戲添加跳和蹲的按鈕

打開main.dart文件,調用runApp方法時,是獲取了Game的widget屬性做爲參數給runApp方法的。既然Game類返回了widget,那麼咱們也能夠把它放到flutter的其它組件中,例如給它套一個Stack, 把遊戲返回的widget放在底下,把一些按鈕添加到遊戲的上面,而後經過按鈕的點擊事件,實現對遊戲的控制。

可是想偷懶,不想寫一堆flutter的widget怎麼辦?

在fleam0.18.0以上的版本,提供了一個HasWidgetsOverlay類,只要咱們在Game類中with了這個類,就能夠使用addWidgetOverlay方法,把一個widget添加到遊戲的上面了,它底層就是使用Stack封裝的。

打開game.dart文件,給MyGame類添加一個建立按鈕的方法

...
class MyGame...
    Widget createButton({@required IconData icon, double right=0, double
      bottom=0,
      ValueChanged<bool>
      onHighlightChanged}){
        return Positioned(
          right: right,
          bottom: bottom,
          child: MaterialButton(
            onHighlightChanged: onHighlightChanged,
            onPressed: (){},
            splashColor: Colors.transparent,
            highlightColor: Colors.transparent,
            child: Container(
              width: 50,
              height: 50,
              decoration: new BoxDecoration(
                color: Color.fromRGBO(0, 0, 0, 0.5),
                //設置四周圓角 角度
                borderRadius: BorderRadius.all(Radius.circular(50)),
                //設置四周邊框
                border: new Border.all(width: 2, color: Colors.black),
              ),
              child: Icon(icon, color: Colors.black,),
            ),
          ),
        );
    }
    ...
複製代碼

該方法接收一個按鈕長按事件的回調函數onHighlightChanged,要想按鈕監聽長按事件,必需要給按鈕一個點擊事件onPressed,因此我在按鈕的onPressed中寫了一個空的回調函數。

爲何不直接用點擊事件呢?

由於點擊事件是在手指離開屏幕以後才觸發的,會有一點延遲,因此用長按事件,能夠監聽到玩家按下和鬆開,在這裏我須要它按下後就立刻跳,還有蹲下須要一直按住按鈕。

onHighlightChanged每次點擊都會觸發兩次,在按下和鬆開按鈕的時候觸發,回調中接收了一個bool類型的參數,按下是true、鬆開是false

而後咱們在MyGame的resize方法中,建立跳和蹲的按鈕,而後調用addWidgetOverlay添加到遊戲的上面

void resize(ui.Size size) {
      ...
      this
        ..add(gameBg)..add(horizon)..add(cloud)..add(dino)..add(obstacle)
        ..addWidgetOverlay('upButton', createButton(
          icon: Icons.arrow_drop_up,
          right: 50,
          bottom: 120,
          onHighlightChanged: (isOn)=>dino?.jump(isOn),
        ))
        ..addWidgetOverlay('downButton', createButton(
          icon: Icons.arrow_drop_down,
          right: 50,
          bottom: 50,
          onHighlightChanged: (isOn)=>dino?.down(isOn),
        ));
        ...

複製代碼

在onHighlightChanged中調用dino類的jump和down方法,這兩個方法尚未,咱們須要在dino類中實現它。

class Dino...
  ...
  bool isJump = false;
  bool isDown = false;
  double jumpVelocity = 0.0;
  ...
  void jump(bool isOn) {
    if(status == DinoStatus.running && isOn){
      status = DinoStatus.jumping;
      this.jumpVelocity = jumpPos;
      isJump = true;
      return;
    }
    isJump = false;
  }

  void down(bool isOn){
    isDown = isOn;
    if(status == DinoStatus.running && isOn){
      status = DinoStatus.downing;
      return;
    }
    if(status == DinoStatus.downing && !isOn){
      status = DinoStatus.running;
      return;
    }
  }
  
  @override
  void update(double t) {
    if (status == DinoStatus.jumping) {
      y += jumpVelocity;
      jumpVelocity += gravity;
      if(y > maxY){
        status = DinoStatus.running;
        y = maxY;
        //一直按住,不斷跳
        jump(isJump);
        //跳的過程當中按了蹲,角色落地時蹲下
        down(isDown);
      }
    }
    actualDino..x=x..y=y;
    actualDino.update(t);
  }
複製代碼

跳躍的時候給了它一個瞬間向上的力,而後不斷給它一個重力讓它回到地面。只有跑的時候能跳或者蹲,若是是跳,回到地面後還按着跳沒鬆開那麼將繼續跳,蹲的時候按下立刻蹲,鬆開了就站着跑。

把默認狀態改成runing, 運行後..

錄成gif看着有點卡,其實是很流暢的..

下一章繼續完善...

相關文章
相關標籤/搜索