在這一節中,我們會來研究,兩種在模擬世界中創造出作用力的方法。這兩種方法是:
- 自己打造! 畢竟寫程式的人是你,你就是那個世界的主宰,沒道理不能自已打造要用的作用力。
- 依照物理定律打造! 將真實世界中的物理公式寫成程式,在模擬世界中製造符合真實世界物理定律的作用力。
製造作用力最簡單的方式,就是直接給一個數字。例如,要讓一股由左邊吹向右邊的微風,吹在mover這個由Mover類別產生的物件上,程式可以這麼寫:
wind = pygame.Vector2(0.01, 0) mover.apply_force(wind)
下面這個例子多了點變化,我們加入向下的重力,而且只當按下滑鼠左鍵時才有風。
Example 2.1: Forces
gravity = pygame.Vector2(0, 0.1) mover.apply_force(gravity) wind = pygame.Vector2(0.1, 0) if pygame.mouse.get_pressed()[0]: mover.apply_force(wind)
這個例子有點單調,因為就只有一個形單影隻的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.top_surface = pygame.Surface((self.width, self.height), 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.top_surface.fill((255, 255, 255, 0)) # 畫出具有透明度的mover pygame.draw.circle(self.top_surface, (0, 0, 0, 50), self.position, self.size) # 把mover所在的surface貼到最後要顯示的畫面上 self.screen.blit(self.top_surface, (0, 0)) 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
import pygame import sys pygame.init() pygame.display.set_caption("Example 2.2: Forces Acting on Two Objects") FPS = 60 WHITE = (255, 255, 255) screen_size = 640, 360 screen = pygame.display.set_mode(screen_size) frame_rate = pygame.time.Clock() wind = pygame.Vector2(0.1, 0) gravity = pygame.Vector2(0, 0.1) 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
物體越靠近邊緣,反推的力量越大,這股看不到的作用力大小,可以這樣子設定:$$
\begin{align*}
上方邊緣反推力 &= 1 - \frac{物體距上方邊緣的距離}{畫面高度} \\ \\
下方邊緣反推力 &= 1 - \frac{物體距下方邊緣的距離}{畫面高度} \\ \\
左側邊緣反推力 &= 1 - \frac{物體距左側邊緣的距離}{畫面寬度} \\ \\
右側邊緣反推力 &= 1 - \frac{物體距右側邊緣的距離}{畫面寬度}
\end{align*}
$$
在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 return distances
利用上述設定反推力大小的公式,並考慮反推力的方向,即可算出4個邊緣作用在物體上的反推力。主程式如下:
import pygame import random import sys pygame.init() pygame.display.set_caption("Exercise 2.3") FPS = 60 WHITE = (255, 255, 255) screen_size = WIDTH, HEIGHT = 640, 360 screen = pygame.display.set_mode(screen_size) frame_rate = pygame.time.Clock() 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位置的單位向量,就是風力的方向。算出風力的單位向量之後,就可以縮放成實際的風力大小。
direction = mover.position - pygame.Vector2(pygame.mouse.get_pos()) if direction.length() > 0: direction.normalize_ip() wind = 0.3*direction
執行Example 2.2時應該會發現,當力量作用時,小的那一個球反應會明顯比大的那一個快。之所以會如此,是因為在apply_force()這個方法中,計算加速度時,是用力量除以質量。所以,質量越大的物體,算出來的加速度就會越小,因而速度的改變就越慢。對於風力而言,這樣子的結果挺合理的,質量越大的物體,當然越難推動。但是對於重力而言,就不是這麼回事了。
眾所周知,當不同質量的兩個物體從相同高度同時往下掉時,它們到達地面的時間是一樣的。這也就是說,它們往下掉的速度是一樣的。所以,照道理Example 2.2中的兩個球,它們往下掉的速度應該都一樣才對,可是畫面顯示出來的,顯然不是這樣。那問題出在哪裡呢?
我們在模擬的時候,是設定作用力,然後把作用力作用在物體上,進而算出加速度。所以,在相同的作用力下,根據牛頓第二運動定律,質量越大的物體加速度會越小。但是真實世界的實際狀況是,質量越大的物體,受到的重力作用會越大,而不是如我們程式所設定的,所有物體感受到的重力都一樣大。因為這樣子的差異,所以造成模擬的結果跟實際狀況不一樣。
既然質量越大的物體受到的重力作用會越大,那在模擬時,該怎麼設定重力的大小呢?其實重力有個特性,那就是不同質量的物體在重力的作用下,都會有相同的加速度。這個相同的加速度,就是大家熟知的重力加速度。當然啦,這個結果是有許多假設前提的,不過一般的使用上,這些假設都不會導致什麼太大的差異,所以可以放心的使用。
知道不同質量的物體在重力的作用下,都會有相同的加速度這個特性後,重力大小的設定問題就水到渠成了。既然不管質量大小,重力加速度的值都一樣不會變,那從牛頓第二運動定律可以知道
$$
\frac{F}{m} = 常數
$$也就是說,只要能讓作用在物體上的重力除以物體的質量所得到的值是個常數,那就可以達到我們的目的了。那該怎麼做呢?最簡單的做法,就是把要作用在物體上的作用力,給他乘上物體的質量。這樣當我們呼叫apply_force()計算加速度時,就會把物體的質量給消掉,讓它沒辦法作怪。
程式需修改的地方不多,就只需讓作用在球上的重力,是gravity乘上球的質量後的值就可以了。
Example 2.3: Gravity Scaled by Mass
import pygame import sys pygame.init() pygame.display.set_caption("Example 2.3: Gravity Scaled by Mass") FPS = 60 WHITE = (255, 255, 255) screen_size = 640, 360 screen = pygame.display.set_mode(screen_size) frame_rate = pygame.time.Clock() wind = pygame.Vector2(0.1, 0) gravity = pygame.Vector2(0, 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) moverA.check_edges() moverA.update() moverA.show() moverB.check_edges() moverB.update() moverB.show() pygame.display.update() frame_rate.tick(FPS)
修改過後,大、小兩個球向下掉的速度都已經變成一樣了。不過,比較小的球橫向移動的速度仍然會比較快,這是因為我們並沒有去改wind,所以作用在大、小兩個球上的風力都一樣大,質量比較小的球自然跑得比較快。
沒有留言:
張貼留言