主要内容
粒子类型
现在,我们将使用更高级的面向对象编程技术(如继承),因此你可能想要复习 JS 入门课程里的“继承” 然后再回来。别担心,我们会等你的!
感觉对于继承(inheritance)是如何运行的理解得还不错?很好,因为我们将使用继承来制作不同类型的
Particle
(粒子)子对象,这些子对象共享许多相同的功能,但在一些点上有所不同。让我们回顾一下简化的
Particle
实现:var Particle = function(position) {
this.acceleration = new PVector(0, 0.05);
this.velocity = new PVector(random(-1, 1), random(-1, 0));
this.position = position.get();
};
Particle.prototype.run = function() {
this.update();
this.display();
};
Particle.prototype.update = function(){
this.velocity.add(this.acceleration);
this.position.add(this.velocity);
};
Particle.prototype.display = function() {
fill(127, 127, 127);
ellipse(this.position.x, this.position.y, 12, 12);
};
接下来,我们基于
Particle
创建一个新的对象类型,我们将其称为 Confetti
。我们从接受相同数量的参数的构造函数开始,只需调用 Particle
构造函数,将它们传递下去:var Confetti = function(position) {
Particle.call(this, position);
};
现在,为了确保我们的
Confetti
对象与 Particle
对象拥有相同的方法,我们需要指定 Confetti 的原型(prototype)应该基于 Particle
原型:Confetti.prototype = Object.create(Particle.prototype);
Confetti.prototype.constructor = Confetti;
到目前为止,我们有和
Particle
对象以完全相同的方式运作的 Confetti
对象。继承的目的不是复制,而是制作新的对象,这些对象共享大量功能但也有所不同。那么,Confetti
对象有什么不同呢?嗯,只是从名字看,它应该看起来不太一样。我们的 Particle
对象是椭圆,但彩色纸屑(confetti)通常是小方片,因此,我们至少应该更改 display
方法,将它们显示为矩形:Confetti.prototype.display = function(){
rectMode(CENTER);
fill(0, 0, 255, this.timeToLive);
stroke(0, 0, 0, this.timeToLive);
strokeWeight(2);
rect(0, 0, 12, 12);
};
以下是一个有一个
Particle
对象实例和一个 Confetti
对象实例的程序。注意它们行动相似但看起来不同:添加旋转
让我们把它做得更复杂一点。假设我们希望让
Confetti
粒子在空中飞行时旋转。当然,我们可以像在振荡部分那样,模拟角速度和加速度。但是我们这次尝试一个又快又不严谨的解决方案。我们知道粒子的
x
位置在0和窗口的宽度之间。如果我们说:当粒子的 x
位置为 0时,其旋转应为 0;当它的 x
位置等于宽度时,它的旋转应该等于 TWO_PI
?听起来很熟悉?每当我们有一个值与一个范围,想要映射到另一个范围时,我们可以使用 ProcessingJS 的 map()
函数,以轻松地计算新值。var theta = map(this.position.x, 0, width, 0, TWO_PI);
为了让它旋转地更多,我们可以把角度的范围映射到0至
TWO_PI*2
。让我们看看如何将这段代码放入 display()
方法。Confetti.prototype.display = function(){
rectMode(CENTER);
fill(0, 0, 255);
stroke(0, 0, 0);
strokeWeight(2);
pushMatrix();
translate(this.position.x, this.position.y);
var theta = map(this.position.x, 0, width, 0, TWO_PI * 2);
rotate(theta);
rect(0, 0, 12, 12);
popMatrix();
};
下面是它看起来是什么样的——重新启动它几次,看看旋转的效果:
我们也可以把旋转角度建立在
y
位置上,会有一点不同的效果。为什么?因为粒子在 y
方向上有一个非零的恒定加速度(acceleration),这意味着 y
速度(velocity)是时间的线性函数,y
位置(position)实际上是时间的抛物线函数。你可以在下面的图表中看到这意味着什么(这是在上述程序的基础上生成的):这意味着如果我们以
y
位置为基准写confetti的旋转,那也是抛物线性的。这不会非常准确,因为真实的纸屑飘舞的运动非常复杂,你自己试试看它是有多真实吧!你可以想到其它能够运行得更加真实的方法吗?多样化的粒子系统(ParticleSystem)
我们真正想要做到的是创建许多
Particle
对象和许多 Confetti
对象。这就是我们制作 ParticleSystem
对象的目的,所以也许我们可以将其扩展到也追踪 Confetti
对象?下面是我们可以做到这一点的一种方法,复制我们对 Particle
对象的操作:var ParticleSystem = function(position) {
this.origin = position;
this.particles = [];
this.confettis = [];
};
ParticleSystem.prototype.addParticle = function() {
this.particles.push(new Particle(this.origin));
this.confettis.push(new Confetti(this.origin));
};
ParticleSystem.prototype.run = function(){
for (var i = this.particles.length-1; i >= 0; i--) {
var p = this.particles[i];
p.run();
}
for (var i = this.confettis.length-1; i >= 0; i--) {
var p = this.confettis[i]; p.run();
}
};
请注意,我们有两个单独的数组,一个用于粒子,另一个用于彩色纸屑。每次对粒子数组做一些事情,都要对彩色纸屑数组做同样的事情!这很烦人,因为这意味着我们必须编写两倍多的代码,如果改变一些东西,就必须在两个地方改变它。实际上我们可以避免这种重复,因为 JavaScript 中的数组可以存储不同类型的对象,而且因为我们的对象具有相同的接口——我们调用的是
run()
方法,这两种类型的对象都定义了该接口。因此,我们可以只存储单个数组,我们将随机决定要添加的粒子对象的类型,然后遍历单个数组。这是一个简单得多的更改——最终修改的只是 addParticle
方法:var ParticleSystem = function(position) {
this.origin = position;
this.particles = [];
};
ParticleSystem.prototype.addParticle = function() {
var r = random(1);
if (r < 0.5) {
this.particles.push(new Particle(this.origin));
} else {
this.particles.push(new Confetti(this.origin));
}
};
ParticleSystem.prototype.run = function(){
for (var i = this.particles.length-1; i >= 0; i--) {
var p = this.particles[i];
p.run();
if (p.isDead()) {
this.particles.splice(i, 1);
}
}
};
现在所有都合在一起!