2024年2月29日 星期四

2.4 Creating Forces

在這一節中,我們會來研究,兩種在模擬世界中創造出作用力的方法。這兩種方法是:

  1. 自己打造! 畢竟寫程式的人是你,你就是那個世界的主宰,沒道理不能自已打造要用的作用力。
  2. 依照物理定律打造! 將真實世界中的物理公式寫成程式,在模擬世界中製造符合真實世界物理定律的作用力。

製造作用力最簡單的方式,就是直接給一個數字。例如,要讓一股由左邊吹向右邊的微風,吹在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 &lt 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 &lt 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,所以作用在大、小兩個球上的風力都一樣大,質量比較小的球自然跑得比較快。

沒有留言:

張貼留言

4.9 Image Textures and Additive Blending

粒子系統可以用來製作視覺特效(visual effect, VFX),而怎麼呈現粒子的外觀,以及粒子具有怎樣的紋理(texture),都會影響特效所展現出來的效果。例如在下圖中,可以很清楚地看到,使用兩種不同的粒子紋理所呈現出來的特效效果,就有很大的不同。 要賦予粒子不同的紋理...