Flutter 模仿虎撲APP勳章效果(二)邊界碰撞

246 阅读2分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天

思路

現實中小球撞到牆壁或是掉到地上,一部分的能量會被吸收,剩餘的能量會使物體往反方向彈,於是球越彈越低直到停止。示意圖如下⬇️

ReboteImagen.gif

於是可以列出幾個重點:

  • 接觸:判斷小球與邊界位置
  • 反彈:碰撞後速度減少並相反
  • 停球:速度足夠小的時候停球

邊界碰撞

給出邊界

先設定好尺寸,把邊界的區域上色,用Stack疊在後方,比較方便觀察。

Size size = const Size(300, 400);

Stack(
  children: [
    SizedBox.fromSize(
        size: size, child: const ColoredBox(color: Colors.blue)),
    CustomPaint(
      size: size,
      painter: BallPainter(balls: balls, phoneXY: phoneXY),
    ),
  ]
)

接觸

回到Painer的paint裡,小球的圓心位置加上半徑,剛好接觸或是超過設定的邊界尺寸,即可視為是接觸碰撞。於是要針對四個邊分別判斷,當然你要是數學能力足夠強你可以利用三角函數統一判斷(有數學強的大佬來分享看看嗎😊)

還需要判斷速度的方向,不然小球反彈的可能會被卡在邊界無限反彈之類的。

@override
void paint(Canvas canvas, Size size) {
  for (var ball in balls) {
    ball.speedX -= phoneXY.value[0] / 20;
    ball.speedY += phoneXY.value[1] / 20;

    double dx = ball.speedX;
    double dy = ball.speedY;

    if (ball.position.dy >= size.height - ball.radius && ball.speedY > 0) {
        。
        。
    }
    if (ball.position.dy <= ball.radius && ball.speedY < 0) {
        。
        。
    }
    if (ball.position.dx >= size.width - ball.radius && ball.speedX > 0) {
        。
        。
    }

    if (ball.position.dx <= ball.radius && ball.speedX < 0) {
        。
        。
    }
    ball.position += Offset(dx, dy);

    _paint.color = ball.color;
    canvas.drawCircle(ball.position, ball.radius, _paint);
  }
}

反彈

以豎直方向下方的邊為例,竪直方向的速度speedY在碰撞之後速度會相反並且減弱,我這裡先假定每次碰撞速度會少兩成(可以自己調整決定要的效果。

if (ball.position.dy >= size.height - ball.radius && ball.speedY > 0) {
  //速度反彈
  ball.speedY = -ball.speedY * 0.8;
  //覆蓋原來的位移
  dy = ball.speedY;
}

停球

有了上述的反彈運動後,我們會發現小球下落後,會邊彈邊被削弱但是永遠不會停止,因為加速度計一直持續加上加速度,所以應該要給他一個停球的判斷,讓球⚽️休息一下。

可以試著print一下速度ball.speedY或ball.speedX,觀察小球在最小彈跳時其速度變化情況。我的手機在此時的值會在+-2內變化以此給停球判斷(不同平台或手機型號的值我不確定是否一致)。

if (ball.speedY.abs() < 2) dy = 0;

Code與效果

@override
void paint(Canvas canvas, Size size) {
  for (var ball in balls) {
    ball.speedX -= phoneXY.value[0] / 20;
    ball.speedY += phoneXY.value[1] / 20;

    double dx = ball.speedX;
    double dy = ball.speedY;

    if (ball.position.dy >= size.height - ball.radius && ball.speedY > 0) {
      ball.speedY = -ball.speedY * 0.8;
      dy = ball.speedY;
      if (ball.speedY.abs() < 2) dy = 0;
    }
    if (ball.position.dy <= ball.radius && ball.speedY < 0) {
      ball.speedY = -ball.speedY * 0.8;
      dy = ball.speedY;
      if (ball.speedY.abs() < 2) dy = 0;
    }
    if (ball.position.dx >= size.width - ball.radius && ball.speedX > 0) {
      ball.speedX = -ball.speedX * 0.8;
      dx = ball.speedX;
      if (ball.speedX.abs() < 2) dy = 0;
    }

    if (ball.position.dx <= ball.radius && ball.speedX < 0) {
      ball.speedX = -ball.speedX * 0.8;
      dx = ball.speedX;
      if (ball.speedX.abs() < 2) dy = 0;
    }
    ball.position += Offset(dx, dy);

    _paint.color = ball.color;
    canvas.drawCircle(ball.position, ball.radius, _paint);
  }
}
re.gif