PVector
丹尼尔 · 希夫曼
编程运动最基本的组成部分是矢量。这就是我们开始的地方。现在,向量这个词可以意味着很多不同的东西。Vector 是 20世纪80年代初中期在加利福尼亚州萨克拉门托形成的新波浪岩带的名称。这是凯洛格加拿大公司生产的早餐麦片的名字。在流行病学领域,载体被用来描述从一个宿主向另一个宿主传播感染的生物体。在 C + + 编程语言中,向量 (std:: Vector) 是可动态调整大小的数组数据结构的实现。虽然都很有趣,但这些不是我们正在寻找的定义。相反,我们想要的是这个向量:
向量是描述空间中相对位置的值的集合。
向量: 你完成我
在我们讨论矢量本身之前,让我们看一个初学者处理示例,它演示了为什么首先我们应该关心它。如果你读过任何入门处理教科书,或者上过一门关于处理编程的课程 (希望你已经完成了其中的一件事来帮助你为这本书做准备),你可能,在某一点上,学会了如何写一个简单的弹跳球草图。
示例: 没有矢量的弹跳球
float x = 100;
float y = 100;
float xspeed = 1;
float yspeed = 3.3;
void setup() {
size(200,200);
smooth();
background(255);
}
void draw() {
noStroke();
fill(255,10);
rect(0,0,width,height);
//将当前速度添加到位置。
x = x + xspeed;
y = y + yspeed;
// 检查是否有弹跳
if ((x > width) || (x < 0)) {
xspeed = xspeed * -1;
}
if ((y > height) || (y < 0)) {
yspeed = yspeed * -1;
}
// 显示在 x,y 位置
stroke(0);
fill(175);
ellipse(x,y,16,16);
}
在上面的例子中,我们有一个非常简单的世界 -- 一个圆形的空白画布 (“球”) 四处走动。这个 “球” 有一些特性。
- 位置: x 和 y
- 速度: xspeed 和 yspeed
在一个更高级的草图中,我们可以想象这个球和世界有更多的属性:
- 加速度: 加速和加速
- 目标位置: xtarget 和 ytarget
- 风: xwind 和 ywind
- 摩擦: 摩擦和摩擦
越来越清楚的是,对于这个世界上的每一个单一概念 (风、位置、加速度等),我们需要两个变量。这只是一个二维世界,在一个 3D 世界中,我们需要 x,y,z,xspeed,yspeed,zspeed 等。本章的第一个目标是学习使用向量背后的基本概念,并重写这个弹跳球示例。毕竟,如果我们可以像下面这样简单地编写代码,那不是很好吗?
而不是:
float x;
float y;
float xspeed;
float yspeed;
拥有它不是很好吗……
Vector location;
Vector speed;
向量不会让我们做任何新的事情。然而,使用矢量不会突然让你的处理草图神奇地模拟物理,它们将简化您的代码,并为在编程运动时反复发生的常见数学运算提供一组函数。
作为向量的介绍,我们将在二维中生活相当长的一段时间 (至少在我们读完前几章之前)。)所有这些例子都可以很容易地扩展到三维 (我们将使用的类 -- PVector -- 允许三维。)然而,就目前而言,从两个开始更容易。
向量: 它们对我们来说是什么,处理程序员?
从技术上讲,向量的定义是两点之间的区别。考虑一下如何提供从一个点走到另一个点的指示。
以下是一些矢量和可能的翻译:
你可能以前在编程运动时做过。对于每一帧动画 (即通过处理的 draw() 循环,您可以指示屏幕上的每个对象水平移动一定数量的像素和一定数量的像素 (垂直)。
对于一个处理程序员来说,我们现在可以理解一个向量作为从 a点到 B 点移动形状的指令,可以说是一个物体的 “像素速度”。
对于每个帧:
location = location + velocity
如果速度是一个向量 (两点之间的差),什么是位置?它也是矢量吗?从技术上讲,人们可能会认为位置不是向量,它不是描述两点之间的变化,它只是描述空间中的一个奇异点 -- 一个位置。所以从概念上来说,我们认为一个位置是不同的: 一个单一的点,而不是两个点之间的区别。
然而,另一种描述位置的方法是从原点到达该位置的路径。因此,位置可以表示为矢量,给出位置和原点之间的差异。因此,如果我们要编写代码来描述一个向量对象,而不是创建单独的点和向量类,我们可以使用一个更方便的类。
让我们检查位置和速度的基础数据。在弹跳球示例中,我们有以下内容:
location --> x,y
velocity --> xspeed,yspeed
请注意我们是如何为两个浮点数 (一个 x 和一个 y) 存储相同的数据的。如果我们自己写一个向量类,我们会从一些相当基本的东西开始:
class PVector {
float x;
float y;
PVector(float x_, float y_) {
x = x_;
y = y_;
}
}
在其核心,PVector 只是存储两个值 (或三个,我们将在 3D 示例中看到) 的一种方便的方式。
所以这个。
float x = 100;
float y = 100;
float xspeed = 1;
float yspeed = 3.3;
上面代码体换成下面向量
PVector location = new PVector(100,100);
PVector velocity = new PVector(1,3.3);
现在我们有了两个矢量对象 (“位置” 和 “速度”),我们准备实现运动算法 -- 位置 = 位置 + 速度。在弹跳球的例子中,没有矢量,我们有:
// 将当前速度添加到位置。
x = x + xspeed;
y = y + yspeed;
在一个理想的世界里,我们只能将上面的内容改写为:
//将当前速度矢量添加到位置矢量。
location = location + velocity;
然而,在处理中,加法运算符 '+' 仅保留给原始值 (整数、浮点数等)。)处理不知道如何一起添加两个 PVector 对象,就像它知道如何添加两个 PFont 对象或 PImage 对象一样。对我们来说幸运的是,PVector 类是用普通数学运算的函数实现的。
向量: 加法
在我们继续查看 PVector 类及其 add() 方法之前 (纯粹是为了学习,因为它已经在处理本身中为我们实现了),让我们使用数学/物理教科书中的符号来检查矢量加法。
矢量通常写成黑体字类型或顶部带有箭头。出于本教程的目的,为了区分向量和标量 (标量指的是单个值,如整数或浮点),我们将使用黑体字类型:
向量(矢量): v
标量: x
假设我有以下两个向量:
u = (5,2)
v = (3,4)
每个向量有两个分量,一个 x 和一个 y。为了将两个向量加在一起,我们只需将 x 和 y 都加在一起。换句话说:
w = u + v
翻译为:
wx = ux + vx
wy = uy + vy
因此:
wx = 5 + 3
wy = 2 + 4
因此:
w = (8,6)
现在我们已经了解了如何将两个向量加在一起,我们可以看看加法是如何在 PVector 类本身中实现的。让我们编写一个名为 add() 的函数,该函数将另一个 PVector 对象作为其参数。
class PVector {
float x;
float y;
PVector(float x_, float y_) {
x = x_;
y = y_;
}
//新!向此 PVector 添加另一个 PVector 的函数。
//只需将 x 分量和 y 分量添加在一起。
void add(PVector v) {
x = x + v.x;
y = y + v.y;
}
}
现在我们看到了 add 是如何在 PVector 内部编写的,我们可以用我们的弹跳球示例返回到位置 + 速度算法,并实现 vector 加法:
//将当前速度添加到位置。
location = location + velocity; //这是过去的加法
location.add(velocity); //这是向量加法
在这里,我们已经准备好成功完成我们的第一个目标 -- 使用 PVector 重写整个弹跳球示例。
示例: 用 PVector 弹跳球!
//我们现在只有两个 PVector 变量,而不是一堆浮点数。
PVector location;
PVector velocity;
void setup() {
size(200,200);
smooth();
background(255);
location = new PVector(100,100);
velocity = new PVector(2.5,5);
}
void draw() {
noStroke();
fill(255,10);
rect(0,0,width,height);
// 将当前速度添加到位置。
location.add(velocity);
//我们有时仍然需要参考 PVector 的单个组件
//并且可以使用点语法 (位置.x 、速度.y 等)。)
if ((location.x > width) || (location.x < 0)) {
velocity.x = velocity.x * -1;
}
if ((location.y > height) || (location.y < 0)) {
velocity.y = velocity.y * -1;
}
//在 x 位置显示圆
stroke(0);
fill(175);
ellipse(location.x,location.y,16,16);
}
现在,你可能会感到有些失望。毕竟,这可能最初似乎使代码比原始版本更复杂。虽然这是一个完全合理和有效的批评,但重要的是要理解我们还没有完全意识到用向量编程的力量。看着一个简单的弹跳球,只实现向量加法只是第一步。随着我们继续研究由多个对象和多个力组成的更复杂的世界 (我们将在下一章中介绍力),PVector 的好处将变得更加明显。
然而,我们应该注意上述向矢量编程过渡的一个重要方面。尽管我们使用 PVector 对象来描述两个值 -- 位置的 x 和 y 以及速度的 x 和 y -- 我们仍然经常需要引用每个值的 x 和 y 分量 PVector 单独。当我们在处理中绘制对象时,我们无法说:
ellipse(location,16,16);
椭圆 () 函数不允许将 PVector 作为参数。只能使用两个标量值 (x 坐标和 y 坐标) 绘制椭圆。因此,我们必须深入 PVector 对象,并使用面向对象的点语法提取 x 和 y 组件。
ellipse(location.x,location.y,16,16);
当需要测试圆是否已经到达窗口边缘时,也会出现同样的问题,我们需要访问两个向量的各个组成部分: 位置和速度。
if ((location.x > width) || (location.x < 0)) {
velocity.x = velocity.x * -1;
}
向量: 更多代数
添加实际上只是第一步。当在屏幕上编程物体的运动时,有一长串与矢量一起使用的常见数学运算。以下是 PVector 类中作为函数可用的所有数学运算的综合列表。然后我们现在将讨论一些关键的问题。随着我们的例子越来越复杂,我们将继续揭示这些函数的细节。
- add() — 添加向量
- sub() — 减去向量
- mult() —用乘法缩放向量
- div() —用除法缩放向量
- mag() — 计算向量的大小
- normalize() — 将向量标准化为单位长度 1
- limit() — 限制向量的大小
- heading() — 以角度表示的矢量的标题
- dist() — 两个向量之间的欧氏距离 (视为点)
- angleBetween() —找到两个向量之间的角度
- dot() — 两个向量的点积
- cross() — 两个向量的交叉乘积
已经完成了加法,让我们从减法开始。这个还不错,只需从加法中取加号并用减号替换即可!
矢量减法: w = u - v
翻译为:
wx = ux - vx
wy = uy - vy
因此,PVector 内部的函数看起来像:
void sub(PVector v) {
x = x - v.x;
y = y - v.y;
}
下面是一个示例,通过取两点之间的差值 (鼠标位置和窗口中心) 来演示矢量减法。
示例: 矢量减法
void setup() {
size(200,200);
smooth();
}
void draw() {
background(255);
// 两个 pvector,一个用于鼠标位置,一个用于窗口中心。
PVector mouse = new PVector(mouseX,mouseY);
PVector center = new PVector(width/2,height/2);
// 矢量减法!
mouse.sub(center);
// 画一条线来表示向量。
translate(width/2,height/2);
line(0,0,mouse.x,mouse.y);
}
向量加法和减法都遵循与实数相同的代数规则。
交换规则: u + v = v + u
关联规则: u + (v + w) = (u + v) + w
除了花哨的术语和符号,这确实是一个非常简单的概念。我们只是说加法的常识性质也适用于向量。
3 + 2 = 2 + 3
(3 + 2) + 1 = 3 + (2 + 1)
转到乘法,我们必须稍微不同地思考。当我们谈论相乘向量时,我们通常的意思是缩放向量。也许我们希望一个向量的大小是它的两倍或 3分之1,等等。在这种情况下,我们说的是 “将一个向量乘以 2” 或 “将一个向量乘以 1/3”。请注意,我们将向量乘以标量,一个数字,而不是另一个向量
为了将向量按单个数字缩放,我们将每个分量 (x 和 y) 乘以该数字。
矢量乘法:
w = v * n
翻译为:
wx = vx * n
wy = vy * n
让我们看一个向量表示法的例子。
u = (-3,7)
n = 3
w = u * n
wx = -3 * 3
wy = 7 * 3
w = (-9, 21)
因此,PVector 类中的函数编写为:
void mult(float n) {
// 使用乘法,向量的所有分量都乘以一个数字。
x = x * n;
y = y * n;
}
在代码中实现乘法就像:
PVector u = new PVector(-3,7);
u.mult(3); // 这个 PVector 现在是大小的三倍,等于 (-9,21)。
示例: 向量乘法
void setup() {
size(200,200);
smooth();
}
void draw() {
background(255);
PVector mouse = new PVector(mouseX,mouseY);
PVector center = new PVector(width/2,height/2);
mouse.sub(center);
// PVector 乘法!
//向量现在是其原始大小的一半 (乘以 0.5)。
mouse.mult(0.5);
translate(width/2,height/2);
line(0,0,mouse.x,mouse.y);
}
除法与乘法完全相同,当然只使用除法而不是乘法。
void div(float n) {
x = x / n;
y = y / n;
}
PVector u = new PVector(8,-4);
u.div(2);
与加法一样,乘法和除法的基本代数规则适用于向量。
关联规则: (n*m)*v = n*(m*v)
分配规则, 2 scalars, 1 vector: (n + m)*v = n*v + m*v
分配规则, 2 vectors, 1 scalar : (u +v)*n = n*u + n*v
向量: 幅度
正如我们刚才看到的,乘法和除法是一种方法,通过这种方法可以在不影响方向的情况下改变向量的长度。所以,也许你想知道: “好吧,我怎么知道向量的长度是多少?“ 我知道组件 (x 和 y),但我不知道实际箭头本身有多长 (以像素为单位)?!
向量的长度或 “大小” 通常写成: | | v | |
理解如何计算长度 (从这里开始称为数量级) 是非常有用和重要的。
请注意,在上图中,当我们绘制一个矢量作为箭头和两个分量 (x 和 y) 时,我们最终会得到一个直角三角形。侧面是组件,斜边是箭头本身。我们很幸运有了这个直角三角形,因为从前,一个名叫毕达哥拉斯的希腊数学家开发了一个很好的公式来描述直角三角形的边与斜边之间的关系。
勾股定理: a 平方加 b 平方等于 c 平方。
有了这个可爱的公式,我们现在可以计算如下的大小:
||v|| = sqrt(vx*vx + vy*vy)
或者在 PVector 中:
float mag() {
return sqrt(x*x + y*y);
}
示例: 矢量幅度
void setup() {
size(200,200);
smooth();
}
void draw() {
background(255);
PVector mouse = new PVector(mouseX,mouseY);
PVector center = new PVector(width/2,height/2);
mouse.sub(center);
//量级 (即矢量的长度) 可以通过 mag() 函数访问。
//这里它被用作窗口顶部绘制的矩形的宽度。
float m = mouse.mag();
fill(0);
rect(0,0,m,10);
translate(width/2,height/2);
line(0,0,mouse.x,mouse.y);
}
向量: 归一化(向量单元化)
计算向量的大小只是开始。大小函数打开了许多可能性的大门,其中第一个是正常化。正常化指的是使某些东西 “标准” 或 “正常” 的过程。就向量而言,让我们暂时假设一个标准向量的长度为 1。因此,标准化一个向量就是取一个任意长度的向量,并保持它指向同一个方向,将它的长度改为 1,将其转化为所谓的单位向量。
能够快速访问单位向量是有用的,因为它描述了向量的方向,而不考虑长度。对于任何给定的向量 u,其单位向量 (写成 û) 计算如下:
û = u / ||u||
换句话说,要使向量正常化,只需将每个分量除以其大小。这非常直观。假设向量的长度为 5。嗯,5 除以 5 就是 1。因此,看看我们的直角三角形,我们需要通过除以 5 来缩小斜边。因此,在这个过程中,边收缩,也除以 5。
因此,在 PVector 类中,我们按如下方式编写规范化函数:
void normalize() {
float m = mag();
div(m);
}
当然,有一个小问题。如果向量的大小为零呢?我们不能除以零!一些快速错误检查将解决这个问题:
void normalize() {
float m = mag();
if (m != 0) {
div(m);
}
}
示例: 归一化向量
void setup() {
size(200,200);
smooth();
}
void draw() {
background(255);
PVector mouse = new PVector(mouseX,mouseY);
PVector center = new PVector(width/2,height/2);
mouse.sub(center);
//在这个例子中,在向量被归一化后,它是
//乘以 50,以便可以在屏幕上查看。
//请注意,无论鼠标在哪里,向量都会
//由于标准化过程,具有相同的长度 (50)。
mouse.normalize();
mouse.mult(50);
translate(width/2,height/2);
line(0,0,mouse.x,mouse.y);
}
矢量: 运动
我们为什么要关心?是的,所有这些矢量数学的东西听起来像是我们应该知道的,但是为什么呢?它实际上将如何帮助我编写代码?事实是我们需要一些耐心。使用 PVector 类的令人敬畏之处将需要一些时间来充分揭示。当第一次学习新的数据结构时,这实际上很常见。例如,当你第一次了解一个数组时,使用一个数组似乎比仅仅有几个变量来谈论多个事情要多得多。但是当你需要一百件、一千件或 10,000 件东西时,这很快就会崩溃。PVector 也是如此。现在看起来更多的工作将会在以后得到回报,并且会得到很好的回报。
然而,目前,我们希望专注于简单性。用矢量编程运动意味着什么?我们已经在本书的第一个例子中看到了这一点的开始: 弹跳球。屏幕上的对象有一个位置 (它在任何给定时刻的位置) 和一个速度 (它应该如何从一个时刻移动到下一个时刻的说明)。速度被添加到位置:
location.add(velocity);
然后我们在该位置绘制对象:
ellipse(location.x,location.y,16,16);
这是议案 101。
- 将速度添加到位置
- 在位置绘制对象
在弹跳球示例中,所有这些代码都发生在处理的主选项卡中,在 setup() 和 draw() 中。我们现在要做的是向封装类内部所有运动逻辑的方向发展,这样我们就可以为在处理中编程运动对象创建一个基础。现在,我们将花一点时间来回顾一下面向对象编程的基础知识,但是这本书将假设使用对象的知识 (从这一点开始,这对于几乎每一个例子都是必要的)。但是,如果您需要进一步复习,我鼓励您查看 OOP 教程。
面向对象编程背后的驱动原则是将数据和功能结合在一起。以典型的 OOP 为例: 一辆汽车。汽车有数据 -- 颜色、尺寸、速度等。汽车具有功能 -- 驱动 () 、转向 () 、停止 () 等。一个 car 类将所有这些东西放在一个 car 实例的模板中,即对象,被制作。好处是组织良好的代码,当你阅读它时,它是有意义的。
Car c = new Car(red,big,fast);
c.drive();
c.turn();
c.stop();
在我们的例子中,我们将创建一个通用的 “移动器” 类,一个描述在屏幕上移动的形状的类。因此,我们必须考虑以下两个问题:
1.移动器有什么数据?
2.移动器有什么功能?
我们的 “运动 101” 算法告诉我们这些问题的答案。一个对象的数据是它的位置和速度,两个 PVector 对象。
class Mover {
PVector location;
PVector velocity;
//它的功能就这么简单。它需要移动,需要被看到。我们将把这些作为名为 update() 和 display() 的函数来实现。
//Update () 是我们放置所有运动逻辑代码的地方,而 display() 是我们绘制对象的地方。
void update() {
location.add(velocity);
}
void display() {
stroke(0);
fill(175);
ellipse(location.x,location.y,16,16);
}
}
然而,我们忘记了一个关键的项目,即对象的构造函数。构造函数是类内部的一个特殊函数,它创建对象本身的实例。在这里你可以给出如何设置对象的说明。它总是与类同名,并通过调用新运算符来调用:
“CarmyCar = new Car(); ".
在我们的例子中,让我们通过给它一个随机位置和一个随机速度来初始化我们的移动对象。
Mover() {
location = new PVector(random(width),random(height));
velocity = new PVector(random(-2,2),random(-2,2));
}
让我们通过合并一个函数来结束 Mover 类,以确定当对象到达窗口边缘时应该做什么。现在让我们做一些简单的事情,把它包裹在边缘。
void checkEdges() {
if (location.x > width) {
location.x = 0;
} else if (location.x < 0) {
location.x = width;
}
if (location.y > height) {
location.y = 0;
} else if (location.y < 0) {
location.y = height;
}
}
现在 Mover 类已经完成,我们可以看看我们需要在主程序中做什么。我们首先声明一个移动对象:
Mover mover;
然后在设置 () 中初始化移动器:
mover = new Mover();
并在 draw() 中调用适当的函数:
mover.update();
mover.checkEdges();
mover.display();
以下是整个示例供参考:
示例: 运动 101 (速度)
// 声明移动对象
Mover mover;
void setup() {
size(200,200);
smooth();
background(255);
// 使移动对象
mover = new Mover();
}
void draw() {
noStroke();
fill(255,10);
rect(0,0,width,height);
// 在移动对象上调用函数。
mover.update();
mover.checkEdges();
mover.display();
}
class Mover {
// 我们的对象有两个 pvector: 位置和速度
PVector location;
PVector velocity;
Mover() {
location = new PVector(random(width),random(height));
velocity = new PVector(random(-2,2),random(-2,2));
}
void update() {
// 运动 101: 位置随速度变化。
location.add(velocity);
}
void display() {
stroke(0);
fill(175);
ellipse(location.x,location.y,16,16);
}
void checkEdges() {
if (location.x > width) {
location.x = 0;
} else if (location.x < 0) {
location.x = width;
}
if (location.y > height) {
location.y = 0;
} else if (location.y < 0) {
location.y = height;
}
}
}
好的,在这一点上,我们应该对两件事感到满意 -- (1) 什么是 PVector?(2) 我们如何在物体内部使用 PVectors 来跟踪它的位置和运动?这是一个极好的第一步,值得热烈的掌声。然而,对于起立鼓掌和尖叫的粉丝,我们需要再向前迈出一步,稍微大一点。毕竟,看运动 101 的例子是相当无聊的 -- 圆圈从不加速,从不减速,也从不转动。对于更有趣的运动,对于出现在我们周围的真实世界中的运动,我们需要在我们的类中添加一个 PVector -- 加速。
我们在这里使用的加速度的严格定义是: 速度的变化率。让我们考虑一下这个定义。这是一个新概念吗?不完全是。速度定义为: 位置的变化率。本质上,我们正在开发一种 “涓滴” 效应。加速度会影响速度,而速度又会影响位置 (对于一些简短的铺垫,当我们看到力如何影响加速度时,这一点将在下一章变得更加重要,加速度会影响速度,而速度会影响位置。)在代码中,这看起来像这样:
velocity.add(acceleration);
location.add(velocity);
作为练习,从这一点开始,让我们为自己制定一个规则。让我们在不接触速度和位置值的情况下编写本书其余部分中的每个示例 (除了初始化它们)。换句话说,我们现在编程运动的目标如下 -- 想出一个算法来计算加速度,让涓滴效应发挥其魔力。所以我们需要想出一些方法来计算加速度:
加速算法!
- 构成恒定加速度
- 完全随机的加速度
- 佩尔林噪声加速度
- 向鼠标加速
第一,虽然不是特别有趣,但是最简单的,并将帮助我们开始将加速纳入我们的代码。我们需要做的第一件事是在 Mover 类中添加另一个 PVector:
class Mover {
PVector location;
PVector velocity;
PVector acceleration; // 一个新的加速度矢量。
//并将加速合并到 update() 函数中:
void update() {
// 我们的运动算法现在是两行代码!
velocity.add(acceleration);
location.add(velocity);
}
我们快结束了。唯一缺少的部分是构造函数中的初始化。
Mover() {
//让我们在窗口中间启动 mover 对象。
location = new PVector(width/2,height/2);
//初始速度为零。
velocity = new PVector(0,0);
//这意味着当草图开始时,对象处于静止状态。我们不必再担心速度,
//因为我们完全用加速度控制物体的运动。说到这里,根据 “算法 #1”,
//我们的第一个草图涉及恒定加速度。所以让我们选择一个值。
acceleration = new PVector(-0.001,0.01);
}
你在想 -- “天哪,那些价值看起来非常小!” 是的,没错,它们非常小。重要的是要意识到我们的加速度值 (以像素为单位) 随着时间的推移累积到速度中,大约每秒 30 次,这取决于我们草图的帧速率。因此,为了将速度矢量的大小保持在合理的范围内,我们的加速度值应该保持相当小。我们也可以通过合并 PVector 函数 limit() 来帮助这个原因。
// Limit () 函数约束向量的大小。
velocity.limit(10);
这转化为以下内容:
速度的大小是多少?如果少于 10,不用担心,不管它是什么。但是,如果超过 10,请将其缩小到 10!
现在让我们来看看 Mover 类的更改,包括加速和限制 ()。
示例: 运动 101 (速度和恒定加速度)
class Mover {
PVector location;
PVector velocity;
//加速是关键!
PVector acceleration;
// 变量 topspeed 将限制速度的大小。
float topspeed;
Mover() {
location = new PVector(width/2,height/2);
velocity = new PVector(0,0);
acceleration = new PVector(-0.001,0.01);
topspeed = 10;
}
void update() {
// 速度因加速度而变化,受速度限制。
velocity.add(acceleration);
velocity.limit(topspeed);
location.add(velocity);
}
// display() 是一样的
// checkEdges() 是一样的
}
好的,算法 #2 -- “完全随机的加速度。”在这种情况下,我们希望在每个周期中选择一个新的加速度,而不是在对象的构造函数中初始化加速度,即每次调用 update()。
示例: 运动 101 (速度和随机加速度)
void update() {
acceleration = new PVector(random(-1,1),random(-1,1));
acceleration.normalize();
velocity.add(acceleration);
velocity.limit(topspeed);
location.add(velocity);
}
虽然标准化加速度不是完全必要的,但它确实被证明是有用的,因为它标准化了矢量的大小,允许我们尝试不同的东西,例如:
(A) 将加速度缩放为常数值
acceleration = new PVector(random(-1,1),random(-1,1));
acceleration.normalize();
acceleration.mult(0.5);
(B) 将加速度缩放为随机值
acceleration = new PVector(random(-1,1),random(-1,1));
acceleration.normalize();
acceleration.mult(random(2));
虽然这似乎是一个显而易见的点,但理解加速度不仅仅是指运动物体的加速或减速,而是指速度的任何变化是至关重要的,大小或方向。加速度是用来操纵一个物体的,它是学习编程一个决定如何在屏幕上移动的物体的基础。
向量: 静态与非静态
在我们开始加速算法 #4 (向鼠标加速) 之前,我们需要涵盖使用向量和 PVector 类的另一个相当重要的方面,使用静态方法和非静态方法的区别。
暂时忘记矢量,看看下面的代码:
float x = 0;
float y = 5;
x = x + y;
很简单,对吗?X 的值为 0,我们将 y 添加到它,现在 x 等于 5。根据我们对 PVector 的了解,我们可以很容易地编写相应的代码。
PVector v = new PVector(0,0);
PVector u = new PVector(4,5);
v.add(u);
向量 v 的值为 (0,0),我们将 u 相加,现在 v 等于 (4,5)。放松,对吗?
好的,让我们看看一些简单的浮点数学的另一个例子:
float x = 0;
float y = 5;
float z = x + y;
X 的值为 0,我们将 y 添加到它,并将结果存储在一个新的变量 z 中。在这个例子中 x 的值没有改变 (y 也没有改变)!这似乎是一个微不足道的点,当涉及到浮点数的数学运算时,这是一个非常直观的点。然而,当涉及到 PVector 的数学运算时,这并不明显。让我们试着根据我们目前所知道的编写代码。
PVector v = new PVector(0,0);
PVector u = new PVector(4,5);
PVector w = v.add(u); // 不要被愚弄,这是不正确的!
//以上似乎是一个很好的猜测,但这不是 PVector 类的工作方式。如果我们看一下 add() 的定义。
void add(PVector v) {
x = x + v.x;
y = y + v.y;
}
.我们看到它没有实现我们的目标。第一,它不返回新的 PVector,第二,它改变了调用它的 PVector 的值。为了将两个 PVector 对象添加到一起,并将结果作为新的 PVector 返回,我们必须使用静态 add() 函数。
我们从类名本身 (而不是从特定对象实例) 调用的函数称为静态函数。
//假设两个 PVector 对象: v 和 u
// Static: 从类名调用。
PVector.add(v,u);
//非静态: 从对象实例调用。
v.add(u);
由于你不能自己在处理中编写静态函数,这是你以前可能没有遇到过的事情。在 PVector 的情况下,它允许我们对 PVector 对象进行一般的数学运算,而无需调整输入 PVector 的值。让我们看看如何编写 add 的静态版本:
static PVector add(PVector v1, PVector v2) {
PVector v3 = new PVector(v1.x + v2.x, v1.y + v2.y);
return v3;
}
这里有两个关键区别:
- 该函数被标记为 static。
- 该函数创建一个新的 PVector (v3),并返回在该新的 PVector 中添加 v1 和 v2 组件的结果。
当您调用静态函数时,您只需引用类本身的名称,而不是引用实际的对象实例。
PVector v = new PVector(0,0);
PVector u = new PVector(4,5);
PVector w = v.add(u); //不是采用这种方式
//Add 的静态版本允许我们添加两个 PVectors
//一起并将结果分配给一个新的 PVector,同时
//保持原始 pvector (v 和 u) 完好无损。
PVector w = PVector.add(v,u);
PVector 类具有静态版本的 add(), sub(), mult(), div().
矢量: 链接
上面讨论中遗漏的一个细节是,上面讨论的所有方法都返回 PVector 类型的对象。关键区别在于静态版本返回一个新的 PVector 对象,而不是作用于现有的 PVector 对象。非静态版本返回对现有版本的引用。虽然此功能通常不用于大多数处理示例,但它允许在一行代码中调用方法。这被称为链接。例如,假设你想添加到一个 PVector,然后乘以 2。
PVector a = new PVector(0, 0);
// 将 (5,3) 添加到
a.add(5, 3);
// A 乘以 2
a.mult(2);
//通过链接,上面可以写成:
PVector a = new PVector(0, 0);
a.add(5, 3).mult(2);
矢量: 交互性
好的,为了完成本教程,让我们尝试一些更复杂、更有用的东西。让我们根据加速算法 #4-“对象向鼠标加速” 规则动态计算对象的加速度。
每当我们想根据规则/公式计算向量时,我们需要计算两件事: 大小和方向。让我们从方向开始。我们知道加速度矢量应该从物体的位置指向鼠标的位置。假设对象位于点 (x,y),鼠标位于 (mouseX,mouseY)。
如上图所示,我们可以通过从鼠标位置减去对象的位置来获得向量 (dx,dy)。毕竟,这正是我们开始这一章的地方 -- 向量的定义是 “空间两点之间的区别!”
dx = mouseX - x
dy = mouseY - y
让我们使用 PVector 语法重写上面的内容。假设我们在 Mover 类中,因此可以访问对象的位置 PVector,那么我们有:
PVector mouse = new PVector(mouseX,mouseY);
//看!我们使用静态引用 sub(),因为
//我们想要一个新的 PVector 从一个点指向另一个点。
PVector dir = PVector.sub(mouse,location);
我们现在有一个 PVector,它从移动者的位置一直指向鼠标。如果对象实际使用该向量加速,它将立即出现在鼠标位置。当然,这并不能产生好的动画,我们现在要做的是决定物体向鼠标加速的速度。
为了设置加速度矢量的大小 (无论它是什么),我们必须首先 ________ 那个方向矢量。没错,你说的。正常化。如果我们可以将向量缩小到它的单位向量 (长度为 1),那么我们就有一个向量来告诉我们方向,并且可以很容易地缩放到任何值。一个乘以任何东西等于任何东西。
float anything = ?????
dir.normalize();
dir.mult(anything);
总而言之,我们有以下步骤:
- 计算从对象指向目标位置 (鼠标) 的向量。
- 规范化该向量 (将其长度减少到 1)
- 将该向量缩放到适当的值 (通过将其乘以某个值)
- 将该向量指定给加速度
以下是 update() 函数本身中的步骤:
void update() {
PVector mouse = new PVector(mouseX,mouseY);
// 第一步: 方向
PVector dir = PVector.sub(mouse,location);
//步骤 2: 标准化
dir.normalize();
// 步骤 3: 比例
dir.mult(0.5);
// 第四步: 加速
acceleration = dir;
velocity.add(acceleration);
velocity.limit(topspeed);
location.add(velocity);
}
为什么圆圈到达目标时不停止?
移动的物体不知道试图停在目的地,它只知道目的地在哪里,并试图尽快到达那里。尽可能快地行驶意味着它将不可避免地超出位置,并且必须转身,再次尽可能快地朝目的地行驶,再次超出它,等等,等等。请继续关注后面的章节,在这些章节中,我们将看到如何对对象进行编程,使其 “到达” 某个位置 (减慢方法速度)。)
让我们来看看这个例子对于移动对象的数组 (而不仅仅是一个) 会是什么样子。
示例: 向鼠标加速
//创建对象数组。
Mover[] movers = new Mover[20];
void setup() {
size(200,200);
smooth();
background(255);
//初始化数组的所有元素
for (int i = 0; i < movers.length; i++) {
movers[i] = new Mover();
}
}
void draw() {
noStroke();
fill(255,10);
rect(0,0,width,height);
//调用数组中所有对象的函数。
for (int i = 0; i < movers.length; i++) {
movers[i].update();
movers[i].checkEdges();
movers[i].display();
}
}
class Mover {
PVector location;
PVector velocity;
PVector acceleration;
float topspeed;
Mover() {
location = new PVector(random(width),random(height));
velocity = new PVector(0,0);
topspeed = 4;
}
void update() {
// 我们计算加速度的算法:
PVector mouse = new PVector(mouseX,mouseY);
PVector dir = PVector.sub(mouse,location); // 查找指向鼠标的矢量
dir.normalize(); // 规范
dir.mult(0.5); //规模
acceleration = dir; //设置为加速
//动作 101!速度随加速度而变化。位置随速度变化。
velocity.add(acceleration);
velocity.limit(topspeed);
location.add(velocity);
}
void display() {
stroke(0);
fill(175);
ellipse(location.x,location.y,16,16);
}
void checkEdges() {
if (location.x > width) {
location.x = 0;
} else if (location.x < 0) {
location.x = width;
}
if (location.y > height) {
location.y = 0;
} else if (location.y < 0) {
location.y = height;
}
}
}