flutter开发游戏入门(仿谷歌浏览器小恐龙Chrome dino)三

2,115 阅读6分钟

上一章链接

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里面

配置类的代码就不放了,类名叫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、碰撞检测

在这个游戏中,所有的精灵都可以当作一个矩形

如果两个矩形重叠,就代表它们碰撞了,在检测的时候,数学上可以处理成比较中心点的坐标在x和y方向上的距离和宽度的关系。

在代码中可以简单点,只要判断他们不重叠的情况就可以了。可以先判断x轴

角色的右边 <= 障碍物的左边 || 障碍物的右边 <= 角色的左边

再判断y轴

角色的底部 <= 障碍物的头部 || 障碍物的底部 <= 角色的头部

除了这些情况,其它的都是重叠了。

在flutter中,这些其实都不用我们处理,flutter提供了一个Rect类来表示一个矩形,它提供了overlaps方法来检测重叠,我们要做的只是把一个组件转为Rect实例就可以了。

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

是不是觉得很简单,如果现在就运行的话,你会发现体验很差,明明都没有碰到,游戏却gameOver了。

看图,把它们当成矩形来做判断的话,碰撞的时候,透明区域也算碰上了。

要解决这个问题的方法有很多,最简单的就是基于像素来做判断,先截取他们相交地方的图像

转为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),
      ),
    ));
  }

打包运行

6、结语

整个游戏就这样了,还有很多细节没完善,懒得写。 代码写的也不是很好,感兴趣的朋友可以下载源码来运行玩一下。