2024-07-15|閱讀時間 ‧ 約 50 分鐘

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

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

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(),讓球可以跑到畫面外,這樣模擬效果會好一點。


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