【摸鱼研究所】 让你的屏幕下个雪呗 (后篇)

1,016 阅读4分钟

跟着一起摸鱼摸鱼之后你的屏幕上会变成这样的:

如果你喜欢这个效果并且还有别的想法,欢迎加入摸鱼研究所来提交你的摸鱼成果鸭~

接下来接前篇

1. 开始随机掉落

上一篇最后我们已经用模拟出了一把小球扔进场景中放飞自我的样子,但是我们的主题...是下雪,所以先把自由落体后弹起来的效果给删了,同时我们加入随机的概念:

  • 其实就是把之前我们设置的所有常数系数,全部变为随机数.

这个在动画中非常实用,(当然是xjb随机,有规律的随机只有物理砖家才做得出,对我这种高考物理好像都没及格的来说就算了...)

我们用到的常量有:

  • 纵向的加速度(重力加速度) ay
  • 横向的平均速度 vx
  • 小球的初始大小 r
  • 小球的大小变化加速度 ar

所以我们把这些常量定义到一起:

class SnowInfoUtils {

  // 单个雪花体积
  // static double get snowOriginSize => math.Random().nextInt(30).toDouble() ;
  static double get snowRadius => math.Random().nextInt(4).toDouble() ;

  // 秒级重力加速度 ay
  static double get ga => math.Random().nextInt(100) * math.Random().nextInt(1000).toDouble() / 1000 ;

  // 雪花融化加速度 ar
  static double get meltA => - math.Random().nextInt(100) / 1000000;

  // 秒级风力 vx
  static double get wa => math.Random().nextBool() ? math.Random().nextInt(150) / 1000 :  -math.Random().nextInt(150) / 1000;

  // 秒与单位时间转换,因为默认时间单位是微秒,但是计算时候用微秒是在太慢了
  static double get secondMultiply => 1000 ;

}

为了代码看上去没辣么乱,我们把纵向/横向/小球自己的体积 这三者的独立变化做成三个方法,改起来也方便:

class SnowMoving {

  // 纵向
  SnowBasicInfo freeFall(SnowBasicInfo info) {
    info.ay = info.isUp ? SnowInfoUtils.bouncedGa(info.upTimes) / (SnowInfoUtils.secondMultiply) : SnowInfoUtils.ga / SnowInfoUtils.secondMultiply;
    info.vy = info.vy + info.ay;
    if(info.vy > 0) {
      info.isUp = false;
    }
    info.y = info.y + info.vy;
    return info;
  }

  // 横向
  SnowBasicInfo windPush(SnowBasicInfo info) {
    info.ax = SnowInfoUtils.wa;
    info.vx = info.vx + info.ax;
    info.x  = info.vx + info.x;
    return info;
  }

  // 随着时间变化,雪花会渐渐变小
  SnowBasicInfo melt(SnowBasicInfo info) {
    info.ar = SnowInfoUtils.meltA;
    info.vr = info.vr + info.ar;
    info.r = math.max(0, info.r + info.vr);
    return info;
  }
}

同时因为我们去掉了雪花落到底后就会弹起来的效果,所以这时候当 y >= 屏幕高度的时候,我们把雪花定义成 invalid = true, 意思就是,你已经莫的了~

把这个操作同时移到雪花的model里去,

	class SnowBasicInfo {
	  double x, y, r;
	  double vx, vy, vr;
	  double ax, ay, ar;
	  bool invalid = false;
	  bool melted = false;
	
	  firstUp() {
	    vy = - vy;
	    invalid = true;
	    if(r <= 1) {
	      melted = true;
	    }
	  }
	
	  SnowBasicInfo({this.x = 0, this.y = 0, this.r = 10,
	    this.vx = 0,
	    this.vr = 0,
	    this.vy = 0, this.ax = 0, this.ay = 0, this.ar}) {
	    vx = SnowInfoUtils.wa;
	  }
	}

同时我们假设当 r <= 1,雪花就看不见了,即他真的融化了,先设定个 melted 的布尔以后用~

这样一来,整个动画场景的代码就会变的整洁了一点,最后我们把 painter 中绘制的部分整理下:

 @override
 void paint(Canvas canvas, Size size) {

    for(var snow in snows) {
      if(snow.invalid) {
        continue;
      }

      final falled = SnowMoving().freeFall(snow);
      if(falled.y >= size.height - 20) {
        falled.firstUp();
      }


      final pushed = SnowMoving().windPush(falled);
      if(pushed.x >= size.width || pushed.x < 0) {
        pushed.vx = - pushed.vx;
      }

      final melted = SnowMoving().melt(pushed);

      canvas.drawImageRect(snowImage,
          Rect.fromLTWH(0, 0, snowImage.width.toDouble(), snowImage.height.toDouble()),
          Rect.fromLTWH(melted.x, melted.y, snowImage.width.toDouble() * melted.r, snowImage.height.toDouble() * melted.r) ,
          pointPainter);
    }
 }
 

