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

今天你摸魚了嗎發表於2020-06-22

上一章連結

1、前言

人生是一場遊戲,在這場遊戲中,我們會遇到各種磨難。遊戲也是人生,不斷戰勝每個磨難才是遊戲的樂趣,但是這個遊戲還沒有磨難,所以,當個創世主,給遊戲新增一點磨難吧!

2、遊戲難度設計

像這種無限的跑酷遊戲,普遍的做法是玩的時間越長速度越來越快、障礙物的距離越來越短。但是這些速度和距離都要有個限制,不然的話,就會出現必死結局,對玩家的體驗很不友好。

在本遊戲中,也是從速度和障礙物距離下手。為了方便控制這些引數,我給遊戲新增了一個配置,配置裡面寫上

開始時的速度、最大的速度、每次的加速度和障礙物最小的距離

這樣也方便後面除錯的時候,找到一個合適的速度和距離。

// lib/config.dart
class GameConfig{
  static double minSpeed = 6.5;
  static double maxSpeed = 13.0;
  static double acceleration = 0.001;
  static double obstacleMinDistance = 281;
}
...
複製程式碼

再給game類新增一個當前速度引數,在遊戲開始的時候,把這個引數設定為最小的速度。

// lib/game.dart
class MyGame...
  double currentSpeed;
  
  //with TapDetector這個類,可以給整個遊戲畫布一個點選事件,點選遊戲畫布開始
  void onTap(){
    if(!isPlay){
      isPlay = true;
      currentSpeed = GameConfig.minSpeed;
    }
  }

複製程式碼

重寫update方法,每一幀的時候給當前的速度加速,到最大速度就不加了

  @override
  void update(double t) {
    if(size == null)return;
    if(isPlay){
      if(currentSpeed <= GameConfig.maxSpeed){
        currentSpeed += GameConfig.acceleration;
      }
    }
  }
複製程式碼

game類改了,遊戲每個元件也需要隨著當前速度更新畫面,但是元件的update方法沒有速度這個引數,所以不需要這個方法了。

可以在元件類自定義一個方法,讓它接收之前update的t引數,和當前的速度。

栗子(Horizon地面類):

// lib/sprite/horizon.dart
class Horizon...
  @override
  void update(double t) {}

  void updateWithSpeed(double t, double speed){
    double x =  t * 50 * speed;
    ...之前update的程式碼
  }
複製程式碼

然後在game的update方法中,呼叫元件的updateWithSpeed,把當前速度傳進去

class MyGame...
  @override
  void update(double t) {
    if(size == null)return;
    if(isPlay){
      horizon.updateWithSpeed(t, currentSpeed);
      cloud.updateWithSpeed(t, currentSpeed);
      obstacle.updateWithSpeed(t, currentSpeed);
      dino.updateWithSpeed(t, currentSpeed);
      if(currentSpeed <= GameConfig.maxSpeed){
         currentSpeed += GameConfig.acceleration;
      }
    }
  }
複製程式碼

3、新增障礙物

老規矩,先測量障礙物在圖片中的位置,把它寫進config.dart裡面

flutter開發遊戲入門(仿谷歌瀏覽器小恐龍Chrome dino)三
配置類的程式碼就不放了,類名叫ObstacleConfig

寫好了之後,在lib/sprite目錄下建立obstacle.dart檔案,裡面寫障礙物元件類。

