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

閱讀時間約 55 分鐘

轉向行為(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這樣子的能力。

raw-image

如圖,假設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就是藉由設計目標物位置的改變模式,來設計漫步這種轉向行為。接下來就來看看詳細的做法。

raw-image

如圖,在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)


15會員
129內容數
寫點東西自娛娛人
留言0
查看全部
發表第一個留言支持創作者!
ysf的沙龍 的其他內容
自主代理人指的是一種實體(entity),這種實體在沒有任何人指揮以及事先規劃好的情形下,能夠自主決定在身處的環境中要怎麼行動。
前面幾章所介紹的,都是只在外部環境的作用力下,才會動一動的無生命物體和圖案,整個模擬世界讓人覺得缺乏生命力沒什麼生氣。既然如此,那能不能灌注些生命力給這些無生命的物體和圖案呢?如果讓它們可以依照自己的想法而活,那模擬世界會變成什麼樣子呢?可以讓它們擁有希望和夢想嗎?可以讓它們心存恐懼嗎?
自主代理人指的是一種實體(entity),這種實體在沒有任何人指揮以及事先規劃好的情形下,能夠自主決定在身處的環境中要怎麼行動。
前面幾章所介紹的,都是只在外部環境的作用力下,才會動一動的無生命物體和圖案,整個模擬世界讓人覺得缺乏生命力沒什麼生氣。既然如此,那能不能灌注些生命力給這些無生命的物體和圖案呢?如果讓它們可以依照自己的想法而活,那模擬世界會變成什麼樣子呢?可以讓它們擁有希望和夢想嗎?可以讓它們心存恐懼嗎?
你可能也想看
Google News 追蹤
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
Faker昨天真的太扯了,中國主播王多多點評的話更是精妙,分享給各位 王多多的點評 「Faker是我們的處境,他是LPL永遠繞不開的一個人和話題,所以我們特別渴望在決賽跟他相遇,去直面我們的處境。 我們曾經稱他為最高的山,最長的河,以為山海就是盡頭,可是Faker用他28歲的年齡...
Thumbnail
兩個 #人行道猴子 針峰相對中,到底會發生什麼事呢? 前幾天學長的FB及Line群組裡討論 #狼性 。
今天騎Youbike 去圖書館還書、拿預約書籍,順便至蝦皮店取貨,因為方向完全相反再加上天氣炎熱懶得走太多路程,因而選擇騎腳踏車。 因本身對於馬路上來來去去的大車小車有點畏懼,所以在騎乘過程當中總是特別小心翼翼,這讓我想到最近讀心理學導論當中的知覺篇章當中有提到一個專有名詞叫做選擇性注意力,在其日
又一次夢見跟交通阻礙有關的夢,他想不起來這是第幾次。因為迷路而搭不上車、搭上了車卻飛到空中再掉下懸崖海邊、在車陣中塞車而錯過了重要赴約,而今天的夢境是,沿著筆直的道路開車,卻因為要閃躲迎面而來、佔滿整個車道的大卡車而感覺就要掉進了田裡,在原本要翻車的那一刻,卻像是有輕功一樣飛到了另一條路上而繼續前進
自從識覺中樞中風受損以後,看東西都好像霧裡看花,所以就不敢開車或騎車了!我相信我應該是還可以的,但是不能拿路人做賭注吧?一開始,都是麻煩家人載我,但是麻煩別人多了,總是會招人嫌,那怕是家人?或許有上億的活期存款,會有不一樣的對待,可惜我沒有。而重點是我從小的習慣就是走到那裡,看到那裡,沒有目標也是件
Thumbnail
近年裡,打開新聞,總是有三分之一以上的篇幅是道路事故,好似這塊土地隨時在發生著行車安全的衝突。 突然發覺自己的行車模式與半年前的自己已截然不同,並不是因為任何事故的教訓,僅是在一日行車時的一句自我提問,我的行車模式瞬間扭轉了,一種自然而然、無意識地轉變。
Thumbnail
比起賽車時的緊湊、上下班通勤時的忙碌與一成不變的生活,對照生活在台灣的角色群,當懶蟲自己四處移動也是在四處取景。 獻給大家我在漫畫創作中途取得一些可以搭配劇情的照片,希望這些美景或生活景象能讓大家在忙碌的生活中放緩腳步、心情愉快。
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
Faker昨天真的太扯了,中國主播王多多點評的話更是精妙,分享給各位 王多多的點評 「Faker是我們的處境,他是LPL永遠繞不開的一個人和話題,所以我們特別渴望在決賽跟他相遇,去直面我們的處境。 我們曾經稱他為最高的山,最長的河,以為山海就是盡頭,可是Faker用他28歲的年齡...
Thumbnail
兩個 #人行道猴子 針峰相對中,到底會發生什麼事呢? 前幾天學長的FB及Line群組裡討論 #狼性 。
今天騎Youbike 去圖書館還書、拿預約書籍,順便至蝦皮店取貨,因為方向完全相反再加上天氣炎熱懶得走太多路程,因而選擇騎腳踏車。 因本身對於馬路上來來去去的大車小車有點畏懼,所以在騎乘過程當中總是特別小心翼翼,這讓我想到最近讀心理學導論當中的知覺篇章當中有提到一個專有名詞叫做選擇性注意力,在其日
又一次夢見跟交通阻礙有關的夢,他想不起來這是第幾次。因為迷路而搭不上車、搭上了車卻飛到空中再掉下懸崖海邊、在車陣中塞車而錯過了重要赴約,而今天的夢境是,沿著筆直的道路開車,卻因為要閃躲迎面而來、佔滿整個車道的大卡車而感覺就要掉進了田裡,在原本要翻車的那一刻,卻像是有輕功一樣飛到了另一條路上而繼續前進
自從識覺中樞中風受損以後,看東西都好像霧裡看花,所以就不敢開車或騎車了!我相信我應該是還可以的,但是不能拿路人做賭注吧?一開始,都是麻煩家人載我,但是麻煩別人多了,總是會招人嫌,那怕是家人?或許有上億的活期存款,會有不一樣的對待,可惜我沒有。而重點是我從小的習慣就是走到那裡,看到那裡,沒有目標也是件
Thumbnail
近年裡,打開新聞,總是有三分之一以上的篇幅是道路事故,好似這塊土地隨時在發生著行車安全的衝突。 突然發覺自己的行車模式與半年前的自己已截然不同,並不是因為任何事故的教訓,僅是在一日行車時的一句自我提問,我的行車模式瞬間扭轉了,一種自然而然、無意識地轉變。
Thumbnail
比起賽車時的緊湊、上下班通勤時的忙碌與一成不變的生活,對照生活在台灣的角色群,當懶蟲自己四處移動也是在四處取景。 獻給大家我在漫畫創作中途取得一些可以搭配劇情的照片,希望這些美景或生活景象能讓大家在忙碌的生活中放緩腳步、心情愉快。