這一節要來看看,有許多個力同時作用時,該怎麼處理。
如果想要讓wind
和gravity
這兩個力同時作用在mover
上,程式可以這樣寫
mover.apply_force(wind+gravity)
這樣子的寫法,其實就是先算出合力,然後讓合力作用在mover
上。
另一種處理合力的方法,是將apply_force()
改寫為
def apply_force(self, force):
self.acceleration += force
這樣當每次呼叫apply_force()
時,新的作用力就會累加進去,而不是取代掉前面的作用力。所以,上一節程式的寫法
mover.apply_force(wind)
mover.apply_force(gravity)
就不會發生wind
被gravity
取代掉而沒有作用的情形,而可以達到讓wind
和gravity
同時作用在mover
上的目的。
處理完合力的問題之後,還有個地方必須要處理,否則mover
將會感受到永不停止的作用力。
假設當我們按下滑鼠左鍵時,會有一陣風吹向mover
,而當放開滑鼠左鍵時,風就停止。寫成程式就是
if pygame.mouse.get_pressed()[0]:
mover.apply_force(wind)
放開滑鼠左鍵後,因為已經沒有風了,所以沒有力量作用在mover
上,它的加速度會是0,而根據牛頓第一運動定律,mover
會維持等速運動。但是,當我們呼叫update()
來計算mover
的速度和位置時,因為update()
的寫法是
def update(self):
self.velocity += self.acceleration
self.position += self.velocity
這時候acceleration
的值,還是維持著呼叫apply_force()
時所得到的值;即便來自wind
的作用力早就不存在了。這顯然不是我們所要的結果,我們要的是當合力為0時,acceleration的值是0。那程式該怎麼寫呢?其實挺簡單的,在update()
的最後,讓acceleration
的值歸零就可以了:
def update(self):
self.velocity += self.acceleration
self.position += self.velocity
self.acceleration *= 0
這樣子,對於acceleration
來說,每一幀畫面都是新的開始,不會留有從上一幀畫面而來的記憶,在現在這一幀畫面中有多大的作用力,mover
的加速度就會有多大的值,完全符合牛頓運動定律。
在模擬時,有個東西很重要:時間步長(time step)。時間步長一般記做dt,指的是「delta time」、「時間的變化量」的意思,它的大小會影響模擬的準確度;這也就是為什麼許多物理引擎會把時間步長當成一個可以調整的參數,讓使用者可以調整其大小,來得到更好的模擬結果。
既然時間步長這麼重要,那它究竟是什麼東西,又為什麼會影響模擬的準確度呢?原書在這裡並沒有詳細解釋,而只用「the rate at which the simulation updates」這樣一句話來說明什麼是時間步長。單憑這句話就想搞清楚什麼是時間步長,實在是挺困難的。下面就試著用一個例子來解釋什麼是時間步長。透過這個例子,應該就能比較清楚的知道,原書那句關於時間步長的說明,到底是在說些什麼。
假設我們要模擬一顆被揮棒打擊出去的棒球的運動軌跡,因為我們是使用電腦進行模擬,而電腦每次只能針對某一個時間點,計算出當時棒球的各項運動數據。所以,整個模擬的過程,就是一個時間點接著一個時間點,計算出棒球的運動數據,直到模擬結束為止;而這一個一個時間點之間的間隔,就是時間步長。很明顯的,如果我們要模擬某段時間內棒球的運動軌跡,時間點之間的間隔,也就是時間步長,如果越小,那電腦必須計算並更新數據的時間點數量就會越多;反之,如果時間步長越大,電腦必須計算並更新數據的時間點數量就會越少。原書關於時間步長的那句話,說的其實就是這個。
知道了時間步長到底是什麼之後,接下來就來看看,它的大小會如何影響模擬的準確度。還是用棒球的運動軌跡那個例子來說明。假設我們想知道棒球在落地前飛了多久,那我們會看看,在哪個時間點棒球還在飛,而在下一個時間點則在地上滾,這樣就可以知道棒球至少飛了多少時間。例如,假設時間步長設定為1秒,而棒球在第5秒鐘時還在飛,但在第6秒鐘時則在地上滾,這時就可以知道,棒球的飛行時間介於5~6秒鐘之間。所以,時間步長設定為1秒,我們所得到的棒球飛行時間,誤差範圍會在1秒鐘內。同樣的道理,如果時間步長設定為0.1秒,那我們應該可以得到誤差範圍在0.1秒內的棒球飛行時間。這也就是說,時間步長越小,模擬的準確度會越高。
既然時間步長越小,模擬的準確度越高,那是不是不管三七二十一,就把時間步長設定得非常非常小就好了?這樣一來,不就不用擔心準確度不夠了嗎?理論上是如此啦!但是,你有那麼多時間、有那麼雄厚的資本嗎?時間步長從1秒改成0.1秒,需要計算的時間點數量會變成10倍。電腦模擬需要時間,計算量越大,需要的時間會越長;換台速度快一點的電腦可以縮短模擬的時間,但這也意味著要從口袋掏出更多的錢出來。
總之,時間步長不管太大或太小都有缺點,剛剛好就好。但是多大才是剛剛好呢?這沒有標準答案,只能視情況而定;不過為了能夠專注於瞭解在模擬時所用到的主要原理,一直到第六章介紹物理引擎之前,我們都會假設,主程式中跑完一次while
迴圈,就相當於一個時間步長。
Exercise 2.1
class Balloon:
def __init__(self):
# 取得顯示畫面的大小
self.screen = pygame.display.get_surface()
self.width, self.height = self.screen.get_size()
# 氣球半徑
self.size = 16
# 物件的起始位置、初始速度、初始加速度
self.position = pygame.Vector2(self.width//2, self.height-self.size)
self.velocity = pygame.Vector2(0, 0)
self.acceleration = pygame.Vector2(0, 0)
def apply_force(self, force):
self.acceleration += force
def update(self):
self.velocity += self.acceleration
self.position += self.velocity
self.acceleration *= 0
def show(self):
pygame.draw.circle(self.screen, (200, 0, 0), self.position, self.size)
def check_edges(self):
# 非彈性碰撞恢復係數。氣球撞壁後,依據此係數來降低移動速率
restitution = 0.5
if self.position.x > self.width:
self.position.x = self.width
self.velocity.x = -restitution*self.velocity.x
elif self.position.x < 0:
self.position.x = 0
self.velocity.x = -restitution*self.velocity.x
if self.position.y < 0:
self.position.y = 0
self.velocity.y = -restitution*self.velocity.y
# python version 3.10.9
import random
import sys
import noise # version 1.2.2
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Exercise 2.1")
WHITE = (255, 255, 255)
screen_size = WIDTH, HEIGHT = 640, 360
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
balloon = Balloon()
# 浮力
buoyancy = pygame.Vector2(0, -0.01)
# 最大風力強度
maximum_intensity = 0.03
# Perlin noise的參數起始值
xoff, yoff = 0, 10000
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
# 依循Perlin noise變化的風力
xoff, yoff = xoff+0.01, yoff+0.01
wind = maximum_intensity*pygame.Vector2(noise.pnoise1(xoff), noise.pnoise1(yoff))
balloon.apply_force(buoyancy)
balloon.apply_force(wind)
balloon.update()
balloon.check_edges()
balloon.show()
pygame.display.update()
frame_rate.tick(FPS)