The Nature of Code閱讀心得與Python實作:1.8 Acceleration

閱讀時間約 28 分鐘

加速度就是單位時間內速度的變化量,這個定義很類似於速度的定義:速度就是單位時間內的位移量。從加速度和速度的定義就可以知道,加速度會影響速度,而速度會影響位置,寫成程式,就是對於每一幀畫面而言

velocity += acceleration
position += velocity

所以在寫程式模擬物體如何運動時,只要知道物體的起始位置和初始速度,在給定加速度之後,靠著上面那兩行程式,就能自動搞定一切,程式中並不會出現速度和位置的數值。這也就是說,在模擬物體的運動時,重點會是在於怎麼去計算加速度。

下列是在計算加速度時,可以考慮採用的演算法:

  1. 加速度為定值
  2. 完全隨機的加速度值
  3. 加速度的方向朝向滑鼠游標的位置

接下來,就來看看使用不同的加速度計算演算法時,Mover這個類別裡頭的method要如何寫。

Algorithm 1: Constant Acceleration

把加速度設為定值是最簡單的一種加速度計算方式。當加速度為定值時,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)

Algorithm 2: Random Acceleration

接著來看第二個設定加速度的方式,也就是完全隨機的加速度值。

既然加速度值是隨機的,所以在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

Static vs. NonStatic Methods

這部分的內容主要是在說明p5.js的p5.Vector類別在使用上應注意的地方,故略過。

Algorithm 3: Interactive Motion

第三種加速度設定方式,比較複雜,但也比較有用,我們會讓物體加速度的方向,朝向滑鼠游標所在的位置。

既然我們想要讓物體加速度的方向朝向滑鼠游標的位置,那麼從物體位置到滑鼠游標位置的向量,它的方向就是我們所要的加速度的方向。假設物體現在位於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(),讓球可以跑到畫面外,這樣模擬效果會好一點。


avatar-img
15會員
129內容數
寫點東西自娛娛人
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
ysf的沙龍 的其他內容
介紹pygame支援的向量運算,以及向量的減法、乘法、除法實際上是怎麼計算的。
這一節談的是向量的定義,以及如何運用向量來建立模擬物體運動時,關於位置和速度間的關係式。
使用向量來處理問題有很多好處,其中一個好處,就是可以減少變數的數量。在這節中,會用一個簡單的例子來介紹,使用向量跟不使用向量,對變數的數量會有什麼樣的影響。
介紹pygame支援的向量運算,以及向量的減法、乘法、除法實際上是怎麼計算的。
這一節談的是向量的定義,以及如何運用向量來建立模擬物體運動時,關於位置和速度間的關係式。
使用向量來處理問題有很多好處,其中一個好處,就是可以減少變數的數量。在這節中,會用一個簡單的例子來介紹,使用向量跟不使用向量,對變數的數量會有什麼樣的影響。
你可能也想看
Google News 追蹤
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
11/20日NVDA即將公布最新一期的財報, 今天Sell Side的分析師, 開始調高目標價, 市場的股價也開始反應, 未來一週NVDA將重新回到美股市場的焦點, 今天我們要分析NVDA Sell Side怎麼看待這次NVDA的財報預測, 以及實際上Buy Side的倉位及操作, 從
Thumbnail
Hi 大家好,我是Ethan😊 相近大家都知道保濕是皮膚保養中最基本,也是最重要的一步。無論是在畫室裡長時間對著畫布,還是在旅途中面對各種氣候變化,保持皮膚的水分平衡對我來說至關重要。保濕化妝水不僅能迅速為皮膚補水,還能提升後續保養品的吸收效率。 曾經,我的保養程序簡單到只包括清潔和隨意上乳液
Thumbnail
As we navigate through 2024, the mobile app development landscape continues to evolve, driven by changing consumer behaviors...
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
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
11/20日NVDA即將公布最新一期的財報, 今天Sell Side的分析師, 開始調高目標價, 市場的股價也開始反應, 未來一週NVDA將重新回到美股市場的焦點, 今天我們要分析NVDA Sell Side怎麼看待這次NVDA的財報預測, 以及實際上Buy Side的倉位及操作, 從
Thumbnail
Hi 大家好,我是Ethan😊 相近大家都知道保濕是皮膚保養中最基本,也是最重要的一步。無論是在畫室裡長時間對著畫布,還是在旅途中面對各種氣候變化,保持皮膚的水分平衡對我來說至關重要。保濕化妝水不僅能迅速為皮膚補水,還能提升後續保養品的吸收效率。 曾經,我的保養程序簡單到只包括清潔和隨意上乳液
Thumbnail
As we navigate through 2024, the mobile app development landscape continues to evolve, driven by changing consumer behaviors...
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》第一眼看會以為又是另一個關於天才兒童戰無不勝或者自甘墮落的故事。其實這一套劇集,講的是一個高敏感又內向的女生,如何在女權崛起之前的冷戰年代,以人生輸家之姿,晉身頂級棋手的故事,同時講述一個女孩子的前半生。