snowImage 是网上随便找的雪花素材,当然有心情的可以拿画笔自己画一个~

这里我们的顺序就是让雪花先处理下落,再横移,然后缩小融化,最后把它画出来,灰常简单的动作:

其实这时候我们已经差不多完成了整个场景的搭建了,如果你有兴趣你就可以给你身边的小姐姐们看看了,当然看完后她们会不会觉得你太闲就是另一回事了

2. 积雪呢

当然就像上海的雪,下是肯定会下的,积是肯定不可能的,但是我们还是希望能有点样子让他能积起来

由于作者理科实在是捉急,没法算出雪掉落后然后叠加在地上的公式,所以就换了个思路,如果你对这方面非常感兴趣,非常欢迎你的留言和PR

所以我们做一个假设,落在地上的雪花会变成一个圆角线段,线段的粗细 为h,长度也是h/2,

h = 雪花的r * 一个固定系数 * 落在地上的雪的数量 x

所以我们给雪花增加两个属性:

class SnowBasicInfo {
  ...
  
  Offset meltedStart;
  Offset meltedEnd;
  
  ...
}

用来记录在地上变成线段的初始/末尾的位置,并且由于飘扬的雪花的 meltedStart 一定是 null, 我们还可以用它来判断雪花是不是落地了:

假设固定系数为10

h = r * 10 * snows.where((element) => element.meltedStart != null).length

这样意思就是,地上雪越多,再掉到地上的雪变成的线就越粗,这样仿佛就能模拟出雪堆积的效果了,但是这真的只是个肉眼效果,细看问题还是不少的(可是这已经是作者能想出来最好的方案了)

 snow.r = h;
 snow.meltedStart = Offset(snow.x + h / 2, size.height - h / 2);
 snow.meltedEnd = Offset(snow.x + h, size.height - h / 2);
 

但是,这样的结果特别夸张:

嗯怕不是掉下来的是砖头哟,问题很简单,其实地上单片积雪的尺寸不能随着积雪数量而线性变大,雪越厚,下雪后积雪高度变化越小(别问我哪看来的,没怎么看过积雪的人猜还不行嘛),

所以我脑补了下向右开口的抛物线上半部分好像可以满足我们的需要:

类似这样,所以通过一些系数的尝试,我们最终把落在雪地上的雪数量 x 与下一片落在地上的雪的尺寸h的函数变成了:

h = r * 10 * pow((snows.where((element) => element.meltedStart != null).length / 20), 0.5)

所以最终我们的 paint 方法就是:

@override
  void paint(Canvas canvas, Size size) {

    for(var snow in snows) {
      if(snow.invalid) {
        if(snow.melted) {
          continue;
        } else {
        // 当invalid = true 并且雪还没融化,就说明他会积雪
          if(snow.meltedStart == null) {
            var multip = pow((snows.where((element) => element.meltedStart != null).length / 20), 0.5);
            var referredR = snow.r * 10 * multip;
            snow.r = referredR;
            snow.meltedStart = Offset(snow.x + referredR / 2, size.height - referredR / 2);
            snow.meltedEnd = Offset(snow.x + referredR, size.height - referredR / 2);
          }
          canvas.drawLine(snow.meltedStart,
              snow.meltedEnd, storedPainter..strokeWidth = snow.r);
          continue;
        }
      }

      final falled = SnowMoving().freeFall(snow);
      if(falled.y >= size.height - 20) {
        falled.firstUp();
      }


      final pushed = SnowMoving().windPush(falled);
      if(pushed.x >= size.width || pushed.x < 0) {
        pushed.vx = - pushed.vx;
      }

      final melted = SnowMoving().melt(pushed);

      canvas.drawImageRect(snowImage,
          Rect.fromLTWH(0, 0, snowImage.width.toDouble(), snowImage.height.toDouble()),
          Rect.fromLTWH(melted.x, melted.y, snowImage.width.toDouble() * melted.r, snowImage.height.toDouble() * melted.r) ,
          pointPainter);
    }
  }

这样,我们就能模拟出一个落雪+积雪的场景啦

摸更多鱼?

如果你对相关内容感兴趣呢,欢迎来一起摸鱼鸭: github.com/SpiciedCrab…