本文中,我们继续讲解,怎么让蝌蚪军团,一个个的逼真的动起来的。

前面简单介绍了小蝌蚪这个类的基本变量,及蝌蚪实例化及绘制出来的方法,实现了基本绘制的功能。动起来实际很简单,无非就是每次执行run的时候,给当前蝌蚪实例移动到一个新的位置,然后重绘即可。那么问题是,这个新的位置,我们该如何确定。

最简单粗暴的方式,就是随便去一个随机向量,用该向量,去更新实例坐标。

updateVector () {
  this.vector = paper.Point.random()
}

updatePosition () {
  const newP = this.position.add(this.vector)
  this.position = newP.clone()
}

moveHead () {
  this.head.position = this.position
}
run (boids: Array<Boid>) {
  // this.flock(boids)
  this.updateVector()
  this.updatePosition()
  this.moveHead()
}

通过paper.Point.random()这个api,我们能够获取到一个坐标值在0-1之间的随机点。该点的坐标值随机、角度长度均随机。用改随机值,去更新蝌蚪坐标,效果如下:

但是这显然不是我们想要的,因为每一个点的运动,似乎都朝向了一个方向,显得很僵硬。原因在于,生成的随机值当然是不一样的,只是这个向量值够小,同时我们更新的时候,只是单纯的add相加的方式,导致了上述的结果。太low。我们想尽可能的,让蝌蚪的运动,逼真化。随机向量的值范围在0-1,我们给个[-1, 1]同时加大幅度10倍,效果如何呢?

updateVector () {
  // this.vector = this.acceleration
  this.vector = paper.Point.random().multiply(2).subtract(new paper.Point(1, 1))
  console.log('this.vector>>', this.vector)
}

updatePosition () {
  const newP = this.position.add(this.vector.multiply(10))
  this.position = newP.clone()
}

果然是活力倍增了很多,但总感觉有些不对劲。他们似乎总是在一块区域固定运动,说白了因为是随机值所以方向不固定。这里涉及到了方向和步数的拆分问题。

// 初始化阶段固定方向和步数    
    // 方向
    this.vector = paper.Point.random().multiply(2).subtract(new paper.Point(1, 1))
    // 步数
    this.acceleration = paper.Point.random().multiply(2)

  updatePosition () {
    const newP = this.position.add(this.vector.add(this.acceleration))
    this.position = newP.clone()
  }

初始化的时候,就把方向和不长固定写死,这样对于每一个蝌蚪而言,他们拥有各自的运动方向和运动速度。然后每次更新时,用这两个值去更新实例的位置即可。效果如下:

效果不错。但是运行了一会发现个问题,小蝌蚪都没了….原因很简单,视图区域的大小是确定了。但是画布空间的大小,理论上是无限的。为此,我们需要加一个简单的边缘重置。怎么实现?很简单,坐标取反。

updatePosition () {
  const newP = this.position.add(this.vector.add(this.acceleration))
  if (newP.x >= this.canvasWH.width) {
    newP.x = 0
  } else if (newP.x < 0) {
    newP.x = this.canvasWH.width
  }
  if (newP.y <= 0) {
    newP.y = this.canvasWH.heigth
  } else if (newP.y > this.canvasWH.heigth) {
    newP.y = 0
  }

  this.position = newP.clone()
}

效果如下:

完美实现了我们的需求。一旦越界,自动归位。

完成了基本逻辑后,回首我们的任务:“电子蝌蚪”。为了让他像个蝌蚪,我们需要把之前隐藏的颈部和尾巴释放出来。

this.path = new paper.Path({
  strokeColor: 'green',
  strokWidth: 2,
  strokeCap: 'round'
})
for (let i = 0; i < this.neckAmount; i++) {
  this.path.add(new paper.Point(0))
}
this.shortPath = new paper.Path({
  strokeColor: 'white',
  strokWidth: 4,
  strokeCap: 'round'
})
for (let i = 0; i < this.tailAmount; i++) {
  this.shortPath.add(new paper.Point(0))
}

颈部是长3宽4的直线,尾巴设计为长10宽2且round头部的直接。

每次触发run函数改变蝌蚪位置后,还需要重绘其颈部和尾巴:

createBoid () {
...
...
    this.path = new paper.Path({
      strokeColor: 'green',
      strokWidth: 2,
      strokeCap: 'round'
    })
    for (let i = 0; i < this.tailAmount; i++) {
      this.path.add(new paper.Point(0, 0))
    }
    this.shortPath = new paper.Path({
      strokeColor: 'white',
      strokeWidth: 5,
      strokeCap: 'round'
      // selected: true
    })
    for (let i = 0; i < this.neckAmount; i++) {
      this.shortPath.add(new paper.Point(0, 0))
    }
  } 

...
...
 updateNeckAndTail () {
    const segmentsNeck: Array<paper.Segment> = this.shortPath.segments
    const segmentsTail: Array<paper.Segment> = this.path.segments
    segmentsNeck[0].point = this.position.clone()
    segmentsTail[0].point = this.position.clone()
    for (let i = 1; i < this.neckAmount; i++) {
      const curP = this.position.subtract(this.vector.normalize(1).multiply(i * 5))
      segmentsNeck[i].point = curP
    }
    for (let i = 1; i < this.tailAmount; i++) {
      const curP = this.position.subtract(this.vector.normalize(1).multiply(i * 5))
      segmentsTail[i].point = curP
    }
    // this.shortPath.smooth()
  }

创建蝌蚪时,以path的实例作为颈部和尾巴,并埋上几个点坑。效果如下:

额…蝌蚪的颈部和尾巴有是有了,动也确实是动起来了,但是吧,跟“逼真”相去甚远。接下来,我们就需要充分的运用上帝公式(线性代数的知识)的力量,赋予他们活力