The Nature of Code閱讀心得與Python實作:2.4 Creating Forces

更新於 2024/08/02閱讀時間約 26 分鐘

在真實世界中有各式各樣的作用力影響著我們,那在模擬世界中呢?要怎麼在本來無一物的模擬世界中,製造出作用力呢?

在真實世界中的作用力,可以透過物理定律的數學式來描述。不過可別誤會,並不是大自然利用數學式子來產生作用力,而是先有作用力,然後人們才找出可以描述這些作用力的數學式子。跟真實世界不同的是,在模擬世界中,本來是沒有什麼東西的,更不用說有什麼作用力存在。在模擬世界中的一切一切,都是我們創造出來的,所以,要想在模擬世界中創造出作用力,我們必須先設計好描述作用力的數學式子,然後利用這些式子來產生作用力。

有兩種方式可以用來設計描述虛擬世界作用力的數學式子,一種是自訂規則,完全依照自己的想法來設計,不管那數學式的長相如何,反正只要能呈現出我們所要的效果就可以。另一種方式,則是中規中矩,套用真實世界中的物理定律公式,而利用這種方式所製造出來的作用力,可以呈現出比較接近真實世界物理現象的效果。在這一節中,我們先來看看第一種方式要怎麼做,下一節再來看第二種方式。

不利用物理定律公式,而依照自己設定的規則來打造作用力,最簡單的方式,就是直接給一個數字。例如,要讓一股由左向右的風,吹在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)

這個例子的寫法,是分別針對moverAmoverB來處理,所以控制mover的程式碼都寫了兩次。這樣子的寫法,當mover的數量一多時,就會變得非常繁雜無效率。比較好的做法是用listarray來處理,這會在後續的內容中介紹。

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,所以作用在大、小兩個球上的風力都一樣大,質量比較小的球自然跑得比較快。

其實原書這個例子的寫法很容易讓人混淆,因為在真實世界中,這樣的重力作用方式,根本就不存在。在真實世界中,物體感受到的重力,也就是其重量,等於其質量乘上重力加速度,而且重力加速度是定值。既然作用在moverAmoverB上的重力同樣都是gravity,而且重力加速度都一樣,那moverAmoverB一定會有相同的質量;這個結論和程式中moverAmoverB有不同質量的設定,是矛盾的。所以,以真實世界的重力作用方式來看,程式中的gravity,其實是重力加速度;而gravityAgravityB,才是作用在moverAmoverB上的重力。

雖然這個例子的設定方式並未遵守真實世界的物理定律,但畢竟模擬世界是我們所創造的,我們愛怎麼設定就怎麼設定,那本來就是個可以不遵守真實世界物理定律的地方。不過,話又說回來,既然要讓模擬世界呈現出真實世界中的物理現象,那套用真實世界物理定律的公式,應該會是比較好的做法。


avatar-img
15會員
130內容數
寫點東西自娛娛人
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
ysf的沙龍 的其他內容
到目前為止,為了簡化問題,我們都假設物體的質量是1。接下來,我們將移除這個假設,然後將完全符合牛頓第二運動定律的apply_force()方法,整合到Mover這個類別中。
這一節要來看看,有許多個力同時作用時,該怎麼處理。
這一節談的是牛頓的三大運動定律,以及力對於物體運動狀態的影響。
這一章介紹的是力(force),以及力與加速度間的關係。
介紹以物件導向的方式,以向量來實作物體運動的模擬程式。
介紹如何在模擬物體運動時,引入加速度這個物理量。
到目前為止,為了簡化問題,我們都假設物體的質量是1。接下來,我們將移除這個假設,然後將完全符合牛頓第二運動定律的apply_force()方法,整合到Mover這個類別中。
這一節要來看看,有許多個力同時作用時,該怎麼處理。
這一節談的是牛頓的三大運動定律,以及力對於物體運動狀態的影響。
這一章介紹的是力(force),以及力與加速度間的關係。
介紹以物件導向的方式,以向量來實作物體運動的模擬程式。
介紹如何在模擬物體運動時,引入加速度這個物理量。
你可能也想看
Google News 追蹤
Thumbnail
*合作聲明與警語: 本文係由國泰世華銀行邀稿。 證券服務係由國泰世華銀行辦理共同行銷證券經紀開戶業務,定期定額(股)服務由國泰綜合證券提供。   剛出社會的時候,很常在各種 Podcast 或 YouTube 甚至是在朋友間聊天,都會聽到各種市場動態、理財話題,像是:聯準會降息或是近期哪些科
Nature is a profound source of inspiration and tranquility. From the vibrant colors of blooming flowers to the soothing sounds of flowing rivers, the
Thumbnail
"You live for yourselves, not for humans." About trees and their feelings of sadness due to human interference.
Now I understand: there is no need for futile treatment or ineffective care, allowing our mother nature to take over may be the kindest approach for t
Thumbnail
  簡直像站在球場中看著真實的比賽讓人目不轉睛,呼吸為之屏息——傳說中灌籃高手動畫版的遺珠之憾:湘北對山王之戰終於出世。當年所有人期盼卻沒盼得的最重要的一場比賽,在生父手下脫胎而出,讓這部作品畫下奇蹟的句點。  
