在Exercise 3.7中,我們曾經利用sin函數來模擬彈簧吊錘(bob)的運動,雖然這樣子的做法程式很容易寫,但是卻沒辦法模擬彈簧吊錘受到如風力、重力等環境中其他作用力的影響下,在空間中的運動狀況。要克服這樣子的問題,就不能再倚靠sin函數,而必須改用能夠用來計算彈簧彈力的虎克定律(Hooke's law)。
根據虎克定律
彈簧的彈力正比於彈簧的形變量
這裡的形變量,指的是彈簧被拉長或擠短的量;所以,把彈簧拉得越長或擠得越短,彈簧回復原狀的力量就會越大。
虎克定律的數學式是
Fspring = -kx
其中,k是彈簧常數(spring constant);x是彈簧的形變量,計算方式是將彈簧當前的長度減去在平衡狀態時的長度。彈簧在平衡狀態時的長度,也稱為靜止長度(rest length)。
因為力是向量,在計算時,必須算出它的大小和方向,所以在寫程式時,我們會利用下列向量變數與純量變數來存放彈簧的資料:
anchor # 向量,存放錨釘的位置
bob_position # 向量,存放吊錘的位置
rest_length # 純量,存放彈簧的靜止長度
利用虎克定律來計算彈簧的彈力時,需要知道彈簧常數k和彈簧的形變量x。k就只是個常數,所以設定個適合的常數就可以了:
k = 0.1
要計算x,必須知道彈簧當前的長度和靜止長度。彈簧的靜止長度是原本就知道的數字,假設是放在rest_length
裡頭。彈簧當前的長度,其實就是錨釘和吊錘之間的距離,也就是由錨釘到吊錘的向量長度。所以
current_length = (bob_position - anchor).length()
x = current_length - rest_length
從x的計算方式可以知道,當x>0時,代表彈簧被拉長;而當x<0時,則代表彈簧被擠短。
有了k和x之後,就可以算出彈簧彈力的大小。那彈簧彈力的方向呢?如前面的圖所顯示的,當彈簧被拉長時,會有一股力量讓它朝著錨釘的方向縮回去;而當彈簧被擠短時,則會有一股力量,讓它朝著遠離錨釘的方向伸長,虎克定律式子中的-1,就是在描述這個方向反轉的現象。所以,想要知道彈簧彈力的方向,就要先知道在拉長彈簧時,施加在彈簧上的拉力的方向。要找出這個拉力的方向其實很簡單,既然彈簧的一端是錨釘,而另一端是吊錘,當我們拉著吊錘讓彈簧變長時,拉力的方向,很顯然的,就是從錨釘到吊錘的方向,也就是向量
bob_position - anchor
的方向。
根據上述的分析,利用虎克定律,我們就可以算出彈簧彈力。計算彈簧彈力的程式,可以這樣寫:
k = 0.1
force = bob_position - anchor
current_length = force.length()
x = current_length - rest_length
force = -k*x*force.normalize()
有了可以計算彈簧彈力的方法之後,接下來就要來看看怎麼用物件導向的方式來寫模擬的程式。不過,在實際動手寫之前,要先考慮一下整個程式的架構要長怎樣,畢竟寫法不只一種。
在模擬彈簧的運動時,整個系統是由彈簧和吊錘所組成,彈簧提供彈力,而吊錘受力之後,就在畫面上跑來跑去。既然吊錘會在畫面上跑來跑去,而我們先前所建立的Mover
類別產生的物件,也會在畫面上跑來跑去,那順理成章的,我們可以就把吊錘看成是類似於Mover
類別所建立的物件,然後用處理Mover
類別的方式來處理。略微修改一下Mover
類別,另外再加入阻尼的作用,讓吊錘像真實世界看到的情況那樣,運動速度會慢慢地降下來,最後回到靜止狀態,這樣就可以得到如下用來模擬彈簧吊錘的Bob
類別:
class Bob:
def __init__(self, x, y, mass):
# 取得顯示畫面
self.screen = pygame.display.get_surface()
# 讓傳遞進來的數值來決定物體的質量
self.mass = mass
# 物體的質量越大,尺寸就會越大
self.size = 2*self.mass
self.radius = self.size/2
# 加入阻尼,讓振盪速度越來越小
self.damping = 0.98
# 讓傳遞進來的數值來決定物體的起始位置
self.position = pygame.Vector2(x, y)
# 物件的初始速度、初始加速度
self.velocity = pygame.Vector2(0, 0)
self.acceleration = pygame.Vector2(0, 0)
def apply_force(self, force):
self.acceleration += force/self.mass
def update(self):
self.velocity += self.acceleration
self.velocity *= self.damping
self.position += self.velocity
self.acceleration *= 0
def show(self):
pygame.draw.circle(self.screen, (0, 0, 0), self.position, self.radius)
設計好Bob
類別之後,接下來就來處理彈簧的部分。我們可以直接把計算彈簧彈力的程式碼全都直接寫主程式中,然後用Bob
類別的apply_force()
方法,讓彈力作用在吊錘上,像這樣:
spring_force = 算出來的彈簧彈力
bob.apply_force(spring_force)
但是,考慮到同一條彈簧有可能會掛上不只一個吊錘,或者會有好幾條彈簧串在一起的情況,另外設計一個Spring
類別來處理彈簧的部分,會是比較好的做法。
在Spring
類別中,需要有個方法,用來將彈簧彈力作用在吊錘上。在2.6節設計Body
類別時,我們設計了attract()
這個方法,用來計算引力,並將引力作用在物體上。這樣子的寫法也可以運用到Spring
類別上,用來計算彈簧彈力,並將彈力作用在吊錘上。當我們把吊錘吊掛在彈簧上,跟彈簧連接在一起時,彈簧的彈力才會作用在吊錘上;所以,就把這個讓吊錘連上彈簧的方法命名為connect()
。有了connect()
方法後,當要讓一個spring
物件把彈力作用在一個bob
物件時,程式就可以寫成
spring.connect(bob)
完整的Spring
類別程式碼如下:
class Spring:
def __init__(self, x, y, spring_constant, rest_length):
# 取得顯示畫面
self.screen = pygame.display.get_surface()
# 彈簧常數
self.k = spring_constant
# 錨釘的位置
self.anchor = pygame.Vector2(x, y)
# 彈簧靜止長度
self.rest_length = rest_length
def connect(self, bob):
force = bob.position - self.anchor
current_length = force.length()
x = current_length - self.rest_length # 彈簧形變量
force = -self.k*x*force.normalize()
bob.apply_force(force)
def show(self):
# 畫錨釘
pygame.draw.circle(self.screen, (0, 0, 0), self.anchor, 5)
def show_line(self, bob):
# 畫彈簧線
pygame.draw.line(self.screen, (0, 0, 0, 50), self.anchor, bob.position, 3)
原書在設計Spring
類別時,是把彈簧常數直接寫死。不過,考慮到不同的彈簧,可能會有不同的彈簧常數,所以這裡不把彈簧常數寫死,改為在產生Spring
物件時設定。這樣子的設計方式,在使用上應該會比較有彈性一點。
Example 3.10: A Spring Connection
# python version 3.10.9
import math
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Example 3.10: A Spring Connection")
WHITE = (255, 255, 255)
width, height = screen_size = 640, 360
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
x, y = width//3, 150
mass = 24
bob = Bob(x, y, mass)
x, y = width//2, 10
spring_constant = 0.2
rest_length = 100
spring = Spring(x, y, spring_constant, rest_length)
gravity = pygame.Vector2(0, 2)
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
bob.apply_force(gravity)
spring.connect(bob)
bob.update()
bob.show()
spring.show()
spring.show_line(bob)
pygame.display.update()
frame_rate.tick(FPS)
Exercise 3.13
當彈簧拉長或擠短到超過限定長度時,因為不能再進一步拉長或擠短,而造成吊錘停止運動;所以,這時除了需將彈簧長度調整成限定的長度外,也必須將吊錘的速度歸零。
def constrain_length(self, bob, min_length, max_length):
direction = bob.position - self.anchor
current_length = direction.length()
if current_length <= min_length:
bob.velocity = pygame.Vector2(0, 0)
direction.scale_to_length(min_length)
bob.position = self.anchor + direction
elif current_length >= max_length:
bob.velocity = pygame.Vector2(0, 0)
direction.scale_to_length(max_length)
bob.position = self.anchor + direction
Exercise 3.14
修改Spring
類別。除了刪除錨釘相關的程式碼,也要修改connect()
方法,讓彈簧兩端都能掛上吊錘。要注意的是,當彈簧兩端連接的都是吊錘時,作用在兩個吊錘上的彈力,大小相等但方向會相反。另外,constrain_length()
也需修改,彈簧長度的計算,原先是計算錨釘到吊錘間的距離,現在則是改為計算兩端兩個吊錘間的距離。修改後的Spring
類別及主程式如下:
class Spring:
def __init__(self, spring_constant, rest_length):
# 取得顯示畫面
self.screen = pygame.display.get_surface()
# 彈簧常數
self.k = spring_constant
# 彈簧靜止長度
self.rest_length = rest_length
def connect(self, bob1, bob2):
# 計算作用在bob2的作用力,方向是從bob1到bob2
force = bob2.position - bob1.position
current_length = force.length()
x = current_length - self.rest_length # 彈簧形變量
force = -self.k*x*force.normalize()
# 當彈簧兩端連接的都是吊錘時,作用在兩個吊錘的彈力,大小相等但方向相反。
bob2.apply_force(force)
bob1.apply_force(-force)
def constrain_length(self, bob1, bob2, min_length, max_length):
direction = bob2.position - bob1.position
current_length = direction.length()
if current_length <= min_length:
bob1.velocity = pygame.Vector2(0, 0)
bob2.velocity = pygame.Vector2(0, 0)
direction.scale_to_length(min_length)
bob2.position = bob1.position + direction
elif current_length >= max_length:
bob1.velocity = pygame.Vector2(0, 0)
bob2.velocity = pygame.Vector2(0, 0)
direction.scale_to_length(max_length)
bob2.position = bob1.position + direction
def show_line(self, bob1, bob2):
pygame.draw.line(self.screen, (0, 0, 0), bob1.position, bob2.position, 3)
# python version 3.10.9
import math
import random
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Exercise 3.15")
WHITE = (255, 255, 255)
width, height = screen_size = 640, 360
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
# 吊錘及彈簧的數量
n = 5
x, y = width//2, height//2
mass = 12
bobs = [Bob(x+i*15*random.randint(-n, n), y+i*15*random.randint(-n, n), mass) for i in range(n)]
spring_constant = 0.05
rest_length = 100
springs = [Spring(spring_constant, rest_length) for i in range(n)]
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
for spring in springs:
for i in range(n):
bob1 = bobs[i]
bob2 = bobs[(i+1) % n]
spring.connect(bob1, bob2)
for bob in bobs:
bob.update()
for spring in springs:
for i in range(n):
bob1 = bobs[i]
bob2 = bobs[(i+1) % n]
spring.constrain_length(bob1, bob2, 30, 300)
spring.show_line(bob1, bob2)
for bob in bobs:
bob.show()
pygame.display.update()
frame_rate.tick(FPS)
在模擬時,先將所有的吊錘連接上彈簧,讓彈力作用在吊錘上,然後再更新吊錘的狀態。等吊錘狀態更新後,再來檢查及調整彈簧的長度,並畫出彈簧,最後才畫出吊錘。這樣子的處理順序,主要是在避免類似2.6節所討論過的,因為以不同的順序來處理物件間的引力以及更新物件狀態時,所造成的模擬上的差異。