class Obstacle...
 ...
  void clear() {
    components.clear();
    lastComponent = null;
  }

  void updateWithSpeed(double t, double speed) {
    double x = t * 50 * speed;
    //釋放超出螢幕的
    for (final c in components) {
      final component = c as SpriteComponent;
      if (component.x + component.width < 0) {
        components.remove(component);
        continue;
      }
      component.x -= x;
    }
    //新增障礙
    if (lastComponent == null ||
        (lastComponent.x - lastComponent.width) < size.width) {
      //把遊戲分成3個難度
      final double difficulty = (GameConfig.maxSpeed - GameConfig.minSpeed) / 3;
      speed = speed - GameConfig.minSpeed;
      double distance;

      int obstacleIndex; //隨機建立障礙物

      if (speed <= difficulty) {
        //最小難度
        if (Random().nextInt(2) == 0) return; // 1/2機率不建立
        obstacleIndex = 2; //2種型別障礙物隨機建立
        //障礙物距離在最小障礙物距離到3個螢幕寬度之間隨機
        distance = getRandomNum(GameConfig.obstacleMinDistance, size.width * 3);
      } else if (speed <= difficulty * 2) {
        //普通難度
        if (Random().nextInt(20) == 0) return; // 1/20機率不建立
        obstacleIndex = 3;
        //障礙物距離在最小障礙物的距離到2個螢幕寬度之間隨機
        distance = getRandomNum(GameConfig.obstacleMinDistance, size.width * 2);
      } else {
        // 最難
        if (Random().nextInt(60) == 0) return; // 1/60機率不建立
        obstacleIndex = 5;
        //障礙物距離在最小障礙物的距離到1個螢幕寬度之間隨機
        distance = getRandomNum(GameConfig.obstacleMinDistance, size.width * 1);
      }

      double x = (lastComponent != null
              ? (lastComponent.x + lastComponent.width)
              : size.width) +
          distance;

      lastComponent = createComponent(x, obstacleIndex);
      add(lastComponent);
    }
  }

  SpriteComponent createComponent(double x, int obstacleIndex) {
    //隨機建立障礙物
    final int index = Random().nextInt(obstacleIndex);
    final Sprite sprite = Sprite.fromImage(spriteImage,
        width: ObstacleConfig.list[index].w,
        height: ObstacleConfig.list[index].h,
        y: ObstacleConfig.list[index].y,
        x: ObstacleConfig.list[index].x);
    SpriteComponent component = SpriteComponent.fromSprite(
        ObstacleConfig.list[index].w, ObstacleConfig.list[index].h, sprite);
    component.x = x + ObstacleConfig.list[index].w;
    component.y =
        size.height - (HorizonConfig.h + ObstacleConfig.list[index].h - 22);
    return component;
  }
  ...
複製程式碼

4、碰撞檢測

在這個遊戲中,所有的精靈都可以當作一個矩形

flutter開發遊戲入門(仿谷歌瀏覽器小恐龍Chrome dino)三
如果兩個矩形重疊,就代表它們碰撞了,在檢測的時候,數學上可以處理成比較中心點的座標在x和y方向上的距離和寬度的關係。

在程式碼中可以簡單點,只要判斷他們不重疊的情況就可以了。可以先判斷x軸

角色的右邊 <= 障礙物的左邊 || 障礙物的右邊 <= 角色的左邊

再判斷y軸

角色的底部 <= 障礙物的頭部 || 障礙物的底部 <= 角色的頭部

flutter開發遊戲入門(仿谷歌瀏覽器小恐龍Chrome dino)三
除了這些情況,其它的都是重疊了。

在flutter中,這些其實都不用我們處理,flutter提供了一個Rect類來表示一個矩形,它提供了overlaps方法來檢測重疊,我們要做的只是把一個元件轉為Rect例項就可以了。

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

final Rect rect1 = com1.toRect();
final Rect rect2 = com2.toRect();
rect2.overlaps(rect1) //返回true代表碰撞了
複製程式碼

是不是覺得很簡單,如果現在就執行的話,你會發現體驗很差,明明都沒有碰到,遊戲卻gameOver了。

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

flutter開發遊戲入門(仿谷歌瀏覽器小恐龍Chrome dino)三
看圖,把它們當成矩形來做判斷的話,碰撞的時候,透明區域也算碰上了。

要解決這個問題的方法有很多,最簡單的就是基於畫素來做判斷,先擷取他們相交地方的影像

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

轉為8位的byteList,在這個list中,只要他們相同的位置有顏色就代表他們碰撞了,換句話來說,就是大於0。

ps:8位的影像沒有透明度,或者說透明的地方也是黑色。如果你的圖片有黑色的部分要計算碰撞的話,可以轉為32位,判斷“ (val >> 24) > 0 ”。

5、新增碰撞方法

先建立一個碰撞幫助類(HitHelp)

