加速度就是單位時間內速度的變化量,這個定義很類似於速度的定義:速度就是單位時間內的位移量。從加速度和速度的定義就可以知道,加速度會影響速度,而速度會影響位置,寫成程式,就是對於每一幀畫面而言
velocity += acceleration
position += velocity
所以在寫程式模擬物體如何運動時,只要知道物體的起始位置和初始速度,在給定加速度之後,靠著上面那兩行程式,就能自動搞定一切,程式中並不會出現速度和位置的數值。這也就是說,在模擬物體的運動時,重點會是在於怎麼去計算加速度。
下列是在計算加速度時,可以考慮採用的演算法:
接下來,就來看看使用不同的加速度計算演算法時,Mover這個類別裡頭的method要如何寫。
把加速度設為定值是最簡單的一種加速度計算方式。當加速度為定值時,Mover
的__init__()
要修改成
# 取得顯示畫面的大小
self.screen = pygame.display.get_surface()
self.width, self.height = self.screen.get_size()
# 物件的起始位置
x, y = self.width//2, self.height//2
self.position = pygame.Vector2(x, y)
# 物件的初始速度
self.velocity = pygame.Vector2(0, 0)
# 加速度為定值
self.acceleration = pygame.Vector2(-0.001, 0.01)
而update()
則改成
def update(self):
self.velocity += self.acceleration
self.position += self.velocity
在__init__()
中,我們設定加速度的值是(-0.001, 0.01),而速度的值是(0, 0)。所以,一開始的時候,物件是靜止不動的,然後在加速度的作用下,速度逐漸加快。
除了在__init__()
中可以看到加速度和速度的數值外,在update()
中,並未看到任何數值,那麼加速度究竟是怎麼作用在物件上,而讓它從靜止不動到快速地在畫面上移動的呢?要解答這個疑問,就要從加速度說起。
我們設定加速度的值是(-0.001, 0.01),也就是在某一幀畫面物件的速度,都會比前一幀畫面物件的速度,在x方向會累加-0.001個像素;在y方向會累加0.01個像素。
當動畫開始之後,每一幀畫面的速度值就會累加上加速度的值。雖然加速度的值看起來很小,但不斷累積下來,積少成多之下,物件移動的速度,將會快到讓人在畫面上只能看到它模糊的影子,甚至也有可能根本就看不到。
舉例來說,如果fps是設定成60,那每經過一秒鐘,速度就會增加(-0.001, 0.01)*60=(-0.06, 0.6)。這也就是說,原本靜止的物件,在動畫開始的第一秒鐘,速度就已經變成(-0.06, 0.6)了。在第10秒鐘時,物件的速度變成(-0.6, 6);在第100秒時,物件的速度是(-6, 60)。所以,這時候物件每1/60秒,就會移動(-6, 60),這樣的速度,在640x360的畫面中,只需1/20秒,就能由上而下或由下而上,飛掠過整個畫面。
很顯然的,我們並不希望物件無限制地加速下去,設定一個速度的上限是個很合理的做法。
要設定物件的速度上限,方法很簡單:先算出速度的大小,如果速度小於上限值,就不需調整;如果速度大於上限值,就把速度的大小降為上限值。要達到這個目的,可以利用pygame的scale_to_length()
來調整向量的大小。不過,因為scale_to_length()
不能用在零向量上,所以必須先確定這個向量不是零向量:
if velocity.length() > top_speed:
velocity.scale_to_length(top_speed)
另外,也可以先將向量正規化後,再縮放成需要的大小。而同樣的,normalize()
也不能用在零向量上,所以必須先確定這個向量不是零向量:
if velocity.length() > top_speed:
velocity = top_speed*velocity.normalize()
除了上述兩種方法外,也可以使用clamp_magnitude()
或clamp_magnitude_ip()
來把向量的大小限定在一個範圍內。這裡的ip
指的是in place。這兩個函數的用法為:
clamp_magnitude(max_length)
clamp_magnitude(min_length, max_length)
clamp_magnitude_ip(max_length)
clamp_magnitude_ip(min_length, max_length)
如果只給一個參數的話,會將其視為是max_length
。不過要注意的是,這兩個函數都還在發展階段,可能會有變動。
Exercise 1.4
略
下面這個例子,就是將定值的加速度以及最高速限的功能加入Mover類別中,讓物件的移動速度雖然會越來越快,但快到一個程度之後,就不會再加速。
Example 1.8: Motion 101 (Velocity and Constant Acceleration)
class Mover:
def __init__(self):
# 取得顯示畫面的大小
self.screen = pygame.display.get_surface()
self.width, self.height = self.screen.get_size()
# 物件的起始位置
x, y = self.width//2, self.height//2
self.position = pygame.Vector2(x, y)
# 物件的初始速度
self.velocity = pygame.Vector2(0, 0)
# 加速度為定值
self.acceleration = pygame.Vector2(-0.001, 0.01)
# 最高速限
self.top_speed = 10
def update(self):
self.velocity += self.acceleration
# 加入限速功能
if self.velocity.length() > self.top_speed:
self.velocity.scale_to_length(self.top_speed)
self.position += self.velocity
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 = 0
elif self.position.x < 0:
self.position.x = self.width
if self.position.y > self.height:
self.position.y = 0
elif self.position.y < 0:
self.position.y = self.height
# python version 3.10.9
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Example 1.8: Motion 101 (Velocity and Constant Acceleration)")
WHITE = (255, 255, 255)
screen_size = (640, 360)
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
mover = Mover()
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
mover.update()
mover.check_edges()
mover.show()
pygame.display.update()
frame_rate.tick(FPS)
Exercise 1.5
在Mover
類別新增accelerate()
及brake()
這兩個method,並修改update()
。
在acclerate()
中,加入限速功能。在brake()
中,當速度的大小比加速度的大小還小時,直接將速度歸零,讓物件停止移動,以免最後變成倒著走。
class Mover:
def __init__(self):
# 取得顯示畫面的大小
self.screen = pygame.display.get_surface()
self.width, self.height = self.screen.get_size()
# 物件的起始位置
x, y = self.width//2, self.height//2
self.position = pygame.Vector2(x, y)
# 物件的初始速度
self.velocity = pygame.Vector2(0, 0)
# 加速度為定值
self.acceleration = pygame.Vector2(-0.001, 0.01)
# 最高速限
self.top_speed = 10
def accelerate(self):
self.velocity += self.acceleration
# 限速功能
if self.velocity.length() > self.top_speed:
self.velocity.scale_to_length(self.top_speed)
def brake(self):
# 當速度的大小比加速度的大小還小時,必須將速度歸零,以免變成倒著走
if self.velocity.length() > self.acceleration.length():
self.velocity -= self.acceleration
else:
self.velocity *= 0
def update(self):
self.position += self.velocity
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 = 0
elif self.position.x < 0:
self.position.x = self.width
if self.position.y > self.height:
self.position.y = 0
elif self.position.y < 0:
self.position.y = self.height
# python version 3.10.9
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Exercise 1.5")
WHITE = (255, 255, 255)
screen_size = (640, 360)
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
mover = Mover()
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
pressed = pygame.key.get_pressed()
if pressed[pygame.K_UP]:
mover.accelerate()
elif pressed[pygame.K_DOWN]:
mover.brake()
mover.update()
mover.check_edges()
mover.show()
pygame.display.update()
frame_rate.tick(FPS)
接著來看第二個設定加速度的方式,也就是完全隨機的加速度值。
既然加速度值是隨機的,所以在Mover
類別的__init__()
中,就不再設定初始值,而在每次執行update()
時設定。
Example 1.9: Motion 101 (Velocity and Random Acceleration)
class Mover:
def __init__(self):
# 取得顯示畫面的大小
self.screen = pygame.display.get_surface()
self.width, self.height = self.screen.get_size()
# 物件的起始位置
x, y = self.width//2, self.height//2
self.position = pygame.Vector2(x, y)
# 物件的初始速度
self.velocity = pygame.Vector2(0, 0)
# 最高速限
self.top_speed = 6
def update(self):
# 每次執行時,重新設定加速度
# 加速度的方向是隨機的
ax = random.uniform(-1, 1)
ay = random.uniform(-1, 1)
self.acceleration = pygame.Vector2(ax, ay)
# 加速度的大小是隨機的
if self.acceleration.length() > 0:
self.acceleration.scale_to_length(random.uniform(0, 2))
self.velocity += self.acceleration
# 限速功能
if self.velocity.length() > self.top_speed:
self.velocity.scale_to_length(self.top_speed)
self.position += self.velocity
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 = 0
elif self.position.x < 0:
self.position.x = self.width
if self.position.y > self.height:
self.position.y = 0
elif self.position.y < 0:
self.position.y = self.height
# python version 3.10.9
import random
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Example 1.9: Motion 101 (Velocity and Random Acceleration)")
WHITE = (255, 255, 255)
screen_size = (640, 360)
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
mover = Mover()
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
mover.update()
mover.check_edges()
mover.show()
pygame.display.update()
frame_rate.tick(FPS)
在這個例子中,不僅加速度的方向是隨機的,就連大小也是隨機的。而從這個例子的模擬結果可以看出來,加速度並不是只有和運動中的物體的加、減速有關,舉凡速率的大小、方向上的任何變化,都與加速度有關。換句話說,藉由操控加速度,我們就可以操控物體運動的快、慢、方向。
有一點要注意的是,Example 1.9雖然也是隨機漫步的一種,不過和前一章的隨機漫步,卻有本質上的不同。前一章的隨機漫步,是用亂數來決定速度,所以每一步之間都是互相獨立的,彼此間並沒有任何關係;但在Example 1.9中,則是用亂數來決定加速度,然後再算出速度,所以每一步的速度,是根據前一步的速度和加速度來決定的。因為有這樣子的差異,所以Example 1.9的隨機漫步,會呈現出前一章的隨機漫步所沒有的,比較連續、滑順的移動方式。
Exercise 1.6
修改Mover
類別的__init__()
及update()
即可
def __init__(self):
# 取得顯示畫面的大小
self.screen = pygame.display.get_surface()
self.width, self.height = self.screen.get_size()
# 物件的起始位置
x, y = self.width//2, self.height//2
self.position = pygame.Vector2(x, y)
# 物件的初始速度
self.velocity = pygame.Vector2(0, 0)
# 最高速限
self.top_speed = 6
# Perlin noise的參數起始值
self.xoff1, self.xoff2 = 0.21, 11111.57
def update(self):
# 利用二個不同的Perlin noise來設定加速度值
self.xoff1 += 0.01
self.xoff2 += 0.01
ax = noise.pnoise1(self.xoff1)
ay = noise.pnoise1(self.xoff2)
self.acceleration = pygame.Vector2(ax, ay)
self.velocity += self.acceleration
# 限速功能
if self.velocity.length() > self.top_speed:
self.velocity.scale_to_length(self.top_speed)
self.position += self.velocity
這部分的內容主要是在說明p5.js的p5.Vector類別在使用上應注意的地方,故略過。
第三種加速度設定方式,比較複雜,但也比較有用,我們會讓物體加速度的方向,朝向滑鼠游標所在的位置。
既然我們想要讓物體加速度的方向朝向滑鼠游標的位置,那麼從物體位置到滑鼠游標位置的向量,它的方向就是我們所要的加速度的方向。假設物體現在位於position
,而滑鼠游標的位置為mouse
,則從物體到滑鼠游標位置的向量為
acceleration = mouse - position
acceleration
這個向量的方向,就是我們想要的加速度的方向。
有了加速度的方向之後,再把acceleration
這個向量的大小調整成我們要的大小就可以了。這部分的做法,跟先前在Algorithm 1中,針對物體的速度設定最高速限的做法是一樣的。假設acc_length
是我們所要的加速度的大小,程式可以寫成
if acceleration.length() > 0:
acceleration.scale_to_length(acc_length)
或
if acceleration.length() > 0:
acceleration = acc_length*acceleration.normalize()
按照上述的做法來修改Mover類別的update(),就可以讓物件朝滑鼠游標的位置加速飛奔而去。
Example 1.10: Accelerating Towards the Mouse
def update(self):
mouse = pygame.Vector2(pygame.mouse.get_pos())
self.acceleration = mouse - self.position
if self.acceleration.length() > 0:
self.acceleration.scale_to_length(0.5)
self.velocity += self.acceleration
# 限速功能
if self.velocity.length() > self.top_speed:
self.velocity.scale_to_length(self.top_speed)
self.position += self.velocity
執行這個例子時會發現,圓球在抵達滑鼠游標位置時並不會停下來,而是會衝過頭,然後回頭再度往滑鼠游標加速靠近。圓球就這樣一直跑過頭再回頭,永遠到不了滑鼠游標的位置。之所以會出現這樣的情況,是因為我們並沒有設計減速的機制,好讓圓球能真正抵達想要去的位置。這個「抵達」的機制要如何設計,在後面的章節才會提到。
Exercise 1.8
修改Mover
類別的update()
:
def update(self):
mouse = pygame.Vector2(pygame.mouse.get_pos())
# 距離越遠,加速度越大
# 乘上0.001以避免加速度太大,影響模擬效果
self.acceleration = 0.001*(mouse - self.position)
self.velocity += self.acceleration
# 限速功能
if self.velocity.length() > self.top_speed:
self.velocity.scale_to_length(self.top_speed)
self.position += self.velocity
主程式中,不執行mover.check_edges()
,讓球可以跑到畫面外,這樣模擬效果會好一點。