The Nature of Code閱讀心得與Python實作:2.5 Modeling a Force

閱讀時間約 16 分鐘

模擬世界是我們寫程式造出來的,我們就是模擬世界的主宰,所以各種作用力要長什麼樣子、要怎麼個作用法,都由我們決定。不過,如果希望這些作用力看起來像真實世界的作用力一樣,那在寫程式的時候,套用這些作用力在真實世界中的物理公式,會是比較省時省力的做法。

要套用真實世界作用力的物理公式,在模擬世界中呈現出這些作用力的效果,可以依循下列的步驟:

  • 瞭解作用力背後的觀念
  • 將描述作用力的數學式分解成方向和大小兩個部分,以計算作用力向量
  • 將數學式寫成程式碼,以便產生使用Mover類別的apply_force()方法時,所需傳入的pygame.Vector2物件

接下來就針對摩擦力、阻力、萬有引力這三種作用力,介紹如何利用上述步驟,在模擬世界中,創造出這些作用力。

摩擦力

摩擦力(friction)是當兩個物體接觸時,在接觸面所產生的,阻止彼此間相對運動的一種力。摩擦力是一種耗散力(dissipative force),也就是說,運動中的物體,會因為這種力的作用,而導致系統的總機械能降低。例如,開車踩煞車時,煞車皮會利用和輪子間的摩擦力來降低車速。這時候,系統的動能會轉變成熱能。因為熱能並不屬於機械能,所以系統的總機械能因為摩擦力的作用而降低了。

摩擦力分為靜摩擦力(static friction)和動摩擦力(kinetic friction)。靜摩擦力是指,當力量作用在靜止的物體上,而物體仍然保持靜止狀態時的摩擦力;相對的,當物體在運動時的摩擦力,就是動摩擦力。在這裡,為了簡化起見,我們只會看動摩擦力的部分。

raw-image

摩擦力的作用方式如上圖所示,其計算公式是:

f = −μN

其中,是速度的單位向量;μ是摩擦係數(coefficient of friction);N是正向力(normal force)的大小。

摩擦力的公式可以拆解成−及μN兩個部分;前者是摩擦力的方向,後者是摩擦力的大小。由上圖可以看出來,摩擦力的方向和物體的運動方向相反。因為物體的運動方向是,所以公式中的−,就是摩擦力的方向。至於摩擦力的大小方面,要先知道摩擦係數μ及正向力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)

阻力

當物體在流體中運動時,也會感受到摩擦力的作用。不過,在流體中的摩擦力,跟上一節所提到的,因接觸固體表面而產生的摩擦力,在特性上有些不同。雖然兩者不同,但引起的效應是一樣的,就是都會讓物體慢下來。

raw-image

在流體中的摩擦力有許多不同的名稱,例如viscous force、drag force、fluid resistance等。在這本書中使用的,主要是drag force這個名稱。中文翻譯的話,有譯成「阻力」的;也有譯成「拖曳力」的,不過在網路上看到的資料中,用「阻力」的比較多。

阻力,記為Fd,其計算公式是

Fd = − 0.5 ρ v² A Cd

公式中的負號代表作用力的方向和速度的方向相反,其餘各項所代表的,分別是:

  • ρ:流體的密度。
  • v:物體相對於流體的運動速率。如果流體是靜止的,這一項就是物體的運動速率。
  • A:frontal surface area,物體在流體中前進時,垂直於前進方向的截面積。
  • Cd:阻力係數,是個常數。
  • :速度的單位向量。

這條公式可以拆解成−及0.5 ρ v² A Cd兩個部分;前者是阻力的方向,後者是阻力的大小。

瞭解公式中各項所代表的含意之後,可以利用一些小技巧來簡化公式,而不至於影響模擬的效果。例如,公式中的0.5 ρ Cd可以合併成一項來看待,不需要分別設定ρ、Cd的值;也就是說,可以令

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

後續在不至於引起混淆的情況下,為了簡化起見,會將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物件是由pygameRect物件所造出來的,我們可以使用Rectcollidepoint()方法來偵測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

raw-image
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

其中ℓ是box的底部寬度。

設定讓相同質量、底部寬度不同的數個box,從其底部距離液面相同的高度落下。在液體中,底部寬度越小的box受到的阻力越小,所以下沈的速度越快。

raw-image

程式如下:

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的位置向量分別是position1position2;而質量則分別是mass1mass2。假設我們要計算物體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類別產生的物件,那要怎麼讓moverattractor能夠溝通而產生引力,使得moverattractor吸引過去呢?現在就來看看有哪些方法可以達到這個目的。

第一種方法,是使用能傳入attractormover的函數,例如

attraction(attractor, mover)

接下來的兩種方法,都是物件導向式的寫法。這兩種不同的寫法,主要的差異,在於是站在attractormover的角度來看引力這件事。

attractormover吸引過來」,這是站在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)

在這個例子中,我們設定attractormover的半徑大小,是跟它們的質量大小呈正比的關係。這個設定並不怎麼精確,無法反映出真實世界中,物體大小和質量間的關係。因為attractormover都是圓,而半徑為r的圓,面積是π r²,所以比較精確的做法,是設定attractormover的半徑大小,正比於它們質量的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,就可以畫出挺漂亮的圖案。下面是正在畫的當中先後擷取出來的兩張截圖。

