2024-08-02|閱讀時間 ‧ 約 47 分鐘

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

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

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

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

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

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


分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.