typedef DebugCallBack = void Function(ui.Image img1, ui.Image img2);

class HitHelp {
  static checkHit(PositionComponent com1, PositionComponent com2,
      [DebugCallBack debugCallBack]) async {
    final Rect rect1 = com1.toRect();
    final Rect rect2 = com2.toRect();

    //邊碰到了, 判斷畫素是否碰到
    if (rect2.overlaps(rect1)) {
      //相交的矩形
      final Rect dst = Rect.fromLTRB(
          max(rect1.left, rect2.left),
          max(rect1.top, rect2.top),
          min(rect1.right, rect2.right),
          min(rect1.bottom, rect2.bottom));

      final ui.Image img1 = await getImg(com1, dst, rect1);
      final ui.Image img2 = await getImg(com2, dst, rect2);

      if (debugCallBack != null) {
        debugCallBack(img1, img2);
      }

      List<int> list1 = await imageToByteList(img1);
      List<int> list2 = await imageToByteList(img2);
      for (int i = 0; i < list1.length; i++) {
        //無色的畫素點是0
        if (list1[i] > 0 && list2[i] > 0) {
          return true;
        }
      }
    }
    return false;
  }

  static Future<ui.Image> getImg(
    PositionComponent component, Rect dst, Rect comDst) async {
    Sprite sprite;
    if (component is SpriteComponent) {
      sprite = component.sprite;
    } else if (component is AnimationComponent) {
      sprite = component.animation.getSprite();
    } else {
      return null;
    }
    //開啟畫布記錄儀
    final ui.PictureRecorder recorder = ui.PictureRecorder();
    Canvas canvas = Canvas(recorder);
    //根據元件的相交位置繪製圖片
    canvas.drawImageRect(
      sprite.image,
      Rect.fromLTWH(
        sprite.src.right - (comDst.right - dst.left),
        sprite.src.bottom - (comDst.bottom - dst.top),
        dst.width,
        dst.height),
      Rect.fromLTWH(
        0,
        0,
        dst.width,
        dst.height,
      ),
      Paint());
    //關閉記錄
    final ui.Picture picture = recorder.endRecording();
    return picture.toImage(dst.width.ceil(), dst.height.ceil());
  }

  static Future<Uint8List> imageToByteList(ui.Image img) async {
    ByteData byteData = await img.toByteData();
    return byteData.buffer.asUint8List();
  }
}
複製程式碼

然後給Obstacle一個檢測碰撞的方法

class Obstacle...
...
  Future<bool> hitTest(PositionComponent com1, DebugCallBack debugHit) async {
    int i = 0;
    for (final SpriteComponent com2 in components) {
      if (await HitHelp.checkHit(com1, com2, debugHit)) {
        return true;
      }
      //只檢查最前面的兩個
      i++;
      if (i >= 2) break;
    }
    return false;
  }
複製程式碼

最後game類呼叫Obstacle的碰撞方法檢測碰撞

class MyGame...
...
 @override
  void update(double t) async {
    if(size == null)return;
    if(isPlay){
      ...
      if(await obstacle.hitTest(dino.actualDino, this.debugHit)){
        dino.die();
        isPlay = false;
      }
    }
  }
複製程式碼

如果想要直觀的檢視碰撞區域的話,可以在回撥方法中新增兩個image元件顯示

  void debugHit(ui.Image img1, ui.Image img2){
    addWidgetOverlay('a1', Positioned(
      right: 100,
      top: 0,
      child: Container(
        width: 100,
        height: 100,
        color: Colors.blueGrey,
        child: RawImage(image: img1,fit: BoxFit.fill),
      ),
    ));

    addWidgetOverlay('a2', Positioned(
      right: 0,
      top: 0,
      child: Container(
        width: 100,
        height: 100,
        color: Colors.brown,
        child: RawImage(image: img2,fit: BoxFit.fill),
      ),
    ));
  }
複製程式碼

打包執行

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

6、結語

整個遊戲就這樣了,還有很多細節沒完善,懶得寫。 程式碼寫的也不是很好,感興趣的朋友可以下載原始碼來執行玩一下。

相關文章