raw-image
raw-image

主程式如下:

# 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

這樣,當moverattractor之間的距離小於15時,作用在mover上的就會是斥力,而非引力。


15會員
116Content count
寫點東西自娛娛人
留言0
查看全部
發表第一個留言支持創作者!
ysf的沙龍 的其他內容
在真實世界中有各式各樣的作用力影響著我們,那在模擬世界中呢?要怎麼在本來無一物的模擬世界中,製造出作用力呢?
到目前為止,為了簡化問題,我們都假設物體的質量是1。接下來,我們將移除這個假設,然後將完全符合牛頓第二運動定律的apply_force()方法,整合到Mover這個類別中。
這一節要來看看,有許多個力同時作用時,該怎麼處理。
這一節談的是牛頓的三大運動定律,以及力對於物體運動狀態的影響。
這一章介紹的是力(force),以及力與加速度間的關係。
介紹以物件導向的方式,以向量來實作物體運動的模擬程式。
在真實世界中有各式各樣的作用力影響著我們,那在模擬世界中呢?要怎麼在本來無一物的模擬世界中,製造出作用力呢?
到目前為止,為了簡化問題,我們都假設物體的質量是1。接下來,我們將移除這個假設,然後將完全符合牛頓第二運動定律的apply_force()方法,整合到Mover這個類別中。
這一節要來看看,有許多個力同時作用時,該怎麼處理。
這一節談的是牛頓的三大運動定律,以及力對於物體運動狀態的影響。
這一章介紹的是力(force),以及力與加速度間的關係。
介紹以物件導向的方式,以向量來實作物體運動的模擬程式。
你可能也想看
Thumbnail
重點摘要: 1.9 月降息 2 碼、進一步暗示年內還有 50 bp 降息 2.SEP 上修失業率預期,但快速的降息速率將有助失業率觸頂 3.未來幾個月經濟數據將繼續轉弱,經濟復甦的時點或是 1Q25 季底附近
Thumbnail
近期的「貼文發佈流程 & 版型大更新」功能大家使用了嗎? 新版式整體視覺上「更加凸顯圖片」,為了搭配這次的更新,我們推出首次貼文策展 ❤️ 使用貼文功能並完成這次的指定任務,還有機會獲得富士即可拍,讓你的美好回憶都可以用即可拍珍藏!
在真實世界中有各式各樣的作用力影響著我們,那在模擬世界中呢?要怎麼在本來無一物的模擬世界中,製造出作用力呢?
到目前為止,為了簡化問題,我們都假設物體的質量是1。接下來,我們將移除這個假設,然後將完全符合牛頓第二運動定律的apply_force()方法,整合到Mover這個類別中。
Thumbnail
這篇內容,將會講解什麼是變數,以及與變數相關的知識。包括變數、資料型態、變數賦值、變數的命名規則、變數的作用區域、變數的可重複性、內建變數。
這一節要來看看,有許多個力同時作用時,該怎麼處理。
這一節談的是牛頓的三大運動定律,以及力對於物體運動狀態的影響。
介紹以物件導向的方式,以向量來實作物體運動的模擬程式。
介紹如何在模擬物體運動時,引入加速度這個物理量。
Thumbnail
這一節談的是向量的定義,以及如何運用向量來建立模擬物體運動時,關於位置和速度間的關係式。
Thumbnail
重點摘要: 1.9 月降息 2 碼、進一步暗示年內還有 50 bp 降息 2.SEP 上修失業率預期,但快速的降息速率將有助失業率觸頂 3.未來幾個月經濟數據將繼續轉弱,經濟復甦的時點或是 1Q25 季底附近
Thumbnail
近期的「貼文發佈流程 & 版型大更新」功能大家使用了嗎? 新版式整體視覺上「更加凸顯圖片」,為了搭配這次的更新,我們推出首次貼文策展 ❤️ 使用貼文功能並完成這次的指定任務,還有機會獲得富士即可拍,讓你的美好回憶都可以用即可拍珍藏!
在真實世界中有各式各樣的作用力影響著我們,那在模擬世界中呢?要怎麼在本來無一物的模擬世界中,製造出作用力呢?
到目前為止,為了簡化問題,我們都假設物體的質量是1。接下來,我們將移除這個假設,然後將完全符合牛頓第二運動定律的apply_force()方法,整合到Mover這個類別中。
Thumbnail
這篇內容,將會講解什麼是變數,以及與變數相關的知識。包括變數、資料型態、變數賦值、變數的命名規則、變數的作用區域、變數的可重複性、內建變數。
這一節要來看看,有許多個力同時作用時,該怎麼處理。
這一節談的是牛頓的三大運動定律,以及力對於物體運動狀態的影響。
介紹以物件導向的方式,以向量來實作物體運動的模擬程式。
介紹如何在模擬物體運動時,引入加速度這個物理量。
Thumbnail
這一節談的是向量的定義,以及如何運用向量來建立模擬物體運動時,關於位置和速度間的關係式。