Thumbnail
《The Writer And Her Story》是香港漫畫家智海在1999年的作品,黑白色調的漫畫,描寫一個抑鬱的作者,被自己腦袋裡沉重的想法壓駝了背,寫下作品想為腦袋「減重」,卻被各種瑣碎問題所困,就連繪者也彷彿以上帝之手,延伸再延伸,給他沒有盡頭的樓梯和前路...
Thumbnail
在海上看美景,也不忘笑話別人的英語口音👉「It's gorgis, ain't it?」一起來領略一世紀前馬克•吐溫的不倒幽默!
Thumbnail
英文有一些慣用的句子,字面上怎麼讀都參不透,因為不知道背後隱含的思考邏輯。「給他懷疑的好處(give him the benefit of the doubt)」這種話在中文的邏輯中根本不成文也說不通,到底是在講什麼鬼?你只能用外國人的邏輯來想!
Thumbnail
《Queen’s Gambit》第一眼看會以為又是另一個關於天才兒童戰無不勝或者自甘墮落的故事。其實這一套劇集,講的是一個高敏感又內向的女生,如何在女權崛起之前的冷戰年代,以人生輸家之姿,晉身頂級棋手的故事,同時講述一個女孩子的前半生。
Thumbnail
*合作聲明與警語: 本文係由國泰世華銀行邀稿。 證券服務係由國泰世華銀行辦理共同行銷證券經紀開戶業務,定期定額(股)服務由國泰綜合證券提供。   剛出社會的時候,很常在各種 Podcast 或 YouTube 甚至是在朋友間聊天,都會聽到各種市場動態、理財話題,像是:聯準會降息或是近期哪些科
Nature is a profound source of inspiration and tranquility. From the vibrant colors of blooming flowers to the soothing sounds of flowing rivers, the
Thumbnail
"You live for yourselves, not for humans." About trees and their feelings of sadness due to human interference.
Now I understand: there is no need for futile treatment or ineffective care, allowing our mother nature to take over may be the kindest approach for t
Thumbnail
  簡直像站在球場中看著真實的比賽讓人目不轉睛,呼吸為之屏息——傳說中灌籃高手動畫版的遺珠之憾:湘北對山王之戰終於出世。當年所有人期盼卻沒盼得的最重要的一場比賽,在生父手下脫胎而出,讓這部作品畫下奇蹟的句點。  
Thumbnail
《The Writer And Her Story》是香港漫畫家智海在1999年的作品,黑白色調的漫畫,描寫一個抑鬱的作者,被自己腦袋裡沉重的想法壓駝了背,寫下作品想為腦袋「減重」,卻被各種瑣碎問題所困,就連繪者也彷彿以上帝之手,延伸再延伸,給他沒有盡頭的樓梯和前路...
Thumbnail
在海上看美景,也不忘笑話別人的英語口音👉「It's gorgis, ain't it?」一起來領略一世紀前馬克•吐溫的不倒幽默!
Thumbnail
英文有一些慣用的句子,字面上怎麼讀都參不透,因為不知道背後隱含的思考邏輯。「給他懷疑的好處(give him the benefit of the doubt)」這種話在中文的邏輯中根本不成文也說不通,到底是在講什麼鬼?你只能用外國人的邏輯來想!
Thumbnail
《Queen’s Gambit》第一眼看會以為又是另一個關於天才兒童戰無不勝或者自甘墮落的故事。其實這一套劇集,講的是一個高敏感又內向的女生,如何在女權崛起之前的冷戰年代,以人生輸家之姿,晉身頂級棋手的故事,同時講述一個女孩子的前半生。