2024-11-08|閱讀時間 ‧ 約 0 分鐘

The Nature of Code閱讀心得與Python實作:5.2 Vehicles and Steering

轉向行為(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展現出自主移動行為的過程。這三層結構為:

  • 選擇行動(action selection):vehicle會根據從環境中取得的資訊來選擇可以達到目的的行動。如果vehicle的目標是避免撞壞自己,那當他在行進中發現前面有障礙物時,就會根據實際的狀況來選擇該採取哪種行動,以避免撞上去。繞過去?停止前進?不管採取哪種行動,都是為了達成避免撞壞自己這個目標。
  • 轉向(steering):當vehicle決定好要採取的行動之後,接下來就是要去算出執行這個行動所需的各項資料。如果決定要繞過去,那要從左邊或右邊繞過去?要花多少力氣?這個改變移動方向的力,就是轉向力(steering force)。在這一層中,vehicle需算出行動所需的轉向力。
  • 運轉移動(locomotion):在轉向層所得到的結果,要在這一層中付諸行動。因為vehicle是理想化的,所以在轉向層所算出來的轉向力就直接作用在vehicle上來改變移動方向,不需要去管那個力是怎麼來的,又是透過怎樣的傳動機制讓vehicle可以移動的。

The Steering Force

轉向力到底是個什麼樣的力呢?假設有個行進中的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會在畫面上跑來跑去,所以先前設計MoverParticle這兩個類別時所使用的架構,也可以拿來這邊用。這也就是說,在Vehicle類別中,也會有下列屬性:

m:質量,純量
position:位置向量,pygame.Vector2物件
velocity:速度向量,pygame.Vector2物件
acceleration:加速度向量,pygame.Vector2物件

除此之外,因為我們希望自主代理人所表現出的行為,能夠像真實世界中會看到的情況一樣,所以有必要針對他的一些能力加以限制,讓他的行為能更加逼真。基於這樣子的考量,我們在Vehicle類別中,增加了下列兩個屬性來限制他的能力:

max_speed:最高移動速率,純量
max_force:最大出力,純量

規劃好主要的屬性之後,就可以來設計Vehicle類別中最核心的部分,也就是可以計算出需要的轉向力,來讓vehicle可以順利朝目標前進的方法;這個方法,就把它命名為seek

根據先前所推導出的計算式,在計算轉向力時,需要知道vcurrentvdesired這兩個速度向量。向量vcurrent是已知的,存放在屬性velocity中;而向量vdesired,則需根據vehicle及目標物的位置來計算。

要計算vdesired,必須知道vehicle及目標物的位置。vehicle的位置是已知的,存放在屬性position中;而目標物的位置,也會是已知的,因為vehicle可以感知到目標物在哪裡。那程式要怎麼寫,才能讓vehicle感知到目標物的位置呢?這其實挺簡單的,只要把目標物所在的位置傳進seek()方法中就可以了。

知道如何得知vehicle本身及目標物的位置之後,接下來就來看看計算轉向力的程式要怎麼寫。

假設pygame.Vector2物件target_position是目標物所在位置的位置向量,則由positiontarget_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_speedmax_force的機制。vehicle越靠近目標物,max_speedmax_force會越低。另外,因為max_speedmax_force有可能會變成0,導致desiredsteervelocity等向量也變成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

其中P0target目前的位置;T是預測區間,也就是要預測T個單位時間之後的位置;vtargettarget目前的速度。

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)

The Arrive Behavior

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距離目標物是遠是近,作用力的方向永遠會指向目標物,而讓其行為看起來比較呆板、單調、沒生命力。

Your Own Behaviors

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)


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