javaScript 气泡 class 示例使用 P5.js 库,代码在一段时间后崩溃。我该如何规避和克服这个问题?

javaScript bubble class example using P5.js lib, code crashes after a while. how can I circumvent and overcome this?

一直在关注 Dan Shiffman 的视频,尝试使用 类 重温我的面向对象编程。

我写了一些代码,在随机位置生成随机直径的气泡,使用 p5 的噪声函数让气泡运动。

我的目的是让气泡弹出 (使用 splice() 从数组中移除) 每当气泡到达 canvas 的边缘或当两个或多个气泡相交。

代码 运行 根据需要,但过了一会儿它崩溃并抛出错误“Uncaught TypeError: Cannot read 属性 'x' of undefined (sketch: line 15)”

我试过四处乱砍,但没有任何乐趣,如果有人能阐明为什么会发生此错误,或者对我的方法提出一般性建议,我将不胜感激。这是有问题的代码。

var balls = [];

function setup() {
  createCanvas(400, 400);
}

function draw() {
  background(220);

  for (var i = 0; i < balls.length; i++) {
    balls[i].showBall();
    balls[i].moveBall();

    for (var j = 0; j < balls.length; j++) {
      if (balls[i].x < 0 + balls[i].r ||
        balls[i].x > width - balls[i].r ||
        balls[i].y < 0 + balls[i].r ||
        balls[i].y > height - balls[i].r ||
        balls[i].life >= 220 ||
        i != j && balls[i].intersect(balls[j])) {
        balls.splice(i, 1);
      }
    }
  }
}

class Ball {
  constructor(x, y, r) {
    this.x = x;
    this.y = y;
    this.r = r;
    this.t = 0.0;
    this.t2 = 107.0;
    this.life = 0;
  }

  showBall() {
    noFill();
    stroke(this.life);
    strokeWeight(2);
    ellipse(this.x, this.y, this.r * 2);
  }

  moveBall() {
    this.x += map(noise(this.t), 0, 1, -1, 1) * 0.5;
    this.y += map(noise(this.t2), 0, 1, -1, 1) * 0.5;
    this.t += 0.02;
    this.life += 0.15;
  }
  intersect(other) {

    if (dist(this.x, this.y, other.x, other.y) < this.r + other.r) {
      return true;
    } else {
      return false;
    }
  }
}

function randBubbleGen() {
  let foo = floor(random(11));
  console.log(foo);

  if (foo >= 5) {
    let b = new Ball(random(41, 359),
      random(41, 359),
      random(5, 40));
    balls.push(b);
  }
}

setInterval(randBubbleGen, 1000);
<script src="https://cdn.jsdelivr.net/npm/p5@1.3.1/lib/p5.js"></script>

谢谢! P

第一部分问题是经典splice问题:

const arr = [..."abcde"];

for (let i = 0; i < arr.length; i++) {
  if (i === 2) { // some arbitrary condition
    arr.splice(i, 1);
  }
  else {
    console.log(i, arr[i]);
  }
}

这里发生了什么?拼接索引2处的元素"c"后,arr的长度变为4,但i++仍然发生,跳过一个元素"d",该元素从未被打印或访问过环形。解决办法是每次拼接操作后i--或者反向迭代,这样splice就不会导致未访问的元素被跳过

至于你的错误,问题是你的内循环遍历所有 j 拼接出一个元素,然后继续,就好像 balls[i] 没有被删除一样。根据上面的演示,我们知道在 balls.splice(i, 1) 之后, balls[i] 成为 下一个 元素之后的原始 i 的其余迭代外循环体。这是一个错误,因为拼接元素后的 i+1 会跳过一些碰撞,但不会导致错误,除非 i 恰好是 balls 数组中的最后一个元素。在这种情况下,balls[i+1] 是未定义的,您无法访问未定义的属性。

解决方法是在拼接出一个元素后break跳出内层j循环。这是在每次拼接调用后反向迭代或使用 i-- 以避免跳球的补充。


从时间复杂度的角度来看,splice 是一个糟糕的选择,因为它是 O(n)。如果球数组中有 n 次碰撞,则需要将其循环 n 次,导致更新代码中出现三重嵌套循环 运行。

一个更好的通用方法是创建一个在一轮中幸存下来的新元素数组,然后在该帧之后将该新数组重新分配给旧的 balls 数组。这涉及一些分配开销。


其他提示:

  • 尽可能使用 const 而不是 let
  • 对循环计数器和可变变量使用 let 而不是 var。理想情况下,never use var or let,尽管 p5 由于其 window 附加函数而提高了可变性。
  • 首选 forEachfor ... of 循环而不是经典的 C 风格 for 循环。
  • 你可以 return dist(this.x, this.y, other.x, other.y) < this.r + other.r 因为它已经是一个布尔值,不需要 if bool return true else return false 冗长。
  • 尽可能将渲染和位置更新分开。对于这个动画来说,这可能无关紧要,但随着事情变得越来越复杂,当某些东西消失但仍然为一帧渲染时,这可能会很奇怪,就像这里的情况一样。
  • 将碰撞检测和边缘检测移至外部函数 -- if (balls[i].x < 0 + balls[i].r ... 条件难以阅读。