模擬世界是我們寫程式造出來的,我們就是模擬世界的主宰,所以各種作用力要長什麼樣子、要怎麼個作用法,都由我們決定。不過,如果希望這些作用力看起來像真實世界的作用力一樣,那在寫程式的時候,套用這些作用力在真實世界中的物理公式,會是比較省時省力的做法。
要套用真實世界作用力的物理公式,在模擬世界中呈現出這些作用力的效果,可以依循下列的步驟:
Mover
類別的apply_force()
方法時,所需傳入的pygame.Vector2
物件接下來就針對摩擦力、阻力、萬有引力這三種作用力,介紹如何利用上述步驟,在模擬世界中,創造出這些作用力。
摩擦力(friction)是當兩個物體接觸時,在接觸面所產生的,阻止彼此間相對運動的一種力。摩擦力是一種耗散力(dissipative force),也就是說,運動中的物體,會因為這種力的作用,而導致系統的總機械能降低。例如,開車踩煞車時,煞車皮會利用和輪子間的摩擦力來降低車速。這時候,系統的動能會轉變成熱能。因為熱能並不屬於機械能,所以系統的總機械能因為摩擦力的作用而降低了。
摩擦力分為靜摩擦力(static friction)和動摩擦力(kinetic friction)。靜摩擦力是指,當力量作用在靜止的物體上,而物體仍然保持靜止狀態時的摩擦力;相對的,當物體在運動時的摩擦力,就是動摩擦力。在這裡,為了簡化起見,我們只會看動摩擦力的部分。
摩擦力的作用方式如上圖所示,其計算公式是:
f = −μNv̂
其中,v̂是速度的單位向量;μ是摩擦係數(coefficient of friction);N是正向力(normal force)的大小。
摩擦力的公式可以拆解成−v̂及μN兩個部分;前者是摩擦力的方向,後者是摩擦力的大小。由上圖可以看出來,摩擦力的方向和物體的運動方向相反。因為物體的運動方向是v̂,所以公式中的−v̂,就是摩擦力的方向。至於摩擦力的大小方面,要先知道摩擦係數μ及正向力N的值,才有辦法計算。
先來看看在模擬時,摩擦係數怎麼設定。真實世界中,不同的材質表面,會有不同的摩擦係數,摩擦係數越大,摩擦力會越大;摩擦係數越小,則摩擦力會越小。在真實世界中,摩擦係數需經由實驗來測定;但在模擬時,畢竟是在虛擬世界中,只要模擬出來的效果達到要求,摩擦係數的大小,可以自訂。
接著來看正向力N。當物體在某個接觸面上運動時,因為重力的關係,會有力量施加在接觸面上。根據牛頓第三運動定律,這時會有一個垂直於接觸面的反作用力施加在物體上,這個垂直於接觸面的力就是正向力。重力越大,正向力會越大。因為重力跟質量有關,所以質量越大的物體,正向力也會越大,因而感受到的摩擦力也就越大。
在計算正向力時,因為接觸面不一定是水平的,而有可能如上圖般,有個傾斜的角度。這時候,正向力的大小並不等於重力的大小,所以要想知道正向力的大小,就必須知道傾斜的角度,然後利用三角函數來算出正確的值。不過,現在我們先不管那麼多,反正我們主要的目標是要模擬出摩擦力的效果,先假設正向力的大小是1,就足以達成現階段的目標。
知道怎麼計算摩擦力的方向和大小之後,將摩擦力的計算寫成函數,方便後續呼叫使用
def friction_force(mu, velocity, N=1):
if velocity.length() > 0:
v_hat = velocity.normalize()
else:
v_hat = pygame.Vector2(0, 0)
return -mu*N*v_hat
使用friction_force()
這個函數可以算出摩擦力,但在什麼時候使用呢?這個問題沒有標準答案,一切就看我們的需要而定。接下來,就來看看加入摩擦力效果的例子。
假設mover
是個半徑為radius
的圓,我們希望當它接觸畫面底部時,會受到摩擦力的作用,這時程式該怎麼寫呢?
要判斷圓形的mover
有沒有接觸畫面底部,可以在Mover
類別中加入下列方法:
def contact_edge(self):
return self.position.y > self.height - self.radius - 1
這樣子,當mover
跟畫面底部的距離在1個像素以內時,就會被判定是接觸到底部。
接下來,我們再加入非彈性碰撞(inelastic collision)的效果。
先前我們在模擬mover
碰到畫面邊緣而往回彈時,都假設那是不會逸失動能的「理想化彈性碰撞」(idealized elastic collision),所以mover
的速度大小不變。不過,在真實世界中,這種情況幾乎不會出現;在真實世界中的碰撞,絕大多數都是非彈性碰撞。當一個網球向下掉,碰到地面後反彈,每次反彈的高度會越來越低,就是非彈性碰撞的一個例子。
要讓mover
碰到畫面底部或左右兩邊時,可以以非彈性碰撞的方式反彈,方法很簡單,就只要讓它在反彈之後,速度大小以一定的比例減少就可以了。將這個做法寫成Mover類別的方法,程式碼如下:
def bounce_edges(self):
# 碰壁反彈時,喪失10%的速度
bounce = -0.9
if self.position.x > self.width - self.radius:
self.position.x = self.width - self.radius
self.velocity.x *= bounce
elif self.position.x < self.radius:
self.position.x = self.radius
self.velocity.x *= bounce
if self.position.y > self.height - self.radius:
self.position.y = self.height - self.radius
self.velocity.y *= bounce
下面這個例子,就是在Example 2.3中,除了重力、風力之外,再加入摩擦力的效果。
Example 2.4: Including Friction
# python version 3.10.9
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Example 2.4: Including Friction")
WHITE = (255, 255, 255)
width, height = screen_size = 640, 360
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
# 摩擦係數
mu = 0.1
# 由左向右吹的風
wind = pygame.Vector2(0.5, 0)
# 因為只有一個物體,沒有其他質量不同的物體對照下,
# 重力直接設定數值,不會影響模擬效果
gravity = pygame.Vector2(0, 1)
mover = Mover(width//2, 30, 3)
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
mover.apply_force(gravity)
# 按下滑鼠左鍵時才有風
if pygame.mouse.get_pressed()[0]:
mover.apply_force(wind)
# 接觸畫面底部時會感受到摩擦力的作用
if mover.contact_edge():
friction = friction_force(c, mover.velocity)
mover.apply_force(friction)
mover.bounce_edges()
mover.update()
mover.show()
pygame.display.update()
frame_rate.tick(FPS)
加入摩擦力之後,運動中的物體會因為摩擦力的作用而減速。因為摩擦力的作用方向總是和物體的運動方向相反,所以只要摩擦力持續作用,不管物體的運動方向如何改變,它的運動速度就會越來越慢。因此,當mover
因為非彈性碰撞而不再彈跳,就只在底部移動時,速度會越來越慢,最後停止。藉由調整摩擦係數及bounce_edges()
裡頭bounce
的數值大小,可以加快或減慢這個過程。
Exercise 2.6
有兩個物體時,重力部分的做法,就跟Example 2.3中的做法一樣,分別乘上物體各自的質量
gravityA = gravity*moverA.mass
gravityB = gravity*moverB.mass
摩擦力部分,在計算時,分別用物體各自的摩擦係數就可以了。
# python version 3.10.9
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Exercise 2.6")
WHITE = (255, 255, 255)
width, height = screen_size = 640, 360
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
# 摩擦係數
muA = 0.1
muB = 0.05
# 由左向右吹的風
wind = pygame.Vector2(0.5, 0)
# 向下的重力
gravity = pygame.Vector2(0, 1)
moverA = Mover(200, 30, 5)
moverB = Mover(500, 30, 2)
gravityA = gravity*moverA.mass
gravityB = gravity*moverB.mass
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
moverA.apply_force(gravityA)
moverB.apply_force(gravityB)
# 按下滑鼠左鍵時才有風
if pygame.mouse.get_pressed()[0]:
moverA.apply_force(wind)
moverB.apply_force(wind)
# 接觸畫面底部時會感受到摩擦力的作用
if moverA.contact_edge():
friction = friction_force(muA, moverA.velocity)
moverA.apply_force(friction)
# 接觸畫面底部時會感受到摩擦力的作用
if moverB.contact_edge():
friction = friction_force(muB, moverB.velocity)
moverB.apply_force(friction)
moverA.bounce_edges()
moverA.update()
moverA.show()
moverB.bounce_edges()
moverB.update()
moverB.show()
pygame.display.update()
frame_rate.tick(FPS)
把計算摩擦力的功能寫成Mover
類別的方法,並沒有太大的意義。因為摩擦力是和其他物體有交互作用時才會產生的力,並不是mover
物件本身的性質或單獨會有的行為。重力也是類似的情形。既然重力是計算好之後,再用apply_force()
作用在mover
上,並沒有把它寫成是Mover
類別的方法,摩擦力也就比照辦理就可以了。
Exercise 2.7
# python version 3.10.9
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Exercise 2.7")
WHITE = (255, 255, 255)
width, height = screen_size = 640, 360
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
# 摩擦係數
mu = 0.1
# 向右上方拋擲物體的力
toss = pygame.Vector2(10, -25)
# 因為只有一個物體,沒有其他質量不同的物體對照下,
# 重力直接設定數值,不會影響模擬效果
gravity = pygame.Vector2(0, 1)
mover = Mover(width//2, 30, 3)
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
elif event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 1:
# 把速度歸零再施力,效果看起來比較好
mover.velocity *= 0
mover.apply_force(toss)
screen.fill(WHITE)
mover.apply_force(gravity)
# 接觸畫面底部時會感受到摩擦力的作用
if mover.contact_edge():
friction = friction_force(mu, mover.velocity)
mover.apply_force(friction)
mover.bounce_edges()
mover.update()
mover.show()
pygame.display.update()
frame_rate.tick(FPS)
當物體在流體中運動時,也會感受到摩擦力的作用。不過,在流體中的摩擦力,跟上一節所提到的,因接觸固體表面而產生的摩擦力,在特性上有些不同。雖然兩者不同,但引起的效應是一樣的,就是都會讓物體慢下來。
在流體中的摩擦力有許多不同的名稱,例如viscous force、drag force、fluid resistance等。在這本書中使用的,主要是drag force這個名稱。中文翻譯的話,有譯成「阻力」的;也有譯成「拖曳力」的,不過在網路上看到的資料中,用「阻力」的比較多。
阻力,記為Fd,其計算公式是
Fd = − 0.5 ρ v² A Cd v̂
公式中的負號代表作用力的方向和速度的方向相反,其餘各項所代表的,分別是:
這條公式可以拆解成−v̂及0.5 ρ v² A Cd兩個部分;前者是阻力的方向,後者是阻力的大小。
瞭解公式中各項所代表的含意之後,可以利用一些小技巧來簡化公式,而不至於影響模擬的效果。例如,公式中的0.5 ρ Cd可以合併成一項來看待,不需要分別設定ρ、Cd的值;也就是說,可以令
C̃d = 0.5 ρ Cd
然後直接設定C̃d的值來進行模擬。這是因為,最後會影響阻力大小的是C̃d的值,而不是ρ、Cd個別的值;例如ρ=0.5、Cd=1和ρ=1、Cd=0.5這兩種設定,得到的C̃d值是一樣的,既然如此,那直接設定C̃d就可以了。
接下來,來看看該怎麼設定A的值。一般來說,除非是要呈現不同的A值如何影響阻力的大小,不然可以把A值設定為1。若需要調整阻力大小,改變C̃d的值就可以了。
經過上述的簡化後,阻力公式會變成
Fd = − v²C̃d v̂
後續在不至於引起混淆的情況下,為了簡化起見,會將C̃d上頭的那條蚯蚓省略,還是寫成Cd,而且還是把它叫做「阻力係數」。
根據簡化後的公式,計算阻力的程式可以這樣寫:
if velocity.length() > 0:
v_hat = velocity.normalize()
v2 = velocity.magnitude_squared()
drag_force = -v2*Cd*v_hat
else:
drag_force = pygame.Vector2(0, 0)
接下來,就來實作阻力的功能,讓模擬世界中的mover
,在穿越特定區域的時候,會像跳進游泳池一樣,感受到一股阻力。
要讓mover
在特定區域中會感受到阻力,首先需要把這特定區域給生出來。這個特定的區域,我們假設它是個長方形,這樣會比較好處理。所以,要建造這樣的區域,我們需要知道它的位置、長、寬、阻力係數,同時也要有個方法能把它顯示在畫面上。針對這樣子的需求,我們可以設計一個名叫Liquid
的類別如下:
class Liquid:
def __init__(self, x, y, w, h, Cd):
# 顯示畫面
self.screen = pygame.display.get_surface()
# 液體分布範圍
self.region = pygame.Rect(x, y, w, h)
# 液體阻力係數
self.Cd = Cd
def show(self):
pygame.draw.rect(self.screen, (175, 175, 175), self.region)
要產生一個Liquid
物件,可以這樣寫:
liquid = Liquid(0, 320, 640, 180, 0.1)
這就會是一個讓mover
在穿越時,能感受到阻力的區域。
有了Liquid
這個類別之後,再來就是要讓mover
在穿越Liquid
物件時,能感受到來自Liquid
物件的阻力。以物件導向的方式來寫的話,程式會長這樣:
if (liquid.contains(mover)):
drag_force = liquid.calculate_drag(mover)
mover.apply_force(drag_force)
也就是說,我們需要在Liquid
類別中,再加入兩個方法,一個是contains()
方法,用來判定mover
是不是在liquid
中;另一個是calculate_drag()
方法,用來計算要施加在mover
上的阻力。
先來看看contains()
要怎麼寫。
既然liquid
物件是由pygame
的Rect
物件所造出來的,我們可以使用Rect
的collidepoint()
方法來偵測mover
是不是在liquid
的長方形區域中。
def contains(self, mover):
return liquid.region.collidepoint(mover.position.x, mover.position.y)
至於calculate_drag()
方法,把先前寫過計算阻力的程式稍微修改一下就可以了:
def calculate_drag(self, mover):
if mover.velocity.length() > 0:
v_hat = mover.velocity.normalize()
v2 = mover.velocity.magnitude_squared()
return -v2*liquid.Cd*v_hat
else:
return pygame.Vector2(0, 0)
下面的例子,就是模擬大大小小的球,從空中掉進液體中的情形。
Example 2.5: Fluid Resistance
class Mover:
def __init__(self, x, y, mass):
# 取得顯示畫面的大小
self.screen = pygame.display.get_surface()
self.width, self.height = self.screen.get_size()
# 讓傳遞進來的數值來決定物體的質量
self.mass = mass
# 物體的質量越大,尺寸就會越大
self.size = 16*self.mass
self.radius = self.size/2
# 讓傳遞進來的數值來決定物體的起始位置
self.position = pygame.Vector2(x, y)
# 物件的初始速度、初始加速度
self.velocity = pygame.Vector2(0, 0)
self.acceleration = pygame.Vector2(0, 0)
# 設定mover所在surface的格式為per-pixel alpha
self.surface = pygame.Surface((self.size, self.size), pygame.SRCALPHA)
def apply_force(self, force):
self.acceleration += force/self.mass
def update(self):
self.velocity += self.acceleration
self.position += self.velocity
self.acceleration *= 0
def show(self):
# 使用具透明度的白色把mover所在的surface清空
self.surface.fill((255, 255, 255, 0))
# 畫出具有透明度的mover
center = pygame.Vector2(self.radius, self.radius)
pygame.draw.circle(self.surface, (0, 0, 0, 50), center, self.radius)
# 把mover所在的surface貼到最後要顯示的畫面上
self.screen.blit(self.surface, self.position-center)
def check_edges(self):
if self.position.x > self.width - self.radius:
self.position.x = self.width - self.radius
self.velocity.x = -self.velocity.x
elif self.position.x < self.radius:
self.position.x = self.radius
self.velocity.x = -self.velocity.x
if self.position.y > self.height - self.radius:
self.position.y = self.height - self.radius
self.velocity.y = -self.velocity.y
elif self.position.y < self.radius:
self.position.y = self.radius
self.velocity.y = -self.velocity.y
class Liquid:
def __init__(self, x, y, w, h, Cd):
# 顯示畫面
self.screen = pygame.display.get_surface()
# 液體分布範圍
self.region = pygame.Rect(x, y, w, h)
# 液體阻力係數
self.Cd = Cd
def show(self):
pygame.draw.rect(self.screen, (175, 175, 175), self.region)
def contains(self, mover):
return liquid.region.collidepoint(mover.position.x, mover.position.y)
def calculate_drag(self, mover):
if mover.velocity.length() > 0:
v_hat = mover.velocity.normalize()
v2 = mover.velocity.magnitude_squared()
return -v2*liquid.Cd*v_hat
else:
return pygame.Vector2(0, 0)
# python version 3.10.9
import random
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Example 2.5: Fluid Resistance")
WHITE = (255, 255, 255)
WIDTH, HEIGHT = screen_size = 640, 360
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
liquid = Liquid(0, HEIGHT/2, WIDTH, HEIGHT/2, 0.1)
movers = [Mover(40+i*70, 0, random.uniform(0.1, 5))
for i in range(9)]
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
liquid.show()
for mover in movers:
# 如果mover在liquid中,就會感受到來自liquid的阻力
if liquid.contains(mover):
drag_force = liquid.calculate_drag(mover)
mover.apply_force(drag_force)
gravity = pygame.Vector2(0, 0.1*mover.mass)
mover.apply_force(gravity)
mover.update()
mover.show()
mover.check_edges()
pygame.display.update()
frame_rate.tick(FPS)
從模擬的結果可以看出來,越小的球掉得越慢。這是因為阻力的方向是向上,越小的球質量越小,向上的加速度越大,而導致向下掉的速度減少越多。最後的結果就是,越小的球,掉得越慢。
在模擬時,有時當球接觸到液體時,會向上反彈,而不是落入液體中;這種情形通常會發生在阻力係數比較大、球的質量比較小,或球向下掉的速度比較快時。之所以會發生這種在真實世界不存在的現象,是因為模擬時使用的時間步長太大的關係。在真實世界中,當球一接觸液體時,就會開始受到阻力的作用,而讓速度持續減慢;這是一個連續的過程,隨著球的速度不斷變化,阻力的大小也跟著不斷變化,當球速變成0時,阻力也會變成0,所以球不會突然改變運動的方向而向上彈。但在模擬時,在同一個時間步長內,球所感受到的阻力是一個固定的值,所以有可能在進入下一個時間步長時,因為阻力所產生的反向加速度比較大,而改變運動的方向。
那要怎麼做才能讓球不會反彈呢?縮短時間步長是個可行的辦法,不過畢竟有其極限。接下來的練習,就是要在不調整時間步長的前提下,想辦法解決這個問題。
Exercise 2.8
液體的阻力最多只能讓物體的速度變成0,不可能會讓物體反彈。所以,在計算阻力時,應該把阻力限制在不會讓物體的運動方向180度改變的範圍內。因為Mover
類別的update()
方法中,計算速度的方式是
self.velocity += self.acceleration
所以,能夠讓速度變成0的加速度是
-self.velocity
而由牛頓第二運動定律,產生這個加速度所需的力量大小是
mover.mass*mover.velocity.length()
這就是不會讓物體反彈的液體阻力的最大值。
修改Liquid
類別的calculate_drag()
方法如下:
def calculate_drag(self, mover):
if mover.velocity.length() > 0:
drag_force_limit = mover.mass*mover.velocity.length()
v_hat = mover.velocity.normalize()
v2 = mover.velocity.magnitude_squared()
drag_force = -v2*liquid.Cd*v_hat
drag_force.clamp_magnitude_ip(drag_force_limit)
return drag_force
else:
return pygame.Vector2(0, 0)
修改Example 2.5的程式中用來產生mover
的部分,讓9個相同質量的mover
從不同高度落下。
movers = [Mover(40+i*70, 15*i, 1.5) for i in range(9)]
從模擬的結果可以看出,從越高的地方落下,接觸液面時的速度會越快,因而受到來自液體的阻力也就越大,速度也就減慢越多。所以,不管從哪個高度落下,在液體中的速度其實都差不多。
Exercise 2.9
這個練習主要是要呈現不同A值對阻力大小的影響,所以A值不能一律設為1,而應以box的底部寬度來取代。因此,阻力公式應改為
Fd = − v² ℓ Cd v̂
其中ℓ是box的底部寬度。
設定讓相同質量、底部寬度不同的數個box,從其底部距離液面相同的高度落下。在液體中,底部寬度越小的box受到的阻力越小,所以下沈的速度越快。
程式如下:
class Box:
def __init__(self, x, y, w, h):
# 取得顯示畫面的大小
self.screen = pygame.display.get_surface()
self.width, self.height = self.screen.get_size()
# 物件大小
self.w, self.h = w, h
# 物件質量
self.mass = 0.1*self.w*self.h
# 讓傳遞進來的數值來決定物體的起始位置
self.position = pygame.Vector2(x, y)
# 物件的初始速度、初始加速度
self.velocity = pygame.Vector2(0, 0)
self.acceleration = pygame.Vector2(0, 0)
# 設定box所在surface的格式為per-pixel alpha
# self.surface = pygame.Surface((self.width, self.height), pygame.SRCALPHA)
self.surface = pygame.Surface((self.w, self.h), pygame.SRCALPHA)
def apply_force(self, force):
self.acceleration += force/self.mass
def update(self):
self.velocity += self.acceleration
self.position += self.velocity
self.acceleration *= 0
def show(self):
# 使用具透明度的白色把box所在的surface清空
self.surface.fill((255, 255, 255, 0))
# 畫出具有透明度的box
rect = pygame.Rect(0, 0, self.w, self.h)
pygame.draw.rect(self.surface, (75, 75, 75), rect)
# 把box所在的surface貼到最後要顯示的畫面上
# self.screen.blit(self.surface, (self.position.x, self.position.y))
self.screen.blit(self.surface, (self.position.x, self.position.y))
def check_edges(self):
if self.position.y+self.h > self.height:
self.position.y = self.height - self.h
self.velocity.y = -self.velocity.y
class Liquid:
def __init__(self, x, y, w, h, Cd):
# 顯示畫面
self.screen = pygame.display.get_surface()
# 液體分布範圍
self.region = pygame.Rect(x, y, w, h)
# 液體阻力係數
self.Cd = Cd
def show(self):
pygame.draw.rect(self.screen, (175, 175, 175), self.region)
def contains(self, box):
return liquid.region.collidepoint(box.position.x, box.position.y+box.h)
def calculate_drag(self, box):
if box.velocity.length() > 0:
drag_force_limit = box.mass*box.velocity.length()
v_hat = box.velocity.normalize()
v2 = box.velocity.magnitude_squared()
# 計算阻力時,把box的底部寬度納入考量
drag_force = -v2*box.w*liquid.Cd*v_hat
drag_force.clamp_magnitude_ip(drag_force_limit)
return drag_force
else:
return pygame.Vector2(0, 0)
# python version 3.10.9
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Exercise 2.9")
WHITE = (255, 255, 255)
WIDTH, HEIGHT = screen_size = 640, 360
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
liquid = Liquid(0, HEIGHT/4, WIDTH, HEIGHT/4*3, 0.1)
# 各個box底部到液面的距離相同
boxes = [Box(30+60*i, 50-400/(5+5*i), 10+5*i, 400/(5+5*i)) for i in range(10)]
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
liquid.show()
for box in boxes:
# 如果box在liquid中,就會感受到來自liquid的阻力
if liquid.contains(box):
drag_force = liquid.calculate_drag(box)
box.apply_force(drag_force)
gravity = pygame.Vector2(0, 0.1*box.mass)
box.apply_force(gravity)
box.update()
box.check_edges()
box.show()
pygame.display.update()
frame_rate.tick(FPS)
Exercise 2.10
升力(lift)的大小,設定成和阻力的大小一樣,但是方向則是向上。所以,計算方式改寫為
lift_force = pygame.Vector2(0, -v2*liquid.Cd)
其中,v2
是速度向量大小的平方、liquid.Cd
是液體的阻力係數。完整程式如下:
class Airplane:
def __init__(self, x, y, w, h):
# 取得顯示畫面的大小
self.screen = pygame.display.get_surface()
self.width, self.height = self.screen.get_size()
# 物件大小
self.w, self.h = w, h
# 物件質量
self.mass = 0.01*self.w*self.h
# 讓傳遞進來的數值來決定物體的起始位置
self.position = pygame.Vector2(x, y)
# 物件的初始速度、初始加速度
self.velocity = pygame.Vector2(0, 0)
self.acceleration = pygame.Vector2(0, 0)
# 設定airplane所在surface的格式為per-pixel alpha
self.surface = pygame.Surface((self.w, self.h), pygame.SRCALPHA)
def apply_force(self, force):
self.acceleration += force/self.mass
def update(self):
self.velocity += self.acceleration
self.position += self.velocity
self.acceleration *= 0
def show(self):
# 使用具透明度的白色把airplane所在的surface清空
self.surface.fill((255, 255, 255, 0))
# 畫出具有透明度的airplane
rect = pygame.Rect(0, 0, self.w, self.h)
pygame.draw.arc(self.surface, (75, 75, 75), rect,
math.radians(45), math.radians(160), 30)
# 把airplane所在的surface貼到最後要顯示的畫面上
self.screen.blit(self.surface, (self.position.x, self.position.y))
class Liquid:
def __init__(self, x, y, w, h, Cd):
# 顯示畫面
self.screen = pygame.display.get_surface()
# 液體分布範圍
self.region = pygame.Rect(x, y, w, h)
# 液體阻力係數
self.Cd = Cd
def show(self):
pygame.draw.rect(self.screen, (175, 175, 175), self.region)
def contains(self, box):
return liquid.region.collidepoint(box.position.x, box.position.y+box.h)
def calculate_lift(self, airplane):
if airplane.velocity.length() > 0:
v2 = airplane.velocity.magnitude_squared()
# 升力大小和阻力一樣,但是方向向上
lift_force = pygame.Vector2(0, -v2*liquid.Cd)
return lift_force
else:
return pygame.Vector2(0, 0)
# python version 3.10.9
import math
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Exercise 2.10")
WHITE = (255, 255, 255)
WIDTH, HEIGHT = screen_size = 640, 360
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
liquid = Liquid(0, 0, 0.75*WIDTH, HEIGHT, 0.1)
airplane = Airplane(WIDTH, 250, 100, 50)
thrust = pygame.Vector2(-0.1*airplane.mass, 0) # 飛機推力
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
liquid.show()
# 如果airplane在liquid中,就會感受到來自liquid的阻力
if liquid.contains(airplane):
lift_force = liquid.calculate_lift(airplane)
airplane.apply_force(lift_force)
airplane.apply_force(thrust)
airplane.update()
airplane.show()
pygame.display.update()
frame_rate.tick(FPS)
萬有引力,指的是兩個物體之間,互相吸引的力量。重力也是萬有引力的一種,不過因為地球相對於一般的物體而言非常巨大,所以在我們的經驗中,都只覺得是地球的重力讓鳥屎掉下來,而不覺得鳥屎其實也在發揮它的吸引力,把地球吸向它。
萬有引力的計算公式是
Fg = G m1 m2 r̂ / r2
其中,Fg為萬有引力;G為萬有引力常數;m1、m2分別為物體1與物體2的質量;r為物體1與物體2之間的距離;r̂為由物體1指向物體2,或由物體2指向物體1的單位向量。
在真實世界中,萬有引力常數的大小是6.67428×10-11 m3kg-1s-2。既然G是個常數,那就跟先前一些公式中的常數一樣,在模擬時,它的真正大小並不是那麼的重要,可以自己設定,只要能得到想要的效果就可以。如果沒有特別的考量,可以把它設定為1,方便處理。
要計算物體1和物體2兩個物體間的萬有引力,需要計算其方向和大小。在寫程式時,比較好的做法是,先計算引力的方向,也就是公式中的r̂,然後再計算引力的大小。要注意的是,物體1作用在物體2上的萬有引力,和物體2作用在物體1上的萬有引力,這兩者的大小相同,而方向剛好相反。接下來就來看看程式要怎麼寫。
假設物體1和物體2的位置向量分別是position1
和position2
;而質量則分別是mass1
和mass2
。假設我們要計算物體1作用在物體2上的萬有引力,因為這樣子的引力,會把物體2拉向物體1,所以作用力的方向是從物體2指向物體1。要計算r̂,程式可以這樣寫:
d = position1 - position2
r_hat = d.normalize() if d.length() > 0 else pygame.Vector2(0, 0)
至於計算引力大小的程式,可以這麼寫:
distance = d.magnitude()
strength = G*mass1*mass2/distance**2
所以物體1作用在物體2上的引力會是
force = strength*r_hat
計算萬有引力的程式有了,接下來就來設計一個叫做Attractor
的類別,讓透過這個類別所製造出來的物件,能夠具備吸引其他物件靠近的能力。
我們把Attractor
類別所製作出來的物件叫做attractor
,而且為了不要搞得太複雜,我們設定attractor
不會到處跑,只會待在固定的地方。
因為attractor
是個固定不動具有引力的物體,所以會有質量、位置、大小、等特性(attribute),以及能夠在畫面上將其顯示出來的方法;這部分可以參考Mover
類別的寫法來設計。除此之外,為了讓attractor
能夠吸引其他物體,還必須納入萬有引力常數這個特性,以及設計出能產生引力吸引其他物體的方法。
假設mover
是由Mover
類別產生的物件,那要怎麼讓mover
和attractor
能夠溝通而產生引力,使得mover
被attractor
吸引過去呢?現在就來看看有哪些方法可以達到這個目的。
第一種方法,是使用能傳入attractor
和mover
的函數,例如
attraction(attractor, mover)
接下來的兩種方法,都是物件導向式的寫法。這兩種不同的寫法,主要的差異,在於是站在attractor
或mover
的角度來看引力這件事。
「attractor
把mover
吸引過來」,這是站在attractor
的角度來看引力。這種寫法會在Attractor
類別中,寫一個能傳入mover
來處理引力的方法。假設這個方法叫attract
,那attractor
吸引mover
就可以這樣寫:
attractor.attract(mover)
如果是站在mover
的角度來看引力,就會是「mover
被吸引向attractor
」。這種寫法會在Mover
類別中,寫一個能傳入attractor
來處理引力的方法。假設這個方法叫attracted_to()
,那程式就寫成
mover.attracted_to(attractor)
這兩種物件導向式的寫法並沒有優劣之分,不過都要比第一種單純使用函數的寫法來得好。
除了上述三種寫法外,還有一種寫法,這種寫法是在Attractor
類別中,寫一個能傳入mover
來計算並傳回引力的方法,然後再把傳回的引力,透過Mover
類別中的apply_force()
方法,作用在mover
上。這種方法寫出來的程式長這樣:
force = attractor.attract(mover)
mover.apply_force(force)
程式中的attract()
,就是在Attractor
類別中處理引力的方法。原書最後採用這種寫法來處理引力,因為先前在處理不同的作用力時,都是利用apply_force()
來施加作用力於物體上,所以為了保持一貫性,就採用了這種寫法。
在開始寫attract()
這個方法之前,要先來看一下計算引力時,可能會出現的極端狀況。
計算引力時,可能會有的極端狀況有兩個,一個是兩個物體距離非常遠;另一個是兩個物體距離非常近。
當兩個物體距離非常遠,也就是r非常大時,因為在公式中r位於分母,所以計算出來的引力值會非常小。這也就是說,這兩個物體間的引力,基本上等於是沒有任何作用。就好比地球和一萬光年以外的某顆行星,它們之間的引力,會趨近於0。
如果兩個物體距離非常近,也就是r非常小時,因為r位於分母,所以計算出來的引力值會非常大。在模擬時,這種情況會讓mover
在接近attractor
到一定程度時,因為引力很大很大而加速到以高速越過attractor
。這時候,雖然attractor
會把mover
往回拉,但因為mover
速度非常快,所以attractor
一時之間也沒法讓mover
慢下來,只能眼睜睜看著mover
飛出畫面之外。
如果想要避免這些極端的狀況,要怎麼做呢?其實很簡單,就只需要在計算引力時,把兩個物體之間距離的數值,強迫限制在某一個範圍內就可以了。例如,兩個物體之間的距離算出來是distance
,如果想把這數值限制在5~25之間,程式可以這樣寫:
if distance < 5:
distance = 5
elif distance > 25:
distance = 25
這樣子,就不會有過於極端的情況發生。不過,到底要不要避免極端的情況發生,純粹是個選擇,沒有對或錯的問題,就看你想要的效果是什麼。
完成後的Attractor
類別長這樣:
class Attractor:
def __init__(self, x, y, mass, G=1):
# 取得顯示畫面的大小
self.screen = pygame.display.get_surface()
self.width, self.height = self.screen.get_size()
self.G = G
self.mass = mass
# 物體的質量越大,尺寸就會越大
self.size = self.mass
self.radius = self.size/2
# attractor的位置
self.x, self.y = x, y
self.position = pygame.Vector2(x, y)
# 設定attractor所在surface的格式為per-pixel alpha
self.surface = pygame.Surface((self.size, self.size), pygame.SRCALPHA)
def attract(self, mover):
# 引力方向
d = self.position - mover.position
r_hat = d.normalize() if d.length() > 0 else pygame.Vector2(0, 0)
distance = d.magnitude()
# 限制距離數值的範圍,避免極端狀況發生
if distance < 5:
distance = 5
elif distance > 25:
distance = 25
# 引力大小
strength = self.G*self.mass*mover.mass/distance**2
# 引力
force = strength*r_hat
return force
def show(self):
# 使用具透明度的白色把attractor所在的surface清空
self.surface.fill((255, 255, 255, 0))
# 畫出具有透明度的attractor
center = pygame.Vector2(self.radius, self.radius)
pygame.draw.circle(self.surface, (0, 0, 0, 150), center, self.radius)
# 把attractor所在的surface貼到最後要顯示的畫面上
self.screen.blit(self.surface, self.position-center)
下面這個例子,可以看出模擬的效果。因為Mover
類別並未更動,所以程式未列出。
Example 2.6: Attraction
# python version 3.10.9
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Example 2.6: Attraction")
WHITE = (255, 255, 255)
WIDTH, HEIGHT = screen_size = 640, 360
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
mover = Mover(400, 50, 2)
# mover有不同的初始速度,會產生不同的效果
mover.velocity.x = 1
attractor = Attractor(WIDTH/2, HEIGHT/2, 40, 0.4)
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
attractor.show()
force = attractor.attract(mover)
mover.apply_force(force)
mover.update()
mover.show()
pygame.display.update()
frame_rate.tick(FPS)
在這個例子中,我們設定attractor
和mover
的半徑大小,是跟它們的質量大小呈正比的關係。這個設定並不怎麼精確,無法反映出真實世界中,物體大小和質量間的關係。因為attractor
和mover
都是圓,而半徑為r的圓,面積是π r²,所以比較精確的做法,是設定attractor
和mover
的半徑大小,正比於它們質量的0.5次方,也就是
r ∝ m0.5
這樣子才能夠比較真實地呈現出,物體的大小和質量間的關係。
Exercise 2.11
分別修改Mover
類別和Attractor
類別的__init__()
方法中,圓的半徑大小的計算方式
# mover大小
self.size = 2*self.mass**0.5
self.radius = self.size/2
# attractor大小
self.size = 2*self.mass**0.5
self.radius = self.size/2
接下來的這個例子,是像Example 2.5的做法一樣,讓畫面上同時有很多個mover
,在attractor
的引力作用下跑來跑去,主程式如下:
Example 2.7: Attraction with Many Movers
# python version 3.10.9
import random
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Example 2.7: Attraction with Many Movers")
WHITE = (255, 255, 255)
WIDTH, HEIGHT = screen_size = 640, 360
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
movers = [Mover(random.randint(0, WIDTH),
random.randint(0, HEIGHT),
random.uniform(0.1, 2))
for i in range(10)]
# mover有不同的初始速度,會產生不同的效果
for mover in movers:
mover.velocity = pygame.Vector2(1, 0)
attractor = Attractor(WIDTH/2, HEIGHT/2, 20, 0.4)
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
attractor.show()
for mover in movers:
force = attractor.attract(mover)
mover.apply_force(force)
mover.update()
mover.show()
pygame.display.update()
frame_rate.tick(FPS)
Exercise 2.12
兩個mover
加上兩個attractor
,就可以畫出挺漂亮的圖案。下面是正在畫的當中先後擷取出來的兩張截圖。
主程式如下:
# python version 3.10.9
import math
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Exercise 2.12")
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
WIDTH, HEIGHT = screen_size = 640, 600
screen = pygame.display.set_mode(screen_size)
FPS = 500
frame_rate = pygame.time.Clock()
movers = [Mover(450, 120, 0.065), Mover(450, 350, 0.065)]
attractors = [Attractor(100, 300, 20, 0.4), Attractor(500, 300, 20, 0.4)]
screen.fill(WHITE)
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
for attractor in attractors:
for mover in movers:
force = attractor.attract(mover)
mover.apply_force(force)
mover.update()
# 在mover所在的位置上畫點描繪出運動軌跡
x, y = mover.position
screen.set_at((int(x), int(y)), (0, 0, 0))
pygame.display.update()
frame_rate.tick(FPS)
Exercise 2.13
要想距離越大作用力越強;距離越小作用力越小,最簡單的做法,就是把引力公式中在分母的r,改放到分子就可以了。
要讓attractor
吸引遠距離的mover
,但卻排斥近距離的mover
,最簡單的做法,就是當距離小於某個數值時,把作用力反向。要達到這個目的,可以在attract()
方法中,於return force
之前,加入判斷式,例如
if distance < 15:
force = -force
這樣,當mover
跟attractor
之間的距離小於15時,作用在mover
上的就會是斥力,而非引力。