利用向量实现变形虫模拟运动
1. 面向对象编程中的抽象
在面向对象编程里,抽象是一项关键设计理念。我们要决定隐藏哪些细节,通过属性和方法暴露哪些内容。以 Python 为例,汽车对象就是对现实世界汽车的抽象代码表示。为了让车辆在屏幕上移动,无需对每个螺栓、齿轮和电线进行建模,这样做简化了代码。而且,
Car
类会把复杂的 Python 指令(如外观、运动等)简化为直观的方法,像
shiftGear()
、
steer()
和
accelerate()
。
作为程序员,要考虑如何在程序中运用抽象,包括在 Python 里对对象的建模。最佳方法并非总是清晰明确,也没有绝对的对错之分。但要记住,好的抽象能让代码更简洁、清晰、模块化且易于维护。
2. 将 Python 代码拆分为多个文件
随着程序复杂度的提升,代码行数会不断增加。为了更好地组织项目,Python 提供了将代码拆分为多个文件(模块)的机制。
操作步骤如下:
1. 在 Processing 编辑器中,点击
microscopic
标签右侧的箭头,从弹出菜单中选择
New Tab
,并将新文件命名为
amoeba
。Processing 会自动为文件名添加
.py
扩展名,此时
amoeba.py
模块会作为一个标签与
microscopic
标签并列显示。
2. 切换到
microscopic
标签,选中
Amoeba
类的所有代码并剪切,然后切换到
amoeba.py
标签并粘贴代码。
3. 切换回
microscopic
标签,剩余代码从
a1 = Amoeba(400, 200, 100)
开始。
4. 使用
import
关键字导入模块,导入语句必须位于实例化变形虫的代码之前,通常放在文件顶部。以下是
microscopic
标签的完整代码:
from amoeba import Amoeba
a1 = Amoeba(400, 200, 100)
def setup():
size(800, 400)
frameRate(120)
def draw():
background('#004477')
a1.display()
模块的使用有诸多好处,它能减少主草图的代码行数,隐藏每个模块的内部工作原理,让程序员专注于更高级的逻辑。同时,模块也便于其他程序员浏览项目代码,理解程序结构。
3. 用向量编程实现运动
这里使用的是欧几里得向量来为变形虫的运动建模。欧几里得向量具有大小和方向两个属性,可用于表示推动变形虫的力。
例如,变形虫从位置 A 移动到位置 B,移动的总距离 4 个单位就是向量的大小,它描述了力的强度,但不表明力的方向。向量由多个标量组成,如向量
v = (4, 3)
表示一个力,能使变形虫从先前位置向右移动 4 个单位,向上移动 3 个单位。
通过勾股定理可以计算向量的大小,不过 Processing 提供了内置的
PVector
类来处理向量,其中的
mag()
方法可用于计算向量的大小。
v = PVector(4, 3)
magnitude = v.mag()
print(magnitude) # 输出 5.0
4.
PVector
类的使用
PVector
是 Processing 内置的用于处理欧几里得向量的类,无需导入即可在草图中使用。创建二维向量时,
PVector()
类需要
x
和
y
两个参数。
为了让变形虫在显示窗口中移动,我们可以创建一个
PVector
实例来表示推进力。
操作步骤如下:
1. 切换到
amoeba.py
标签,在
__init__()
方法中添加一个新的推进向量:
class Amoeba(object):
def __init__(self, x, y, diameter, xspeed, yspeed):
...
self.propulsion = PVector(xspeed, yspeed)
-
切换到
microscopic标签,使用Amoeba()的第四个和第五个参数将推进向量的x和y分量分别设置为 3 和 -1,并在draw()函数中增加变形虫的x和y属性:
a1 = Amoeba(400, 200, 100, 3, -1)
def draw():
background('#004477')
a1.x += a1.propulsion.x
a1.y += a1.propulsion.y
a1.display()
每帧中,变形虫
a1
的
x
值增加 3 个像素,
y
值减少 1 个像素,在默认的 Processing 坐标系中,
y
值减少意味着变形虫向上移动。运行草图,变形虫将沿对角线轨迹快速移动,从显示窗口中心开始,很快从右上角下方移出。
我们还可以使用
PVector
实例来存储变形虫的
x
和
y
坐标。
操作步骤如下:
1. 切换到
amoeba.py
标签,将
self.x
和
self.y
属性替换为一个名为
self.location
的新向量:
class Amoeba(object):
def __init__(self, x, y, diameter):
print('amoeba initialized')
self.location = PVector(x, y)
...
-
由于
Amoeba类中有多处引用了self.x和self.y,需要将它们全部替换为self.location.x和self.location.y。可以使用 Processing 菜单栏中的Edit -> Find工具进行查找和替换操作。 -
切换到
microscopic标签,将a1.x和a1.y分别改为a1.location.x和a1.location.y:
a1.location.x += a1.propulsion.x
a1.location.y += a1.propulsion.y
使用
PVector
加法可以更高效地实现上述操作,将推进向量和位置向量相加:
def draw():
background('#004477')
a1.location += a1.propulsion
a1.display()
5. 向量的加法和减法
向量加法
PVector
类支持使用
+
运算符进行向量加法,
+=
作为增强赋值运算符,可将左侧向量操作数更新为自身与右侧操作数的和。
例如,添加一个新的水流向量
current
来模拟对角流动的水流,辅助变形虫的运动:
current = PVector(1, -2)
def draw():
background('#004477')
a1.location += a1.propulsion
a1.location += current
a1.display()
向量加法通过将一个向量的
x
分量与另一个向量的
x
分量相加,
y
分量同理。向量加法是可交换的,即改变操作数的顺序不影响结果。
向量减法
向量减法的结果是两个向量的差。与向量加法不同,向量减法是非交换的,改变操作数的顺序会得到不同的结果。
可以使用
-
运算符进行
PVector
实例的减法操作:
print(current - a1.propulsion)
为了让变形虫向鼠标指针移动,我们可以创建一个
pointer
向量来存储鼠标指针的
x-y
坐标,并计算
pointer
与
location
的差向量:
current = PVector(1, -2)
def draw():
background('#004477')
pointer = PVector(mouseX, mouseY)
difference = pointer - a1.location
a1.location += difference
...
但直接这样做会使变形虫瞬间跳到鼠标指针位置,我们需要限制向量的大小,让变形虫逐步向指针移动。
6. 限制向量大小
PVector
类提供了
limit()
方法来限制向量的大小,而不影响其方向。该方法需要一个标量参数,表示最大大小。
修改
draw()
函数,引导变形虫向鼠标指针移动:
def draw():
...
# 1 #a1.location += difference
a1.propulsion += difference.limit(0.03)
a1.location += a1.propulsion.limit(3)
a1.location += current
a1.display()
为了让多个变形虫以不同速度移动,我们可以在
Amoeba
类中添加一个
maxpropulsion
属性:
class Amoeba(object):
def __init__(self, x, y, diameter, xspeed, yspeed):
...
self.maxpropulsion = self.propulsion.mag()
同时调整
microscopic
标签中的代码,使用
maxpropulsion
属性:
a1 = Amoeba(400, 200, 100, 0.3, -0.1)
current = PVector(0.1, -0.2)
def draw():
...
a1.propulsion += difference.limit(a1.maxpropulsion/100)
a1.location += a1.propulsion.limit(a1.maxpropulsion)
...
以下是向量操作的总结表格:
| 操作 | 描述 | 示例代码 |
| — | — | — |
| 向量大小计算 | 使用
mag()
方法计算向量大小 |
v = PVector(4, 3); magnitude = v.mag()
|
| 向量加法 | 使用
+
或
+=
运算符进行向量相加 |
a1.location += a1.propulsion
|
| 向量减法 | 使用
-
运算符进行向量相减 |
difference = pointer - a1.location
|
| 限制向量大小 | 使用
limit()
方法限制向量大小 |
a1.propulsion += difference.limit(0.03)
|
下面是变形虫运动的简单流程图:
graph TD;
A[初始化变形虫和向量] --> B[进入 draw() 函数];
B --> C[计算差向量];
C --> D[更新推进向量];
D --> E[更新位置向量];
E --> F[显示变形虫];
F --> B;
利用向量实现变形虫模拟运动
7. 模拟中添加多个变形虫
之前我们处理的是单个变形虫实例
a1
,现在要创建一个变形虫群体。可以从同一个类创建任意数量的实例,下面将在同一显示窗口中生成八个变形虫,每个变形虫的大小和起始坐标都不同。
手动添加变形虫的问题
手动定义多个变形虫实例,如:
a1 = Amoeba(400, 200, 100, 0.3, -0.1)
sam = Amoeba(643, 105, 56, 0.4, -0.4)
bob = Amoeba(295, 341, 108, -0.3, -0.1)
lee = Amoeba(97, 182, 198, -0.1, 0.2)
这种方式存在明显的缺点。要显示这些变形虫,
draw()
函数需要为每个实例调用
display()
方法:
def draw():
...
a1.display()
sam.display()
bob.display()
lee.display()
...
如果要让它们移动,
draw()
函数还需要更多代码。当变形虫数量增加时,这种方式效率极低。
使用列表管理变形虫
更好的方法是将变形虫存储在列表中,使用循环生成所需数量的变形虫,并通过另一个循环调用每个变形虫的
display()
方法和移动代码。
以下是修改后的代码:
from amoeba import Amoeba
amoebas = []
for i in range(8):
diameter = random(50, 200)
speed = 1000 / (diameter * 50)
x, y = random(800), random(400)
amoebas.append(Amoeba(x, y, diameter, speed, speed))
def draw():
background('#004477')
pointer = PVector(mouseX, mouseY)
for a in amoebas:
difference = pointer - a.location
a.propulsion += difference.limit(a.maxpropulsion/100)
a.location += a.propulsion.limit(a.maxpropulsion)
a.location += current
a.display()
在上述代码中,第一个
for
循环随机生成每个变形虫的直径、速度和起始坐标,并将其添加到
amoebas
列表中。第二个
for
循环遍历列表,为每个变形虫计算更新后的位置并显示。
处理变形虫越界问题
较大、较慢的变形虫可能会被水流推出显示窗口,为避免这种情况,可以添加边界环绕代码,使变形虫离开窗口后从另一侧重新出现。
for a in amoebas:
...
r = a.d / 2
if a.location.x - r > width:
a.location.x = 0 - r
if a.location.x + r < 0:
a.location.x = width + r
if a.location.y - r > height:
a.location.y = 0 - r
if a.location.y + r < 0:
a.location.y = height + r
这四个
if
语句检查变形虫是否完全离开显示窗口的四条边,如果是,则将其重新定位到窗口的另一侧。
8. 总结与拓展
通过以上步骤,我们实现了多个变形虫在显示窗口中的模拟运动,并且可以让它们向鼠标指针移动。整个过程涉及到面向对象编程、向量运算、代码模块化等多个编程概念。
以下是整个项目的关键步骤总结表格:
| 步骤 | 操作 | 代码示例 |
| — | — | — |
| 代码拆分 | 创建
amoeba.py
模块并导入 |
from amoeba import Amoeba
|
| 向量使用 | 创建推进向量和位置向量 |
self.propulsion = PVector(xspeed, yspeed); self.location = PVector(x, y)
|
| 向量运算 | 向量加法、减法和限制大小 |
a1.location += a1.propulsion; difference = pointer - a1.location; a1.propulsion += difference.limit(0.03)
|
| 多变形虫管理 | 使用列表存储变形虫并循环处理 |
amoebas = []; for a in amoebas: ...
|
| 边界处理 | 实现边界环绕 |
if a.location.x - r > width: a.location.x = 0 - r
|
下面是整个变形虫模拟系统的流程图:
graph TD;
A[初始化模块和向量] --> B[生成变形虫列表];
B --> C[进入 draw() 函数];
C --> D[计算鼠标指针与变形虫的差向量];
D --> E[更新变形虫推进向量];
E --> F[更新变形虫位置向量];
F --> G[检查变形虫是否越界并处理];
G --> H[显示变形虫];
H --> C;
向量和
PVector
类还有更多的操作,如向量乘法、除法、归一化和处理三维向量等。这些知识可以应用于需要物理模拟的编程项目,如视频游戏等。你可以根据自己的需求进一步拓展这个变形虫模拟系统,例如添加更多的力、改变变形虫的行为等。
超级会员免费看
465

被折叠的 条评论
为什么被折叠?



