轉向行為(steering behaviors)是Craig W. Reynolds所提出來的,其主要的目的,是要讓電腦動畫及互動媒體如電玩、虛擬實境中,能夠自主行動的角色,可以利用包括尋標(seeking)、逃跑(fleeing)、漫步(wandering)、抵達(arriving)、追補(pursuing)、迴避(evading),以及其他許多的策略,在他們的世界中,以更逼真、更像具有生命般的方式移動。
在Reynolds發表的論文〈Steering Behaviors For Autonomous Characters〉中,他選用了「vehicle」這個字來作為自主代理人的名稱。原書作者沿用這個名稱,在設計自主代理人時,就把類別名稱定為Vehicle
。
Reynolds假設vehicle是理想化的,也就是說,vehicle的質量會集中在一個點上。因此,不管vehicle的實際構造如何,也不管他的工作原理是什麼,更不管他是用輪子、用腳、用翅膀,還是用任何其他的方式移動,他就是會乖乖地工作,並對定義好的規則有所回應。
為了易於瞭解、討論,Reynolds以階層化的三層結構來拆解vehicle展現出自主移動行為的過程。這三層結構為:
轉向力到底是個什麼樣的力呢?假設有個行進中的vehicle正在尋找目標。當這個vehicle發現目標不在他前進的方向上時,他必須改變行進的方向,這樣才能到達目標物所在處。要改變行進的方向,必須有股力量作用在vehicle上,如果這股力量是vehicle自己感知外在環境與自身狀態之後所決定產生的,那就稱這股力量為轉向力。
要讓vehicle朝目標物跑過去,我們可以把目標物設計成一個會散發出吸引力的物體,然後讓vehicle被吸過去。不過這樣子的話,vehicle就不是依照自己的意願主動接近目標物了;它是被動地被目標物吸過去的。在這種被動被吸過去的情況下,vehicle有可能會像是人造衛星繞著地球跑一樣,就在目標物周圍繞啊繞的,永遠到不了目標物所在地。所以,既然vehicle是個自主代理人,那就應該讓他心甘情願自己主動奔向目標物。這樣子,vehicle就會不斷地調整自己的移動方向,不斷地朝目標物接近,最後到達目標物所在處。要讓vehicle能夠主動地調整移動方向朝目標物接近,就必須讓他能夠產生調整移動方向所需要的轉向力。接下來,就來看看要怎麼賦予vehicle這樣子的能力。
如圖,假設vehicle正以速度vcurrent前進,這時他發現目標物並不在前進的路線上。既然目標物的位置不在前方,那最快到達那裡的方式,就是原地調轉方向,對準目標物,然後以最快的速度vdesired前進。如果vehicle是在靜止狀態,原地調轉方向這個動作是可以做到的。可是,他現在正在錯誤的方向前進當中,想要調整成正確的方向,那就必須要靠適當的轉向力才能辦到。
既然vdesired是vehicle能到達目標物所在地最快的速度,那如果vehicle所產生的轉向力,能夠讓他的速度從vcurrent轉變成vdesired,這樣就再好不過了。速度從vcurrent轉變成vdesired,所以速度的改變量為
Δv = vdesired − vcurrent
速度的轉變需要時間,而在我們的模擬世界中,時間的基本單位是幀,這也就是說,vehicle的速度,在1幀的時間內,會有Δv的變化。因此,vehicle實際上是以Δv的加速度,在1幀的時間內,讓他的速度由vcurrent轉變成vdesired。假設vehicle的質量為m,要產生這樣子的加速度來改變速度,由牛頓第二定律,所需的作用力,也就是轉向力,為
Fsteer = mΔv
推導出轉向力的計算式之後,接下來就可以開始設計Vehicle
類別了。因為vehicle會在畫面上跑來跑去,所以先前設計Mover
及Particle
這兩個類別時所使用的架構,也可以拿來這邊用。這也就是說,在Vehicle
類別中,也會有下列屬性:
m
:質量,純量position
:位置向量,pygame.Vector2
物件velocity
:速度向量,pygame.Vector2
物件acceleration
:加速度向量,pygame.Vector2
物件
除此之外,因為我們希望自主代理人所表現出的行為,能夠像真實世界中會看到的情況一樣,所以有必要針對他的一些能力加以限制,讓他的行為能更加逼真。基於這樣子的考量,我們在Vehicle
類別中,增加了下列兩個屬性來限制他的能力:
max_speed
:最高移動速率,純量max_force
:最大出力,純量
規劃好主要的屬性之後,就可以來設計Vehicle
類別中最核心的部分,也就是可以計算出需要的轉向力,來讓vehicle
可以順利朝目標前進的方法;這個方法,就把它命名為seek
。
根據先前所推導出的計算式,在計算轉向力時,需要知道vcurrent和vdesired這兩個速度向量。向量vcurrent是已知的,存放在屬性velocity
中;而向量vdesired,則需根據vehicle
及目標物的位置來計算。
要計算vdesired,必須知道vehicle
及目標物的位置。vehicle
的位置是已知的,存放在屬性position
中;而目標物的位置,也會是已知的,因為vehicle
可以感知到目標物在哪裡。那程式要怎麼寫,才能讓vehicle
感知到目標物的位置呢?這其實挺簡單的,只要把目標物所在的位置傳進seek()
方法中就可以了。
知道如何得知vehicle
本身及目標物的位置之後,接下來就來看看計算轉向力的程式要怎麼寫。
假設pygame.Vector2
物件target_position
是目標物所在位置的位置向量,則由position
到target_position
這個向量的方向,就是vdesired的方向。至於vdesired的大小,則依實際上不同的應用,可以是vehicle
原來的速度大小,也可以是它的最高移動速率。如果要讓vehicle
全速朝目標物飛奔而去,那計算vdesired的程式可以這樣寫
desired = target_position - position
if desired.length() > 1.e-5:
desired.scale_to_length(max_speed)
因為scale_to_length()
無法調整0向量的大小,所以要先判斷desired
是否不是0向量,然後才能調整它的大小。判斷式中使用1.e-5
這個數字而不是0,是因為當向量的長度小於某個數值時,scale_to_lenght()
就會認定它是0向量;1.e-5
這個數字應該夠小了。
算出vdesired之後,就可以利用先前所推導出的計算式來計算轉向力了。不過在計算時,可以稍微簡化一些,把式子當中的m這一項給拿掉,就直接把Δv當作是轉向力;這是因為不管有沒有乘上m,算出來的轉向力大小,通常都會超過max_force
的限制,因而必須被降為max_force
的緣故。根據簡化後的計算式,計算轉向力的程式可以這樣寫:
steer = desired - velocity
if steer.length() > max_force:
steer.scale_to_length(max_force)
當然,算出轉向力之後,還必須讓它作用在vehicle
上頭,也就是
apply_force(steer)
設計完成的Vehicle
類別程式碼如下:
class Vehicle:
def __init__(self, x, y, size=24, mass=1):
# 取得顯示畫面
self.screen = pygame.display.get_surface()
# 讓傳遞進來的數值來決定particle的質量
self.mass = mass
# vehicle的大小,長 x 寬 = size x size/2
self.size = size
# vehicle的初始位置、初始速度、初始加速度
self.position = pygame.Vector2(x, y)
self.velocity = pygame.Vector2(0, 0)
self.acceleration = pygame.Vector2(0, 0)
# vehicle的最大速率、最大出力
self.max_speed = 8
self.max_force = 0.2
# 設定vehicle所在surface的格式為per-pixel alpha,並在上面畫出vehicle
self.surface = pygame.Surface((self.size, self.size/2), pygame.SRCALPHA)
body = [(0, 0), (0, self.size/2), (self.size, self.size//4)]
pygame.draw.polygon(self.surface, (0, 0, 0), body)
def apply_force(self, force):
self.acceleration += force/self.mass
def seek(self, target_position):
desired = target_position - self.position
if desired.length() > 1.e-5:
desired.scale_to_length(self.max_speed)
steer = desired - self.velocity
if steer.length() > self.max_force:
steer.scale_to_length(self.max_force)
self.apply_force(steer)
def update(self):
self.velocity += self.acceleration
if self.velocity.length() > self.max_speed:
self.velocity.scale_to_length(self.max_speed)
self.position += self.velocity
self.acceleration *= 0
def show(self):
# 旋轉surface,讓vehicle面朝前進方向
heading = math.atan2(self.velocity.y, self.velocity.x)
rotated_surface = pygame.transform.rotate(self.surface, -math.degrees(heading))
rect_new = rotated_surface.get_rect(center=self.position)
# 把vehicle所在的surface貼到最後要顯示的畫面上
self.screen.blit(rotated_surface, rect_new)
下面這個例子,是讓vehicle
把滑鼠游標當作搜尋目標,所展現出來的轉向行為。
Example 5.1: Seeking a Target
# python version 3.10.9
import math
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Example 5.1: Seeking a Target")
WHITE = (255, 255, 255)
screen_size = WIDTH, HEIGHT = 640, 360
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
vehicle = Vehicle(WIDTH//2, HEIGHT//2)
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
mouse = pygame.Vector2(pygame.mouse.get_pos())
pygame.draw.circle(screen, (0, 0, 0), mouse, 24, 2)
vehicle.seek(mouse)
vehicle.update()
vehicle.show()
pygame.display.update()
frame_rate.tick(FPS)
Exercise 5.1
在seek()
方法中,計算desired
向量的部分,加個負號即可:
desired = -(target_position - self.position)
Exercise 5.2
修改seek()
方法,加入調整max_speed
及max_force
的機制。vehicle
越靠近目標物,max_speed
和max_force
會越低。另外,因為max_speed
和max_force
有可能會變成0
,導致desired
、steer
、velocity
等向量也變成0
,所以包括update()
方法在內,有使用scale_to_length()
調整向量大小的部分,都必須加上判斷向量大小是否為0
的判斷式,以避免出現錯誤。
def seek(self, target_position):
desired = target_position - self.position
# 依據接近目標的程度,動態調整最大速率及最大力量
proximity = desired.length()/100
self.max_speed = 8*proximity
self.max_force = 0.2*proximity
if desired.length() > 1.e-5:
desired.scale_to_length(self.max_speed)
steer = desired - self.velocity
if self.max_force > 1.e-5:
if steer.length() > self.max_force:
steer.scale_to_length(self.max_force)
self.apply_force(steer)
def update(self):
self.velocity += self.acceleration
if self.max_speed > 1.e-5:
if self.velocity.length() > self.max_speed:
self.velocity.scale_to_length(self.max_speed)
self.position += self.velocity
self.acceleration *= 0
Exercise 5.3
以繼承Vehicle
類別的方式來設計Pursuer
類別及Target
類別。在Pursuer
類別的pursue()
方法中,以外插方式來預測target
在未來的位置P,計算式如下:
P = P0 + T*vtarget
其中P0是target
目前的位置;T是預測區間,也就是要預測T個單位時間之後的位置;vtarget是target
目前的速度。
class Pursuer(Vehicle):
def __init__(self, x, y, size=24, mass=1):
super().__init__(x, y, size=24, mass=1)
self.prediction_interval = 10
def pursue(self, target):
# 以外插方式來預測target在未來的位置
prediction = target.position + self.prediction_interval*target.velocity
desired = prediction - self.position
if desired.length() > 1.e-5:
desired.scale_to_length(self.max_speed)
steer = desired - self.velocity
if steer.length() > self.max_force:
steer.scale_to_length(self.max_force)
self.apply_force(steer)
class Target(Vehicle):
def __init__(self, x, y, size=24, mass=1):
super().__init__(x, y, size=24, mass=1)
# 取得顯示畫面的大小
self.width, self.height = self.screen.get_size()
# target的大小
self.size = size
self.radius = self.size/2
# target的移動速度
velocity = random.choice([-1, 1])*pygame.Vector2(random.randint(1, WIDTH), random.randint(1, HEIGHT))
velocity.scale_to_length(3)
self.velocity = velocity
# 設定target所在surface的格式為per-pixel alpha,並在上面畫出target
self.surface = pygame.Surface((self.size, self.size), pygame.SRCALPHA)
self.center = pygame.Vector2(self.radius, self.radius)
pygame.draw.circle(self.surface, (0, 0, 0), self.center, self.radius)
def update(self):
self.position += self.velocity
def show(self):
self.screen.blit(self.surface, self.position-self.center)
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 math
import random
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Exercise 5.3")
WHITE = (255, 255, 255)
screen_size = WIDTH, HEIGHT = 840, 560
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
pursuer = Pursuer(WIDTH//2, HEIGHT//2)
target = Target(random.randint(0, WIDTH), random.randint(0, HEIGHT), 50)
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
# 擊中目標,重新開始
if (target.position-pursuer.position).length() <= target.radius:
pursuer.position = pygame.Vector2(WIDTH//2, HEIGHT//2)
target = Target(random.randint(0, WIDTH), random.randint(0, HEIGHT), 50)
pursuer.pursue(target)
pursuer.update()
pursuer.show()
target.update()
target.check_edges()
target.show()
pygame.display.update()
frame_rate.tick(FPS)
當vehicle
利用seek()
方法來尋找目標時,即便知道目標物在哪裡,但在抵達目標物所在位置時,總是會衝過頭,然後回頭重新向跑向目標物,然後又衝過頭又再回頭,不斷地重複這個過程,完全沒法停在目標物所在位置。為什麼會這樣呢?這是因為不管距離目標是遠是近,vehicle
無時無刻就只知道要全速衝過去,而不知道這樣做是會衝過頭的。
那要怎麼讓vehicle
不會陷入那種衝過頭,永遠沒法停在目標物所在位置的無窮輪迴呢?想想看火車要進站時會怎麼做就知道了。當火車離站還很遠時,它會以很快的速度前進。隨著越來越接近停靠站,火車的速度會越來越慢、越來越慢,最後準確地停在要停的地方。火車進站的這種做法,就是可以讓vehicle
擺脫不斷衝過頭再回頭的無窮輪迴,真正的抵達目的地的方法。接下來,就來看看這個抵達(arriving)行為的程式要怎麼寫。
要讓vehicle
越接近目標物速度越慢,可以把seek()
方法中計算desired
的部分改成這樣:
slowing_distance = 100
c = 0.5
desired = target_position - self.position
if desired.length() < slowing_distance:
desired *= c
else:
desired.scale_to_length(self.max_speed)
這樣子,當vehicle
與目標物的距離比slowing_distance
設定的數字小時,就代表他已經進入減速區,而desired
的大小,也就會隨著他越來越靠近目標物而越來越小,從而達到減速的目的。
雖然上述的做法可以讓vehicle
越接近目標物時速度越慢,但並不是個很好的做法。怎麼說呢?雖然看起來我們可以藉由調整c
的大小,來調整減速的效果,但卻不一定有用。例如,假設
max_speed = 5
slowing_distance = 100
c = 0.5
當vehicle
距離目標物50
個像素時,算出來的desired
向量,大小會是25
,遠大於max_speed
的值。因此,雖然vehicle
已在減速區內,但desired
的大小,還是max_speed
,沒有任何改變,所以根本不會減速。
那有沒有更好的做法呢?Reynolds的做法就不會有上述的問題。他的做法其實挺簡單的,就是當vehicle
進入減速區後,desired
向量的大小,就會隨著離目標物越來越近,而從max_speed
降為0
;其計算式為
max_speed*desired.length()/slowing_distance
根據Reynolds提出的方法,讓vehicle
能展現抵達行為的方法可以這麼寫:
def arrive(self, target_position):
slowing_distance = 100
desired = target_position - self.position
if desired.length() < slowing_distance:
if desired.length() > 1.e-5:
reduced_speed = self.max_speed*desired.length()/slowing_distance
desired.scale_to_length(reduced_speed)
else:
desired.scale_to_length(self.max_speed)
steer = desired - self.velocity
if steer.length() > self.max_force:
steer.scale_to_length(self.max_force)
self.apply_force(steer)
arrive()
方法的效果,可以從下面這個例子看出來。
Example 5.2: Arriving at a Target
# python version 3.10.9
import math
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Example 5.2: Arriving at a Target")
WHITE = (255, 255, 255)
screen_size = WIDTH, HEIGHT = 640, 360
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
vehicle = Vehicle(WIDTH//2, HEIGHT//2)
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
mouse = pygame.Vector2(pygame.mouse.get_pos())
pygame.draw.circle(screen, (0, 0, 0), mouse, 24, 2)
vehicle.arrive(mouse)
vehicle.update()
vehicle.show()
pygame.display.update()
frame_rate.tick(FPS)
vehicle
的抵達行為,充分展現出了自主代理人的特性。當vehicle
進入減速區之後,會根據他及目標物的位置,以及他和目標物之間的距離,計算出可順利抵達目標物所在處的速度vdesired,然後再根據這個可行的速度與他目前速度之間的差異,計算出需施加的轉向力。這整個過程,都有賴於他能感知到外部環境,也就是目標物的位置,以及內部狀態,也就是他自己的位置、速度,並根據這些資訊計算出該採取的行動。這些都是vehicle
自主完成的,沒有任何人指揮他該怎麼做。
另一個vehicle
的抵達行為所展現出的自主代理人特性,就在於他所產生的轉向力。當vehicle
在減速區外時,他所產生的轉向力,其方向是指向目標物的方向,這樣他才能夠朝目標物接近。然而,當進入減速區之後,他所產生的轉向力,其方向卻是指向遠離目標物的方向,這樣才能降低接近目標物的速度,以避免因為速度太快而衝過頭。與萬有引力這類由非生命體所產生的作用力相較之下,轉向力這種會根據環境而調整的作用力,能讓vehicle
的行為更加的逼真、有生命力;反觀在目標物的萬有引力作用之下,不管vehicle
距離目標物是遠是近,作用力的方向永遠會指向目標物,而讓其行為看起來比較呆板、單調、沒生命力。
Reynolds設計了許多不同的轉向行為。在設計這些轉向行為時,最終都會歸結到一個相同的問題:怎樣的vdesired計算方式,才能夠讓自主代理人展現出我們想要看到的轉向行為?這也就是說,當我們要設計新的轉向行為時,其實就是要設計出新的vdesired計算方式。接下來,就用兩個例子來看看這種設計模式是怎麼進行的。這兩個例子,一個是Reynolds設計的漫步(wander)轉向行為,另一個是新的、原書提出來的,叫做「stay within walls」的轉向行為。
漫步是隨機轉向的一種類型,最簡單的實作方式,就是在每一幀中,都產生隨機的轉向力。不過,這樣子的做法,會讓自主代理人的行動看起來神經兮兮的,實在不怎麼有趣。
針對漫步這種轉向行為,Reynolds設計了一個巧妙的方式,來避免自主代理人出現神經兮兮的行為。Reynolds的設計,會讓自主代理人移動的方式,看起來就像是朝某個方向移動一會兒,然後轉頭朝另一個方向移動一會兒,然後又轉頭朝另一個方向移動一會兒,如此不斷地重複這樣子的行為。接下來,就來看看這個巧妙的方式是怎麼做的。不過,要注意的是,接下來所描述的,並不完全是Reynolds論文中所提出的原始做法,而是經過原書稍微簡化過之後的做法。
先前提到過,在設計轉向行為時,最重要的關鍵在於設計vdesired的計算方式;而在計算vdesired時,唯一的未知數,就是目標物的位置。Reynolds就是藉由設計目標物位置的改變模式,來設計漫步這種轉向行為。接下來就來看看詳細的做法。
如圖,在vehicle前進方向距離d的地方,畫一個半徑為r的圓,然後在圓上任意取一個點作為目標物的位置。假設目標物的位置和vehicle前進方向間的夾角是θ,而且Δθ是個介於−ϕ和ϕ之間的隨機數值。在下一幀時,在圓上那個和vehicle前進方向間的夾角為θ+Δθ的點,就是目標物新的位置。利用這樣子的做法,就可以讓目標物在一個有限的範圍內,隨機地改變位置;而vehicle在尋標的過程中所展現出來的移動方式,也會是隨機的,但又不至於看起來神經兮兮的。
Exercise 5.4
class Wanderer(Vehicle):
def __init__(self, x, y, size=24, mass=1):
super().__init__(x, y, size=24, mass=1)
# 取得顯示畫面的大小
self.width, self.height = self.screen.get_size()
# wanderer的最大速率、最大出力
self.max_speed = 2
self.max_force = 0.05
# 前方大圓距離d、半徑r、圓心位置center
self.d = 80
self.r = 25
self.center = pygame.Vector2()
# 目標物偏移角度theta、角度變動範圍: -phi ~ phi
# 角度單位:度
self.theta = 0
self.phi = 15
# 目標物位置
self.target = pygame.Vector2()
def wander(self):
direction = self.velocity.normalize() if self.velocity.length()>0 else pygame.Vector2(1, 0)
self.center = self.position + self.d*direction
heading = math.degrees(math.atan2(self.velocity.y, self.velocity.x))
self.theta += random.uniform(-self.phi, self.phi)
self.target = self.center + pygame.Vector2.from_polar((self.r, self.theta+heading))
self.seek(self.target)
def show_target(self):
# 在顯示畫面上畫出wanderer前方的大圓、wanderer到大圓之間的直線、大圓圓心到目標物之間的直線
pygame.draw.circle(self.screen, (0, 0, 0), self.center, self.r, 1)
pygame.draw.line(self.screen, (0, 0, 0), self.position, self.center)
pygame.draw.line(self.screen, (0, 0, 0), self.center, self.target)
pygame.draw.circle(self.screen, (0, 0, 0), self.target, 3)
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 math
import random
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Exercise 5.4")
WHITE = (255, 255, 255)
screen_size = WIDTH, HEIGHT = 640, 360
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
wanderer = Wanderer(WIDTH//2, HEIGHT//2)
show_target_position = True
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
elif event.type == pygame.MOUSEBUTTONDOWN:
show_target_position = not show_target_position
screen.fill(WHITE)
wanderer.wander()
wanderer.update()
wanderer.check_edges()
wanderer.show()
if show_target_position:
wanderer.show_target()
pygame.display.update()
frame_rate.tick(FPS)
接下來,再來看另一個例子。在這個例子中,當vehicle發現自己太靠近牆壁時,就會轉個彎,快速遠離牆壁。例如,當他跟某個垂直方向的牆壁之間的距離太近時,就會產生轉向力,在保持速度的垂直分量不變的情況下,讓速度的水平分量,轉變成能遠離牆壁方向的最大速率,以遠離牆壁。
Example 5.3: "Stay Within Walls” Steering Behavior
在Vehicle
類別新增一個boundaries()
方法來產生前述之轉向力。假設顯示畫面的四個邊就是四面牆,而vehicle
跟牆壁之間的距離小於offset
時,就會產生能遠離牆壁的轉向力;程式如下:
def boundaries(self, offset):
desired = None
# 進入垂直邊界,以最大的水平方向速率遠離
if self.position.x > self.width - offset:
desired = pygame.Vector2(-self.max_speed, self.velocity.y)
elif self.position.x < offset:
desired = pygame.Vector2(self.max_speed, self.velocity.y)
# 進入水平邊界,以最大的垂直方向速率分量遠離
if self.position.y > self.height - offset:
desired = pygame.Vector2(self.velocity.x, -self.max_speed)
elif self.position.y < offset:
desired = pygame.Vector2(self.velocity.x, self.max_speed)
if desired is not None:
desired.scale_to_length(self.max_speed)
steer = desired - self.velocity
if steer.length() > 1.e-5:
steer.scale_to_length(self.max_force)
self.apply_force(steer)
在這個方法的開頭,為了要知道vehicle
是不是因為跟牆壁之間的距離小於offset
而需要產生轉向力,所以設定
desired = None
並在後面藉由檢查desired
是不是仍然為None
來判斷vehicle
是否需產生轉向力。那為什麼不把desired
設定成0向量,然後檢查desired
是否為0向量呢?根據原書的說明,這會讓vehicle
最後靜止不動。不過,實際用原書提供的程式以及python來測試,如果把desired
設定成0向量,然後藉由檢查desired
的大小是否為0來判斷是否需產生轉向力,就可以避免這種狀況發生。因此,boundaries()
方法,也可以寫成
def boundaries(self, offset):
desired = pygame.Vector2(0, 0)
:
:
if desired.length() != 0:
:
:
不過,這種寫法並沒有比較好,畢竟去檢查變數是不是等於某個數值,就是個會讓人沒有安全感的寫法。
主程式部分如下:
# python version 3.10.9
import math
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Example 5.3: “Stay Within Walls” Steering Behavior")
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
screen_size = WIDTH, HEIGHT = 640, 360
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
vehicle = Vehicle(WIDTH//2, HEIGHT//2)
wall_offset = 25
bound = pygame.Rect(wall_offset, wall_offset, WIDTH-2*wall_offset, HEIGHT-2*wall_offset)
show_boundaries = True
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
elif event.type == pygame.MOUSEBUTTONDOWN:
show_boundaries = not show_boundaries
screen.fill(WHITE)
vehicle.boundaries(wall_offset)
vehicle.update()
vehicle.show()
if show_boundaries:
pygame.draw.rect(screen, BLACK, bound, 1)
pygame.display.update()
frame_rate.tick(FPS)
Exercise 5.5
讓vehicle像魚一樣扭動著游向滑鼠指標;距離越近,扭動速度越慢。程式寫法很簡單,修改wander()
方法中,用來計算theta
的式子就可以了。
class Fish(Vehicle):
def __init__(self, x, y, size=24, mass=1):
super().__init__(x, y, size=24, mass=1)
# 最大速率、最大出力
self.max_speed = 2
self.max_force = 0.2
# 前方大圓距離d、半徑r、圓心位置center
self.d = 80
self.r = 50
self.center = pygame.Vector2()
# 前方大圓上之假想目標物偏移角度,單位「度」
self.theta = 0
def swim(self, food_position):
direction = self.velocity.normalize() if self.velocity.length()>0 else pygame.Vector2(1, 0)
self.center = self.position + self.d*direction
heading = math.degrees(math.atan2(self.velocity.y, self.velocity.x))
# 依據與食物之間的距離來控制扭動速度,距離越近,扭動速度越慢
self.theta += (self.position-food_position).length()/10
if self.theta > 360:
self.theta -= 360
target = food_position + pygame.Vector2.from_polar((self.r, self.theta+heading))
self.seek(target)
# python version 3.10.9
import math
import sys
import pygame # version 2.3.0
pygame.init()
pygame.display.set_caption("Exercise 5.5")
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
screen_size = WIDTH, HEIGHT = 640, 360
screen = pygame.display.set_mode(screen_size)
FPS = 60
frame_rate = pygame.time.Clock()
fish = Fish(WIDTH//2, HEIGHT//2)
show_target_position = True
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(WHITE)
mouse = pygame.Vector2(pygame.mouse.get_pos())
pygame.draw.circle(screen, (0, 0, 0), mouse, 5, 2)
fish.swim(mouse)
fish.update()
fish.show()
pygame.display.update()
frame_rate.tick(FPS)