在真實世界中有各式各樣的作用力影響著我們,那在模擬世界中呢?要怎麼在本來無一物的模擬世界中,製造出作用力呢?
在真實世界中的作用力,可以透過物理定律的數學式來描述。不過可別誤會,並不是大自然利用數學式子來產生作用力,而是先有作用力,然後人們才找出可以描述這些作用力的數學式子。跟真實世界不同的是,在模擬世界中,本來是沒有什麼東西的,更不用說有什麼作用力存在。在模擬世界中的一切一切,都是我們創造出來的,所以,要想在模擬世界中創造出作用力,我們必須先設計好描述作用力的數學式子,然後利用這些式子來產生作用力。
有兩種方式可以用來設計描述虛擬世界作用力的數學式子,一種是自訂規則,完全依照自己的想法來設計,不管那數學式的長相如何,反正只要能呈現出我們所要的效果就可以。另一種方式,則是中規中矩,套用真實世界中的物理定律公式,而利用這種方式所製造出來的作用力,可以呈現出比較接近真實世界物理現象的效果。在這一節中,我們先來看看第一種方式要怎麼做,下一節再來看第二種方式。
不利用物理定律公式,而依照自己設定的規則來打造作用力,最簡單的方式,就是直接給一個數字。例如,要讓一股由左向右的風,吹在mover
這個由Mover
類別產生的物件上,程式可以這麼寫:
wind = pygame.Vector2(0.01, 0)
mover.apply_force(wind)
下面這個例子則多了點變化,只有在按下滑鼠左鍵時才有風,而且除了風之外,還加入向下的重力。
Example 2.1: Forces
class Mover:
def __init__(self):
# 取得顯示畫面的大小
self.screen = pygame.display.get_surface()
self.width, self.height = self.screen.get_size()
# 物件的質量
self.mass = 1
# 物件的起始位置、初始速度、初始加速度
self.position = pygame.Vector2(30, 30)
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.position += self.velocity
self.acceleration *= 0
def show(self):
pygame.draw.circle(self.screen, (0, 0, 0), self.position, 24)
def check_edges(self):
if self.position.x > self.width:
self.position.x = self.width
self.velocity.x = -self.velocity.x
elif self.position.x < 0:
self.position.x = 0
self.velocity.x = -self.velocity.x
if self.position.y > self.height:
self.position.y = self.height
self.velocity.y = -self.velocity.y
# python version 3.10.9
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Example 2.1: Forces")
WHITE = (255, 255, 255)
screen_size = WIDTH, HEIGHT = 640, 360
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
# 方向向下的重力
gravity = pygame.Vector2(0, 0.1)
# 由左向右吹的風
wind = pygame.Vector2(0.1, 0)
mover = Mover()
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)
mover.update()
mover.check_edges()
mover.show()
pygame.display.update()
frame_rate.tick(FPS)
這個例子有點單調,因為就只有一個形單影隻的mover
而已。接下來,就來讓這個模擬世界熱鬧一些,讓許多大小不一有胖有瘦的mover
,一起加入這個模擬世界。
要能夠很容易又快速地製造出許多胖瘦不一的mover
,就不能把Mover類別裡頭的質量這個變數寫死,而要改成由參數傳遞數值來設定。另外,mover
生出來的位置,也可以用這樣子的方式來設定,這樣就可以很容易地讓mover
從不同的地方冒出來。修改後的Mover
類別如下:
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.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
radius = self.size/2
center = pygame.Vector2(radius, radius)
pygame.draw.circle(self.surface, (0, 0, 0, 50), center, radius)
# 把mover所在的surface貼到最後要顯示的畫面上
self.screen.blit(self.surface, self.position-center)
def check_edges(self):
if self.position.x > self.width:
self.position.x = self.width
self.velocity.x = -self.velocity.x
elif self.position.x < 0:
self.position.x = 0
self.velocity.x = -self.velocity.x
if self.position.y > self.height:
self.position.y = self.height
self.velocity.y = -self.velocity.y
當要製造一個質量10
,起始位置為(20, 30)
的mover
時,程式就可以這樣寫:
mover = Mover(20, 30, 10)
依樣畫葫蘆,我們就可以製造出許多大小不一,散佈在畫面上的mover
。下面這個例子,就是用這樣子的方式造出一大一小兩個受重力和風力影響的mover
。
Example 2.2: Forces Acting on Two Objects
# python version 3.10.9
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Example 2.2: Forces Acting on Two Objects")
WHITE = (255, 255, 255)
screen_size = 640, 360
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
# 方向向下的重力
gravity = pygame.Vector2(0, 0.1)
# 由左向右吹的風
wind = pygame.Vector2(0.1, 0)
moverA = Mover(200, 30, 5)
moverB = Mover(500, 30, 2)
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
# 施加重力
moverA.apply_force(gravity)
moverB.apply_force(gravity)
# 按下滑鼠左鍵時才有風
if pygame.mouse.get_pressed()[0]:
moverA.apply_force(wind)
moverB.apply_force(wind)
moverA.check_edges()
moverA.update()
moverA.show()
moverB.check_edges()
moverB.update()
moverB.show()
pygame.display.update()
frame_rate.tick(FPS)
這個例子的寫法,是分別針對moverA
和moverB
來處理,所以控制mover
的程式碼都寫了兩次。這樣子的寫法,當mover
的數量一多時,就會變得非常繁雜無效率。比較好的做法是用list
或array
來處理,這會在後續的內容中介紹。
Exercise 2.3
物體越靠近邊緣,反推的力量越大,這股看不到的作用力大小,可以這樣子設定:
上方邊緣反推力 = 1 - 物體距上方邊緣的距離 / 畫面高度
下方邊緣反推力 = 1 - 物體距下方邊緣的距離 / 畫面高度
左側邊緣反推力 = 1 - 物體距左側邊緣的距離 / 畫面寬度
右側邊緣反推力 = 1 - 物體距右側邊緣的距離 / 畫面寬度
在Mover
這個類別新增一個distances_to_edges()
方法,用來計算物體距視窗4個邊緣的距離:
def distances_to_edges(self):
distances = {}
distances["top"] = self.position.y
distances["bottom"] = self.height - self.position.y
distances["left"] = self.position.x
distances["right"] = self.width - self.position.x
利用上述設定反推力大小的公式,並考慮反推力的方向,即可算出4個邊緣作用在物體上的反推力。主程式如下:
# python version 3.10.9
import random
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Exercise 2.3")
WHITE = (255, 255, 255)
screen_size = WIDTH, HEIGHT = 640, 360
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
# x、y方向的單位向量
vec_x = pygame.Vector2(1, 0)
vec_y = pygame.Vector2(0, 1)
push_back_force = {}
mover = Mover(random.uniform(0, WIDTH), random.uniform(0, HEIGHT), 2)
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
distances = mover.distances_to_edges()
# 計算4個邊緣作用在mover上的反推力大小
push_back_force["top"] = (1-distances["top"]/HEIGHT)*vec_y
push_back_force["bottom"] = -(1-distances["bottom"]/HEIGHT)*vec_y
push_back_force["left"] = (1-distances["left"]/WIDTH)*vec_x
push_back_force["right"] = -(1-distances["right"]/WIDTH)*vec_x
# 將反推力作用在mover上
for f in push_back_force:
mover.apply_force(push_back_force[f])
mover.update()
mover.show()
pygame.display.update()
frame_rate.tick(FPS)
Exercise 2.4
修改check_edges()
即可。假設圓的半徑是radius
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
Exercise 2.5
從滑鼠到mover
位置的單位向量,就是風力的方向。算出風力的單位向量之後,就可以縮放成實際的風力大小。
wind = mover.position - pygame.Vector2(pygame.mouse.get_pos())
if wind.length() > 0:
wind.scale_to_length(0.3)
執行Example 2.2時應該會發現,當力量作用時,小的那一個球反應會明顯比大的那一個快。之所以會如此,是因為在apply_force()
這個方法中,計算加速度時,是用力量除以質量,所以質量越大的物體,算出來的加速度就會越小,因而速度的改變就越慢。對於風力而言,這樣子的結果挺合理的,質量越大的物體,當然越難推動;但是對於重力而言,就不是這麼回事了。
眾所周知,當不同質量的兩個物體從相同高度同時往下掉時,它們到達地面的時間是一樣的;這也就是說,它們往下掉的速度是一樣的。所以,照道理Example 2.2中的兩個球,它們往下掉的速度應該都一樣才對;可是畫面顯示出來的,顯然不是這樣。那問題出在哪裡呢?
我們在模擬的時候,是設定作用力,然後把作用力作用在物體上,進而算出加速度。所以,在相同的作用力下,根據牛頓第二運動定律,質量越大的物體加速度會越小。但是真實世界的實際狀況是,質量越大的物體,受到的重力作用會越大,而不是如我們程式所設定的,所有物體感受到的重力都一樣大。就因為這樣子的差異,所以造成模擬的結果跟實際狀況不一樣。
既然質量越大的物體受到的重力作用會越大,那在模擬時,該怎麼設定重力的大小呢?其實重力有個特性,那就是不同質量的物體在重力的作用下,都會有相同的加速度,這個相同的加速度,就是大家熟知的重力加速度。當然啦,這個結果是有許多假設前提的,不過一般的使用上,這些假設都不會導致什麼太大的差異,所以可以放心的使用。
知道不同質量的物體在重力的作用下,都會有相同的加速度這個特性後,重力大小的設定問題就水到渠成了。既然不管質量大小,重力加速度的值都一樣,那根據牛頓第二運動定律,作用在物體上的重力除以物體的質量所得到的值,一定會是個常數。所以,只要把要作用在物體上的作用力,給他乘上物體的質量,這樣當我們呼叫apply_force()
計算加速度時,就會把物體的質量給消掉,讓它沒辦法作怪。
程式需修改的地方不多,就只需讓作用在mover
上的重力,是gravity
乘上mover
的質量就可以了。
Example 2.3: Gravity Scaled by Mass
# python version 3.10.9
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Example 2.3: Gravity Scaled by Mass")
WHITE = (255, 255, 255)
screen_size = 640, 360
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
# 方向向下的重力
gravity = pygame.Vector2(0, 0.1)
# 由左向右吹的風
wind = pygame.Vector2(0.1, 0)
moverA = Mover(200, 30, 5)
moverB = Mover(500, 30, 2)
# 修正作用在mover上的重力
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)
moverA.check_edges()
moverA.update()
moverA.show()
moverB.check_edges()
moverB.update()
moverB.show()
pygame.display.update()
frame_rate.tick(FPS)
修改過後,大、小兩個球向下掉的速度都已經變成一樣了。不過,比較小的球橫向移動的速度仍然會比較快,這是因為我們並沒有去改wind
,所以作用在大、小兩個球上的風力都一樣大,質量比較小的球自然跑得比較快。
其實原書這個例子的寫法很容易讓人混淆,因為在真實世界中,這樣的重力作用方式,根本就不存在。在真實世界中,物體感受到的重力,也就是其重量,等於其質量乘上重力加速度,而且重力加速度是定值。既然作用在moverA
和moverB
上的重力同樣都是gravity
,而且重力加速度都一樣,那moverA
和moverB
一定會有相同的質量;這個結論和程式中moverA
和moverB
有不同質量的設定,是矛盾的。所以,以真實世界的重力作用方式來看,程式中的gravity
,其實是重力加速度;而gravityA
和gravityB
,才是作用在moverA
和moverB
上的重力。
雖然這個例子的設定方式並未遵守真實世界的物理定律,但畢竟模擬世界是我們所創造的,我們愛怎麼設定就怎麼設定,那本來就是個可以不遵守真實世界物理定律的地方。不過,話又說回來,既然要讓模擬世界呈現出真實世界中的物理現象,那套用真實世界物理定律的公式,應該會是比較好的做法。